diff --git a/.gitignore b/.gitignore index 76a786d..ab1be21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store .idea/ .vscode/ +src/balamod_version.lua +src/https.dll \ No newline at end of file diff --git a/src/balamod.lua b/src/balamod.lua index cd514f0..a948cbd 100644 --- a/src/balamod.lua +++ b/src/balamod.lua @@ -9,12 +9,7 @@ local https = require('https') logger = logging.getLogger('balamod') mods = {} -local apis = { - logging = logging, - console = console, - math = math, - platform = platform, -} +local apis = {logging = logging, console = console, math = math, platform = platform} is_loaded = false local RESULT = { SUCCESS = 0, @@ -25,33 +20,33 @@ local RESULT = { MOD_FS_LOAD_ERROR = 5, MOD_PCALL_ERROR = 6, TAR_DECOMPRESS_ERROR = 7, - MOD_NOT_CONFORM = 8, + MOD_NOT_CONFORM = 8 } local paths = {} -- Paths to the files that will be loaded local _VERSION = require('balamod_version') local function splitstring(inputstr, sep) if sep == nil then - sep = "%s" + sep = '%s' end - local t={} - for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + local t = {} + for str in string.gmatch(inputstr, '([^' .. sep .. ']+)') do table.insert(t, str) end return t end -function buildPaths(root,ignore) +function buildPaths(root, ignore) local items = love.filesystem.getDirectoryItems(root) for _, file in ipairs(items) do - if root ~= "" then - file = root.."/"..file + if root ~= '' then + file = root .. '/' .. file end local info = love.filesystem.getInfo(file) if info then - if info.type == "file" and file:match("%.lua$") then - table.insert(paths,file) - elseif info.type == "directory" then + if info.type == 'file' and file:match('%.lua$') then + table.insert(paths, file) + elseif info.type == 'directory' then local valid = true for _, i in ipairs(ignore) do if i == file then @@ -59,13 +54,22 @@ function buildPaths(root,ignore) end end if valid then - buildPaths(file,ignore) + buildPaths(file, ignore) end end end end end +local function table_contains(table, element) + for _, value in pairs(table) do + if value == element then + return value + end + end + return nil +end + local function request(url) logger:debug('Request made with url: ', url) local code @@ -79,24 +83,24 @@ local function request(url) end local function extractFunctionBody(path, function_name) - local pattern = "\n?%s*function%s+" .. function_name .. "%(" + local pattern = '\n?%s*function%s+' .. function_name .. '%(' local func_begin, fin = current_game_code[path]:find(pattern) if not func_begin then - return "Can't find function begin " .. function_name + return 'Can\'t find function begin ' .. function_name end - local func_end = current_game_code[path]:find("\n\r?end", fin) + local func_end = current_game_code[path]:find('\n\r?end', fin) -- This is to catch functions that have incorrect ending indentation by catching the next function in line. -- Can be removed once Card:calculate_joker no longer has this typo. - local typocatch_func_end = current_game_code[path]:find("\n\r?function", fin) + local typocatch_func_end = current_game_code[path]:find('\n\r?function', fin) if typocatch_func_end and typocatch_func_end < func_end then func_end = typocatch_func_end - 3 end if not func_end then - return "Can't find function end " .. function_name + return 'Can\'t find function end ' .. function_name end local func_body = current_game_code[path]:sub(func_begin, func_end + 3) @@ -107,13 +111,13 @@ local function inject(path, function_name, to_replace, replacement) -- Injects code into a function (replaces a string with another string inside a function) local function_body = extractFunctionBody(path, function_name) local modified_function_code = function_body:gsub(to_replace, replacement) - escaped_function_body = function_body:gsub("([^%w])", "%%%1") -- escape function body for use in gsub - escaped_modified_function_code = modified_function_code:gsub("([^%w])", "%%%1") + escaped_function_body = function_body:gsub('([^%w])', '%%%1') -- escape function body for use in gsub + escaped_modified_function_code = modified_function_code:gsub('([^%w])', '%%%1') current_game_code[path] = current_game_code[path]:gsub(escaped_function_body, escaped_modified_function_code) -- update current game code in memory local new_function, load_error = load(modified_function_code) -- load modified function if not new_function then - logger:error("Error loading modified function", function_name, ": ", (load_error or "Unknown error")) + logger:error('Error loading modified function', function_name, ': ', (load_error or 'Unknown error')) logger:error(modified_function_code) end @@ -125,7 +129,7 @@ local function inject(path, function_name, to_replace, replacement) if status then testFunction = result -- Overwrite the original function with the result of the new function else - logger:error("Error executing modified function", function_name, ": ", result) -- Safeguard against errors + logger:error('Error executing modified function', function_name, ': ', result) -- Safeguard against errors logger:error(modified_function_code) end end @@ -133,22 +137,23 @@ end local function injectHead(path, function_name, code) local function_body = extractFunctionBody(path, function_name) - local pattern = "(function%s+" .. function_name .. ".-)\n" - local modified_function_code, number_of_subs = function_body:gsub(pattern, "%1\n" .. code .. "\n") + local pattern = '(function%s+' .. function_name .. '.-)\n' + local modified_function_code, number_of_subs = function_body:gsub(pattern, '%1\n' .. code .. '\n') if number_of_subs == 0 then - logger:error("Error: Function start not found in function body or multiple matches encountered.") + logger:error('Error: Function start not found in function body or multiple matches encountered.') logger:error(modified_function_code) return end - escaped_function_body = function_body:gsub("([^%w])", "%%%1") - escaped_modified_function_code = modified_function_code:gsub("([^%w])", "%%%1") + escaped_function_body = function_body:gsub('([^%w])', '%%%1') + escaped_modified_function_code = modified_function_code:gsub('([^%w])', '%%%1') current_game_code[path] = current_game_code[path]:gsub(escaped_function_body, escaped_modified_function_code) local new_function, load_error = load(modified_function_code) if not new_function then - logger:error("Error loading modified function ", function_name, " with head injection: ", (load_error or "Unknown error")) + logger:error('Error loading modified function ', function_name, ' with head injection: ', + (load_error or 'Unknown error')) logger:error(modified_function_code) return end @@ -161,7 +166,7 @@ local function injectHead(path, function_name, code) if status then testFunction = result else - logger:error("Error executing modified function ", function_name, " with head injection: ", result) + logger:error('Error executing modified function ', function_name, ' with head injection: ', result) logger:error(modified_function_code) end end @@ -169,22 +174,25 @@ end local function injectTail(path, function_name, code) local function_body = extractFunctionBody(path, function_name) - local pattern = "(.-)(end[ \t]*\n?)$" - local modified_function_code, number_of_subs = function_body:gsub(pattern, "%1" .. string.gsub(code, '(.-)%s*$', '%1') .. "\n" .. "%2") + local pattern = '(.-)(end[ \t]*\n?)$' + local modified_function_code, number_of_subs = function_body:gsub(pattern, '%1' .. + string.gsub(code, '(.-)%s*$', '%1') .. '\n' .. + '%2') if number_of_subs == 0 then - logger:error("Error: 'end' not found in function '", function_name, "' body or multiple ends encountered.") + logger:error('Error: \'end\' not found in function \'', function_name, '\' body or multiple ends encountered.') logger:error(modified_function_code) return end - escaped_function_body = function_body:gsub("([^%w])", "%%%1") - escaped_modified_function_code = modified_function_code:gsub("([^%w])", "%%%1") + escaped_function_body = function_body:gsub('([^%w])', '%%%1') + escaped_modified_function_code = modified_function_code:gsub('([^%w])', '%%%1') current_game_code[path] = current_game_code[path]:gsub(escaped_function_body, escaped_modified_function_code) local new_function, load_error = load(modified_function_code) if not new_function then - logger:error("Error loading modified function ", function_name, " with tail injection: ", (load_error or "Unknown error")) + logger:error('Error loading modified function ', function_name, ' with tail injection: ', + (load_error or 'Unknown error')) logger:error(modified_function_code) return end @@ -197,7 +205,7 @@ local function injectTail(path, function_name, code) if status then testFunction = result else - logger:error("Error executing modified function ", function_name, " with tail injection: ", result) + logger:error('Error executing modified function ', function_name, ' with tail injection: ', result) logger:error(modified_function_code) end end @@ -231,10 +239,8 @@ local function getRepoMods() logger:error('Response: ' .. repoBody) else for modInfo in string.gmatch(repoBody, '([^\n]+)') do - local modId, modVersion, modName, modDesc, modUrl = string.match( - modInfo, - '([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]+)' - ) + local modId, modVersion, modName, modDesc, modUrl = string.match(modInfo, + '([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]+)') local modPresent = isModPresent(modId) local needUpdate = true local version = modVersion @@ -255,7 +261,7 @@ local function getRepoMods() version = version, newVersion = modVersion, present = modPresent, - needUpdate = needUpdate, + needUpdate = needUpdate }) end end @@ -274,7 +280,7 @@ local function validateManifest(modFolder, manifest) load_after = true, min_balamod_version = false, max_balamod_version = false, - dependencies = false, + dependencies = false } -- check that all manifest expected fields are present @@ -384,12 +390,13 @@ local function validateManifest(modFolder, manifest) end end if not versionConstraintCorrect then - table.insert(incorrectDependencies, modId..':'..version) + table.insert(incorrectDependencies, modId .. ':' .. version) end end if #incorrectDependencies > 0 then -- some of the dependencies are incorrect for the mod, let's log them and return false - logger:error('Manifest in folder ', modFolder, ' has incorrect dependencies field: ', table.concat(incorrectDependencies, ', ')) + logger:error('Manifest in folder ', modFolder, ' has incorrect dependencies field: ', + table.concat(incorrectDependencies, ', ')) return false end end @@ -412,9 +419,9 @@ local function loadMod(modFolder) logger:error('Mod folder ', modFolder, ' does not contain a manifest.json file') return nil end - logger:debug("Loading manifest from: ", 'mods/'..modFolder..'/manifest.json') + logger:debug('Loading manifest from: ', 'mods/' .. modFolder .. '/manifest.json') -- load the manifest - local manifest = json.decode(love.filesystem.read('mods/'..modFolder..'/manifest.json')) + local manifest = json.decode(love.filesystem.read('mods/' .. modFolder .. '/manifest.json')) if not validateManifest(modFolder, manifest) then return nil end @@ -435,8 +442,8 @@ local function loadMod(modFolder) end end -- load the hooks (on_enable, on_game_load, etc...) - logger:debug("Loading hooks from: ", 'mods/'..modFolder..'/main.lua') - local modHooks = require('mods/'..modFolder..'/main') + logger:debug('Loading hooks from: ', 'mods/' .. modFolder .. '/main.lua') + local modHooks = require('mods/' .. modFolder .. '/main') for hookName, hook in pairs(modHooks) do mod[hookName] = hook end @@ -445,7 +452,7 @@ local function loadMod(modFolder) end mod.enabled = true logger:debug('Mod loaded: ', mod.id) - logger:debug("Checking if mod is disabled") + logger:debug('Checking if mod is disabled') if love.filesystem.getInfo('mods/' .. modFolder .. '/disable.it', 'file') then mod.enabled = false end @@ -476,13 +483,13 @@ local function callModCallbacksIfExists(mods, callback_name, should_log, ...) local mod_returns = {} -- pre loading all mods for _, mod in ipairs(sorted) do - if mod.enabled and mod[callback_name] and type(mod[callback_name]) == "function" then + if mod.enabled and mod[callback_name] and type(mod[callback_name]) == 'function' then if should_log then - logger:info("Calling mod callback", callback_name, "for", mod.id) + logger:info('Calling mod callback', callback_name, 'for', mod.id) end local status, message = pcall(mod[callback_name], ...) -- Call the on_pre_load function of the mod if it exists if not status then - logger:warn("Callback", callback_name, "for mod ", mod.id, "failed: ", message) + logger:warn('Callback', callback_name, 'for mod ', mod.id, 'failed: ', message) else table.insert(mod_returns, {modId = mod.id, result = message}) end @@ -537,7 +544,7 @@ local function installMod(modInfo) logger:debug('Downloaded tarball with body ', body) -- decompress the archive in memory - local decompressedData = love.data.decompress("data", "gzip", body) + local decompressedData = love.data.decompress('data', 'gzip', body) local success, result = pcall(tar.unpack, decompressedData) if not success then logger:error('Error decompressing tarball') @@ -552,18 +559,20 @@ local function installMod(modInfo) -- type = "file" | "directory" -- } -- sort the result table so that directories are processed first - table.sort(result, function(a, b) return a.type < b.type end) + table.sort(result, function(a, b) + return a.type < b.type + end) -- check that the downloaded mod contains a main.lua file as well as a manifest.json file local mainLua = false local manifestJson = false for _, file in ipairs(result) do - if file.type == "file" then + if file.type == 'file' then -- file.name is a path, we only want the filename - local _, _, filename = file.name:find(".+/(.+)") - if filename == "main.lua" then + local _, _, filename = file.name:find('.+/(.+)') + if filename == 'main.lua' then mainLua = true end - if filename == "manifest.json" then + if filename == 'manifest.json' then manifestJson = true end end @@ -578,11 +587,11 @@ local function installMod(modInfo) end for _, file in ipairs(result) do -- replace the first part of file.name with the modId - file.name = modId .. file.name:sub(file.name:find("/")) - if file.type == "directory" then - love.filesystem.createDirectory("mods/"..file.name) - elseif file.type == "file" then - love.filesystem.write("mods/"..file.name, file.data) + file.name = modId .. file.name:sub(file.name:find('/')) + if file.type == 'directory' then + love.filesystem.createDirectory('mods/' .. file.name) + elseif file.type == 'file' then + love.filesystem.write('mods/' .. file.name, file.data) end end @@ -595,7 +604,7 @@ local function installMod(modInfo) return RESULT.SUCCESS end -buildPaths("",{"mods","apis","resources","localization"}) +buildPaths('', {'mods', 'apis', 'resources', 'localization'}) -- current_game_code = love.filesystem.read(path) buildPaths = nil -- prevent rerunning (i think) @@ -604,36 +613,33 @@ for _, path in ipairs(paths) do current_game_code[path] = love.filesystem.read(path) end -if not love.filesystem.getInfo("mods", "directory") then -- Create mods folder if it doesn't exist - love.filesystem.createDirectory("mods") +if not love.filesystem.getInfo('mods', 'directory') then -- Create mods folder if it doesn't exist + love.filesystem.createDirectory('mods') end -if not love.filesystem.getInfo("logs", "directory") then -- Create logs folder if it doesn't exist - love.filesystem.createDirectory("logs") +if not love.filesystem.getInfo('logs', 'directory') then -- Create logs folder if it doesn't exist + love.filesystem.createDirectory('logs') end -if not love.filesystem.getInfo("apis", "directory") then -- Create apis folder if it doesn't exist - love.filesystem.createDirectory("apis") +if not love.filesystem.getInfo('apis', 'directory') then -- Create apis folder if it doesn't exist + love.filesystem.createDirectory('apis') end -- apis will be loaded first, then mods -mods["dev_console"] = { - id = "dev_console", - name = "Dev Console", - version = "0.6.0", - author = "sbordeyne & UwUDev", - description = { - "Press F2 to open/close the console", - "Use command `help` for a list of ", - "available commands and shortcuts", - }, +mods['dev_console'] = { + id = 'dev_console', + name = 'Dev Console', + version = '0.6.0', + author = 'sbordeyne & UwUDev', + description = {'Press F2 to open/close the console', 'Use command `help` for a list of ', + 'available commands and shortcuts'}, enabled = true, on_game_load = function(args) - console.logger:info("Game loaded", args) + console.logger:info('Game loaded', args) for _, arg in ipairs(args) do - local split = splitstring(arg, "=") - if split[0] == "--log-level" then + local split = splitstring(arg, '=') + if split[0] == '--log-level' then console.logger.level = split[1]:upper() console.log_level = split[1]:upper() end @@ -641,405 +647,557 @@ mods["dev_console"] = { logging.saveLogs() end, on_game_quit = function() - console.logger:info("Quitting Balatro...") + console.logger:info('Quitting Balatro...') logging.saveLogs() end, on_error = function(message) - console.logger:error("Error: ", message) + console.logger:error('Error: ', message) -- on error, write all messages to a file logging.saveLogs() end, on_enable = function() - console.logger:debug("Dev Console enabled") + console.logger:debug('Dev Console enabled') contents, size = love.filesystem.read(console.history_path) if contents then - console.logger:trace("History file size", size) - for line in contents:gmatch("[^\r\n]+") do - if line and line ~= "" then + console.logger:trace('History file size', size) + for line in contents:gmatch('[^\r\n]+') do + if line and line ~= '' then table.insert(console.command_history, line) end end end - console.logger:debug("Registering commands") - console:registerCommand( - "help", - function() - console.logger:print("Available commands:") - for name, cmd in pairs(console.commands) do - if cmd.desc then - console.logger:print(name .. ": " .. cmd.desc) - end - end - return true - end, - "Prints a list of available commands", - function(current_arg) - local completions = {} - for name, _ in pairs(console.commands) do - if name:find(current_arg, 1, true) == 1 then - table.insert(completions, name) - end - end - return completions - end, - "Usage: help " - ) - - console:registerCommand( - "shortcuts", - function() - console.logger:print("Available shortcuts:") - console.logger:print("F2: Open/Close the console") - console.logger:print("F4: Toggle debug mode") - if platform.is_mac then - console.logger:print("Cmd+C: Copy the current command to the clipboard.") - console.logger:print("Cmd+Shift+C: Copies all messages to the clipboard") - console.logger:print("Cmd+V: Paste the clipboard into the current command") - else - console.logger:print("Ctrl+C: Copy the current command to the clipboard.") - console.logger:print("Ctrl+Shift+C: Copies all messages to the clipboard") - console.logger:print("Ctrl+V: Paste the clipboard into the current command") - end - return true - end, - "Prints a list of available shortcuts", - function(current_arg) - return nil - end, - "Usage: shortcuts" - ) - - console:registerCommand( - "history", - function() - console.logger:print("Command history:") - for i, cmd in ipairs(console.command_history) do - console.logger:print(i .. ": " .. cmd) + console.logger:debug('Registering commands') + console:registerCommand('help', function() + console.logger:print('Available commands:') + for name, cmd in pairs(console.commands) do + if cmd.desc then + console.logger:print(name .. ': ' .. cmd.desc) end - return true - end, - "Prints the command history" - ) - - console.logger:debug("Registering command: clear") - console:registerCommand( - "clear", - function() - logging.clearLogs() - return true - end, - "Clear the console" - ) - - console:registerCommand( - "exit", - function() - console:toggle() - return true - end, - "Close the console" - ) - - console:registerCommand( - "give", - function(args) - local id = args[1] - local c1 = nil - if string.sub(id, 1, 2) == "j_" then - c1 = create_card(nil, G.jokers, nil, 1, true, false, id, nil) - else - c1 = create_card(nil, G.consumeables, nil, 1, true, false, id, nil) + end + return true + end, 'Prints a list of available commands', function(current_arg) + local completions = {} + for name, _ in pairs(console.commands) do + if name:find(current_arg, 1, true) == 1 then + table.insert(completions, name) end - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.1, - func = function() - c1:add_to_deck() - if string.sub(id, 1, 2) == "j_" then - G.jokers:emplace(c1) - else - G.consumeables:emplace(c1) - end - - G.CONTROLLER:save_cardarea_focus('jokers') - G.CONTROLLER:recall_cardarea_focus('jokers') - return true - end - })) - return true - end, - "Give an item to the player", - function(current_arg) - local ret = {} - for k,_ in pairs(G.P_CENTERS) do - if string.find(k, current_arg) == 1 then - table.insert(ret, k) + end + return completions + end, 'Usage: help ') + + console:registerCommand('shortcuts', function() + console.logger:print('Available shortcuts:') + console.logger:print('F2: Open/Close the console') + console.logger:print('F4: Toggle debug mode') + if platform.is_mac then + console.logger:print('Cmd+C: Copy the current command to the clipboard.') + console.logger:print('Cmd+Shift+C: Copies all messages to the clipboard') + console.logger:print('Cmd+V: Paste the clipboard into the current command') + else + console.logger:print('Ctrl+C: Copy the current command to the clipboard.') + console.logger:print('Ctrl+Shift+C: Copies all messages to the clipboard') + console.logger:print('Ctrl+V: Paste the clipboard into the current command') + end + return true + end, 'Prints a list of available shortcuts', function(current_arg) + return nil + end, 'Usage: shortcuts') + + console:registerCommand('history', function() + console.logger:print('Command history:') + for i, cmd in ipairs(console.command_history) do + console.logger:print(i .. ': ' .. cmd) + end + return true + end, 'Prints the command history') + + console.logger:debug('Registering command: clear') + console:registerCommand('clear', function() + logging.clearLogs() + return true + end, 'Clear the console') + + console:registerCommand('exit', function() + console:toggle() + return true + end, 'Close the console') + + console:registerCommand('give', function(args) + local id = args[1] + local c1 = nil + if string.sub(id, 1, 2) == 'j_' then + c1 = create_card(nil, G.jokers, nil, 1, true, false, id, nil) + else + c1 = create_card(nil, G.consumeables, nil, 1, true, false, id, nil) + end + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.1, + func = function() + c1:add_to_deck() + if string.sub(id, 1, 2) == 'j_' then + G.jokers:emplace(c1) + else + G.consumeables:emplace(c1) end + + G.CONTROLLER:save_cardarea_focus('jokers') + G.CONTROLLER:recall_cardarea_focus('jokers') + return true + end + })) + return true + end, 'Give an item to the player', function(current_arg) + local ret = {} + for k, _ in pairs(G.P_CENTERS) do + if string.find(k, current_arg) == 1 then + table.insert(ret, k) end - return ret end - ) - - console:registerCommand( - "money", - function(args) - if args[1] and args[2] then - local amount = tonumber(args[2]) - if amount then - if args[1] == "add" then - ease_dollars(amount, true) - console.logger:info("Added " .. amount .. " money to the player") - elseif args[1] == "remove" then - ease_dollars(-amount, true) - console.logger:info("Removed " .. amount .. " money from the player") - elseif args[1] == "set" then - local currentMoney = G.GAME.dollars - local diff = amount - currentMoney - ease_dollars(diff, true) - console.logger:info("Set player money to " .. amount) - else - console.logger:error("Invalid operation, use add, remove or set") - end + return ret + end) + + console:registerCommand('money', function(args) + if args[1] and args[2] then + local amount = tonumber(args[2]) + if amount then + if args[1] == 'add' then + ease_dollars(amount, true) + console.logger:info('Added ' .. amount .. ' money to the player') + elseif args[1] == 'remove' then + ease_dollars(-amount, true) + console.logger:info('Removed ' .. amount .. ' money from the player') + elseif args[1] == 'set' then + local currentMoney = G.GAME.dollars + local diff = amount - currentMoney + ease_dollars(diff, true) + console.logger:info('Set player money to ' .. amount) else - console.logger:error("Invalid amount") - return false + console.logger:error('Invalid operation, use add, remove or set') end else - console.logger:warn("Usage: money ") + console.logger:error('Invalid amount') return false end - return true - end, - "Change the player's money", - function (current_arg) - local subcommands = {"add", "remove", "set"} - for i, v in ipairs(subcommands) do - if v:find(current_arg, 1, true) == 1 then - return {v} - end + else + console.logger:warn('Usage: money ') + return false + end + return true + end, 'Change the player\'s money', function(current_arg) + local subcommands = {'add', 'remove', 'set'} + for i, v in ipairs(subcommands) do + if v:find(current_arg, 1, true) == 1 then + return {v} end - return nil end - ) - - console:registerCommand( - "discards", - function(args) - if args[1] and args[2] then - local amount = tonumber(args[2]) - if amount then - if args[1] == "add" then - ease_discard(amount, true) - console.logger:info("Added " .. amount .. " discards to the player") - elseif args[1] == "remove" then - ease_discard(-amount, true) - console.logger:info("Removed " .. amount .. " discards from the player") - elseif args[1] == "set" then - local currentDiscards = G.GAME.current_round.discards_left - local diff = amount - currentDiscards - ease_discard(diff, true) - console.logger:info("Set player discards to " .. amount) - else - console.logger:error("Invalid operation, use add, remove or set") - return false - end + return nil + end) + + console:registerCommand('discards', function(args) + if args[1] and args[2] then + local amount = tonumber(args[2]) + if amount then + if args[1] == 'add' then + ease_discard(amount, true) + console.logger:info('Added ' .. amount .. ' discards to the player') + elseif args[1] == 'remove' then + ease_discard(-amount, true) + console.logger:info('Removed ' .. amount .. ' discards from the player') + elseif args[1] == 'set' then + local currentDiscards = G.GAME.current_round.discards_left + local diff = amount - currentDiscards + ease_discard(diff, true) + console.logger:info('Set player discards to ' .. amount) else - console.logger:error("Invalid amount") + console.logger:error('Invalid operation, use add, remove or set') return false end else - console.logger:warn("Usage: discards ") + console.logger:error('Invalid amount') return false end - return true - end, - "Change the player's discards", - function (current_arg) - local subcommands = {"add", "remove", "set"} - for i, v in ipairs(subcommands) do - if v:find(current_arg, 1, true) == 1 then - return {v} - end + else + console.logger:warn('Usage: discards ') + return false + end + return true + end, 'Change the player\'s discards', function(current_arg) + local subcommands = {'add', 'remove', 'set'} + for i, v in ipairs(subcommands) do + if v:find(current_arg, 1, true) == 1 then + return {v} end - return nil end - ) - - console:registerCommand( - "hands", - function(args) - if args[1] and args[2] then - local amount = tonumber(args[2]) - if amount then - if args[1] == "add" then - ease_hands_played(amount, true) - console.logger:info("Added " .. amount .. " hands to the player") - elseif args[1] == "remove" then - ease_hands_played(-amount, true) - console.logger:info("Removed " .. amount .. " hands from the player") - elseif args[1] == "set" then - local currentHands = G.GAME.current_round.hands_left - local diff = amount - currentHands - ease_hands_played(diff, true) - console.logger:info("Set player hands to " .. amount) - else - console.logger:error("Invalid operation, use add, remove or set") - return false - end + return nil + end) + + console:registerCommand('hands', function(args) + if args[1] and args[2] then + local amount = tonumber(args[2]) + if amount then + if args[1] == 'add' then + ease_hands_played(amount, true) + console.logger:info('Added ' .. amount .. ' hands to the player') + elseif args[1] == 'remove' then + ease_hands_played(-amount, true) + console.logger:info('Removed ' .. amount .. ' hands from the player') + elseif args[1] == 'set' then + local currentHands = G.GAME.current_round.hands_left + local diff = amount - currentHands + ease_hands_played(diff, true) + console.logger:info('Set player hands to ' .. amount) else - console.logger:error("Invalid amount") + console.logger:error('Invalid operation, use add, remove or set') return false end else - console.logger:warn("Usage: hands ") + console.logger:error('Invalid amount') return false end - return true - end, - "Change the player's remaining hands", - function (current_arg) - local subcommands = {"add", "remove", "set"} - for i, v in ipairs(subcommands) do - if v:find(current_arg, 1, true) == 1 then - return {v} - end + else + console.logger:warn('Usage: hands ') + return false + end + return true + end, 'Change the player\'s remaining hands', function(current_arg) + local subcommands = {'add', 'remove', 'set'} + for i, v in ipairs(subcommands) do + if v:find(current_arg, 1, true) == 1 then + return {v} end - return nil end - ) - - console:registerCommand( - "luamod", - function(args) - if args[1] then - local modId = args[1] - if isModPresent(modId) then - local mod = mods[modId] - if mod.enabled and mod.on_disable and type(mod.on_disable) == "function" then - local success, result = pcall(mod.on_disable) - if not success then - console.logger:error("Error disabling mod: " .. modId) - console.logger:error(result) - return false - end + return nil + end) + + console:registerCommand('luamod', function(args) + if args[1] then + local modId = args[1] + if isModPresent(modId) then + local mod = mods[modId] + if mod.enabled and mod.on_disable and type(mod.on_disable) == 'function' then + local success, result = pcall(mod.on_disable) + if not success then + console.logger:error('Error disabling mod: ' .. modId) + console.logger:error(result) + return false end - mod = loadMod(modId) - mods[modId] = mod - mods = sortMods(mods) - -- no need to redo the whole shebang, just call on_enable - -- this is because the dependencies are most likely already loaded - if mod.enabled then - if mod.on_enable and type(mod.on_enable) == 'function' then - local status, message = pcall(mod.on_enable) - if not status then - console.logger:error("Error enabling mod: " .. modId) - console.logger:error(message) - return false - end + end + mod = loadMod(modId) + mods[modId] = mod + mods = sortMods(mods) + -- no need to redo the whole shebang, just call on_enable + -- this is because the dependencies are most likely already loaded + if mod.enabled then + if mod.on_enable and type(mod.on_enable) == 'function' then + local status, message = pcall(mod.on_enable) + if not status then + console.logger:error('Error enabling mod: ' .. modId) + console.logger:error(message) + return false end end - console.logger:info("Reloaded mod: " .. modId) - else - console.logger:error("Mod not found: " .. modId) - return false end + console.logger:info('Reloaded mod: ' .. modId) else - console.logger:error("Usage: luamod ") + console.logger:error('Mod not found: ' .. modId) return false end + else + console.logger:error('Usage: luamod ') + return false + end + return true + end, 'Reload a mod using its id', function(current_arg) + local completions = {} + for modId, _ in pairs(mods) do + if modId:find(current_arg, 1, true) == 1 then + table.insert(completions, modId) + end + end + return completions + end, 'Usage: luamod ') + + console:registerCommand('sandbox', function(args) + G:sandbox() + return true + end, 'Goes to the sandbox stage', function(current_arg) + return nil + end, 'Usage: sandbox') + + console:registerCommand('luarun', function(args) + local code = table.concat(args, ' ') + local func, err = load(code) + if func then + console.logger:info('Lua code executed successfully') + console.logger:print(func()) return true - end, - "Reload a mod using its id", - function (current_arg) - local completions = {} - for modId, _ in pairs(mods) do - if modId:find(current_arg, 1, true) == 1 then - table.insert(completions, modId) + else + console.logger:error('Error loading lua code: ', err) + return false + end + end, 'Run lua code in the context of the game', function(current_arg) + return nil + end, 'Usage: luarun ') + + console:registerCommand('installmod', function(args) + local url = args[1] + local modInfo = {id = 'testmod', url = url, present = false, needUpdate = true} + local result = installModFromTar(modInfo) + if result == RESULT.SUCCESS then + console.logger:info('Mod installed successfully') + return true + else + console.logger:error('Error installing mod: ', result) + return false + end + end, 'Install a mod from a tarball', function(current_arg) + return nil + end, 'Usage: installmod ') + + local booster_list_shown = false + console:registerCommand('booster', function(args) + booster_list_shown = false + + local function pull_pack(_key, _type) + local cume, it, center = 0, 0, nil + for k, v in ipairs(G.P_CENTER_POOLS['Booster']) do + if (not _type or _type == v.kind) then + cume = cume + (v.weight or 1) + end + end + local poll = pseudorandom(pseudoseed((_key or 'pack_generic') .. G.GAME.round_resets.ante)) * cume + for k, v in ipairs(G.P_CENTER_POOLS['Booster']) do + if not _type or _type == v.kind then + it = it + (v.weight or 1) + end + if it >= poll and it - (v.weight or 1) <= poll then + center = v; + break end end - return completions - end, - "Usage: luamod " - ) - - console:registerCommand( - "sandbox", - function (args) - G:sandbox() + G.FUNCS.use_card({config = {ref_table = Card(0, 0, 0, 0, center, center)}}) + end + + local arg = args[1] or nil + + if arg == nil or arg == '' then + logger:info('Opening random booster pack') + pull_pack('shop_pack', nil) return true - end, - "Goes to the sandbox stage", - function (current_arg) - return nil - end, - "Usage: sandbox" - ) - - console:registerCommand( - "luarun", - function (args) - local code = table.concat(args, " ") - local func, err = load(code) - if func then - console.logger:info("Lua code executed successfully") - console.logger:print(func()) - return true - else - console.logger:error("Error loading lua code: ", err) - return false + else + local kinds = {} + for k, v in pairs(G.P_CENTER_POOLS['Booster']) do + if string.lower(v.key) == string.lower(arg) then + logger:info('Opening the ' .. v.key .. ' booster pack.') + G.FUNCS.use_card({ + config = {ref_table = Card(0, 0, 0, 0, G.P_CENTERS[v.key], G.P_CENTERS[v.key])} + }) + return true + end + + if not table_contains(kinds, v.kind) then + table.insert(kinds, v.kind) + end end - end, - "Run lua code in the context of the game", - function (current_arg) - return nil - end, - "Usage: luarun " - ) - - console:registerCommand( - "installmod", - function (args) - local url = args[1] - local modInfo = { - id = "testmod", - url = url, - present = false, - needUpdate = true, - } - local result = installModFromTar(modInfo) - if result == RESULT.SUCCESS then - console.logger:info("Mod installed successfully") - return true - else - console.logger:error("Error installing mod: ", result) - return false + + for k, v in pairs(kinds) do + if (string.lower(v) == string.lower(arg)) then + logger:info('Opening a ' .. v .. ' booster pack.') + pull_pack('shop_pack', v) + return true + end end - end, - "Install a mod from a tarball", - function (current_arg) - return nil - end, - "Usage: installmod " - ) + end + + logger:info('No booster pack matched ' .. arg) + end, 'Generate a booster pack from a key or kind (random if no argument, otherwise specify type)', + function(current_arg) + local subcommands = {} + + local kinds = {} + for k, v in pairs(G.P_CENTER_POOLS['Booster']) do + table.insert(subcommands, v.key) + + if not table_contains(kinds, v.kind) then + table.insert(kinds, v.kind) + end + end + + for i = #kinds, 1, -1 do + table.insert(subcommands, 1, kinds[i]) + end + + if (booster_list_shown == false) then + booster_list_shown = true + logger:info('Booster packs (' .. #G.P_CENTER_POOLS['Booster'] .. ' available)') + for k, v in pairs(G.P_CENTER_POOLS['Booster']) do + logger:info(v.key .. ' (' .. v.kind .. ')') + end + end + + for i, v in ipairs(subcommands) do + if v:find(current_arg, 1, true) == 1 then + return {v} + end + end + return nil + end, 'Usage: booster [Standard/Spectral/Arcana/Celestial/Buffoon/p_arcana_normal_1/p_celestial_jumbo_2/...]') + + console:registerCommand('enhance', function(args) + local arg = args[1] or nil + if arg == nil then + logger:info('No enhancement specified') + return + end + + local tables = {} + + if G.pack_cards then + table.insert(tables, G.pack_cards.highlighted) + end + if G.shop_vouchers then + table.insert(tables, G.shop_vouchers.highlighted) + end + if G.shop_jokers then + table.insert(tables, G.shop_jokers.highlighted) + end + if G.shop_booster then + table.insert(tables, G.shop_booster.highlighted) + end + if G.consumeables then + table.insert(tables, G.consumeables.highlighted) + end + if G.jokers then + table.insert(tables, G.jokers.highlighted) + end + if G.discard then + table.insert(tables, G.discard.highlighted) + end + if G.deck then + table.insert(tables, G.deck.highlighted) + end + if G.hand then + table.insert(tables, G.hand.highlighted) + end + if G.play then + table.insert(tables, G.play.highlighted) + end + + for i, t in ipairs(tables) do + for j, card in ipairs(t) do + if card.config then + logger:info('Enhancing ' .. card.config.center.key) + + for k, enhancement in pairs(args) do + + for k, v in pairs(G.P_CENTER_POOLS['Default']) do + local name = string.gsub(v.label, ' ', '') + if enhancement == v.key or string.lower(enhancement) == string.lower(name) then + if (card.config.center.set == 'Default' or card.config.center.set == 'Enhanced') then + card:set_ability(G.P_CENTERS[v.key], nil, false) + logger:info('Card set to ' .. v.label .. ' (' .. card.config.center.key .. ').') + else + logger:info('Only standard cards should be set to ' .. v.label .. ' (' .. + card.config.center.key .. ').') + end + end + end + + for k, v in pairs(G.P_CENTER_POOLS['Enhanced']) do + local name = string.gsub(v.label, ' ', '') + if enhancement == v.key or string.lower(enhancement) == string.lower(name) then + if (card.config.center.set == 'Default' or card.config.center.set == 'Enhanced') then + card:set_ability(G.P_CENTERS[v.key], nil, false) + logger:info('Card set to ' .. v.label .. ' (' .. card.config.center.key .. ').') + else + logger:info('Only standard cards should be set to ' .. v.label .. ' (' .. + card.config.center.key .. ').') + end + end + end + + for k, v in pairs(G.P_CENTER_POOLS['Edition']) do + local name = string.gsub(v.name, ' ', '') + if enhancement == v.key or string.lower(enhancement) == string.lower(name) then + local editionKey = string.sub(v.key, 3) + card:set_edition({[editionKey] = true}, true) + logger:info('Card set to ' .. v.name .. ' edition (' .. card.config.center.key .. + ').') + end + end - console.logger:debug("Dev Console on_enable completed") + for k, v in pairs(G.P_CENTER_POOLS['Seal']) do + if string.lower(enhancement) == string.lower(v.key .. 'Seal') then + card:set_seal(v.key, true) + logger:info('Added ' .. v.key .. ' Seal to card (' .. card.config.center.key .. ').') + end + end + + end + + end + end + end + + return true + end, + 'Add one or more enhancements to selected cards. Can use key or name (e.g. c_base or BaseCard, e_negative or Negative). Must add Seal to seals (e.g. RedSeal, BlueSeal)', + function(current_arg, previous_arg) + local subcommands = {} + + for k, v in pairs(G.P_CENTER_POOLS['Default']) do + local name = string.gsub(v.label, ' ', '') + table.insert(subcommands, v.key) + table.insert(subcommands, name) + end + + for k, v in pairs(G.P_CENTER_POOLS['Enhanced']) do + local name = string.gsub(v.label, ' ', '') + table.insert(subcommands, v.key) + table.insert(subcommands, name) + end + + for k, v in pairs(G.P_CENTER_POOLS['Edition']) do + local name = string.gsub(v.name, ' ', '') + table.insert(subcommands, v.key) + table.insert(subcommands, name) + end + + for k, v in pairs(G.P_CENTER_POOLS['Seal']) do + table.insert(subcommands, v.key .. 'Seal') + end + + local completions = {} + for k, v in pairs(subcommands) do + if v:find(current_arg, 1, true) == 1 then + table.insert(completions, v) + end + end + return completions + end, + 'Usage: enhance [arg2] [arg3] ...') + + console.logger:debug('Dev Console on_enable completed') end, on_disable = function() - console.removeCommand("help") - console.removeCommand("shortcuts") - console.removeCommand("history") - console.removeCommand("clear") - console.removeCommand("exit") - console.removeCommand("quit") - console.removeCommand("give") - console.removeCommand("money") - console.removeCommand("discards") - console.removeCommand("hands") - console.logger:debug("Dev Console disabled") + console.removeCommand('help') + console.removeCommand('shortcuts') + console.removeCommand('history') + console.removeCommand('clear') + console.removeCommand('exit') + console.removeCommand('quit') + console.removeCommand('give') + console.removeCommand('money') + console.removeCommand('discards') + console.removeCommand('hands') + console.removeCommand('booster') + console.removeCommand('enhance') + console.logger:debug('Dev Console disabled') end, - on_key_pressed = function (key_name) - if key_name == "f2" then + on_key_pressed = function(key_name) + if key_name == 'f2' then console:toggle() return true end @@ -1048,22 +1206,22 @@ mods["dev_console"] = { return true end - if key_name == "f4" then + if key_name == 'f4' then G.DEBUG = not G.DEBUG if G.DEBUG then - console.logger:info("Debug mode enabled") + console.logger:info('Debug mode enabled') else - console.logger:info("Debug mode disabled") + console.logger:info('Debug mode disabled') end end return false end, - on_post_render = function () - console.max_lines = math.floor(love.graphics.getHeight() / console.line_height) - 5 -- 5 lines of bottom padding + on_post_render = function() + console.max_lines = math.floor(love.graphics.getHeight() / console.line_height) - 5 -- 5 lines of bottom padding local font = love.graphics.getFont() if console.is_open then love.graphics.setColor(0, 0, 0, 0.3) - love.graphics.rectangle("fill", 0, 0, love.graphics.getWidth(), love.graphics.getHeight()) + love.graphics.rectangle('fill', 0, 0, love.graphics.getWidth(), love.graphics.getHeight()) local messagesToDisplay = console:getMessagesToDisplay() local i = 1 for _, message in ipairs(messagesToDisplay) do @@ -1085,38 +1243,38 @@ mods["dev_console"] = { love.graphics.print(console.cmd, 10, love.graphics.getHeight() - 30) end end, - on_key_released = function (key_name) - if key_name == "capslock" then + on_key_released = function(key_name) + if key_name == 'capslock' then console.modifiers.capslock = not console.modifiers.capslock console:modifiersListener() return end - if key_name == "scrolllock" then + if key_name == 'scrolllock' then console.modifiers.scrolllock = not console.modifiers.scrolllock console:modifiersListener() return end - if key_name == "numlock" then + if key_name == 'numlock' then console.modifiers.numlock = not console.modifiers.numlock console:modifiersListener() return end - if key_name == "lalt" or key_name == "ralt" then + if key_name == 'lalt' or key_name == 'ralt' then console.modifiers.alt = false console:modifiersListener() return false end - if key_name == "lctrl" or key_name == "rctrl" then + if key_name == 'lctrl' or key_name == 'rctrl' then console.modifiers.ctrl = false console:modifiersListener() return false end - if key_name == "lshift" or key_name == "rshift" then + if key_name == 'lshift' or key_name == 'rshift' then console.modifiers.shift = false console:modifiersListener() return false end - if key_name == "lgui" or key_name == "rgui" then + if key_name == 'lgui' or key_name == 'rgui' then console.modifiers.meta = false console:modifiersListener() return false @@ -1125,17 +1283,16 @@ mods["dev_console"] = { end, on_mouse_pressed = function(x, y, button, touches) if console.is_open then - return true -- Do not press buttons through the console, this cancels the event + return true -- Do not press buttons through the console, this cancels the event end end, on_mouse_released = function(x, y, button) if console.is_open then return true -- Do not release buttons through the console, this cancels the event end - end, + end } - -- Topological sort of mods based on load_before and load_after fields -- 1. Create a directed graph with the mods as nodes and the load_before and load_after fields as edges -- 2. Run a topological sort on the graph @@ -1144,9 +1301,7 @@ local function sortMods(mods) logger:trace('Sorting mods', utils.keys(mods)) local graph = {} for modId, mod in pairs(mods) do - graph[modId] = { - before = {}, - } + graph[modId] = {before = {}} end logger:trace('Graph generated', graph) for modId, mod in pairs(mods) do @@ -1155,7 +1310,7 @@ local function sortMods(mods) logger:error('Mod ', mod.id, ' has a load_before field that references a non-existent mod: ', before) return nil end - graph[modId].before[before] = true -- we set to true just because we want a table behaving like a set() instead of an array + graph[modId].before[before] = true -- we set to true just because we want a table behaving like a set() instead of an array end for i, after in ipairs(mod.load_after or {}) do -- load_after is a list of mod ids -- load_after is there to ensure that a mod is loaded after another mod @@ -1165,35 +1320,35 @@ local function sortMods(mods) logger:error('Mod ', mod.id, ' has a load_after field that references a non-existent mod: ', after) return nil end - graph[after].before[modId] = true -- we set to true just because we want a table behaving like a set() instead of an array + graph[after].before[modId] = true -- we set to true just because we want a table behaving like a set() instead of an array end end logger:trace('Graph nodes and edges', graph) local sorted = {} local visited = {} local function visit(node) - logger:trace("Visiting node ", node) - if visited[node] == "permanent" then - logger:trace("Node ", node, " already visited") + logger:trace('Visiting node ', node) + if visited[node] == 'permanent' then + logger:trace('Node ', node, ' already visited') return true end - if visited[node] == "temporary" then + if visited[node] == 'temporary' then logger:error('Mod ', node, ' has a circular dependency') return false end - visited[node] = "temporary" + visited[node] = 'temporary' for other, _ in pairs(graph[node].before) do if not visit(other) then return false end end table.insert(sorted, node) - logger:trace("Inserted node ", node, " in sorted list", sorted) - logger:trace("Marking node ", node, " as visited") - visited[node] = "permanent" + logger:trace('Inserted node ', node, ' in sorted list', sorted) + logger:trace('Marking node ', node, ' as visited') + visited[node] = 'permanent' return true end - logger:trace("Starting to visit nodes") + logger:trace('Starting to visit nodes') for node, _ in pairs(graph) do if not visited[node] then visit(node) @@ -1210,7 +1365,7 @@ local function sortMods(mods) mod.order = modCount - i sortedMods[modId] = mod end - logger:trace("Built sorted mods", utils.keys(sortedMods)) + logger:trace('Built sorted mods', utils.keys(sortedMods)) return sortedMods end @@ -1231,5 +1386,5 @@ return { loadMod = loadMod, toggleMod = toggleMod, sortMods = sortMods, - callModCallbacksIfExists = callModCallbacksIfExists, + callModCallbacksIfExists = callModCallbacksIfExists } diff --git a/src/console.lua b/src/console.lua index f3c12de..cc81851 100644 --- a/src/console.lua +++ b/src/console.lua @@ -1,21 +1,21 @@ -local logging = require("logging") -local platform = require("platform") -local utf8 = require("utf8") -local math = require("math") +local logging = require('logging') +local platform = require('platform') +local utf8 = require('utf8') +local math = require('math') -local logger = logging.getLogger("console") +local logger = logging.getLogger('console') return { logger = logger, - log_level = "INFO", + log_level = 'INFO', is_open = false, - cmd = "> ", + cmd = '> ', line_height = 20, max_lines = love.graphics.getHeight() / 20, start_line_offset = 1, history_index = 0, command_history = {}, - history_path = "dev_console.history", + history_path = 'dev_console.history', modifiers = { capslock = false, scrolllock = false, @@ -23,12 +23,12 @@ return { shift = false, ctrl = false, alt = false, - meta = false, + meta = false }, commands = {}, toggle = function(self) self.is_open = not self.is_open - love.keyboard.setKeyRepeat(self.is_open) -- set key repeat to true when console is open + love.keyboard.setKeyRepeat(self.is_open) -- set key repeat to true when console is open if self.is_open then self.start_line_offset = self.max_lines - 1 local oldTextInput = love.textinput @@ -41,7 +41,7 @@ return { end, longestCommonPrefix = function(self, strings) if #strings == 0 then - return "" + return '' end local prefix = strings[1] for i = 2, #strings do @@ -58,12 +58,12 @@ return { local command = self.cmd:sub(3) -- remove the "> " prefix local cmd = {} -- split command into parts - for part in command:gmatch("%S+") do + for part in command:gmatch('%S+') do table.insert(cmd, part) end if #cmd == 0 then -- no command typed, do nothing (no completions possible) - logger:trace("No command typed") + logger:trace('No command typed') return nil end local completions = {} @@ -85,7 +85,7 @@ return { completions = command.autocomplete(current_arg, previousArgs) or {} end end - logger:trace("Autocomplete matches: " .. #completions .. " " .. table.concat(completions, ", ")) + logger:trace('Autocomplete matches: ' .. #completions .. ' ' .. table.concat(completions, ', ')) if #completions == 0 then -- no completions found return nil @@ -96,23 +96,23 @@ return { return self:longestCommonPrefix(completions) end end, - getMessageColor = function (self, message) - if message.level == "PRINT" then + getMessageColor = function(self, message) + if message.level == 'PRINT' then return 1, 1, 1 end - if message.level == "INFO" then + if message.level == 'INFO' then return 0, 0.9, 1 end - if message.level == "WARN" then + if message.level == 'WARN' then return 1, 0.5, 0 end - if message.level == "ERROR" then + if message.level == 'ERROR' then return 1, 0, 0 end - if message.level == "DEBUG" then + if message.level == 'DEBUG' then return 0.16, 0, 1 end - if message.level == "TRACE" then + if message.level == 'TRACE' then return 1, 1, 1 end return 1, 1, 1 @@ -130,7 +130,26 @@ return { local text = {} local i = 1 local textLength = 0 - local all_messages = self:getFilteredMessages() + + local base_messages = self:getFilteredMessages() + local all_messages = {} + + for _, message in ipairs(base_messages) do + local wrappedLines = self:wrapText(message.text, love.graphics.getWidth() - 20) + for _, line in ipairs(wrappedLines) do + table.insert(all_messages, { + text = line, + level = message.level, + name = message.name, + time = message.time, + level_numeric = message.level_numeric, + formatted = function() + return line + end + }) + end + end + while textLength < self.max_lines do local index = #all_messages - i + self.start_line_offset if index < 1 then @@ -145,16 +164,25 @@ return { end -- define locally to not pollute the global namespace scope local function reverse(tab) - for i = 1, math.floor(#tab/2), 1 do - tab[i], tab[#tab-i+1] = tab[#tab-i+1], tab[i] + for i = 1, math.floor(#tab / 2), 1 do + tab[i], tab[#tab - i + 1] = tab[#tab - i + 1], tab[i] end return tab end text = reverse(text) -- pad text table so that we always have max_lines lines in there local nLinesToPad = #text - self.max_lines - for i=1,nLinesToPad do - table.insert(text, {text = "", level = "PRINT", name = "", time = 0, level_numeric = 1000, formatted = function() return "" end}) + for i = 1, nLinesToPad do + table.insert(text, { + text = '', + level = 'PRINT', + name = '', + time = 0, + level_numeric = 1000, + formatted = function() + return '' + end + }) end return text end, @@ -162,7 +190,7 @@ return { -- disable text input if ctrl or cmd is pressed -- this is to fallback to love.keypressed when a modifier is pressed that can -- link to a command (like ctrl+c, ctrl+v, etc) - self.logger:trace("modifiers", self.modifiers) + self.logger:trace('modifiers', self.modifiers) if self.modifiers.ctrl or self.modifiers.meta then love.textinput = nil else @@ -171,20 +199,22 @@ return { end end end, - typeKey = function (self, key_name) + typeKey = function(self, key_name) -- cmd+shift+C on mac, ctrl+shift+C on windows/linux - if key_name == "c" and ((platform.is_mac and self.modifiers.meta and self.modifiers.shift) or (not platform.is_mac and self.modifiers.ctrl and self.modifiers.shift)) then + if key_name == 'c' and ((platform.is_mac and self.modifiers.meta and self.modifiers.shift) or + (not platform.is_mac and self.modifiers.ctrl and self.modifiers.shift)) then local messages = self:getFilteredMessages() - local text = "" + local text = '' for _, message in ipairs(messages) do - text = text .. message:formatted() .. "\n" + text = text .. message:formatted() .. '\n' end love.system.setClipboardText(text) return end -- cmd+C on mac, ctrl+C on windows/linux - if key_name == "c" and ((platform.is_mac and self.modifiers.meta) or (not platform.is_mac and self.modifiers.ctrl)) then - if self.cmd:sub(3) == "" then + if key_name == 'c' and + ((platform.is_mac and self.modifiers.meta) or (not platform.is_mac and self.modifiers.ctrl)) then + if self.cmd:sub(3) == '' then -- do nothing if the buffer is empty return end @@ -192,67 +222,68 @@ return { return end -- cmd+V on mac, ctrl+V on windows/linux - if key_name == "v" and ((platform.is_mac and self.modifiers.meta) or (not platform.is_mac and self.modifiers.ctrl)) then + if key_name == 'v' and + ((platform.is_mac and self.modifiers.meta) or (not platform.is_mac and self.modifiers.ctrl)) then self.cmd = self.cmd .. love.system.getClipboardText() return end - if key_name == "escape" then + if key_name == 'escape' then -- close the console self:toggle() return end -- Delete the current command, on mac it's cmd+backspace - if key_name == "delete" or (platform.is_mac and self.modifiers.meta and key_name == "backspace") then - self.cmd = "> " + if key_name == 'delete' or (platform.is_mac and self.modifiers.meta and key_name == 'backspace') then + self.cmd = '> ' return end - if key_name == "end" or (platform.is_mac and key_name == "right" and self.modifiers.meta) then + if key_name == 'end' or (platform.is_mac and key_name == 'right' and self.modifiers.meta) then -- move text to the most recent (bottom) self.start_line_offset = self.max_lines return end - if key_name == "home" or (platform.is_mac and key_name == "left" and self.modifiers.meta) then + if key_name == 'home' or (platform.is_mac and key_name == 'left' and self.modifiers.meta) then -- move text to the oldest (top) local messages = self:getFilteredMessages() self.start_line_offset = self.max_lines - #messages return end - if key_name == "pagedown" or (platform.is_mac and key_name == "down" and self.modifiers.meta) then + if key_name == 'pagedown' or (platform.is_mac and key_name == 'down' and self.modifiers.meta) then -- move text down by max_lines self.start_line_offset = math.min(self.start_line_offset + self.max_lines, self.max_lines) return end - if key_name == "pageup" or (platform.is_mac and key_name == "up" and self.modifiers.meta) then + if key_name == 'pageup' or (platform.is_mac and key_name == 'up' and self.modifiers.meta) then -- move text up by max_lines local messages = self:getFilteredMessages() self.start_line_offset = math.max(self.start_line_offset - self.max_lines, self.max_lines - #messages) return end - if key_name == "up" then + if key_name == 'up' then -- move to the next command in the history (in reverse order of insertion) self.history_index = math.min(self.history_index + 1, #self.command_history) if self.history_index == 0 then - self.cmd = "> " + self.cmd = '> ' return end - self.cmd = "> " .. self.command_history[#self.command_history - self.history_index + 1] + self.cmd = '> ' .. self.command_history[#self.command_history - self.history_index + 1] return end - if key_name == "down" then + if key_name == 'down' then -- move to the previous command in the history (in reverse order of insertion) self.history_index = math.max(self.history_index - 1, 0) if self.history_index == 0 then - self.cmd = "> " + self.cmd = '> ' return end - self.cmd = "> " .. self.command_history[#self.command_history - self.history_index + 1] + self.cmd = '> ' .. self.command_history[#self.command_history - self.history_index + 1] return end - if key_name == "tab" then + if key_name == 'tab' then local completion = self:tryAutocomplete() if completion then -- get the last part of the console command - local lastPart = self.cmd:match("%S+$") + local lastPart = self.cmd:match('%S+$') if lastPart == nil then -- cmd ends with a space, so we stop the completion return end @@ -261,28 +292,28 @@ return { end return end - if key_name == "lalt" or key_name == "ralt" then + if key_name == 'lalt' or key_name == 'ralt' then self.modifiers.alt = true self:modifiersListener() return end - if key_name == "lctrl" or key_name == "rctrl" then + if key_name == 'lctrl' or key_name == 'rctrl' then self.modifiers.ctrl = true self:modifiersListener() return end - if key_name == "lshift" or key_name == "rshift" then + if key_name == 'lshift' or key_name == 'rshift' then self.modifiers.shift = true self:modifiersListener() return end - if key_name == "lgui" or key_name == "rgui" then + if key_name == 'lgui' or key_name == 'rgui' then -- windows key / meta / cmd key (on macos) self.modifiers.meta = true self:modifiersListener() return end - if key_name == "backspace" then + if key_name == 'backspace' then if #self.cmd > 2 then local byteoffset = utf8.offset(self.cmd, -1) if byteoffset then @@ -293,17 +324,17 @@ return { end return end - if key_name == "return" or key_name == "kpenter" then + if key_name == 'return' or key_name == 'kpenter' then self.logger:print(self.cmd) local cmdName = self.cmd:sub(3) - cmdName = cmdName:match("%S+") + cmdName = cmdName:match('%S+') if cmdName == nil then return end local args = {} local argString = self.cmd:sub(3 + #cmdName + 1) if argString then - for arg in argString:gmatch("%S+") do + for arg in argString:gmatch('%S+') do table.insert(args, arg) end end @@ -311,29 +342,29 @@ return { if self.commands[cmdName] then success = self.commands[cmdName].call(args) else - self.logger:error("Command not found: " .. cmdName) + self.logger:error('Command not found: ' .. cmdName) end if success then -- only add the command to the history if it was successful self:addToHistory(self.cmd:sub(3)) end - self.cmd = "> " + self.cmd = '> ' return end end, addToHistory = function(self, command) - if command == nil or command == "" then + if command == nil or command == '' then return end table.insert(self.command_history, command) self.history_index = 0 - local success, errormsg = love.filesystem.append(self.history_path, command .. "\n") + local success, errormsg = love.filesystem.append(self.history_path, command .. '\n') if not success then - self.logger:warn("Error appending ", command, " to history file: ", errormsg) - success, errormsg = love.filesystem.write(self.history_path, command .. "\n") + self.logger:warn('Error appending ', command, ' to history file: ', errormsg) + success, errormsg = love.filesystem.write(self.history_path, command .. '\n') if not success then - self.logger:error("Error writing to history file: ", errormsg) + self.logger:error('Error writing to history file: ', errormsg) end end end, @@ -345,58 +376,58 @@ return { -- @param usage: string, a string describing the usage of the command (longer, more detailed description of the command's usage) registerCommand = function(self, name, callback, short_description, autocomplete, usage) if name == nil then - self.logger:error("registerCommand -- name is required") + self.logger:error('registerCommand -- name is required') end if callback == nil then - self.logger:error("registerCommand -- callback is required on command", name) + self.logger:error('registerCommand -- callback is required on command', name) end - if type(callback) ~= "function" then - self.logger:error("registerCommand -- callback must be a function on command", name) + if type(callback) ~= 'function' then + self.logger:error('registerCommand -- callback must be a function on command', name) end - if name == nil or callback == nil or type(callback) ~= "function" then + if name == nil or callback == nil or type(callback) ~= 'function' then return end if short_description == nil then - self.logger:warn("registerCommand -- no description provided, please provide a description for the `help` command") - short_description = "No help provided" + self.logger:warn( + 'registerCommand -- no description provided, please provide a description for the `help` command') + short_description = 'No help provided' end if usage == nil then usage = short_description end if autocomplete == nil then - autocomplete = function(current_arg, previous_args) return nil end + autocomplete = function(current_arg, previous_args) + return nil + end end - if type(autocomplete) ~= "function" then - self.logger:warn("registerCommand -- autocomplete must be a function for command: ", name) - autocomplete = function(current_arg, previous_args) return nil end + if type(autocomplete) ~= 'function' then + self.logger:warn('registerCommand -- autocomplete must be a function for command: ', name) + autocomplete = function(current_arg, previous_args) + return nil + end end if self.commands[name] then - self.logger:warn("Command " .. name .. " already exists") + self.logger:warn('Command ' .. name .. ' already exists') return end - self.logger:info("Registering command: ", name) - self.commands[name] = { - call = callback, - desc = short_description, - autocomplete = autocomplete, - usage = usage, - } + self.logger:info('Registering command: ', name) + self.commands[name] = {call = callback, desc = short_description, autocomplete = autocomplete, usage = usage} end, removeCommand = function(self, cmd_name) if cmd_name == nil then - self.logger:error("removeCommand -- cmd_name is required") + self.logger:error('removeCommand -- cmd_name is required') end if self.commands[cmd_name] == nil then - self.logger:error("removeCommand -- command not found: ", cmd_name) + self.logger:error('removeCommand -- command not found: ', cmd_name) return end self.commands[cmd_name] = nil end, wrapText = function(self, message, screenWidth) local lines = {} - local line = "" - for word in message:gmatch("%S+") do - local testLine = line .. " " .. word + local line = '' + for word in message:gmatch('%S+') do + local testLine = line .. ' ' .. word if love.graphics.getFont():getWidth(testLine) > screenWidth then table.insert(lines, line) line = word @@ -406,5 +437,5 @@ return { end table.insert(lines, line) return lines - end, -} \ No newline at end of file + end +} diff --git a/src/logging.lua b/src/logging.lua index 85b3d4e..26a6d2f 100644 --- a/src/logging.lua +++ b/src/logging.lua @@ -5,73 +5,64 @@ local utils = require('utils') LOGGERS = {} START_TIME = socket.gettime() -- in seconds with ms precision -local _MODULE = { - _VERSION = "0.1.0", - LOGGERS = LOGGERS, -} +local _MODULE = {_VERSION = '0.1.0', LOGGERS = LOGGERS} local function createLogger(name, lvl) - local log_levels = { - TRACE = 10, - DEBUG = 20, - INFO = 30, - WARN = 40, - ERROR = 50, - PRINT = 1000, - } + local log_levels = {TRACE = 10, DEBUG = 20, INFO = 30, WARN = 40, ERROR = 50, PRINT = 1000} return { - name=name, - log_levels=log_levels, - level=lvl or "INFO", - numeric_level=log_levels[lvl] or 30, - messages={}, - log=function(self, level, ...) + name = name, + log_levels = log_levels, + level = lvl or 'INFO', + numeric_level = log_levels[lvl] or 30, + messages = {}, + log = function(self, level, ...) local args = {...} - local text = "" - if not love.filesystem.getInfo("logs/" .. generateDateTime(START_TIME) .. ".log") then - love.filesystem.write("logs/" .. generateDateTime(START_TIME) .. ".log", "") + local text = '' + if not love.filesystem.getInfo('logs/' .. generateDateTime(START_TIME) .. '.log') then + love.filesystem.write('logs/' .. generateDateTime(START_TIME) .. '.log', '') end for i, v in ipairs(args) do - text = text .. utils.stringify(v) .. " " + text = text .. utils.stringify(v) .. ' ' end local message = { - level=level, - level_numeric=self.log_levels[level] or 0, - text=text, - time=socket.gettime(), - name=self.name, - formatted=function(self, dump) - if self.level == "PRINT" and not dump then + level = level, + level_numeric = self.log_levels[level] or 0, + text = text, + time = socket.gettime(), + name = self.name, + formatted = function(self, dump) + if self.level == 'PRINT' and not dump then return self.text end if dump then - return string.format("%s [%s] - %s :: %s", generateDateTime(self.time), self.name, self.level, self.text) + return string.format('%s [%s] - %s :: %s', generateDateTime(self.time), self.name, self.level, + self.text) end - return string.format("[%s] - %s :: %s", self.name, self.level, self.text) - end, + return string.format('[%s] - %s :: %s', self.name, self.level, self.text) + end } table.insert(self.messages, message) - love.filesystem.append("logs/" .. generateDateTime(START_TIME) .. ".log", message:formatted(true) .. "\n") - love.filesystem.append("console.txt", message:formatted(true) .. "\n") - end, - info=function(self, ...) - self:log("INFO", ...) + love.filesystem.append('logs/' .. generateDateTime(START_TIME) .. '.log', message:formatted(true) .. '\n') + love.filesystem.append('console.txt', message:formatted(true) .. '\n') end, - warn=function(self, ...) - self:log("WARN", ...) + info = function(self, ...) + self:log('INFO', ...) end, - error=function(self, ...) - self:log("ERROR", ...) + warn = function(self, ...) + self:log('WARN', ...) end, - debug=function(self, ...) - self:log("DEBUG", ...) + error = function(self, ...) + self:log('ERROR', ...) end, - trace=function(self, ...) - self:log("TRACE", ...) + debug = function(self, ...) + self:log('DEBUG', ...) end, - print=function(self, message) - self:log("PRINT", message) + trace = function(self, ...) + self:log('TRACE', ...) end, + print = function(self, message) + self:log('PRINT', message) + end } end @@ -92,7 +83,9 @@ local function getAllMessages() table.insert(messages, message) end end - table.sort(messages, function(a, b) return a.time < b.time end) + table.sort(messages, function(a, b) + return a.time < b.time + end) return messages end @@ -101,17 +94,15 @@ function generateDateTime(start) -- need to round to seconds for an accurate date/time start = math.floor(start or socket.gettime()) local dateTimeTable = os.date('*t', start) - local dateTime = dateTimeTable.year .. "-" - .. addZeroForLessThan10(dateTimeTable.month) .. "-" - .. addZeroForLessThan10(dateTimeTable.day) .. "-" - .. addZeroForLessThan10(dateTimeTable.hour) .. "-" - .. addZeroForLessThan10(dateTimeTable.min) .. "-" - .. addZeroForLessThan10(dateTimeTable.sec) + local dateTime = dateTimeTable.year .. '-' .. addZeroForLessThan10(dateTimeTable.month) .. '-' .. + addZeroForLessThan10(dateTimeTable.day) .. '-' .. addZeroForLessThan10(dateTimeTable.hour) .. + '-' .. addZeroForLessThan10(dateTimeTable.min) .. '-' .. + addZeroForLessThan10(dateTimeTable.sec) return dateTime end function addZeroForLessThan10(number) - if(number < 10) then + if (number < 10) then return 0 .. number else return number @@ -119,14 +110,13 @@ function addZeroForLessThan10(number) end function saveLogs() - local filename = "logs/" .. generateDateTime() .. ".log" - love.filesystem.write(filename, "") + local filename = 'logs/' .. generateDateTime() .. '.log' + love.filesystem.write(filename, '') for _, message in ipairs(getAllMessages()) do - love.filesystem.append(filename, message:formatted(true) .. "\n") + love.filesystem.append(filename, message:formatted(true) .. '\n') end end - local function clearLogs() for name, logger in pairs(LOGGERS) do logger.messages = {}