LOVELY_INTEGRITY = '99fc88fd391f4645cfa6f1f6b39e3ef72258ab69c56792fe9d6f119d8aab9a88' --- STEAMODDED CORE --- MODULE STACKTRACE -- NOTE: This is a modifed version of https://github.com/ignacio/StackTracePlus/blob/master/src/StackTracePlus.lua -- Licensed under the MIT License. See https://github.com/ignacio/StackTracePlus/blob/master/LICENSE -- The MIT License -- Copyright (c) 2010 Ignacio Burgueño -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- tables function loadStackTracePlus() local _G = _G local string, io, debug, coroutine = string, io, debug, coroutine -- functions local tostring, print, require = tostring, print, require local next, assert = next, assert local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs local error = error assert(debug, "debug table must be available at this point") local io_open = io.open local string_gmatch = string.gmatch local string_sub = string.sub local table_concat = table.concat local _M = { max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)' } -- this tables should be weak so the elements in them won't become uncollectable local m_known_tables = { [_G] = "_G (global table)" } local function add_known_module(name, desc) local ok, mod = pcall(require, name) if ok then m_known_tables[mod] = desc end end add_known_module("string", "string module") add_known_module("io", "io module") add_known_module("os", "os module") add_known_module("table", "table module") add_known_module("math", "math module") add_known_module("package", "package module") add_known_module("debug", "debug module") add_known_module("coroutine", "coroutine module") -- lua5.2 add_known_module("bit32", "bit32 module") -- luajit add_known_module("bit", "bit module") add_known_module("jit", "jit module") -- lua5.3 if _VERSION >= "Lua 5.3" then add_known_module("utf8", "utf8 module") end local m_user_known_tables = {} local m_known_functions = {} for _, name in ipairs { -- Lua 5.2, 5.1 "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", -- Lua 5.1 "gcinfo", "getfenv", "loadstring", "module", "newproxy", "setfenv", "unpack" -- TODO: add table.* etc functions } do if _G[name] then m_known_functions[_G[name]] = name end end local m_user_known_functions = {} local function safe_tostring(value) local ok, err = pcall(tostring, value) if ok then return err else return (": '%s'"):format(err) end end -- Private: -- Parses a line, looking for possible function definitions (in a very naïve way) -- Returns '(anonymous)' if no function name was found in the line local function ParseLine(line) assert(type(line) == "string") -- print(line) local match = line:match("^%s*function%s+(%w+)") if match then -- print("+++++++++++++function", match) return match end match = line:match("^%s*local%s+function%s+(%w+)") if match then -- print("++++++++++++local", match) return match end match = line:match("^%s*local%s+(%w+)%s+=%s+function") if match then -- print("++++++++++++local func", match) return match end match = line:match("%s*function%s*%(") -- this is an anonymous function if match then -- print("+++++++++++++function2", match) return "(anonymous)" end return "(anonymous)" end -- Private: -- Tries to guess a function's name when the debug info structure does not have it. -- It parses either the file or the string where the function is defined. -- Returns '?' if the line where the function is defined is not found local function GuessFunctionName(info) -- print("guessing function name") if type(info.source) == "string" and info.source:sub(1, 1) == "@" then local file, err = io_open(info.source:sub(2), "r") if not file then print("file not found: " .. tostring(err)) -- whoops! return "?" end local line for _ = 1, info.linedefined do line = file:read("*l") end if not line then print("line not found") -- whoops! return "?" end return ParseLine(line) elseif type(info.source) == "string" and info.source:sub(1, 6) == "=[love" then return "(LÖVE Function)" else local line local lineNumber = 0 for l in string_gmatch(info.source, "([^\n]+)\n-") do lineNumber = lineNumber + 1 if lineNumber == info.linedefined then line = l break end end if not line then print("line not found") -- whoops! return "?" end return ParseLine(line) end end --- -- Dumper instances are used to analyze stacks and collect its information. -- local Dumper = {} Dumper.new = function(thread) local t = { lines = {} } for k, v in pairs(Dumper) do t[k] = v end t.dumping_same_thread = (thread == coroutine.running()) -- if a thread was supplied, bind it to debug.info and debug.get -- we also need to skip this additional level we are introducing in the callstack (only if we are running -- in the same thread we're inspecting) if type(thread) == "thread" then t.getinfo = function(level, what) if t.dumping_same_thread and type(level) == "number" then level = level + 1 end return debug.getinfo(thread, level, what) end t.getlocal = function(level, loc) if t.dumping_same_thread then level = level + 1 end return debug.getlocal(thread, level, loc) end else t.getinfo = debug.getinfo t.getlocal = debug.getlocal end return t end -- helpers for collecting strings to be used when assembling the final trace function Dumper:add(text) self.lines[#self.lines + 1] = text end function Dumper:add_f(fmt, ...) self:add(fmt:format(...)) end function Dumper:concat_lines() return table_concat(self.lines) end --- -- Private: -- Iterates over the local variables of a given function. -- -- @param level The stack level where the function is. -- function Dumper:DumpLocals(level) local prefix = "\t " local i = 1 if self.dumping_same_thread then level = level + 1 end local name, value = self.getlocal(level, i) if not name then return end self:add("\tLocal variables:\r\n") while name do if type(value) == "number" then self:add_f("%s%s = number: %g\r\n", prefix, name, value) elseif type(value) == "boolean" then self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value)) elseif type(value) == "string" then self:add_f("%s%s = string: %q\r\n", prefix, name, value) elseif type(value) == "userdata" then self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value)) elseif type(value) == "nil" then self:add_f("%s%s = nil\r\n", prefix, name) elseif type(value) == "table" then if m_known_tables[value] then self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value]) elseif m_user_known_tables[value] then self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value]) else local txt = "{" for k, v in pairs(value) do txt = txt .. safe_tostring(k) .. ":" .. safe_tostring(v) if #txt > _M.max_tb_output_len then txt = txt .. " (more...)" break end if next(value, k) then txt = txt .. ", " end end self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt .. "}") end elseif type(value) == "function" then local info = self.getinfo(value, "nS") local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value] if info.what == "C" then self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value))) else local source = info.short_src if source:sub(2, 7) == "string" then source = source:sub(9) -- uno más, por el espacio que viene (string "Baragent.Main", por ejemplo) end -- for k,v in pairs(info) do print(k,v) end fun_name = fun_name or GuessFunctionName(info) self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source) end elseif type(value) == "thread" then self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value)) end i = i + 1 name, value = self.getlocal(level, i) end end --- -- Public: -- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc. -- This function is suitable to be used as an error handler with pcall or xpcall -- -- @param thread An optional thread whose stack is to be inspected (defaul is the current thread) -- @param message An optional error string or object. -- @param level An optional number telling at which level to start the traceback (default is 1) -- -- Returns a string with the stack trace and a string with the original error. -- function _M.stacktrace(thread, message, level) if type(thread) ~= "thread" then -- shift parameters left thread, message, level = nil, thread, message end thread = thread or coroutine.running() level = level or 1 local dumper = Dumper.new(thread) local original_error if type(message) == "table" then dumper:add("an error object {\r\n") local first = true for k, v in pairs(message) do if first then dumper:add(" ") first = false else dumper:add(",\r\n ") end dumper:add(safe_tostring(k)) dumper:add(": ") dumper:add(safe_tostring(v)) end dumper:add("\r\n}") original_error = dumper:concat_lines() elseif type(message) == "string" then dumper:add(message) original_error = message end dumper:add("\r\n") dumper:add [[ Stack Traceback =============== ]] -- print(error_message) local level_to_show = level if dumper.dumping_same_thread then level = level + 1 end local info = dumper.getinfo(level, "nSlf") while info do if info.what == "main" then if string_sub(info.source, 1, 1) == "@" then dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, string_sub(info.source, 2), info.currentline) elseif info.source and info.source:sub(1, 1) == "=" then local str = info.source:sub(3, -2) local props = {} -- Split by space for v in string.gmatch(str, "[^%s]+") do table.insert(props, v) end local source = table.remove(props, 1) if source == "love" then dumper:add_f("(%d) main chunk of LÖVE file '%s' at line %d\r\n", level_to_show, table.concat(props, " "):sub(2, -2), info.currentline) elseif source == "SMODS" then local modID = table.remove(props, 1) local fileName = table.concat(props, " ") if modID == '_' then dumper:add_f("(%d) main chunk of Steamodded file '%s' at line %d\r\n", level_to_show, fileName:sub(2, -2), info.currentline) else dumper:add_f("(%d) main chunk of file '%s' at line %d (from mod with id %s)\r\n", level_to_show, fileName:sub(2, -2), info.currentline, modID) end elseif source == "lovely" then local module = table.remove(props, 1) local fileName = table.concat(props, " ") dumper:add_f("(%d) main chunk of file '%s' at line %d (from lovely module %s)\r\n", level_to_show, fileName:sub(2, -2), info.currentline, module) else dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.source, info.currentline) end else dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.source, info.currentline) end elseif info.what == "C" then -- print(info.namewhat, info.name) -- for k,v in pairs(info) do print(k,v, type(v)) end local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func) dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name) -- dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value))) elseif info.what == "tail" then -- print("tail") -- for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name) dumper:add_f("(%d) tail call\r\n", level_to_show) dumper:DumpLocals(level) elseif info.what == "Lua" then local source = info.short_src local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name if source:sub(2, 7) == "string" then source = source:sub(9) end local was_guessed = false if not function_name or function_name == "?" then -- for k,v in pairs(info) do print(k,v, type(v)) end function_name = GuessFunctionName(info) was_guessed = true end -- test if we have a file name local function_type = (info.namewhat == "") and "function" or info.namewhat if info.source and info.source:sub(1, 1) == "@" then dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") elseif info.source and info.source:sub(1, 1) == '#' then dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") elseif info.source and info.source:sub(1, 1) == "=" then local str = info.source:sub(3, -2) local props = {} -- Split by space for v in string.gmatch(str, "[^%s]+") do table.insert(props, v) end local source = table.remove(props, 1) if source == "love" then dumper:add_f("(%d) LÖVE %s at file '%s:%d'%s\r\n", level_to_show, function_type, table.concat(props, " "):sub(2, -2), info.currentline, was_guessed and " (best guess)" or "") elseif source == "SMODS" then local modID = table.remove(props, 1) local fileName = table.concat(props, " ") if modID == '_' then dumper:add_f("(%d) Lua %s '%s' at Steamodded file '%s:%d' %s\r\n", level_to_show, function_type, function_name, fileName:sub(2, -2), info.currentline, was_guessed and " (best guess)" or "") else dumper:add_f("(%d) Lua %s '%s' at file '%s:%d' (from mod with id %s)%s\r\n", level_to_show, function_type, function_name, fileName:sub(2, -2), info.currentline, modID, was_guessed and " (best guess)" or "") end elseif source == "lovely" then local module = table.remove(props, 1) local fileName = table.concat(props, " ") dumper:add_f("(%d) Lua %s '%s' at file '%s:%d' (from lovely module %s)%s\r\n", level_to_show, function_type, function_name, fileName:sub(2, -2), info.currentline, module, was_guessed and " (best guess)" or "") else dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source) end else dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source) end dumper:DumpLocals(level) else dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what) end level = level + 1 level_to_show = level_to_show + 1 info = dumper.getinfo(level, "nSlf") end return dumper:concat_lines(), original_error end -- -- Adds a table to the list of known tables function _M.add_known_table(tab, description) if m_known_tables[tab] then error("Cannot override an already known table") end m_user_known_tables[tab] = description end -- -- Adds a function to the list of known functions function _M.add_known_function(fun, description) if m_known_functions[fun] then error("Cannot override an already known function") end m_user_known_functions[fun] = description end return _M end -- Note: The below code is not from the original StackTracePlus.lua local stackTraceAlreadyInjected = false function getDebugInfoForCrash() local version = VERSION if not version or type(version) ~= "string" then local versionFile = love.filesystem.read("version.jkr") if versionFile then version = versionFile:match("[^\n]*") .. " (best guess)" else version = "???" end end local modded_version = MODDED_VERSION if not modded_version or type(modded_version) ~= "string" then local moddedSuccess, reqVersion = pcall(require, "SMODS.version") if moddedSuccess and type(reqVersion) == "string" then modded_version = reqVersion else modded_version = "???" end end local info = "Additional Context:\nBalatro Version: " .. version .. "\nModded Version: " .. (modded_version) local major, minor, revision, codename = love.getVersion() info = info .. string.format("\nLÖVE Version: %d.%d.%d", major, minor, revision) local lovely_success, lovely = pcall(require, "lovely") if lovely_success then info = info .. "\nLovely Version: " .. lovely.version end if SMODS and SMODS.Mods then local mod_strings = "" local lovely_strings = "" local i = 1 local lovely_i = 1 for _, v in pairs(SMODS.Mods) do if (v.can_load and (not v.meta_mod or v.lovely_only)) or (v.lovely and not v.can_load and not v.disabled) then if v.lovely_only or (v.lovely and not v.can_load) then lovely_strings = lovely_strings .. "\n " .. lovely_i .. ": " .. v.name lovely_i = lovely_i + 1 if not v.can_load then lovely_strings = lovely_strings .. "\n Has Steamodded mod that failed to load." if #v.load_issues.dependencies > 0 then lovely_strings = lovely_strings .. "\n Missing Dependencies:" for k, v in ipairs(v.load_issues.dependencies) do lovely_strings = lovely_strings .. "\n " .. k .. ". " .. v end end if #v.load_issues.conflicts > 0 then lovely_strings = lovely_strings .. "\n Conflicts:" for k, v in ipairs(v.load_issues.conflicts) do lovely_strings = lovely_strings .. "\n " .. k .. ". " .. v end end if v.load_issues.outdated then lovely_strings = lovely_strings .. "\n Outdated Mod." end if v.load_issues.main_file_not_found then lovely_strings = lovely_strings .. "\n Main file not found. (" .. v.main_file ..")" end end else mod_strings = mod_strings .. "\n " .. i .. ": " .. v.name .. " by " .. table.concat(v.author, ", ") .. " [ID: " .. v.id .. (v.priority ~= 0 and (", Priority: " .. v.priority) or "") .. (v.version and v.version ~= '0.0.0' and (", Version: " .. v.version) or "") .. (v.lovely and (", Uses Lovely") or "") .. "]" i = i + 1 local debugInfo = v.debug_info if debugInfo then if type(debugInfo) == "string" then if #debugInfo ~= 0 then mod_strings = mod_strings .. "\n " .. debugInfo end elseif type(debugInfo) == "table" then for kk, vv in pairs(debugInfo) do if type(vv) ~= 'nil' then vv = tostring(vv) end if #vv ~= 0 then mod_strings = mod_strings .. "\n " .. kk .. ": " .. vv end end end end end end end info = info .. "\nSteamodded Mods:" .. mod_strings .. "\nLovely Mods:" .. lovely_strings end return info end function injectStackTrace() if (stackTraceAlreadyInjected) then return end stackTraceAlreadyInjected = true local STP = loadStackTracePlus() local utf8 = require("utf8") -- Modifed from https://love2d.org/wiki/love.errorhandler function love.errorhandler(msg) msg = tostring(msg) if not sendErrorMessage then function sendErrorMessage(msg) print(msg) end end if not sendInfoMessage then function sendInfoMessage(msg) print(msg) end end sendErrorMessage("Oops! The game crashed\n" .. STP.stacktrace(msg), 'StackTrace') if not love.window or not love.graphics or not love.event then return end if not love.graphics.isCreated() or not love.window.isOpen() then local success, status = pcall(love.window.setMode, 800, 600) if not success or not status then return end end -- Reset state. if love.mouse then love.mouse.setVisible(true) love.mouse.setGrabbed(false) love.mouse.setRelativeMode(false) if love.mouse.isCursorSupported() then love.mouse.setCursor() end end if love.joystick then -- Stop all joystick vibrations. for i, v in ipairs(love.joystick.getJoysticks()) do v:setVibration() end end if love.audio then love.audio.stop() end love.graphics.reset() local font = love.graphics.setNewFont("resources/fonts/m6x11plus.ttf", 20) local background = {0, 0, 1} if G and G.C and G.C.BLACK then background = G.C.BLACK end love.graphics.clear(background) love.graphics.origin() local trace = STP.stacktrace("", 3) local sanitizedmsg = {} for char in msg:gmatch(utf8.charpattern) do table.insert(sanitizedmsg, char) end sanitizedmsg = table.concat(sanitizedmsg) local err = {} table.insert(err, "Oops! The game crashed:") if sanitizedmsg:find("Syntax error: game.lua:4: '=' expected near 'Game'") then table.insert(err, 'Duplicate installation of Steamodded detected! Please clean your installation: Steam Library > Balatro > Properties > Installed Files > Verify integrity of game files.') else table.insert(err, sanitizedmsg) end if #sanitizedmsg ~= #msg then table.insert(err, "Invalid UTF-8 string in error message.") end local success, msg = pcall(getDebugInfoForCrash) if success and msg then table.insert(err, '\n' .. msg) sendInfoMessage(msg, 'StackTrace') else table.insert(err, "\n" .. "Failed to get additional context :/") sendErrorMessage("Failed to get additional context :/\n" .. msg, 'StackTrace') end for l in trace:gmatch("(.-)\n") do table.insert(err, l) end local p = table.concat(err, "\n") p = p:gsub("\t", "") p = p:gsub("%[string \"(.-)\"%]", "%1") local scrollOffset = 0 local endHeight = 0 love.keyboard.setKeyRepeat(true) local function scrollDown(amt) if amt == nil then amt = 18 end scrollOffset = scrollOffset + amt if scrollOffset > endHeight then scrollOffset = endHeight end end local function scrollUp(amt) if amt == nil then amt = 18 end scrollOffset = scrollOffset - amt if scrollOffset < 0 then scrollOffset = 0 end end local pos = 70 local arrowSize = 20 local function calcEndHeight() local font = love.graphics.getFont() local rw, lines = font:getWrap(p, love.graphics.getWidth() - pos * 2) local lineHeight = font:getHeight() local atBottom = scrollOffset == endHeight and scrollOffset ~= 0 endHeight = #lines * lineHeight - love.graphics.getHeight() + pos * 2 if (endHeight < 0) then endHeight = 0 end if scrollOffset > endHeight or atBottom then scrollOffset = endHeight end end local function draw() if not love.graphics.isActive() then return end love.graphics.clear(background) calcEndHeight() love.graphics.printf(p, pos, pos - scrollOffset, love.graphics.getWidth() - pos * 2) if scrollOffset ~= endHeight then love.graphics.polygon("fill", love.graphics.getWidth() - (pos / 2), love.graphics.getHeight() - arrowSize, love.graphics.getWidth() - (pos / 2) + arrowSize, love.graphics.getHeight() - (arrowSize * 2), love.graphics.getWidth() - (pos / 2) - arrowSize, love.graphics.getHeight() - (arrowSize * 2)) end if scrollOffset ~= 0 then love.graphics.polygon("fill", love.graphics.getWidth() - (pos / 2), arrowSize, love.graphics.getWidth() - (pos / 2) + arrowSize, arrowSize * 2, love.graphics.getWidth() - (pos / 2) - arrowSize, arrowSize * 2) end love.graphics.present() end local fullErrorText = p local function copyToClipboard() if not love.system then return end love.system.setClipboardText(fullErrorText) p = p .. "\nCopied to clipboard!" end p = p .. "\n\nPress ESC to exit\nPress R to restart the game" if love.system then p = p .. "\nPress Ctrl+C or tap to copy this error" end if G then -- Kill threads (makes restarting possible) if G.SOUND_MANAGER and G.SOUND_MANAGER.channel then G.SOUND_MANAGER.channel:push({ type = 'kill' }) end if G.SAVE_MANAGER and G.SAVE_MANAGER.channel then G.SAVE_MANAGER.channel:push({ type = 'kill' }) end if G.HTTP_MANAGER and G.HTTP_MANAGER.channel then G.HTTP_MANAGER.channel:push({ type = 'kill' }) end end return function() love.event.pump() for e, a, b, c in love.event.poll() do if e == "quit" then return 1 elseif e == "keypressed" and a == "escape" then return 1 elseif e == "keypressed" and a == "c" and love.keyboard.isDown("lctrl", "rctrl") then copyToClipboard() elseif e == "keypressed" and a == "r" then SMODS.restart_game() elseif e == "keypressed" and a == "down" then scrollDown() elseif e == "keypressed" and a == "up" then scrollUp() elseif e == "keypressed" and a == "pagedown" then scrollDown(love.graphics.getHeight()) elseif e == "keypressed" and a == "pageup" then scrollUp(love.graphics.getHeight()) elseif e == "keypressed" and a == "home" then scrollOffset = 0 elseif e == "keypressed" and a == "end" then scrollOffset = endHeight elseif e == "wheelmoved" then scrollUp(b * 20) elseif e == "gamepadpressed" and b == "dpdown" then scrollDown() elseif e == "gamepadpressed" and b == "dpup" then scrollUp() elseif e == "gamepadpressed" and b == "a" then return "restart" elseif e == "gamepadpressed" and b == "x" then copyToClipboard() elseif e == "gamepadpressed" and (b == "b" or b == "back" or b == "start") then return 1 elseif e == "touchpressed" then local name = love.window.getTitle() if #name == 0 or name == "Untitled" then name = "Game" end local buttons = {"OK", "Cancel", "Restart"} if love.system then buttons[4] = "Copy to clipboard" end local pressed = love.window.showMessageBox("Quit " .. name .. "?", "", buttons) if pressed == 1 then return 1 elseif pressed == 3 then return "restart" elseif pressed == 4 then copyToClipboard() end end end draw() if love.timer then love.timer.sleep(0.1) end end end end injectStackTrace() -- ---------------------------------------------- -- --------MOD CORE API STACKTRACE END----------- local Cartomancer_replacements = { { find = [[ for k, v in ipairs%(G%.playing_cards%) do if v%.base%.suit then table%.insert%(SUITS%[v%.base%.suit%], v%) end]], -- Steamodded<0917b find_alt = [[ for k, v in ipairs%(G%.playing_cards%) do table%.insert%(SUITS%[v%.base%.suit%], v%)]], place = [[ local SUITS_SORTED = Cartomancer.tablecopy(SUITS) for k, v in ipairs(G.playing_cards) do if v.base.suit then local greyed if unplayed_only and not ((v.area and v.area == G.deck) or v.ability.wheel_flipped) then greyed = true end local card_string = v:cart_to_string() if greyed then card_string = card_string .. "Greyed" -- for some reason format doesn't work and final string is `sGreyed` end if greyed and Cartomancer.SETTINGS.deck_view_hide_drawn_cards then -- Ignore this card. elseif not Cartomancer.SETTINGS.deck_view_stack_enabled then -- Don't stack cards local _scale = 0.7 local copy = copy_card(v, nil, _scale) copy.greyed = greyed copy.stacked_quantity = 1 table.insert(SUITS_SORTED[v.base.suit], copy) elseif not SUITS[v.base.suit][card_string] then -- Initiate stack table.insert(SUITS_SORTED[v.base.suit], card_string) local _scale = 0.7 local copy = copy_card(v, nil, _scale) copy.greyed = greyed copy.stacked_quantity = 1 SUITS[v.base.suit][card_string] = copy else -- Stack cards local stacked_card = SUITS[v.base.suit][card_string] stacked_card.stacked_quantity = stacked_card.stacked_quantity + 1 end end]] }, { find = "card_limit = #SUITS%[suit_map%[j%]%],", place = "card_limit = #SUITS_SORTED[suit_map[j]]," }, { find = [[ for i = 1%, %#SUITS%[suit_map%[j%]%] do if SUITS%[suit_map%[j%]%]%[i%] then local greyed%, _scale = nil%, 0%.7 if unplayed_only and not %(%(SUITS%[suit_map%[j%]%]%[i%]%.area and SUITS%[suit_map%[j%]%]%[i%]%.area == G%.deck%) or SUITS%[suit_map%[j%]%]%[i%]%.ability%.wheel_flipped%) then greyed = true end local copy = copy_card%(SUITS%[suit_map%[j%]%]%[i%]%, nil%, _scale%) copy%.greyed = greyed copy%.T%.x = view_deck%.T%.x %+ view_deck%.T%.w %/ 2 copy%.T%.y = view_deck%.T%.y copy:hard_set_T%(%) view_deck:emplace%(copy%) end end]], place = [[ for i = 1%, %#SUITS_SORTED%[suit_map%[j%]%] do local card if not Cartomancer.SETTINGS.deck_view_stack_enabled then card = SUITS_SORTED%[suit_map%[j%]%]%[i%] else local card_string = SUITS_SORTED%[suit_map%[j%]%]%[i%] card = SUITS%[suit_map%[j%]%]%[card_string%] end card%.T%.x = view_deck%.T%.x %+ view_deck%.T%.w%/2 card%.T%.y = view_deck%.T%.y card:create_quantity_display%(%) card:hard_set_T%(%) view_deck:emplace%(card%) end]] }, { find = ' modded and {n = G.UIT.R, config = {align = "cm"}, nodes = {', place = [=[ not unplayed_only and Cartomancer.add_unique_count() or nil, modded and {n = G.UIT.R, config = {align = "cm"}, nodes = {]=] }, } -- Mom, can we have lovely patches for overrides.lua? -- No, we have lovely patches at home -- Lovely patches at home: local Cartomancer_nfs_read local Cartomancer_nfs_read_override = function (containerOrName, nameOrSize, sizeOrNil) local data, size = Cartomancer_nfs_read(containerOrName, nameOrSize, sizeOrNil) if type(containerOrName) ~= "string" then return data, size end local overrides = '/overrides.lua' if containerOrName:sub(-#overrides) ~= overrides then return data, size end local replaced = 0 local total_replaced = 0 for _, v in ipairs(Cartomancer_replacements) do data, replaced = string.gsub(data, v.find, v.place) if replaced == 0 and v.find_alt then data, replaced = string.gsub(data, v.find_alt, v.place) end if replaced == 0 then print("Failed to replace " .. v.find .. " for overrides.lua") else total_replaced = total_replaced + 1 end end print("Totally applied " .. total_replaced .. " replacements to overrides.lua") -- We no longer need this override NFS.read = Cartomancer_nfs_read return data, size end if (love.system.getOS() == 'OS X' ) and (jit.arch == 'arm64' or jit.arch == 'arm') then jit.off() end do local logger = require("debugplus.logger") logger.registerLogHandler() end require "engine/object" require "bit" require "engine/string_packer" require "engine/controller" require "back" require "tag" require "engine/event" require "engine/node" require "engine/moveable" require "engine/sprite" require "engine/animatedsprite" require "functions/misc_functions" require "game" require "globals" require "engine/ui" require "functions/UI_definitions" require "functions/state_events" require "functions/common_events" require "functions/button_callbacks" require "functions/misc_functions" require "functions/test_functions" require "card" require "cardarea" require "blind" require "card_character" require "engine/particles" require "engine/text" require "challenges" math.randomseed( G.SEED ) function love.run() if love.load then love.load(love.arg.parseGameArguments(arg), arg) end -- We don't want the first frame's dt to include time taken by love.load. if love.timer then love.timer.step() end local dt = 0 local dt_smooth = 1/100 local run_time = 0 -- Main loop time. return function() run_time = love.timer.getTime() -- Process events. if love.event and G and G.CONTROLLER then love.event.pump() local _n,_a,_b,_c,_d,_e,_f,touched for name, a,b,c,d,e,f in love.event.poll() do if name == "quit" then if not love.quit or not love.quit() then return a or 0 end end if name == 'touchpressed' then touched = true elseif name == 'mousepressed' then _n,_a,_b,_c,_d,_e,_f = name,a,b,c,d,e,f else love.handlers[name](a,b,c,d,e,f) end end if _n then love.handlers['mousepressed'](_a,_b,_c,touched) end end -- Update dt, as we'll be passing it to update if love.timer then dt = love.timer.step() end dt_smooth = math.min(0.8*dt_smooth + 0.2*dt, 0.1) -- Call update and draw if love.update then love.update(dt_smooth) end -- will pass 0 if love.timer is disabled if love.graphics and love.graphics.isActive() then if love.draw then love.draw() end love.graphics.present() end run_time = math.min(love.timer.getTime() - run_time, 0.1) G.FPS_CAP = G.FPS_CAP or 500 if run_time < 1./G.FPS_CAP then love.timer.sleep(1./G.FPS_CAP - run_time) end end end Cryptid = {} Cryptid.memepack = {} Cryptid.aliases = {} Cryptid.food = {} Cryptid.M_jokers = {} Cryptid.Megavouchers = {} function love.load() G:start_up() --Steam integration local os = love.system.getOS() if os == 'OS X' or os == 'Windows' then local st = nil --To control when steam communication happens, make sure to send updates to steam as little as possible if os == 'OS X' then local dir = love.filesystem.getSourceBaseDirectory() local old_cpath = package.cpath package.cpath = package.cpath .. ';' .. dir .. '/?.so' st = require 'luasteam' package.cpath = old_cpath else st = require 'luasteam' end st.send_control = { last_sent_time = -200, last_sent_stage = -1, force = false, } if not (st.init and st:init()) then st = nil end --Set up the render window and the stage for the splash screen, then enter the gameloop with :update G.STEAM = st else end --Set the mouse to invisible immediately, this visibility is handled in the G.CONTROLLER love.mouse.setVisible(false) end function love.quit() --Steam integration if G.SOUND_MANAGER then G.SOUND_MANAGER.channel:push({type = 'stop'}) end if G.STEAM then G.STEAM:shutdown() end end function love.update( dt ) --Perf monitoring checkpoint timer_checkpoint(nil, 'update', true) G:update(dt) end function love.draw() --Perf monitoring checkpoint timer_checkpoint(nil, 'draw', true) G:draw() do local console = require("debugplus.console") console.doConsoleRender() timer_checkpoint('DebugPlus Console', 'draw') end end function love.keypressed(key) if Handy.controller.process_key(key, false) then return end local console = require("debugplus.console") if not console.consoleHandleKey(key) then return end if not _RELEASE_MODE and G.keybind_mapping[key] then love.gamepadpressed(G.CONTROLLER.keyboard_controller, G.keybind_mapping[key]) else G.CONTROLLER:set_HID_flags('mouse') G.CONTROLLER:key_press(key) end end function love.keyreleased(key) if Handy.controller.process_key(key, true) then return end if not _RELEASE_MODE and G.keybind_mapping[key] then love.gamepadreleased(G.CONTROLLER.keyboard_controller, G.keybind_mapping[key]) else G.CONTROLLER:set_HID_flags('mouse') G.CONTROLLER:key_release(key) end end function love.gamepadpressed(joystick, button) button = G.button_mapping[button] or button G.CONTROLLER:set_gamepad(joystick) G.CONTROLLER:set_HID_flags('button', button) G.CONTROLLER:button_press(button) end function love.gamepadreleased(joystick, button) button = G.button_mapping[button] or button G.CONTROLLER:set_gamepad(joystick) G.CONTROLLER:set_HID_flags('button', button) G.CONTROLLER:button_release(button) end function love.mousepressed(x, y, button, touch) if not touch and Handy.controller.process_mouse(button, false) then return end G.CONTROLLER:set_HID_flags(touch and 'touch' or 'mouse') if button == 1 then G.CONTROLLER:queue_L_cursor_press(x, y) end if button == 2 then G.CONTROLLER:queue_R_cursor_press(x, y) end end function love.mousereleased(x, y, button) if Handy.controller.process_mouse(button, true) then return end if button == 1 then G.CONTROLLER:L_cursor_release(x, y) end end function love.mousemoved(x, y, dx, dy, istouch) G.CONTROLLER.last_touch_time = G.CONTROLLER.last_touch_time or -1 if next(love.touch.getTouches()) ~= nil then G.CONTROLLER.last_touch_time = G.TIMERS.UPTIME end G.CONTROLLER:set_HID_flags(G.CONTROLLER.last_touch_time > G.TIMERS.UPTIME - 0.2 and 'touch' or 'mouse') end function love.joystickaxis( joystick, axis, value ) if math.abs(value) > 0.2 and joystick:isGamepad() then G.CONTROLLER:set_gamepad(joystick) G.CONTROLLER:set_HID_flags('axis') end end if false then if G.F_NO_ERROR_HAND then return end msg = tostring(msg) if G.SETTINGS.crashreports and _RELEASE_MODE and G.F_CRASH_REPORTS then local http_thread = love.thread.newThread([[ local https = require('https') CHANNEL = love.thread.getChannel("http_channel") while true do --Monitor the channel for any new requests local request = CHANNEL:demand() if request then https.request(request) end end ]]) local http_channel = love.thread.getChannel('http_channel') http_thread:start() local httpencode = function(str) local char_to_hex = function(c) return string.format("%%%02X", string.byte(c)) end str = str:gsub("\n", "\r\n"):gsub("([^%w _%%%-%.~])", char_to_hex):gsub(" ", "+") return str end local error = msg local file = string.sub(msg, 0, string.find(msg, ':')) local function_line = string.sub(msg, string.len(file)+1) function_line = string.sub(function_line, 0, string.find(function_line, ':')-1) file = string.sub(file, 0, string.len(file)-1) local trace = debug.traceback() local boot_found, func_found = false, false for l in string.gmatch(trace, "(.-)\n") do if string.match(l, "boot.lua") then boot_found = true elseif boot_found and not func_found then func_found = true trace = '' function_line = string.sub(l, string.find(l, 'in function')+12)..' line:'..function_line end if boot_found and func_found then trace = trace..l..'\n' end end http_channel:push('https://958ha8ong3.execute-api.us-east-2.amazonaws.com/?error='..httpencode(error)..'&file='..httpencode(file)..'&function_line='..httpencode(function_line)..'&trace='..httpencode(trace)..'&version='..(G.VERSION)) end if not love.window or not love.graphics or not love.event then return end if not love.graphics.isCreated() or not love.window.isOpen() then local success, status = pcall(love.window.setMode, 800, 600) if not success or not status then return end end -- Reset state. if love.mouse then love.mouse.setVisible(true) love.mouse.setGrabbed(false) love.mouse.setRelativeMode(false) end if love.joystick then -- Stop all joystick vibrations. for i,v in ipairs(love.joystick.getJoysticks()) do v:setVibration() end end if love.audio then love.audio.stop() end love.graphics.reset() local font = love.graphics.setNewFont("resources/fonts/m6x11plus.ttf", 20) love.graphics.clear(G.C.BLACK) love.graphics.origin() local p = 'Oops! Something went wrong:\n'..msg..'\n\n'..(not _RELEASE_MODE and debug.traceback() or G.SETTINGS.crashreports and 'Since you are opted in to sending crash reports, LocalThunk HQ was sent some useful info about what happened.\nDon\'t worry! There is no identifying or personal information. If you would like\nto opt out, change the \'Crash Report\' setting to Off' or 'Crash Reports are set to Off. If you would like to send crash reports, please opt in in the Game settings.\nThese crash reports help us avoid issues like this in the future') local function draw() local pos = love.window.toPixels(70) love.graphics.push() love.graphics.clear(G.C.BLACK) love.graphics.setColor(1., 1., 1., 1.) love.graphics.printf(p, font, pos, pos, love.graphics.getWidth() - pos) love.graphics.pop() love.graphics.present() end while true do love.event.pump() for e, a, b, c in love.event.poll() do if e == "quit" then return elseif e == "keypressed" and a == "escape" then return elseif e == "touchpressed" then local name = love.window.getTitle() if #name == 0 or name == "Untitled" then name = "Game" end local buttons = {"OK", "Cancel"} local pressed = love.window.showMessageBox("Quit "..name.."?", "", buttons) if pressed == 1 then return end end end draw() if love.timer then love.timer.sleep(0.1) end end end function love.resize(w, h) if w/h < 1 then --Dont allow the screen to be too square, since pop in occurs above and below screen h = w/1 end --When the window is resized, this code resizes the Canvas, then places the 'room' or gamearea into the middle without streching it if w/h < G.window_prev.orig_ratio then G.TILESCALE = G.window_prev.orig_scale*w/G.window_prev.w else G.TILESCALE = G.window_prev.orig_scale*h/G.window_prev.h end if G.ROOM then G.ROOM.T.w = G.TILE_W G.ROOM.T.h = G.TILE_H G.ROOM_ATTACH.T.w = G.TILE_W G.ROOM_ATTACH.T.h = G.TILE_H if w/h < G.window_prev.orig_ratio then G.ROOM.T.x = G.ROOM_PADDING_W G.ROOM.T.y = (h/(G.TILESIZE*G.TILESCALE) - (G.ROOM.T.h+G.ROOM_PADDING_H))/2 + G.ROOM_PADDING_H/2 else G.ROOM.T.y = G.ROOM_PADDING_H G.ROOM.T.x = (w/(G.TILESIZE*G.TILESCALE) - (G.ROOM.T.w+G.ROOM_PADDING_W))/2 + G.ROOM_PADDING_W/2 end G.ROOM_ORIG = { x = G.ROOM.T.x, y = G.ROOM.T.y, r = G.ROOM.T.r } if G.buttons then G.buttons:recalculate() end if G.HUD then G.HUD:recalculate() end end G.WINDOWTRANS = { x = 0, y = 0, w = G.TILE_W+2*G.ROOM_PADDING_W, h = G.TILE_H+2*G.ROOM_PADDING_H, real_window_w = w, real_window_h = h } G.CANV_SCALE = 1 if love.system.getOS() == 'Windows' and false then --implement later if needed local render_w, render_h = love.window.getDesktopDimensions(G.SETTINGS.WINDOW.selcted_display) local unscaled_dims = love.window.getFullscreenModes(G.SETTINGS.WINDOW.selcted_display)[1] local DPI_scale = math.floor((0.5*unscaled_dims.width/render_w + 0.5*unscaled_dims.height/render_h)*500 + 0.5)/500 if DPI_scale > 1.1 then G.CANV_SCALE = 1.5 G.AA_CANVAS = love.graphics.newCanvas(G.WINDOWTRANS.real_window_w*G.CANV_SCALE, G.WINDOWTRANS.real_window_h*G.CANV_SCALE, {type = '2d', readable = true}) G.AA_CANVAS:setFilter('linear', 'linear') else G.AA_CANVAS = nil end end G.CANVAS = love.graphics.newCanvas(w*G.CANV_SCALE, h*G.CANV_SCALE, {type = '2d', readable = true}) G.CANVAS:setFilter('linear', 'linear') end Handy = setmetatable({ last_clicked_area = nil, last_clicked_card = nil, utils = {}, }, {}) --- @generic T --- @generic S --- @param target T --- @param source S --- @param ... any --- @return T | S function Handy.utils.table_merge(target, source, ...) assert(type(target) == "table", "Target is not a table") local tables_to_merge = { source, ... } if #tables_to_merge == 0 then return target end for k, t in ipairs(tables_to_merge) do assert(type(t) == "table", string.format("Expected a table as parameter %d", k)) end for i = 1, #tables_to_merge do local from = tables_to_merge[i] for k, v in pairs(from) do if type(k) == "number" then table.insert(target, v) elseif type(k) == "string" then if type(v) == "table" then target[k] = target[k] or {} target[k] = Handy.utils.table_merge(target[k], v) else target[k] = v end end end end return target end function Handy.utils.table_contains(t, value) for i = #t, 1, -1 do if t[i] and t[i] == value then return true end end return false end -- Handy.config = { default = { notifications_level = 3, insta_highlight = { enabled = true, }, insta_buy_or_sell = { enabled = true, key_1 = "Shift", key_2 = nil, }, insta_use = { enabled = true, key_1 = "Ctrl", key_2 = nil, }, move_highlight = { enabled = true, swap = { enabled = true, key_1 = "Shift", key_2 = nil, }, to_end = { enabled = true, key_1 = "Ctrl", key_2 = nil, }, dx = { one_left = { enabled = true, key_1 = "Left", key_2 = nil, }, one_right = { enabled = true, key_1 = "Right", key_2 = nil, }, }, }, insta_cash_out = { enabled = true, key_1 = "Enter", key_2 = nil, }, insta_booster_skip = { enabled = true, key_1 = "Enter", key_2 = nil, }, dangerous_actions = { enabled = false, immediate_buy_and_sell = { enabled = true, key_1 = "Middle Mouse", key_2 = nil, queue = { enabled = false, }, }, nopeus_unsafe = { enabled = true, }, }, speed_multiplier = { enabled = true, key_1 = "Alt", key_2 = nil, }, shop_reroll = { enabled = true, key_1 = "Q", key_2 = nil, }, play_and_discard = { enabled = true, play = { enabled = true, key_1 = nil, key_2 = nil, }, discard = { enabled = true, key_1 = nil, key_2 = nil, }, }, nopeus_interaction = { enabled = true, key_1 = "]", key_2 = nil, }, not_just_yet_interaction = { enabled = true, key_1 = "Enter", key_2 = nil, }, }, current = {}, save = function() if Handy.current_mod then Handy.current_mod.config = Handy.config.current SMODS.save_mod_config(Handy.current_mod) end end, } Handy.config.current = Handy.utils.table_merge({}, Handy.config.default) -- Handy.fake_events = { check = function(arg) local fake_event = { UIBox = arg.UIBox, config = { ref_table = arg.card, button = arg.button, id = arg.id, }, } arg.func(fake_event) return fake_event.config.button ~= nil, fake_event.config.button end, execute = function(arg) if type(arg.func) == "function" then arg.func({ UIBox = arg.UIBox, config = { ref_table = arg.card, button = arg.button, id = arg.id, }, }) end end, } Handy.controller = { bind_module = nil, bind_key = nil, bind_button = nil, update_bind_button_text = function(text) local button_text = Handy.controller.bind_button.children[1].children[1] button_text.config.text_drawable = nil button_text.config.text = text button_text:update_text() button_text.UIBox:recalculate() end, init_bind = function(button) button.config.button = nil Handy.controller.bind_button = button Handy.controller.bind_module = button.config.ref_table.module Handy.controller.bind_key = button.config.ref_table.key Handy.controller.update_bind_button_text( "[" .. (Handy.controller.bind_module[Handy.controller.bind_key] or "None") .. "]" ) end, complete_bind = function(key) Handy.controller.bind_module[Handy.controller.bind_key] = key Handy.controller.update_bind_button_text(key or "None") Handy.controller.bind_button.config.button = "handy_init_keybind_change" Handy.controller.bind_button = nil Handy.controller.bind_module = nil Handy.controller.bind_key = nil end, cancel_bind = function() Handy.controller.update_bind_button_text(Handy.controller.bind_module[Handy.controller.bind_key] or "None") Handy.controller.bind_button.config.button = "handy_init_keybind_change" Handy.controller.bind_button = nil Handy.controller.bind_module = nil Handy.controller.bind_key = nil end, process_bind = function(key) if not Handy.controller.bind_button then return false end local parsed_key = Handy.controller.parse(key) if parsed_key == "Escape" then parsed_key = nil end Handy.controller.complete_bind(parsed_key) Handy.config.save() return true end, parse_table = { ["mouse1"] = "Left Mouse", ["mouse2"] = "Right Mouse", ["mouse3"] = "Middle Mouse", ["mouse4"] = "Mouse 4", ["mouse5"] = "Mouse 5", ["wheelup"] = "Wheel Up", ["wheeldown"] = "Wheel Down", ["lshift"] = "Shift", ["rshift"] = "Shift", ["lctrl"] = "Ctrl", ["rctrl"] = "Ctrl", ["lalt"] = "Alt", ["ralt"] = "Alt", ["lgui"] = "GUI", ["rgui"] = "GUI", ["return"] = "Enter", ["kpenter"] = "Enter", ["pageup"] = "Page Up", ["pagedown"] = "Page Down", ["numlock"] = "Num Lock", ["capslock"] = "Caps Lock", ["scrolllock"] = "Scroll Lock", }, resolve_table = { ["Left Mouse"] = { "mouse1" }, ["Right Mouse"] = { "mouse2" }, ["Middle Mouse"] = { "mouse3" }, ["Mouse 4"] = { "mouse4" }, ["Mouse 5"] = { "mouse5" }, ["Wheel Up"] = { "wheelup" }, ["Wheel Down"] = { "wheeldown" }, ["Shift"] = { "lshift", "rshift" }, ["Ctrl"] = { "lctrl", "rctrl" }, ["Alt"] = { "lalt", "ralt" }, ["GUI"] = { "lgui", "rgui" }, ["Enter"] = { "return", "kpenter" }, ["Page Up"] = { "pageup" }, ["Page Down"] = { "pagedown" }, ["Num Lock"] = { "numlock" }, ["Caps Lock"] = { "capslock" }, ["Scroll Lock"] = { "scrolllock" }, }, mouse_to_key_table = { [1] = "mouse1", [2] = "mouse2", [3] = "mouse3", [4] = "mouse4", [5] = "mouse5", }, wheel_to_key_table = { [1] = "wheelup", [2] = "wheeldown", }, mouse_buttons = { ["Left Mouse"] = 1, ["Right Mouse"] = 2, ["Middle Mouse"] = 3, ["Mouse 4"] = 4, ["Mouse 5"] = 5, }, wheel_buttons = { ["Wheel Up"] = 1, ["Wheel Down"] = 2, }, parse = function(raw_key) if not raw_key then return nil end if Handy.controller.parse_table[raw_key] then return Handy.controller.parse_table[raw_key] elseif string.sub(raw_key, 1, 2) == "kp" then return "NUM " .. string.sub(raw_key, 3) else return string.upper(string.sub(raw_key, 1, 1)) .. string.sub(raw_key, 2) end end, resolve = function(parsed_key) if not parsed_key then return nil end if Handy.controller.resolve_table[parsed_key] then return unpack(Handy.controller.resolve_table[parsed_key]) elseif string.sub(parsed_key, 1, 4) == "NUM " then return "kp" .. string.sub(parsed_key, 5) else local str = string.gsub(string.lower(parsed_key), "%s+", "") return str end end, is_down = function(...) local parsed_keys = { ... } for i = 1, #parsed_keys do local parsed_key = parsed_keys[i] if parsed_key and parsed_key ~= "Unknown" then if Handy.controller.wheel_buttons[parsed_key] then -- Well, skip elseif Handy.controller.mouse_buttons[parsed_key] then if love.mouse.isDown(Handy.controller.mouse_buttons[parsed_key]) then return true end else local success, is_down = pcall(function() return love.keyboard.isDown(Handy.controller.resolve(parsed_key)) end) if success and is_down then return true end end end end return false end, is = function(raw_key, ...) if not raw_key then return false end local parsed_keys = { ... } for i = 1, #parsed_keys do local parsed_key = parsed_keys[i] if parsed_key then local resolved_key_1, resolved_key_2 = Handy.controller.resolve(parsed_key) if raw_key and raw_key ~= "Unknown" and (raw_key == resolved_key_1 or raw_key == resolved_key_2) then return true end end end return false end, is_module_key_down = function(module) return module and module.enabled and Handy.controller.is_down(module.key_1, module.key_2) end, is_module_key = function(module, raw_key) return module and module.enabled and Handy.controller.is(raw_key, module.key_1, module.key_2) end, process_key = function(key, released) if not released then if Handy.controller.process_bind(key) then return true end Handy.move_highlight.use(key) Handy.speed_multiplier.use(key) Handy.shop_reroll.use(key) Handy.play_and_discard.use(key) end Handy.insta_booster_skip.use(key, released) Handy.insta_cash_out.use(key, released) Handy.not_just_yet_interaction.use(key, released) Handy.dangerous_actions.toggle_queue(key, released) Handy.UI.state_panel.update(key, released) return false end, process_mouse = function(mouse, released) local key = Handy.controller.mouse_to_key_table[mouse] if not released then if Handy.controller.process_bind(key) then return true end Handy.move_highlight.use(key) Handy.speed_multiplier.use(key) Handy.shop_reroll.use(key) Handy.play_and_discard.use(key) end Handy.insta_booster_skip.use(key, released) Handy.insta_cash_out.use(key, released) Handy.not_just_yet_interaction.use(key, released) Handy.dangerous_actions.toggle_queue(key, released) Handy.UI.state_panel.update(key, released) return false end, process_wheel = function(wheel) local key = Handy.controller.wheel_to_key_table[wheel] if Handy.controller.process_bind(key) then return true end Handy.move_highlight.use(key) Handy.speed_multiplier.use(key) Handy.nopeus_interaction.use(key) Handy.shop_reroll.use(key) Handy.play_and_discard.use(key) Handy.UI.state_panel.update(key, false) end, process_card_click = function(card) if Handy.insta_actions.use(card) then return true end Handy.last_clicked_card = card Handy.last_clicked_area = card.area return false end, process_card_hover = function(card) if Handy.insta_highlight.use(card) then return true end if Handy.dangerous_actions.use(card) then return true end return false end, process_update = function(dt) Handy.insta_booster_skip.update() Handy.insta_cash_out.update() Handy.not_just_yet_interaction.update() Handy.UI.update(dt) end, } -- Handy.insta_cash_out = { is_hold = false, is_skipped = false, is_button_created = false, dollars = nil, can_execute = function(check) if check then return not not ( Handy.insta_cash_out.is_hold and G.STAGE == G.STAGES.RUN and Handy.insta_cash_out.is_skipped and not G.SETTINGS.paused and G.round_eval ) else return not not ( Handy.insta_cash_out.is_hold and G.STAGE == G.STAGES.RUN and not Handy.insta_cash_out.is_skipped and Handy.insta_cash_out.dollars and not G.SETTINGS.paused and G.round_eval ) end end, execute = function() Handy.insta_cash_out.is_skipped = true if Handy.insta_cash_out.is_button_created then G.GAME.current_round.dollars = Handy.insta_cash_out.dollars Handy.insta_cash_out.dollars = nil end G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() G.FUNCS.cash_out({ config = { id = "cash_out_button", }, }) return true end, })) return true end, use = function(key, released) if Handy.controller.is_module_key(Handy.config.current.insta_cash_out, key) then Handy.insta_cash_out.is_hold = not released end return false end, update = function() if not Handy.config.current.insta_cash_out.enabled then Handy.insta_cash_out.is_hold = false end return Handy.insta_cash_out.can_execute() and Handy.insta_cash_out.execute() or false end, update_state_panel = function(state, key, released) -- if G.STAGE ~= G.STAGES.RUN then -- return false -- end -- if Handy.config.current.notifications_level < 4 then -- return false -- end -- if Handy.insta_cash_out.can_execute(true) then -- state.items.insta_cash_out = { -- text = "Skip Cash Out", -- hold = false, -- order = 10, -- } -- return true -- end -- return false end, } Handy.insta_booster_skip = { is_hold = false, is_skipped = false, can_execute = function(check) if check then return not not ( Handy.insta_booster_skip.is_hold and G.STAGE == G.STAGES.RUN and not G.SETTINGS.paused and G.booster_pack ) end return not not ( Handy.insta_booster_skip.is_hold and not Handy.insta_booster_skip.is_skipped and G.STAGE == G.STAGES.RUN and not G.SETTINGS.paused and G.booster_pack and Handy.fake_events.check({ func = G.FUNCS.can_skip_booster, }) ) end, execute = function() Handy.insta_booster_skip.is_skipped = true G.E_MANAGER:add_event(Event({ func = function() G.FUNCS.skip_booster() return true end, })) return true end, use = function(key, released) if Handy.controller.is_module_key(Handy.config.current.insta_booster_skip, key) then Handy.insta_booster_skip.is_hold = not released end return false end, update = function() if not Handy.config.current.insta_booster_skip.enabled then Handy.insta_booster_skip.is_hold = false end return Handy.insta_booster_skip.can_execute() and Handy.insta_booster_skip.execute() or false end, update_state_panel = function(state, key, released) if G.STAGE ~= G.STAGES.RUN then return false end if Handy.config.current.notifications_level < 4 then return false end if Handy.insta_booster_skip.can_execute(true) then state.items.insta_booster_skip = { text = "Skip Booster Packs", hold = Handy.insta_booster_skip.is_hold, order = 10, } return true end return false end, } Handy.insta_highlight = { can_execute = function(card) return G.STAGE == G.STAGES.RUN and Handy.config.current.insta_highlight.enabled and card and card.area == G.hand -- TODO: fix it and not next(love.touch.getTouches()) and love.mouse.isDown(1) and not card.highlighted end, execute = function(card) card.area:add_to_highlighted(card) return false end, use = function(card) return Handy.insta_highlight.can_execute(card) and Handy.insta_highlight.execute(card) or false end, update_state_panel = function(state, key, released) end, } Handy.insta_actions = { get_actions = function() return { buy_or_sell = Handy.controller.is_module_key_down(Handy.config.current.insta_buy_or_sell), use = Handy.controller.is_module_key_down(Handy.config.current.insta_use), } end, can_execute = function(card, buy_or_sell, use) return not not (G.STAGE == G.STAGES.RUN and (buy_or_sell or use) and card and card.area) end, execute = function(card, buy_or_sell, use, only_sell) local target_button = nil local is_shop_button = false local is_custom_button = false local is_playable_consumeable = false local base_background = G.UIDEF.card_focus_ui(card) local base_attach = base_background:get_UIE_by_ID("ATTACH_TO_ME").children local card_buttons = G.UIDEF.use_and_sell_buttons(card) local result_funcs = {} for _, node in ipairs(card_buttons.nodes) do if node.config and node.config.func then result_funcs[node.config.func] = node end end local is_booster_pack_card = (G.pack_cards and card.area == G.pack_cards) and not card.ability.consumeable if use then if card.area == G.hand and card.ability.consumeable then local success, playale_consumeable_button = pcall(function() -- G.UIDEF.use_and_sell_buttons(G.hand.highlighted[1]).nodes[1].nodes[2].nodes[1].nodes[1] return card_buttons.nodes[1].nodes[2].nodes[1].nodes[1] end) if success and playale_consumeable_button then target_button = playale_consumeable_button is_custom_button = true is_playable_consumeable = true end elseif result_funcs.can_select_alchemical or result_funcs.can_select_crazy_card then -- Prevent cards to be selected when usage is required: -- Alchemical cards, Cines else target_button = base_attach.buy_and_use or (not is_booster_pack_card and base_attach.use) or card.children.buy_and_use_button is_shop_button = target_button == card.children.buy_and_use_button end elseif buy_or_sell then target_button = card.children.buy_button or result_funcs.can_select_crazy_card -- Cines or result_funcs.can_select_alchemical -- Alchemical cards or result_funcs.can_use_mupack -- Multipacks or result_funcs.can_reserve_card -- Code cards, for example or base_attach.buy or base_attach.redeem or base_attach.sell or (is_booster_pack_card and base_attach.use) if only_sell and target_button ~= base_attach.sell then target_button = nil end is_shop_button = target_button == card.children.buy_button end if target_button and not is_custom_button and not is_shop_button then for _, node in ipairs(card_buttons.nodes) do if target_button == node then is_custom_button = true end end end local target_button_UIBox local target_button_definition local cleanup = function() base_background:remove() if target_button_UIBox and is_custom_button then target_button_UIBox:remove() end end if target_button then if is_playable_consumeable then card.area:add_to_highlighted(card) if not card.highlighted then cleanup() return false end end target_button_UIBox = (is_custom_button and UIBox({ definition = target_button, config = {}, })) or target_button target_button_definition = (is_custom_button and target_button) or (is_shop_button and target_button.definition) or target_button.definition.nodes[1] local check, button = Handy.fake_events.check({ func = G.FUNCS[target_button_definition.config.func], button = nil, id = target_button_definition.config.id, card = card, UIBox = target_button_UIBox, }) if check then Handy.fake_events.execute({ func = G.FUNCS[button or target_button_definition.config.button], button = nil, id = target_button_definition.config.id, card = card, UIBox = target_button_UIBox, }) cleanup() return true end end cleanup() return false end, use = function(card) if card.ability and card.ability.handy_dangerous_actions_used then return true end local actions = Handy.insta_actions.get_actions() return Handy.insta_actions.can_execute(card, actions.buy_or_sell, actions.use) and Handy.insta_actions.execute(card, actions.buy_or_sell, actions.use) or false end, update_state_panel = function(state, key, released) if G.STAGE ~= G.STAGES.RUN then return false end if Handy.config.current.notifications_level < 4 then return false end local result = false local actions = Handy.insta_actions.get_actions() if actions.use then state.items.insta_use = { text = "Quick use", hold = true, order = 10, } result = true end if actions.buy_or_sell then state.items.quick_buy_and_sell = { text = "Quick buy and sell", hold = true, order = 11, } result = true end return result end, } Handy.move_highlight = { dx = { one_left = -1, one_right = 1, }, get_dx = function(key, area) for module_key, module in pairs(Handy.config.current.move_highlight.dx) do if Handy.controller.is_module_key(module, key) then return Handy.move_highlight.dx[module_key] end end return nil end, get_actions = function(key, area) return { swap = Handy.controller.is_module_key_down(Handy.config.current.move_highlight.swap), to_end = Handy.controller.is_module_key_down(Handy.config.current.move_highlight.to_end), } end, can_swap = function(key, area) if not area then return false end return not Handy.utils.table_contains({ G.pack_cards, G.shop_jokers, G.shop_booster, G.shop_vouchers, }, area) end, cen_execute = function(key, area) return not not ( Handy.config.current.move_highlight.enabled and G.STAGE == G.STAGES.RUN and area and area.highlighted and area.highlighted[1] and Handy.utils.table_contains({ G.consumeables, G.jokers, G.cine_quests, G.pack_cards, G.shop_jokers, G.shop_booster, G.shop_vouchers, }, area) ) end, execute = function(key, area) local dx = Handy.move_highlight.get_dx(key, area) if not dx then return false end local current_card = area.highlighted[1] for current_index = #area.cards, 1, -1 do if area.cards[current_index] == current_card then local actions = Handy.move_highlight.get_actions(key, area) local next_index = actions.to_end and (dx > 0 and #area.cards or 1) or ((#area.cards + current_index + dx - 1) % #area.cards) + 1 if current_index == next_index then return end local next_card = area.cards[next_index] if not next_card then return end if actions.swap and Handy.move_highlight.can_swap(key, area) then if actions.to_end or next_index == 1 or next_index == #area.cards then table.remove(area.cards, current_index) table.insert(area.cards, next_index, current_card) else area.cards[next_index] = current_card area.cards[current_index] = next_card end else area:remove_from_highlighted(current_card) area:add_to_highlighted(next_card) end return end end end, use = function(key, area) area = area or Handy.last_clicked_area return Handy.move_highlight.cen_execute(key, area) and Handy.move_highlight.execute(key, area) or false end, update_state_panel = function(state, key, released) end, } Handy.dangerous_actions = { sell_queue = {}, sell_next_card = function() local card = table.remove(Handy.dangerous_actions.sell_queue, 1) if not card then stop_use() return end G.GAME.STOP_USE = 0 Handy.insta_actions.execute(card, true, false, true) G.E_MANAGER:add_event(Event({ blocking = false, func = function() if card.ability then card.ability.handy_dangerous_actions_used = nil end return true end, })) Handy.dangerous_actions.sell_next_card() end, can_execute = function(card) return G.STAGE == G.STAGES.RUN and Handy.config.current.dangerous_actions.enabled and card and not (card.ability and card.ability.handy_dangerous_actions_used) end, execute = function(card) if Handy.controller.is_module_key_down(Handy.config.current.dangerous_actions.immediate_buy_and_sell) then if Handy.config.current.dangerous_actions.immediate_buy_and_sell.queue.enabled then if not card.ability then card.ability = {} end card.ability.handy_dangerous_actions_used = true table.insert(Handy.dangerous_actions.sell_queue, card) Handy.UI.state_panel.update(nil, nil) return false else local result = Handy.insta_actions.execute(card, true, false) if result then if not card.ability then card.ability = {} end card.ability.handy_dangerous_actions_used = true G.CONTROLLER.locks.selling_card = nil G.CONTROLLER.locks.use = nil G.GAME.STOP_USE = 0 G.E_MANAGER:add_event(Event({ func = function() if card.ability then card.ability.handy_dangerous_actions_used = nil end return true end, })) end return result end end return false end, use = function(card) return Handy.dangerous_actions.can_execute(card) and Handy.dangerous_actions.execute(card) or false end, toggle_queue = function(key, released) if Handy.controller.is_module_key(Handy.config.current.dangerous_actions.immediate_buy_and_sell, key) then if released then Handy.dangerous_actions.sell_next_card() else Handy.dangerous_actions.sell_queue = {} end end end, update_state_panel = function(state, key, released) if G.STAGE ~= G.STAGES.RUN then return false end if not Handy.config.current.dangerous_actions.enabled then return false end if Handy.config.current.notifications_level < 2 then return false end if Handy.controller.is_module_key_down(Handy.config.current.dangerous_actions.immediate_buy_and_sell) then state.dangerous = true state.items.dangerous_hint = { text = "[Unsafe] Bugs can appear!", dangerous = true, hold = true, order = 99999999, } if state.items.quick_buy_and_sell then state.items.quick_buy_and_sell.dangerous = true elseif Handy.insta_actions.get_actions().buy_or_sell then local text = "Quick sell" if Handy.config.current.dangerous_actions.immediate_buy_and_sell.queue.enabled then text = text .. " [" .. #Handy.dangerous_actions.sell_queue .. " in queue]" end state.items.quick_buy_and_sell = { text = text, hold = true, order = 11, dangerous = true, } end return true end return false end, } Handy.speed_multiplier = { value = 1, get_actions = function(key) return { multiply = key == Handy.controller.wheel_to_key_table[1], divide = key == Handy.controller.wheel_to_key_table[2], } end, can_execute = function(key) return Handy.config.current.speed_multiplier.enabled and not G.OVERLAY_MENU and Handy.controller.is_module_key_down(Handy.config.current.speed_multiplier) end, execute = function(key) local actions = Handy.speed_multiplier.get_actions(key) if actions.multiply then Handy.speed_multiplier.multiply() end if actions.divide then Handy.speed_multiplier.divide() end return false end, multiply = function() Handy.speed_multiplier.value = math.min(512, Handy.speed_multiplier.value * 2) end, divide = function() Handy.speed_multiplier.value = math.max(0.001953125, Handy.speed_multiplier.value / 2) end, use = function(key) return Handy.speed_multiplier.can_execute(key) and Handy.speed_multiplier.execute(key) or false end, update_state_panel = function(state, key, released) if not key or not Handy.speed_multiplier.can_execute(key) then return false end if Handy.config.current.notifications_level < 3 then return false end local actions = Handy.speed_multiplier.get_actions(key) if actions.multiply or actions.divide then state.items.change_speed_multiplier = { text = "Game speed multiplier: " .. ( Handy.speed_multiplier.value >= 1 and Handy.speed_multiplier.value or ("1/" .. (1 / Handy.speed_multiplier.value)) ), hold = false, order = 5, } return true end return false end, } Handy.shop_reroll = { can_execute = function(key) return G.STATE == G.STATES.SHOP and Handy.fake_events.check({ func = G.FUNCS.can_reroll, button = "reroll_shop" }) and Handy.controller.is_module_key(Handy.config.current.shop_reroll, key) end, execute = function(key) G.FUNCS.reroll_shop() return false end, use = function(key) return Handy.shop_reroll.can_execute(key) and Handy.shop_reroll.execute(key) or false end, } Handy.play_and_discard = { get_actions = function(key) return { discard = Handy.controller.is_module_key(Handy.config.current.play_and_discard.discard, key), play = Handy.controller.is_module_key(Handy.config.current.play_and_discard.play, key), } end, can_execute = function(play, discard) return not not ( Handy.config.current.play_and_discard.enabled and G.STATE == G.STATES.SELECTING_HAND and ( (discard and Handy.fake_events.check({ func = G.FUNCS.can_discard, })) or (play and Handy.fake_events.check({ func = G.FUNCS.can_play, })) ) ) end, execute = function(play, discard) if discard then Handy.fake_events.execute({ func = G.FUNCS.discard_cards_from_highlighted, }) elseif play then Handy.fake_events.execute({ func = G.FUNCS.play_cards_from_highlighted, }) end return false end, use = function(key) local actions = Handy.play_and_discard.get_actions(key) return Handy.play_and_discard.can_execute(actions.play, actions.discard) and Handy.play_and_discard.execute(actions.play, actions.discard) or false end, } Handy.nopeus_interaction = { is_present = function() return type(Nopeus) == "table" end, get_actions = function(key) return { increase = key == Handy.controller.wheel_to_key_table[1], decrease = key == Handy.controller.wheel_to_key_table[2], } end, can_dangerous = function() return not not ( Handy.config.current.dangerous_actions.enabled and Handy.config.current.dangerous_actions.nopeus_unsafe.enabled ) end, can_execute = function(key) return not not ( Handy.config.current.nopeus_interaction.enabled and Handy.nopeus_interaction.is_present() and not G.OVERLAY_MENU and Handy.controller.is_module_key_down(Handy.config.current.nopeus_interaction) ) end, execute = function(key) local actions = Handy.nopeus_interaction.get_actions(key) if actions.increase then Handy.nopeus_interaction.increase() end if actions.decrease then Handy.nopeus_interaction.decrease() end end, change = function(dx) if not Handy.nopeus_interaction.is_present() then G.SETTINGS.FASTFORWARD = 0 elseif Nopeus.Optimised then G.SETTINGS.FASTFORWARD = math.min( Handy.nopeus_interaction.can_dangerous() and 4 or 3, math.max(0, (G.SETTINGS.FASTFORWARD or 0) + dx) ) else G.SETTINGS.FASTFORWARD = math.min( Handy.nopeus_interaction.can_dangerous() and 3 or 2, math.max(0, (G.SETTINGS.FASTFORWARD or 0) + dx) ) end end, increase = function() Handy.nopeus_interaction.change(1) end, decrease = function() Handy.nopeus_interaction.change(-1) end, use = function(key) return Handy.nopeus_interaction.can_execute(key) and Handy.nopeus_interaction.execute(key) or false end, update_state_panel = function(state, key, released) if not Handy.nopeus_interaction.is_present() then return false end if not key or not Handy.nopeus_interaction.can_execute(key) then return false end local actions = Handy.nopeus_interaction.get_actions(key) if actions.increase or actions.decrease then local states = { Nopeus.Off, Nopeus.Planets, Nopeus.On, Nopeus.Unsafe, } if Nopeus.Optimised then states = { Nopeus.Off, Nopeus.Planets, Nopeus.On, Nopeus.Optimised, Nopeus.Unsafe, } end local is_dangerous = G.SETTINGS.FASTFORWARD == (#states - 1) if is_dangerous then state.dangerous = true if Handy.config.current.notifications_level < 2 then return false end else if Handy.config.current.notifications_level < 3 then return false end end state.items.change_nopeus_fastforward = { text = "Nopeus fast-forward: " .. states[(G.SETTINGS.FASTFORWARD or 0) + 1], hold = false, order = 4, dangerous = is_dangerous, } return true end return false end, } Handy.not_just_yet_interaction = { is_present = function() return G and G.FUNCS and G.FUNCS.njy_endround ~= nil end, can_execute = function(check) return not not ( Handy.not_just_yet_interaction.is_present() and GLOBAL_njy_vanilla_override and G.STATE_COMPLETE and G.buttons and G.buttons.states and G.buttons.states.visible and G.GAME and G.GAME.chips and G.GAME.blind and G.GAME.blind.chips and to_big(G.GAME.chips) >= to_big(G.GAME.blind.chips) ) end, execute = function() stop_use() G.STATE = G.STATES.NEW_ROUND end_round() end, use = function(key, released) if Handy.controller.is_module_key(Handy.config.current.not_just_yet_interaction, key) then GLOBAL_njy_vanilla_override = not released end return false end, update = function() if not Handy.config.current.not_just_yet_interaction.enabled then GLOBAL_njy_vanilla_override = nil end return Handy.not_just_yet_interaction.can_execute() and Handy.not_just_yet_interaction.execute() or false end, } -- -- Handy.UI = { counter = 1, C = { TEXT = HEX("FFFFFF"), BLACK = HEX("000000"), RED = HEX("FF0000"), DYN_BASE_APLHA = { CONTAINER = 0.6, TEXT = 1, TEXT_DANGEROUS = 1, }, DYN = { CONTAINER = HEX("000000"), TEXT = HEX("FFFFFF"), TEXT_DANGEROUS = HEX("FFEEEE"), }, }, state_panel = { element = nil, title = nil, items = nil, previous_state = { dangerous = false, title = {}, items = {}, sub_items = {}, hold = false, }, current_state = { dangerous = false, title = {}, items = {}, sub_items = {}, hold = false, }, get_definition = function() local state_panel = Handy.UI.state_panel local items_raw = {} for _, item in pairs(state_panel.current_state.items) do table.insert(items_raw, item) end table.sort(items_raw, function(a, b) return a.order < b.order end) local items = {} for _, item in ipairs(items_raw) do table.insert(items, { n = G.UIT.R, config = { align = "cm", padding = 0.035, }, nodes = { { n = G.UIT.T, config = { text = item.text, scale = 0.225, colour = item.dangerous and Handy.UI.C.DYN.TEXT_DANGEROUS or Handy.UI.C.DYN.TEXT, shadow = true, }, }, }, }) end return { n = G.UIT.ROOT, config = { align = "cm", padding = 0.1, r = 0.1, colour = G.C.CLEAR, id = "handy_state_panel" }, nodes = { { n = G.UIT.C, config = { align = "cm", padding = 0.125, r = 0.1, colour = Handy.UI.C.DYN.CONTAINER, }, nodes = { { n = G.UIT.R, config = { align = "cm", }, nodes = { { n = G.UIT.T, config = { text = state_panel.current_state.title.text, scale = 0.3, colour = Handy.UI.C.DYN.TEXT, shadow = true, id = "handy_state_title", }, }, }, }, { n = G.UIT.R, config = { align = "cm", }, nodes = { { n = G.UIT.C, config = { align = "cm", id = "handy_state_items", }, nodes = items, }, }, }, }, }, }, } end, emplace = function() if Handy.UI.state_panel.element then Handy.UI.state_panel.element:remove() end local element = UIBox({ definition = Handy.UI.state_panel.get_definition(), config = { instance_type = "ALERT", align = "cm", major = G.ROOM_ATTACH, can_collide = false, offset = { x = 0, y = 3.5, }, }, }) Handy.UI.state_panel.element = element Handy.UI.state_panel.title = element:get_UIE_by_ID("handy_state_title") Handy.UI.state_panel.items = element:get_UIE_by_ID("handy_state_items") end, update = function(key, released) local state_panel = Handy.UI.state_panel local state = { dangerous = false, title = {}, items = {}, sub_items = {}, } local is_changed = false for _, part in ipairs({ Handy.speed_multiplier, Handy.insta_booster_skip, Handy.insta_cash_out, Handy.insta_actions, Handy.insta_highlight, Handy.move_highlight, Handy.nopeus_interaction, Handy.dangerous_actions, }) do local temp_result = part.update_state_panel(state, key, released) is_changed = is_changed or temp_result or false end if is_changed then if state.dangerous then state.title.text = "Dangerous actions" else state.title.text = "Quick actions" end for _, item in pairs(state.items) do if item.hold then state.hold = true end end local color = Handy.UI.C.DYN.CONTAINER local target_color = state.dangerous and Handy.UI.C.RED or Handy.UI.C.BLACK color[1] = target_color[1] color[2] = target_color[2] color[3] = target_color[3] Handy.UI.counter = 0 state_panel.previous_state = state_panel.current_state state_panel.current_state = state state_panel.emplace() else state_panel.current_state.hold = false end end, }, update = function(dt) if Handy.UI.state_panel.current_state.hold then Handy.UI.counter = 0 elseif Handy.UI.counter < 1 then Handy.UI.counter = Handy.UI.counter + dt end local multiplier = math.min(1, math.max(0, (1 - Handy.UI.counter) * 2)) for key, color in pairs(Handy.UI.C.DYN) do color[4] = (Handy.UI.C.DYN_BASE_APLHA[key] or 1) * multiplier end end, } function Handy.UI.init() Handy.UI.counter = 1 Handy.UI.state_panel.emplace() Handy.UI.update(0) end -- local love_update_ref = love.update function love.update(dt, ...) love_update_ref(dt, ...) Handy.controller.process_update(dt) end local wheel_moved_ref = love.wheelmoved or function() end function love.wheelmoved(x, y) wheel_moved_ref(x, y) Handy.controller.process_wheel(y > 0 and 1 or 2) end -- function Handy.emplace_steamodded() Handy.current_mod = SMODS.current_mod Handy.config.current = Handy.utils.table_merge({}, Handy.config.default, SMODS.current_mod.config) Handy.current_mod.extra_tabs = function() return { { label = "Overall", tab_definition_function = function() return Handy.UI.get_config_tab("Overall") end, }, { label = "Interactions", tab_definition_function = function() return Handy.UI.get_config_tab("Interactions") end, }, { label = "Dangerous", tab_definition_function = function() return Handy.UI.get_config_tab("Dangerous") end, }, { label = "Keybinds", tab_definition_function = function() return Handy.UI.get_config_tab("Keybinds") end, }, { label = "More keybinds", tab_definition_function = function() return Handy.UI.get_config_tab("Keybinds 2") end, }, } end G.E_MANAGER:add_event(Event({ func = function() G.njy_keybind = nil return true end, })) end function G.FUNCS.handy_toggle_module_enabled(arg, module) if not module then return end module.enabled = arg if module == Handy.config.current.speed_multiplier then Handy.speed_multiplier.value = 1 elseif module == Handy.config.current.dangerous_actions or module == Handy.config.current.nopeus_interaction or module == Handy.config.current.dangerous_actions.nopeus_unsafe then Handy.nopeus_interaction.change(0) end Handy.config.save() end function G.FUNCS.handy_change_notifications_level(arg) Handy.config.current.notifications_level = arg.to_key Handy.config.save() end function G.FUNCS.handy_init_keybind_change(e) Handy.controller.init_bind(e) end Handy.UI.PARTS = { create_module_checkbox = function(module, label, text_prefix, text_lines, skip_keybinds) local desc_lines = { { n = G.UIT.R, config = { minw = 5.25 } }, } if skip_keybinds then table.insert(desc_lines, { n = G.UIT.R, config = { padding = 0.025 }, nodes = { { n = G.UIT.T, config = { text = text_prefix .. " " .. text_lines[1], scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, }, }) else local key_desc = module.key_2 and { { n = G.UIT.T, config = { text = text_prefix .. " [", scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, { n = G.UIT.T, config = { ref_table = module, ref_value = "key_1", scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, { n = G.UIT.T, config = { text = "] or [", scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, { n = G.UIT.T, config = { ref_table = module, ref_value = "key_2", scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, { n = G.UIT.T, config = { text = "] " .. text_lines[1], scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, } or { { n = G.UIT.T, config = { text = text_prefix .. " [", scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, { n = G.UIT.T, config = { ref_table = module, ref_value = "key_1", scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, { n = G.UIT.T, config = { text = "] " .. text_lines[1], scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, } table.insert(desc_lines, { n = G.UIT.R, config = { padding = 0.025 }, nodes = key_desc, }) end for i = 2, #text_lines do table.insert(desc_lines, { n = G.UIT.R, config = { padding = 0.025 }, nodes = { { n = G.UIT.T, config = { text = text_lines[i], scale = 0.3, colour = G.C.TEXT_LIGHT, }, }, }, }) end local label_lines = {} if type(label) == "string" then label = { label } end for i = 1, #label do table.insert(label_lines, { n = G.UIT.R, config = { minw = 2.75 }, nodes = { { n = G.UIT.T, config = { text = label[i], scale = 0.4, colour = G.C.WHITE, }, }, }, }) end return { n = G.UIT.R, config = { align = "cm" }, nodes = { { n = G.UIT.C, config = { align = "cm" }, nodes = label_lines, }, { n = G.UIT.C, config = { align = "cm" }, nodes = { create_toggle({ callback = function(b) return G.FUNCS.handy_toggle_module_enabled(b, module) end, label_scale = 0.4, label = "", ref_table = module, ref_value = "enabled", w = 0, }), }, }, { n = G.UIT.C, config = { minw = 0.1 }, }, { n = G.UIT.C, config = { align = "cm" }, nodes = desc_lines, }, }, } end, create_module_section = function(label) return { n = G.UIT.R, config = { align = "cm", padding = 0.1 }, nodes = { { n = G.UIT.T, config = { text = label, colour = G.C.WHITE, scale = 0.4, align = "cm" }, }, }, } end, create_module_keybind = function(module, label, plus, dangerous) return { n = G.UIT.R, config = { align = "cm", padding = 0.05 }, nodes = { { n = G.UIT.C, config = { align = "c", minw = 4 }, nodes = { { n = G.UIT.T, config = { text = label, colour = G.C.WHITE, scale = 0.35 }, }, }, }, { n = G.UIT.C, config = { align = "cm", minw = 0.75 }, }, UIBox_button({ label = { module.key_1 or "None" }, col = true, colour = dangerous and G.C.MULT or G.C.CHIPS, scale = 0.35, minw = 2.75, minh = 0.45, ref_table = { module = module, key = "key_1", }, button = "handy_init_keybind_change", }), { n = G.UIT.C, config = { align = "cm", minw = 0.6 }, nodes = { { n = G.UIT.T, config = { text = plus and "+" or "or", colour = G.C.WHITE, scale = 0.3 }, }, }, }, UIBox_button({ label = { module.key_2 or "None" }, col = true, colour = dangerous and G.C.MULT or G.C.CHIPS, scale = 0.35, minw = 2.75, minh = 0.45, ref_table = { module = module, key = "key_2", }, button = "handy_init_keybind_change", }), }, } end, } Handy.UI.get_config_tab_overall = function() return { { n = G.UIT.R, config = { padding = 0.05, align = "cm" }, nodes = { create_option_cycle({ minw = 3, label = "Notifications level", scale = 0.8, options = { "None", "Dangerous", "Game state", "All", }, opt_callback = "handy_change_notifications_level", current_option = Handy.config.current.notifications_level, }), }, }, { n = G.UIT.R, config = { padding = 0.05 }, nodes = {} }, { n = G.UIT.R, nodes = { { n = G.UIT.C, nodes = { Handy.UI.PARTS.create_module_checkbox( Handy.config.current.insta_highlight, "Quick Highlight", "Hold [Left Mouse]", { "and", "hover cards in hand to highlight", }, true ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.insta_buy_or_sell, "Quick Buy/Sell", "Hold", { "to", "buy or sell card on Left-Click", "instead of selection", } ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox(Handy.config.current.insta_use, "Quick use", "Hold", { "to", "use (if possible) card on Left-Click", "instead of selection", "(overrides Quick Buy/Sell)", }), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.move_highlight, "Move highlight", "Press", { "[" .. tostring(Handy.config.current.move_highlight.dx.one_left.key_1) .. "] or [" .. tostring(Handy.config.current.move_highlight.dx.one_right.key_1) .. "]", "to move highlight in card area.", "Hold [" .. tostring(Handy.config.current.move_highlight.swap.key_1) .. "] to move card instead.", "Hold [" .. tostring(Handy.config.current.move_highlight.to_end.key_1) .. "] to move to first/last card", }, true ), }, }, { n = G.UIT.C, config = { minw = 4 }, nodes = { Handy.UI.PARTS.create_module_checkbox( Handy.config.current.insta_cash_out, "Quick Cash Out", "Press", { "to", "speedup animation and", "skip Cash Out stage", } ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.insta_booster_skip, { "Quick skip", "Booster Packs" }, "Hold", { "to", "skip booster pack", } ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.speed_multiplier, "Speed Multiplier", "Hold", { "and", "[Wheel Up] to multiply or", "[Wheel Down] to divide game speed", } ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.shop_reroll, "Shop Reroll", "Press", { "to", "reroll a shop", } ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.play_and_discard, "Play/Discard", "Press", { "[" .. tostring(Handy.config.current.play_and_discard.play.key_1) .. "] to play a hand", "or [" .. tostring(Handy.config.current.play_and_discard.discard.key_1) .. "] to discard", }, true ), }, }, }, }, } end Handy.UI.get_config_tab_interactions = function() return { { n = G.UIT.R, nodes = { { n = G.UIT.C, nodes = { Handy.UI.PARTS.create_module_checkbox( Handy.config.current.nopeus_interaction, { "Nopeus:", "fast-forward" }, "Hold", { "and", "[Wheel Up] to increase or", "[Wheel Down] to decrease", "fast-forward setting", } ), { n = G.UIT.R, config = { minh = 0.25 }, }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.not_just_yet_interaction, { "NotJustYet:", "End round" }, "Press", { "to", "end round", } ), }, }, }, }, } end Handy.UI.get_config_tab_dangerous = function() return { -- { -- n = G.UIT.R, -- config = { padding = 0.05, align = "cm" }, -- nodes = { -- }, -- }, -- { n = G.UIT.R, config = { padding = 0.05 }, nodes = {} }, { n = G.UIT.R, nodes = { { n = G.UIT.C, nodes = { Handy.UI.PARTS.create_module_checkbox( Handy.config.current.dangerous_actions, { "Dangerous", "actions" }, "Enable", { "unsafe controls. They're", "designed to be speed-first,", "which can cause bugs or crashes", }, true ), { n = G.UIT.R, config = { minh = 0.5 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.dangerous_actions.immediate_buy_and_sell, "Instant Sell", "Hold", { "to", "sell card on hover", "very fast", } ), { n = G.UIT.R, config = { minh = 0.1 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.dangerous_actions.immediate_buy_and_sell.queue, "Sell Queue", "Start", { "selling cards only when", "keybind was released", }, true ), { n = G.UIT.R, config = { minh = 0.25 } }, Handy.UI.PARTS.create_module_checkbox( Handy.config.current.dangerous_actions.nopeus_unsafe, { "Nopeus: Unsafe", "fast-forward" }, "Allow", { "increase fast-forward", 'setting to "Unsafe"', }, true ), }, }, }, }, } end Handy.UI.get_config_tab_keybinds = function() return { Handy.UI.PARTS.create_module_section("Quick Actions"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_buy_or_sell, "Quick Buy/Sell"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_use, "Quick Use"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_cash_out, "Quick Cash Out"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.insta_booster_skip, "Quick skip Booster Packs"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.shop_reroll, "Shop reroll"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.play_and_discard.play, "Play hand"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.play_and_discard.discard, "Discard"), Handy.UI.PARTS.create_module_keybind( Handy.config.current.dangerous_actions.immediate_buy_and_sell, "Instant Buy/Sell", false, true ), Handy.UI.PARTS.create_module_keybind(Handy.config.current.not_just_yet_interaction, "NotJustYet: End round"), } end Handy.UI.get_config_tab_keybinds_2 = function() return { Handy.UI.PARTS.create_module_section("Game state"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.speed_multiplier, "Speed Multiplier"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.nopeus_interaction, "Nopeus: fast-forward"), Handy.UI.PARTS.create_module_section("Move highlight"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.dx.one_left, "Move one left"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.dx.one_right, "Move one right"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.swap, "Move card"), Handy.UI.PARTS.create_module_keybind(Handy.config.current.move_highlight.to_end, "Move to end"), } end Handy.UI.get_config_tab = function(_tab) local result = { n = G.UIT.ROOT, config = { align = "cm", padding = 0.05, colour = G.C.CLEAR, minh = 5, minw = 5 }, nodes = {}, } if _tab == "Overall" then result.nodes = Handy.UI.get_config_tab_overall() elseif _tab == "Interactions" then result.nodes = Handy.UI.get_config_tab_interactions() elseif _tab == "Dangerous" then result.nodes = Handy.UI.get_config_tab_dangerous() elseif _tab == "Keybinds" then result.nodes = Handy.UI.get_config_tab_keybinds() elseif _tab == "Keybinds 2" then result.nodes = Handy.UI.get_config_tab_keybinds_2() end return result end --- STEAMODDED CORE --- MODULE CORE SMODS = {} MODDED_VERSION = require'SMODS.version' SMODS.id = 'Steamodded' SMODS.version = MODDED_VERSION:gsub('%-STEAMODDED', '') SMODS.can_load = true SMODS.meta_mod = true -- Include lovely and nativefs modules local nativefs = require "nativefs" local lovely = require "lovely" local json = require "json" local lovely_mod_dir = lovely.mod_dir:gsub("/$", "") NFS = nativefs -- make lovely_mod_dir an absolute path. -- respects symlink/.. combos NFS.setWorkingDirectory(lovely_mod_dir) lovely_mod_dir = NFS.getWorkingDirectory() -- make sure NFS behaves the same as love.filesystem NFS.setWorkingDirectory(love.filesystem.getSaveDirectory()) JSON = json local function set_mods_dir() local love_dirs = { love.filesystem.getSaveDirectory(), love.filesystem.getSourceBaseDirectory() } for _, love_dir in ipairs(love_dirs) do if lovely_mod_dir:sub(1, #love_dir) == love_dir then -- relative path from love_dir SMODS.MODS_DIR = lovely_mod_dir:sub(#love_dir+2) if nfs_success then -- make sure NFS behaves the same as love.filesystem. -- not perfect: NFS won't read from both getSaveDirectory() -- and getSourceBaseDirectory() NFS.setWorkingDirectory(love_dir) end return end end SMODS.MODS_DIR = lovely_mod_dir end set_mods_dir() local function find_self(directory, target_filename, target_line, depth) depth = depth or 1 if depth > 3 then return end for _, filename in ipairs(NFS.getDirectoryItems(directory)) do local file_path = directory .. "/" .. filename local file_type = NFS.getInfo(file_path).type if file_type == 'directory' or file_type == 'symlink' then local f = find_self(file_path, target_filename, target_line, depth+1) if f then return f end elseif filename == target_filename then local first_line = NFS.read(file_path):match('^(.-)\n') if first_line == target_line then -- use parent directory return directory:match('^(.+/)') end end end end SMODS.path = find_self(SMODS.MODS_DIR, 'core.lua', '--- STEAMODDED CORE') Cartomancer_nfs_read = NFS.read NFS.read = Cartomancer_nfs_read_override for _, path in ipairs { "src/ui.lua", "src/index.lua", "src/utils.lua", "src/overrides.lua", "src/game_object.lua", "src/logging.lua", "src/compat_0_9_8.lua", "src/loader.lua", } do assert(load(NFS.read(SMODS.path..path), ('=[SMODS _ "%s"]'):format(path)))() end local lovely = require("lovely") local nativefs = require("nativefs") if not nativefs.getInfo(lovely.mod_dir .. "/Talisman") then error( 'Could not find proper Talisman folder.\nPlease make sure the folder for Talisman is named exactly "Talisman" and not "Talisman-main" or anything else.') end Talisman = {config_file = {disable_anims = true, break_infinity = "omeganum", score_opt_id = 2}} if nativefs.read(lovely.mod_dir.."/Talisman/config.lua") then Talisman.config_file = STR_UNPACK(nativefs.read(lovely.mod_dir.."/Talisman/config.lua")) if Talisman.config_file.break_infinity and type(Talisman.config_file.break_infinity) ~= 'string' then Talisman.config_file.break_infinity = "omeganum" end end if not SMODS or not JSON then local createOptionsRef = create_UIBox_options function create_UIBox_options() contents = createOptionsRef() local m = UIBox_button({ minw = 5, button = "talismanMenu", label = { "Talisman" }, colour = G.C.GOLD }) table.insert(contents.nodes[1].nodes[1].nodes[1].nodes, #contents.nodes[1].nodes[1].nodes[1].nodes + 1, m) return contents end end Talisman.config_tab = function() tal_nodes = {{n=G.UIT.R, config={align = "cm"}, nodes={ {n=G.UIT.O, config={object = DynaText({string = "Select features to enable:", colours = {G.C.WHITE}, shadow = true, scale = 0.4})}}, }},create_toggle({label = "Disable Scoring Animations", ref_table = Talisman.config_file, ref_value = "disable_anims", callback = function(_set_toggle) nativefs.write(lovely.mod_dir .. "/Talisman/config.lua", STR_PACK(Talisman.config_file)) end}), create_option_cycle({ label = "Score Limit (requires game restart)", scale = 0.8, w = 6, options = {"Vanilla (e308)", "BigNum (ee308)", "OmegaNum (e10##1000)"}, opt_callback = 'talisman_upd_score_opt', current_option = Talisman.config_file.score_opt_id, })} return { n = G.UIT.ROOT, config = { emboss = 0.05, minh = 6, r = 0.1, minw = 10, align = "cm", padding = 0.2, colour = G.C.BLACK }, nodes = tal_nodes } end G.FUNCS.talismanMenu = function(e) local tabs = create_tabs({ snap_to_nav = true, tabs = { { label = "Talisman", chosen = true, tab_definition_function = Talisman.config_tab }, }}) G.FUNCS.overlay_menu{ definition = create_UIBox_generic_options({ back_func = "options", contents = {tabs} }), config = {offset = {x=0,y=10}} } end G.FUNCS.talisman_upd_score_opt = function(e) Talisman.config_file.score_opt_id = e.to_key local score_opts = {"", "bignumber", "omeganum"} Talisman.config_file.break_infinity = score_opts[e.to_key] nativefs.write(lovely.mod_dir .. "/Talisman/config.lua", STR_PACK(Talisman.config_file)) end if Talisman.config_file.break_infinity then Big, err = nativefs.load(lovely.mod_dir.."/Talisman/big-num/"..Talisman.config_file.break_infinity..".lua") if not err then Big = Big() else Big = nil end Notations = nativefs.load(lovely.mod_dir.."/Talisman/big-num/notations.lua")() -- We call this after init_game_object to leave room for mods that add more poker hands Talisman.igo = function(obj) for _, v in pairs(obj.hands) do v.chips = to_big(v.chips) v.mult = to_big(v.mult) v.s_chips = to_big(v.s_chips) v.s_mult = to_big(v.s_mult) v.l_chips = to_big(v.l_chips) v.l_mult = to_big(v.l_mult) end return obj end local nf = number_format function number_format(num, e_switch_point) if type(num) == 'table' then num = to_big(num) G.E_SWITCH_POINT = G.E_SWITCH_POINT or 100000000000 if num < to_big(e_switch_point or G.E_SWITCH_POINT) then return nf(num:to_number(), e_switch_point) else return Notations.Balatro:format(num, 3) end else return nf(num, e_switch_point) end end local mf = math.floor function math.floor(x) if type(x) == 'table' then return x:floor() end return mf(x) end local l10 = math.log10 function math.log10(x) if type(x) == 'table' then return l10(math.min(x:to_number(),1e300)) end--x:log10() end return l10(x) end local lg = math.log function math.log(x, y) if not y then y = 2.718281828459045 end if type(x) == 'table' then return lg(math.min(x:to_number(),1e300),y) end --x:log(y) end return lg(x,y) end if SMODS then function SMODS.get_blind_amount(ante) local k = to_big(0.75) local scale = G.GAME.modifiers.scaling local amounts = { to_big(300), to_big(700 + 100*scale), to_big(1400 + 600*scale), to_big(2100 + 2900*scale), to_big(15000 + 5000*scale*math.log(scale)), to_big(12000 + 8000*(scale+1)*(0.4*scale)), to_big(10000 + 25000*(scale+1)*((scale/4)^2)), to_big(50000 * (scale+1)^2 * (scale/7)^2) } if ante < 1 then return to_big(100) end if ante <= 8 then local amount = amounts[ante] if (amount:lt(R.E_MAX_SAFE_INTEGER)) then local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number() amount = math.floor(amount / exponent):to_number() * exponent end amount:normalize() return amount end local a, b, c, d = amounts[8], amounts[8]/amounts[7], ante-8, 1 + 0.2*(ante-8) local amount = math.floor(a*(b + (b*k*c)^d)^c) if (amount:lt(R.E_MAX_SAFE_INTEGER)) then local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number() amount = math.floor(amount / exponent):to_number() * exponent end amount:normalize() return amount end end -- There's too much to override here so we just fully replace this function -- Note that any ante scaling tweaks will need to manually changed... local gba = get_blind_amount function get_blind_amount(ante) if G.GAME.modifiers.scaling and G.GAME.modifiers.scaling > 3 then return SMODS.get_blind_amount(ante) end if type(to_big(1)) == 'number' then return gba(ante) end local k = to_big(0.75) if not G.GAME.modifiers.scaling or G.GAME.modifiers.scaling == 1 then local amounts = { to_big(300), to_big(800), to_big(2000), to_big(5000), to_big(11000), to_big(20000), to_big(35000), to_big(50000) } if ante < 1 then return to_big(100) end if ante <= 8 then return amounts[ante] end local a, b, c, d = amounts[8],1.6,ante-8, 1 + 0.2*(ante-8) local amount = a*(b+(k*c)^d)^c if (amount:lt(R.E_MAX_SAFE_INTEGER)) then local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number() amount = math.floor(amount / exponent):to_number() * exponent end amount:normalize() return amount elseif G.GAME.modifiers.scaling == 2 then local amounts = { to_big(300), to_big(900), to_big(2600), to_big(8000), to_big(20000), to_big(36000), to_big(60000), to_big(100000) --300, 900, 2400, 7000, 18000, 32000, 56000, 90000 } if ante < 1 then return to_big(100) end if ante <= 8 then return amounts[ante] end local a, b, c, d = amounts[8],1.6,ante-8, 1 + 0.2*(ante-8) local amount = a*(b+(k*c)^d)^c if (amount:lt(R.E_MAX_SAFE_INTEGER)) then local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number() amount = math.floor(amount / exponent):to_number() * exponent end amount:normalize() return amount elseif G.GAME.modifiers.scaling == 3 then local amounts = { to_big(300), to_big(1000), to_big(3200), to_big(9000), to_big(25000), to_big(60000), to_big(110000), to_big(200000) --300, 1000, 3000, 8000, 22000, 50000, 90000, 180000 } if ante < 1 then return to_big(100) end if ante <= 8 then return amounts[ante] end local a, b, c, d = amounts[8],1.6,ante-8, 1 + 0.2*(ante-8) local amount = a*(b+(k*c)^d)^c if (amount:lt(R.E_MAX_SAFE_INTEGER)) then local exponent = to_big(10)^(math.floor(amount:log10() - to_big(1))):to_number() amount = math.floor(amount / exponent):to_number() * exponent end amount:normalize() return amount end end function check_and_set_high_score(score, amt) if G.GAME.round_scores[score] and to_big(math.floor(amt)) > to_big(G.GAME.round_scores[score].amt) then G.GAME.round_scores[score].amt = to_big(math.floor(amt)) end if G.GAME.seeded then return end --[[if G.PROFILES[G.SETTINGS.profile].high_scores[score] and math.floor(amt) > G.PROFILES[G.SETTINGS.profile].high_scores[score].amt then if G.GAME.round_scores[score] then G.GAME.round_scores[score].high_score = true end G.PROFILES[G.SETTINGS.profile].high_scores[score].amt = math.floor(amt) G:save_settings() end--]] --going to hold off on modifying this until proper save loading exists end local sn = scale_number function scale_number(number, scale, max, e_switch_point) if not Big then return sn(number, scale, max, e_switch_point) end scale = to_big(scale) G.E_SWITCH_POINT = G.E_SWITCH_POINT or 100000000000 if not number or not is_number(number) then return scale end if not max then max = 10000 end if to_big(number).e and to_big(number).e == 10^1000 then scale = scale*math.floor(math.log(max*10, 10))/7 end if to_big(number) >= to_big(e_switch_point or G.E_SWITCH_POINT) then if (to_big(to_big(number):log10()) <= to_big(999)) then scale = scale*math.floor(math.log(max*10, 10))/math.floor(math.log(1000000*10, 10)) else scale = scale*math.floor(math.log(max*10, 10))/math.floor(math.max(7,string.len(number_format(number))-1)) end elseif to_big(number) >= to_big(max) then scale = scale*math.floor(math.log(max*10, 10))/math.floor(math.log(number*10, 10)) end return math.min(3, scale:to_number()) end local tsj = G.FUNCS.text_super_juice function G.FUNCS.text_super_juice(e, _amount) if _amount > 2 then _amount = 2 end return tsj(e, _amount) end local max = math.max --don't return a Big unless we have to - it causes nativefs to break function math.max(x, y) if type(x) == 'table' or type(y) == 'table' then x = to_big(x) y = to_big(y) if (x > y) then return x else return y end else return max(x,y) end end local min = math.min function math.min(x, y) if type(x) == 'table' or type(y) == 'table' then x = to_big(x) y = to_big(y) if (x < y) then return x else return y end else return min(x,y) end end local sqrt = math.sqrt function math.sqrt(x) if type(x) == 'table' then if getmetatable(x) == BigMeta then return x:sqrt() end if getmetatable(x) == OmegaMeta then return x:pow(0.5) end end return sqrt(x) end local old_abs = math.abs function math.abs(x) if type(x) == 'table' then x = to_big(x) if (x < to_big(0)) then return -1 * x else return x end else return old_abs(x) end end end function is_number(x) if type(x) == 'number' then return true end if type(x) == 'table' and ((x.e and x.m) or (x.array and x.sign)) then return true end return false end function to_big(x, y) if Big and Big.m then return Big:new(x,y) elseif Big and Big.array then local result = Big:create(x) result.sign = y or result.sign or x.sign or 1 return result elseif is_number(x) then return x * 10^(y or 0) elseif type(x) == "nil" then return 0 else if ((#x>=2) and ((x[2]>=2) or (x[2]==1) and (x[1]>308))) then return 1e309 end if (x[2]==1) then return math.pow(10,x[1]) end return x[1]*(y or 1); end end function to_number(x) if type(x) == 'table' and (getmetatable(x) == BigMeta or getmetatable(x) == OmegaMeta) then return x:to_number() else return x end end --patch to remove animations local cest = card_eval_status_text function card_eval_status_text(a,b,c,d,e,f) if not Talisman.config_file.disable_anims then cest(a,b,c,d,e,f) end end local jc = juice_card function juice_card(x) if not Talisman.config_file.disable_anims then jc(x) end end function tal_uht(config, vals) local col = G.C.GREEN if vals.chips and G.GAME.current_round.current_hand.chips ~= vals.chips then local delta = (is_number(vals.chips) and is_number(G.GAME.current_round.current_hand.chips)) and (vals.chips - G.GAME.current_round.current_hand.chips) or 0 if to_big(delta) < to_big(0) then delta = number_format(delta); col = G.C.RED elseif to_big(delta) > to_big(0) then delta = '+'..number_format(delta) else delta = number_format(delta) end if type(vals.chips) == 'string' then delta = vals.chips end G.GAME.current_round.current_hand.chips = vals.chips if G.hand_text_area.chips.config.object then G.hand_text_area.chips:update(0) end end if vals.mult and G.GAME.current_round.current_hand.mult ~= vals.mult then local delta = (is_number(vals.mult) and is_number(G.GAME.current_round.current_hand.mult))and (vals.mult - G.GAME.current_round.current_hand.mult) or 0 if to_big(delta) < to_big(0) then delta = number_format(delta); col = G.C.RED elseif to_big(delta) > to_big(0) then delta = '+'..number_format(delta) else delta = number_format(delta) end if type(vals.mult) == 'string' then delta = vals.mult end G.GAME.current_round.current_hand.mult = vals.mult if G.hand_text_area.mult.config.object then G.hand_text_area.mult:update(0) end end if vals.handname and G.GAME.current_round.current_hand.handname ~= vals.handname then G.GAME.current_round.current_hand.handname = vals.handname end if vals.chip_total then G.GAME.current_round.current_hand.chip_total = vals.chip_total;G.hand_text_area.chip_total.config.object:pulse(0.5) end if vals.level and G.GAME.current_round.current_hand.hand_level ~= ' '..localize('k_lvl')..tostring(vals.level) then if vals.level == '' then G.GAME.current_round.current_hand.hand_level = vals.level else G.GAME.current_round.current_hand.hand_level = ' '..localize('k_lvl')..tostring(vals.level) if type(vals.level) == 'number' then G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[math.min(vals.level, 7)] else G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[1] end end end return true end local uht = update_hand_text function update_hand_text(config, vals) if Talisman.config_file.disable_anims then if G.latest_uht then local chips = G.latest_uht.vals.chips local mult = G.latest_uht.vals.mult if not vals.chips then vals.chips = chips end if not vals.mult then vals.mult = mult end end G.latest_uht = {config = config, vals = vals} else uht(config, vals) end end local upd = Game.update function Game:update(dt) upd(self, dt) if G.latest_uht and G.latest_uht.config and G.latest_uht.vals then tal_uht(G.latest_uht.config, G.latest_uht.vals) G.latest_uht = nil end if Talisman.dollar_update then G.HUD:get_UIE_by_ID('dollar_text_UI').config.object:update() G.HUD:recalculate() Talisman.dollar_update = false end end --scoring coroutine local oldplay = G.FUNCS.evaluate_play function G.FUNCS.evaluate_play() G.SCORING_COROUTINE = coroutine.create(oldplay) G.LAST_SCORING_YIELD = love.timer.getTime() G.CARD_CALC_COUNTS = {} -- keys = cards, values = table containing numbers local success, err = coroutine.resume(G.SCORING_COROUTINE) if not success then error(err) end end local oldupd = love.update function love.update(dt, ...) oldupd(dt, ...) if G.SCORING_COROUTINE then if collectgarbage("count") > 1024*1024 then collectgarbage("collect") end if coroutine.status(G.SCORING_COROUTINE) == "dead" then G.SCORING_COROUTINE = nil G.FUNCS.exit_overlay_menu() local totalCalcs = 0 for i, v in pairs(G.CARD_CALC_COUNTS) do totalCalcs = totalCalcs + v[1] end G.GAME.LAST_CALCS = totalCalcs else G.SCORING_TEXT = nil if not G.OVERLAY_MENU then G.scoring_text = {"Calculating...", "", "", ""} G.SCORING_TEXT = { {n = G.UIT.C, nodes = { {n = G.UIT.R, config = {padding = 0.1, align = "cm"}, nodes = { {n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 1}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 1, silent = true})}}, }},{n = G.UIT.R, nodes = { {n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 2}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}}, }},{n = G.UIT.R, nodes = { {n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 3}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}}, }},{n = G.UIT.R, nodes = { {n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 4}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}}, }}}}} G.FUNCS.overlay_menu({ definition = {n=G.UIT.ROOT, minw = G.ROOM.T.w*5, minh = G.ROOM.T.h*5, config={align = "cm", padding = 9999, offset = {x = 0, y = -3}, r = 0.1, colour = {G.C.GREY[1], G.C.GREY[2], G.C.GREY[3],0.7}}, nodes= G.SCORING_TEXT}, config = {align="cm", offset = {x=0,y=0}, major = G.ROOM_ATTACH, bond = 'Weak'} }) else if G.OVERLAY_MENU and G.scoring_text then local totalCalcs = 0 for i, v in pairs(G.CARD_CALC_COUNTS) do totalCalcs = totalCalcs + v[1] end local jokersYetToScore = #G.jokers.cards + #G.play.cards - #G.CARD_CALC_COUNTS G.scoring_text[1] = "Calculating..." G.scoring_text[2] = "Elapsed calculations: "..tostring(totalCalcs) G.scoring_text[3] = "Cards yet to score: "..tostring(jokersYetToScore) G.scoring_text[4] = "Calculations last played hand: " .. tostring(G.GAME.LAST_CALCS or "Unknown") end end --this coroutine allows us to stagger GC cycles through --the main source of waste in terms of memory (especially w joker retriggers) is through local variables that become garbage --this practically eliminates the memory overhead of scoring --event queue overhead seems to not exist if Talismans Disable Scoring Animations is off. --event manager has to wait for scoring to finish until it can keep processing events anyways. G.LAST_SCORING_YIELD = love.timer.getTime() local success, msg = coroutine.resume(G.SCORING_COROUTINE) if not success then error(msg) end end end end TIME_BETWEEN_SCORING_FRAMES = 0.03 -- 30 fps during scoring -- we dont want overhead from updates making scoring much slower -- originally 10 fps, I think 30 fps is a good way to balance it while making it look smooth, too --wrap everything in calculating contexts so we can do more things with it Talisman.calculating_joker = false Talisman.calculating_score = false Talisman.calculating_card = false Talisman.dollar_update = false local ccj = Card.calculate_joker function Card:calculate_joker(context) --scoring coroutine G.CURRENT_SCORING_CARD = self G.CARD_CALC_COUNTS = G.CARD_CALC_COUNTS or {} if G.CARD_CALC_COUNTS[self] then G.CARD_CALC_COUNTS[self][1] = G.CARD_CALC_COUNTS[self][1] + 1 else G.CARD_CALC_COUNTS[self] = {1, 1} end if G.LAST_SCORING_YIELD and ((love.timer.getTime() - G.LAST_SCORING_YIELD) > TIME_BETWEEN_SCORING_FRAMES) and coroutine.running() then coroutine.yield() end Talisman.calculating_joker = true local ret = ccj(self, context) if ret and type(ret) == "table" and ret.repetitions then G.CARD_CALC_COUNTS[ret.card] = G.CARD_CALC_COUNTS[ret.card] or {1,1} G.CARD_CALC_COUNTS[ret.card][2] = G.CARD_CALC_COUNTS[ret.card][2] + ret.repetitions end Talisman.calculating_joker = false return ret end local cuc = Card.use_consumable function Card:use_consumable(x,y) Talisman.calculating_score = true local ret = cuc(self, x,y) Talisman.calculating_score = false return ret end local gfep = G.FUNCS.evaluate_play G.FUNCS.evaluate_play = function(e) Talisman.calculating_score = true local ret = gfep(e) Talisman.calculating_score = false return ret end --[[local ec = eval_card function eval_card() Talisman.calculating_card = true local ret = ec() Talisman.calculating_card = false return ret end--]] local sm = Card.start_materialize function Card:start_materialize(a,b,c) if Talisman.config_file.disable_anims and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then return end return sm(self,a,b,c) end local sd = Card.start_dissolve function Card:start_dissolve(a,b,c,d) if Talisman.config_file.disable_anims and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then self:remove() return end return sd(self,a,b,c,d) end local ss = Card.set_seal function Card:set_seal(a,b,immediate) return ss(self,a,b,Talisman.config_file.disable_anims and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) or immediate) end function Card:get_chip_x_bonus() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.x_chips or 0) <= 1 then return 0 end return self.ability.x_chips end function Card:get_chip_e_bonus() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.e_chips or 0) <= 1 then return 0 end return self.ability.e_chips end function Card:get_chip_ee_bonus() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.ee_chips or 0) <= 1 then return 0 end return self.ability.ee_chips end function Card:get_chip_eee_bonus() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.eee_chips or 0) <= 1 then return 0 end return self.ability.eee_chips end function Card:get_chip_hyper_bonus() if self.debuff then return {0,0} end if self.ability.set == 'Joker' then return {0,0} end if type(self.ability.hyper_chips) ~= 'table' then return {0,0} end if (self.ability.hyper_chips[1] <= 0 or self.ability.hyper_chips[2] <= 0) then return {0,0} end return self.ability.hyper_chips end function Card:get_chip_e_mult() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.e_mult or 0) <= 1 then return 0 end return self.ability.e_mult end function Card:get_chip_ee_mult() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.ee_mult or 0) <= 1 then return 0 end return self.ability.ee_mult end function Card:get_chip_eee_mult() if self.debuff then return 0 end if self.ability.set == 'Joker' then return 0 end if (self.ability.eee_mult or 0) <= 1 then return 0 end return self.ability.eee_mult end function Card:get_chip_hyper_mult() if self.debuff then return {0,0} end if self.ability.set == 'Joker' then return {0,0} end if type(self.ability.hyper_mult) ~= 'table' then return {0,0} end if (self.ability.hyper_mult[1] <= 0 or self.ability.hyper_mult[2] <= 0) then return {0,0} end return self.ability.hyper_mult end --Easing fixes --Changed this to always work; it's less pretty but fine for held in hand things local edo = ease_dollars function ease_dollars(mod, instant) if Talisman.config_file.disable_anims then--and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then mod = mod or 0 if mod < 0 then inc_career_stat('c_dollars_earned', mod) end G.GAME.dollars = G.GAME.dollars + mod Talisman.dollar_update = true else return edo(mod, instant) end end local su = G.start_up function safe_str_unpack(str) local chunk, err = loadstring(str) if chunk then setfenv(chunk, {Big = Big, BigMeta = BigMeta, OmegaMeta = OmegaMeta, to_big = to_big, inf = 1.79769e308}) -- Use an empty environment to prevent access to potentially harmful functions local success, result = pcall(chunk) if success then return result else print("Error unpacking string: " .. result) return nil end else print("Error loading string: " .. err) return nil end end function G:start_up() STR_UNPACK = safe_str_unpack su(self) STR_UNPACK = safe_str_unpack end --Skip round animation things local gfer = G.FUNCS.evaluate_round function G.FUNCS.evaluate_round() if Talisman.config_file.disable_anims then if to_big(G.GAME.chips) >= to_big(G.GAME.blind.chips) then add_round_eval_row({dollars = G.GAME.blind.dollars, name='blind1', pitch = 0.95}) else add_round_eval_row({dollars = 0, name='blind1', pitch = 0.95, saved = true}) end local arer = add_round_eval_row add_round_eval_row = function() return end local dollars = gfer() add_round_eval_row = arer add_round_eval_row({name = 'bottom', dollars = Talisman.dollars}) else return gfer() end end --some debugging functions --[[local callstep=0 function printCallerInfo() -- Get debug info for the caller of the function that called printCallerInfo local info = debug.getinfo(3, "Sl") callstep = callstep+1 if info then print("["..callstep.."] "..(info.short_src or "???")..":"..(info.currentline or "unknown")) else print("Caller information not available") end end local emae = EventManager.add_event function EventManager:add_event(x,y,z) printCallerInfo() return emae(self,x,y,z) end--]] require 'cartomancer.init' Cartomancer.path = assert( Cartomancer.find_self('cartomancer.lua'), "Failed to find mod folder. Make sure that `Cartomancer` folder has `cartomancer.lua` file!" ) Cartomancer.load_mod_file('internal/config.lua', 'internal.config') Cartomancer.load_mod_file('internal/atlas.lua', 'internal.atlas') Cartomancer.load_mod_file('internal/ui.lua', 'internal.ui') Cartomancer.load_mod_file('internal/keybinds.lua', 'internal.keybinds') Cartomancer.load_mod_file('core/view-deck.lua', 'core.view-deck') Cartomancer.load_mod_file('core/flames.lua', 'core.flames') Cartomancer.load_mod_file('core/optimizations.lua', 'core.optimizations') Cartomancer.load_mod_file('core/jokers.lua', 'core.jokers') Cartomancer.load_mod_file('core/hand.lua', 'core.hand') Cartomancer.load_config() Cartomancer.INTERNAL_jokers_menu = false -- TODO dedicated keybinds file? keybinds need to load after config Cartomancer.register_keybind { name = 'hide_joker', func = function (controller) Cartomancer.hide_hovered_joker(controller) end } Cartomancer.register_keybind { name = 'toggle_tags', func = function (controller) Cartomancer.SETTINGS.hide_tags = not Cartomancer.SETTINGS.hide_tags Cartomancer.update_tags_visibility() end } Cartomancer.register_keybind { name = 'toggle_consumables', func = function (controller) Cartomancer.SETTINGS.hide_consumables = not Cartomancer.SETTINGS.hide_consumables end } Cartomancer.register_keybind { name = 'toggle_deck', func = function (controller) Cartomancer.SETTINGS.hide_deck = not Cartomancer.SETTINGS.hide_deck end } Cartomancer.register_keybind { name = 'toggle_jokers', func = function (controller) if not (G and G.jokers) then return end G.jokers.cart_hide_all = not G.jokers.cart_hide_all if G.jokers.cart_hide_all then Cartomancer.hide_all_jokers() else Cartomancer.show_all_jokers() end Cartomancer.align_G_jokers() end } Cartomancer.register_keybind { name = 'toggle_jokers_buttons', func = function (controller) Cartomancer.SETTINGS.jokers_controls_buttons = not Cartomancer.SETTINGS.jokers_controls_buttons end }