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 '" 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