balatro-mods/DebugPlus/console.lua
2025-01-19 15:01:49 +08:00

727 lines
26 KiB
Lua

local util = require("debugplus.util")
local utf8 = require("utf8")
local watcher = require("debugplus.watcher")
local config = require("debugplus.config")
local logger = require("debugplus.logger")
local global = {}
local showTime = 5 -- Amount of time new console messages show up
local fadeTime = 1 -- Amount of time it takes for a message to fade
local consoleOpen = false
local openNextFrame = false
local gameKeyRepeat = love.keyboard.hasKeyRepeat()
local showNewLogs = config.getValue("showNewLogs")
local firstConsoleRender = nil
local history = {}
local currentHistory = nil
local commands = nil
local controller = nil
local logOffset = 0
commands = {{
name = "echo",
source = "debugplus",
shortDesc = "Repeat's what you say",
desc = "Mostly just a testing command. Outputs what you input.",
exec = function(args, rawArgs, dp)
return rawArgs
end
}, {
name = "help",
source = "debugplus",
shortDesc = "Get command info",
desc = "Get's help about commands. When run without args, lists all commands and their short descriptions. When run with a command name, shows info about that command.",
exec = function(args, rawArgs, dp)
local toLookup = args[1]
if not toLookup then
local out = "Help:\nBelow is a list of commands.\n"
for k, v in ipairs(commands) do
out = out .. v.name .. ": " .. v.shortDesc .. "\n"
end
out = out .. "\nFor more information about a specific command, run 'help <commandName>'"
return out
end
local cmdName = string.lower(string.gsub(toLookup, "^(%S+).*", "%1"))
local cmd
for i, c in ipairs(commands) do
if c.source .. ":" .. c.name == cmdName then
cmd = c
break
end
if c.name == cmdName then
cmd = c
break
end
end
if not cmd then
return '"' .. cmdName .. '" could not be found. To see a list of all commands, run "help" without any args',
"ERROR"
end
return cmd.name .. ":\n" .. cmd.desc .. "\n\nThis command can be run by typing '" .. cmd.name .. "' or '" ..
cmd.source .. ":" .. cmd.name .. "'."
end
}, {
name = "eval",
source = "debugplus",
shortDesc = "Evaluate lua code",
desc = "Execute's lua code. This code has access to all the globals that the game has, as well as a dp object, with some DebugPlus specific stuff.",
exec = function(args, rawArgs, dp)
local env = {}
for k, v in pairs(_G) do
env[k] = v
end
env.dp = dp
local func, err = load("return " .. rawArgs, "DebugPlus Eval", "t", env)
if not func then
func, err = load(rawArgs, "DebugPlus Eval", "t", env)
end
if not func then
return "Syntax Error: " .. err, "ERROR"
end
local success, res = pcall(func)
if not success then
return "Error: " .. res, "ERROR"
end
return util.stringifyTable(res)
end
}, {
name = "money",
source = "debugplus",
shortDesc = "Set or add money",
desc = "Set or add to your money. Usage:\nmoney set [amount] - Set your money to the given amount\nmoney add [amount] - Adds the given amount to your money.",
exec = function(args, rawArgs, dp)
if G.STAGE ~= G.STAGES.RUN then
return "This command must be run during a run.", "ERROR"
end
local subCmd = args[1]
local amount = tonumber(args[2])
if subCmd == "set" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.dollars = amount
elseif subCmd == "add" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.dollars = G.GAME.dollars + amount
else
return "Please choose whether you want to add or set. For more info, run 'help money'"
end
return "Money is now $" .. G.GAME.dollars
end
}, {
name = "round",
source = "debugplus",
shortDesc = "Set or add to your round",
desc = "Set or add to your round. Usage:\nround set [amount] - Set the current round to the given amount\nround add [amount] - Adds the given number of rounds.",
exec = function(args, rawArgs, dp)
if G.STAGE ~= G.STAGES.RUN then
return "This command must be run during a run.", "ERROR"
end
local subCmd = args[1]
local amount = tonumber(args[2])
if subCmd == "set" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.round = amount
elseif subCmd == "add" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.round = G.GAME.round + amount
else
return "Please choose whether you want to add or set. For more info, run 'help round'"
end
return "Round is now " .. G.GAME.round
end
}, {
name = "ante",
source = "debugplus",
shortDesc = "Set or add to your ante",
desc = "Set or add to your ante. Usage:\nante set [amount] - Set the current ante to the given amount\nante add [amount] - Adds the given number of antes.",
exec = function(args, rawArgs, dp)
if G.STAGE ~= G.STAGES.RUN then
return "This command must be run during a run.", "ERROR"
end
local subCmd = args[1]
local amount = tonumber(args[2])
if subCmd == "set" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.round_resets.ante = amount
elseif subCmd == "add" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.round_resets.ante = G.GAME.round_resets.ante + amount
else
return "Please choose whether you want to add or set. For more info, run 'help ante'"
end
return "Ante is now " .. G.GAME.round_resets.ante
end
}, {
name = "discards",
source = "debugplus",
shortDesc = "Set or add to your hand",
desc = "Set or add to your hand. Usage:\ndiscards set [amount] - Set the current hand to the given amount\ndiscards add [amount] - Adds the given number of discards.",
exec = function(args, rawArgs, dp)
if G.STAGE ~= G.STAGES.RUN then
return "This command must be run during a run.", "ERROR"
end
local subCmd = args[1]
local amount = tonumber(args[2])
if subCmd == "set" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.current_round.discards_left = amount
elseif subCmd == "add" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.current_round.discards_left = G.GAME.current_round.discards_left + amount
else
return "Please choose whether you want to add or set. For more info, run 'help hand'"
end
return "Discards are now " .. G.GAME.current_round.discards_left
end
}, {
name = "hands",
source = "debugplus",
shortDesc = "Set or add to your hand",
desc = "Set or add to your hand. Usage:\nhands set [amount] - Set the current hand to the given amount\nhands add [amount] - Adds the given number of hands.",
exec = function(args, rawArgs, dp)
if G.STAGE ~= G.STAGES.RUN then
return "This command must be run during a run.", "ERROR"
end
local subCmd = args[1]
local amount = tonumber(args[2])
if subCmd == "set" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.current_round.hands_left = amount
elseif subCmd == "add" then
if not amount then
return "Please provide a valid number to set/add.", "ERROR"
end
G.GAME.current_round.hands_left = G.GAME.current_round.hands_left + amount
else
return "Please choose whether you want to add or set. For more info, run 'help hand'"
end
return "Hands are now " .. G.GAME.current_round.hands_left
end
}, {
name = "watch",
source = "debugplus",
shortDesc = "Watch and execute a file when it changes.",
desc = "Watch and execute a file when it changes. Usage:\nwatch stop - Stop's watching files.\n".. watcher.subCommandDesc .."Files should be a relative path to a file in the save directory (e.g. `Mods/Example/test.lua`)",
exec = function(args, rawArgs, dp)
local subCmd = args[1]
local file = args[2]
if subCmd == "stop" then
watcher.stopWatching()
return "I will stop watching for file changes."
elseif watcher.types[subCmd] then
local succ, err = watcher.startWatching(file, subCmd)
if not succ then return err, "ERROR" end
return "Started watching " .. file
else
return "Please provide a valid sub command. For more info, run 'help watch'"
end
end
}, {
name = "tutorial",
source = "debugplus",
shortDesc = "Modify the tutorial state.",
desc = "Modify the tutorial state. Usage:\ntutorial finish - Finish the tutorial.\ntutorial reset - Reset the tutorial progress to a fresh state.\ntutorial new - Starts a new tutorial run (like hitting play for the first time)",
exec = function(args, rawArgs, dp)
local subCmd = args[1]
if subCmd == "finish" then
if G.OVERLAY_TUTORIAL then
G.FUNCS.skip_tutorial_section()
end
G.SETTINGS.tutorial_complete = true
G.SETTINGS.tutorial_progress = nil
return "Tutorial finished."
elseif subCmd == "reset" then
G.SETTINGS.tutorial_complete = false
G.SETTINGS.tutorial_progress = {
forced_shop = {'j_joker', 'c_empress'},
forced_voucher = 'v_grabber',
forced_tags = {'tag_handy', 'tag_garbage'},
hold_parts = {},
completed_parts = {}
}
return "Tutorial reset."
elseif subCmd == "new" then
G.FUNCS.start_tutorial()
return "Starting a new run."
else
return "Please provide a valid sub command. For more info, run 'help tutorial'"
end
end
}, {
name = "resetshop",
source = "debugplus",
shortDesc = "Reset the shop.",
desc = "Resets the shop.",
exec = function(args, rawArgs, dp)
if G.STATE ~= G.STATES.SHOP then
return "This command can only be run in a shop.", 'ERROR'
end
G.shop:remove()
G.shop = nil
G.SHOP_SIGN:remove()
G.SHOP_SIGN = nil
G.GAME.current_round.used_packs = nil
G.STATE_COMPLETE = false
G:update_shop()
return "Reset shop."
end
}, {
name = "value",
source = "debugplus",
shortDesc = "Get and modify highlighted card values",
desc = "Retrives or modifies the values of the currently hovered card. Usage:\nvalue get - Gets all detected values on the hovered card.\nvalue set [keys] [value] - Modifies a value of hovered card. The format of keys should match the 'get' command.\nvalue set_center [keys] [value] - Modifies a value on the center of the hovered card. This will modify future versions of the card.",
exec = function (args, rawArgs, dp)
local unmodified_vals = {
bonus = 0,
perma_bonus = 0,
extra_value = 0,
p_dollars = 0,
h_mult = 0,
h_x_mult = 0,
h_dollars = 0,
h_size = 0,
d_size = 0,
hands_played_at_create = 0,
mult = 0,
x_mult = 1,
e_mult = 0,
ee_mult = 0,
eee_mult = 0,
x_chips = 0,
e_chips = 0,
ee_chips = 0,
eee_chips = 0,
t_mult = 0,
t_chips = 0,
}
local ignore_vals = {
name = true,
set = true,
order = true,
consumeable = true
}
if dp.hovered:is(Card) then
if args[1] == "get" then
local values = "Values:"
for k, v in pairs(dp.hovered.ability) do
if (not ignore_vals[k]) and (not unmodified_vals[k] or unmodified_vals[k] ~= dp.hovered.ability[k]) then
if k == "hyper_chips" or k == "hyper_mult" then
if dp.hovered.ability[k][1] ~= 0 or dp.hovered.ability[k][2] ~= 0 then
values = values .. "\n" .. tostring(k) .. " " .. tostring(dp.hovered.ability[k][1]) .. " " .. tostring(dp.hovered.ability[k][2])
end
elseif type(dp.hovered.ability[k]) == "table" then
for kk, vv in pairs(dp.hovered.ability[k]) do
values = values .. "\n" .. tostring(k) .. " " .. tostring(kk) .. " " .. tostring(vv)
end
elseif dp.hovered.ability[k] ~= "" then
values = values .. "\n" .. tostring(k) .. " " .. tostring(dp.hovered.ability[k])
end
end
end
return values
elseif args[1] == "set" or args[1] == "set_center" then
local root = dp.hovered.ability
if args[1] == "set_center" then
root = dp.hovered.config.center.config
end
local rootC
if dp.hovered.ability.consumeable then
rootC = root.consumeable
end
if #args < 2 then
return "Please provide a key to set", "ERROR"
end
if #args < 3 then
return "Please provide a value to set", "ERROR"
end
for i = 2, #args-2 do
root = root[args[i]]
if rootC then rootC = rootC[args[i]] end
end
if tonumber(args[#args]) then --number
root[args[#args-1]] = tonumber(args[#args])
if rootC then rootC[args[#args-1]] = tonumber(args[#args]) end
elseif args[#args] == "true" then --bool
root[args[#args-1]] = true
if rootC then rootC[args[#args-1]] = true end
elseif args[#args] == "false" then
root[args[#args-1]] = false
if rootC then rootC[args[#args-1]] = false end
else
root[args[#args-1]] = args[#args]
if rootC then rootC[args[#args-1]] = args[#args] end
end
return "Value set successfully."
else
return "Invalid argument. Use 'get' or 'set' or 'set_center'.", "ERROR"
end
else
return "This command only works while hovering over a card. Rerun it while hovering over a card.", "ERROR"
end
end
}}
local inputText = ""
local function runCommand()
inputText = util.trim(inputText)
if inputText == "" then
return
end
logger.handleLog({1, 0, 1}, "INFO", "> " .. inputText)
if history[1] ~= inputText then
table.insert(history, 1, inputText)
end
local cmdName = string.lower(string.gsub(inputText, "^(%S+).*", "%1"))
local rawArgs = string.gsub(inputText, "^%S+%s*(.*)", "%1")
local args = {}
for w in string.gmatch(rawArgs, "%S+") do
table.insert(args, w)
end
inputText = ""
consoleOpen = false
love.keyboard.setKeyRepeat(gameKeyRepeat)
local cmd
for i, c in ipairs(commands) do
if c.source .. ":" .. c.name == cmdName then
cmd = c
break
end
if c.name == cmdName then
cmd = c
break
end
end
if not cmd then
return logger.handleLog({1, 0, 0}, "ERROR", "< ERROR: Command '" .. cmdName .. "' not found.")
end
local dp = {
hovered = G.CONTROLLER.hovering.target,
handleLog = logger.handleLog
}
local success, result, loglevel, colourOverride = pcall(cmd.exec, args, rawArgs, dp)
if not success then
return logger.handleLog({1, 0, 0}, "ERROR", "< An error occurred processing the command:", result)
end
local level = loglevel or "INFO"
if not logger.levelMeta[level] then
level = "INFO"
logger.handleLogAdvanced({
level = "WARN",
}, "[DebugPlus] Command ".. cmdName.. " returned an invalid log level. Defaulting to INFO.")
end
local colour = colourOverride or logger.levelMeta[level].colour
if success and success ~= "" then
return logger.handleLog(colour, level, "<", result)
else
return logger.handleLog(colour, level, "< Command exited without a response.")
end
end
function global.consoleHandleKey(key)
if not consoleOpen then
if key == '/' or key == 'kp/' then
if util.isShiftDown() then
showNewLogs = not showNewLogs
else
openNextFrame = true -- This is to prevent the keyboard handler from typing this key
end
end
return true
end
if key == "escape" then
consoleOpen = false
love.keyboard.setKeyRepeat(gameKeyRepeat)
inputText = ""
end
-- This bit stolen from https://love2d.org/wiki/love.textinput
if key == "backspace" then
-- get the byte offset to the last UTF-8 character in the string.
local byteoffset = utf8.offset(inputText, -1)
if byteoffset then
-- remove the last UTF-8 character.
-- string.sub operates on bytes rather than UTF-8 characters, so we couldn't do string.sub(text, 1, -2).
inputText = string.sub(inputText, 1, byteoffset - 1)
end
end
if key == "return" then
if util.isShiftDown() then
inputText = inputText .. "\n"
else
runCommand()
end
end
if key == "v" and util.isCtrlDown() then
inputText = inputText .. love.system.getClipboardText()
end
if key == "up" then
if currentHistory.index >= #history then
return
end
if currentHistory.index == 0 then
currentHistory.val = inputText
end
currentHistory.index = currentHistory.index + 1
inputText = history[currentHistory.index]
end
if key == "down" then
if currentHistory.index <= 0 then
return
end
currentHistory.index = currentHistory.index - 1
if currentHistory.index == 0 then
inputText = currentHistory.val
else
inputText = history[currentHistory.index]
end
end
end
local orig_textinput = love.textinput
local function textinput(t)
if orig_textinput then
orig_textinput(t)
end -- That way if another mod uses this, I don't clobber it's implementation
if not consoleOpen then
return
end
inputText = inputText .. t
end
love.textinput = textinput
local orig_wheelmoved = love.wheelmoved
local function wheelmoved(x, y)
if orig_wheelmoved then
orig_wheelmoved(x, y)
end
if not consoleOpen then
return
end
logOffset = math.min(math.max(logOffset + y, 0), #logger.logs - 1)
end
love.wheelmoved = wheelmoved
local function calcHeight(text, width)
local font = love.graphics.getFont()
local rw, lines = font:getWrap(text, width)
local lineHeight = font:getHeight()
return #lines * lineHeight, rw, lineHeight
end
local function hyjackErrorHandler()
local orig = love.errorhandler
if not orig then -- Vanilla
return -- Doesn't work with love.errhand (need love.errorhandler)
end
local function safeCall(func, ...)
local succ, res = pcall(func, ...)
if not succ then print("ERROR", res)
else return res end
end
function love.errorhandler(msg)
local ret = orig(msg)
orig_wheelmoved = nil
orig_textinput = nil
consoleOpen = false
inputText = ""
love.keyboard.setKeyRepeat(gameKeyRepeat)
local justCrashed = true
local present = love.graphics.present
function love.graphics.present()
local r, g, b, a = love.graphics.getColor()
if justCrashed then
firstConsoleRender = love.timer.getTime()
justCrashed = false
end
safeCall(global.doConsoleRender)
love.graphics.setColor(r,g,b,a)
present()
end
return function()
love.event.pump()
local evts = {}
for e, a, b, c in love.event.poll() do
if consoleOpen and e == "textinput" then
safeCall(textinput, a)
elseif consoleOpen and e == "wheelmoved" then
safeCall(wheelmoved, a, b)
elseif e == "keypressed" then
if safeCall(global.consoleHandleKey, a) then
table.insert(evts, {e,a,b,c})
end
else
table.insert(evts, {e,a,b,c})
end
end
for _,v in ipairs(evts) do -- Add back for the original handler
love.event.push(unpack(v))
end
return ret()
end
end
end
function global.doConsoleRender()
if openNextFrame then
consoleOpen = true
openNextFrame = false
currentHistory = {
index = 0,
val = ""
}
logOffset = 0
gameKeyRepeat = love.keyboard.hasKeyRepeat()
love.keyboard.setKeyRepeat(true)
end
if not consoleOpen and not showNewLogs then
return
end
-- Setup
local width, height = love.graphics.getDimensions()
local padding = 10
local lineWidth = width - padding * 2
local bottom = height - padding * 2
local now = love.timer.getTime()
if firstConsoleRender == nil then
if config.getValue("hyjackErrorHandler") then hyjackErrorHandler() end
firstConsoleRender = now
logger.logCmd("Press [/] to toggle console and press [shift] + [/] to toggle new log previews")
end
-- Input Box
love.graphics.setColor(0, 0, 0, .5)
if consoleOpen then
bottom = bottom - padding * 2
local text = "> " .. inputText
local lineHeight, realWidth, singleLineHeight = calcHeight(text, lineWidth)
love.graphics.rectangle("fill", padding, bottom - lineHeight + padding, lineWidth, lineHeight + padding * 2)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.printf(text, padding * 2, bottom - lineHeight + singleLineHeight, lineWidth - padding * 2)
bottom = bottom - lineHeight - padding * 2
end
-- Main window
if consoleOpen then
love.graphics.setColor(0, 0, 0, .5)
love.graphics.rectangle("fill", padding, padding, lineWidth, bottom)
end
for i = #logger.logs, 1, -1 do
local v = logger.logs[i]
if consoleOpen and #logger.logs - logOffset < i then -- TODO: could this be more efficent?
goto finishrender
end
if not consoleOpen and v.time < firstConsoleRender then
break
end
local age = now - v.time
if not consoleOpen and age > showTime + fadeTime then
break
end
if not logger.levelMeta[v.level].shouldShow and not v.command then
goto finishrender
end
if not v.command and config.getValue("onlyCommands") then
goto finishrender
end
local msg = v.str
if consoleOpen and not v.hack_no_prefix then
msg = "[" .. string.sub(v.level, 1, 1) .. "] " .. msg
end
local lineHeight, realWidth = calcHeight(msg, lineWidth)
bottom = bottom - lineHeight
if bottom < padding then
break
end
local opacityPercent = 1
if not consoleOpen and age > showTime then
opacityPercent = (fadeTime - (age - showTime)) / fadeTime
end
if not consoleOpen then
love.graphics.setColor(0, 0, 0, .5 * opacityPercent)
love.graphics.rectangle("fill", padding, bottom, lineWidth, lineHeight)
end
love.graphics.setColor(v.colour[1], v.colour[2], v.colour[3], opacityPercent)
love.graphics.printf(msg, padding * 2, bottom, lineWidth - padding * 2)
::finishrender::
end
end
function global.registerCommand(id, options)
if not options then
error("Options must be provided")
end
if not options.name and not string.match(options.name, "^[%l%d_-]$") then
error("Options.name must be provided and match pattern `^[%l%d_-]$`.")
end
if not options.exec or type(options.exec) ~= "function" then
error("Options.exec must be a function")
end
if not options.shortDesc or type(options.shortDesc) ~= "string" then
error("Options.shortDesc must be a string")
end
if not options.desc or type(options.desc) ~= "string" then
error("Options.desc must be a string")
end
local cmd = {
source = id,
name = options.name,
exec = options.exec,
shortDesc = options.shortDesc,
desc = options.desc
}
for k, v in ipairs(commands) do
if v.source == cmd.source and v.name == cmd.name then
error("This command already exists")
end
end
table.insert(commands, cmd)
end
local function handleLogsChange(added)
added = added or 0
logOffset = math.min(logOffset + added, #logger.logs)
end
logger.handleLogsChange = handleLogsChange
config.configDefinition.showNewLogs.onUpdate = function(v)
showNewLogs = v
end
return global