balatro-mods/DebugPlus/debugplus/watcher.lua
2025-03-25 03:09:00 +08:00

322 lines
10 KiB
Lua

local logger = require("debugplus.logger")
local modtime
local global = {}
local event
local file
local running = false
local currentType
-- For edition type
local editionIndex
local function genSafeFunc(name, fn)
return function(...)
if not fn or type(fn) ~= "function" then
return
end
local res = {pcall(fn, ...)}
local succ = table.remove(res, 1)
if not succ then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Center function \"" .. name .. "\" errored:", unpack(res))
return
end
return unpack(res)
end
end
local function evalLuaFile(content)
local fn, err = load(content, "@" .. file)
if not fn then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Error Loading File:", err)
return false
end
local succ, err = pcall(fn)
if not succ then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Error Running File:", err)
return false
end
return true, err
end
local function showTabOverlay(definition, tabName)
tabName = tabName or "Tab"
return G.FUNCS.overlay_menu({
definition = create_UIBox_generic_options({
contents = {{
n = G.UIT.R,
nodes = {create_tabs({
snap_to_nav = true,
colour = G.C.BOOSTER,
tabs = {{
label = tabName,
chosen = true,
tab_definition_function = function()
return definition
end
}}
})}
}}
})
})
end
local types = {
lua = {
desc = "Starts watching the lua file provided.",
run = function(content)
return evalLuaFile(content)
end,
},
config_tab = {
desc = "Starts watching the lua file provided. The returned value is rendered like a config tab (such as the one in SMODS.current_mod.config_tab). Note that invalid tabs will likely crash the game.",
run = function(content)
local success, res = evalLuaFile(content)
if not success then return false end
if type(res) ~= "table" or next(res) == nil then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Config tab doesn't look valid. Not rendering to prevent a crash. Make sure you're returning something.")
return
end
showTabOverlay(res)
end,
},
shader = {
desc = "Starts watching the the shader file provided. Pops up a ui with a joker to preview the shader on.",
check = function()
if SMODS and SMODS.Shaders and SMODS.Edition then
return true
end
return false, "Steamodded (v1.0.0~+) is necessary to watch shader files."
end,
run = function(content)
local result, shader = pcall(love.graphics.newShader, content)
if not result then
return logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Error Loading Shader:", shader)
end
local name = content:match("extern [%w_]+ vec2 (%w+);");
if not name then
return logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Could not guess name of shader :/. Not applying to avoid crash.")
end
G.SHADERS.debugplus_watcher_shader = shader
SMODS.Shaders.debugplus_watcher_shader = {
original_key = name
}
if not editionIndex then
editionIndex = #G.P_CENTER_POOLS.Edition + 1
end
G.P_CENTER_POOLS.Edition[editionIndex] = {
key = "e_debugplus_watcher_edition",
shader = "debugplus_watcher_shader"
}
-- Make an area with a joker with our editon
local area = CardArea(
G.ROOM.T.x + 0.2*G.ROOM.T.w/2,G.ROOM.T.h,
G.CARD_W,
G.CARD_H,
{card_limit = 5, type = 'title', highlight_limit = 0, deck_height = 0.75, thin_draw = 1}
)
local card = Card(area.T.x + area.T.w/2, area.T.y, G.CARD_W, G.CARD_H,
nil, G.P_CENTERS["j_joker"])
card.edition = {debugplus_watcher_edition = true}
area:emplace(card)
showTabOverlay({
-- ROOT NODE
n = G.UIT.ROOT,
config = {
r = 0.1,
minw = 7,
minh = 5,
align = "tm",
padding = 1,
colour = G.C.BLACK
},
nodes = {{
n = G.UIT.R,
config = {
align = "cm",
padding = 0.07,
no_fill = true,
scale = 1
},
nodes = {{
n = G.UIT.O,
config = {
object = area
}
}}
}}
}, "Shader Test")
return true
end,
cleanup = function()
table.remove(G.P_CENTER_POOLS.Edition, editionIndex)
G.SHADERS.debugplus_watcher_shader = nil
SMODS.Shaders.debugplus_watcher_shader = nil
end
},
center = {
desc = "Starts watching the lua file provided. The returned table is used to modify the center given in the key value. The table is similar to SMODS.Joker and friends.",
check = function() -- Not entirely sure what to all check for here.
if SMODS and SMODS.Joker then
return true
end
return false, "Steamodded (v1.0.0~+) is necessary to watch centers."
end,
run = function(content)
local success, res = evalLuaFile(content)
if not success then return false end
if not res or type(res) ~= "table" then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Center config doesn't look correct. Make sure you are returning an object.")
return
end
if not res.key then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Center config is missing a key.")
return
end
local center = G.P_CENTERS[res.key]
if not center then
logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] The key \"" .. res.key .. "\" does not exist. Make sure your object has been loaded and the key is correct (don't forget the object prefix (e.g. j_) and your mod prefix) you can get the key by hovering over your object and then running `eval dp.hovered.config.center.key`.")
return
end
if res.loc_txt then
local loc_txt = res.loc_txt
local loc = G.localization.descriptions[center.set][res.key]
local loc_changed = false
if loc_txt.name then
if loc_txt.name ~= loc.name then
loc_changed = true
loc.name = loc_txt.name
end
end
if loc_txt.text then
if #loc_txt.text ~= #loc.text then
loc_changed = true
else
for k, v in ipairs(loc_txt.text) do
if v ~= loc.text[k] then
loc_changed = true
break
end
end
end
loc.text = loc_txt.text
end
if loc_changed then
init_localization()
end
end
if res.pos then
center.pos.x = res.pos.x
center.pos.y = res.pos.y
end
for k,v in pairs(res) do
if type(v) ~= "function" then
goto finishfunc
end
center[k] = genSafeFunc(k, v)
::finishfunc::
end
return true
end,
}
}
local function loadFile()
local info = love.filesystem.getInfo(file) or {}
local showReloaded = modtime ~= nil
if info.modtime == modtime then
return
end
modtime = info.modtime
local content = love.filesystem.read(file)
local result, subResult = pcall(currentType.run, content)
if not result then
return logger.handleLog({1, 0, 0}, "ERROR", "[Watcher] Error Running Watcher:", subResult)
end
if showReloaded and subResult then
logger.handleLog({0, 1, 0}, "INFO", "[Watcher] Reloaded")
end
return true
end
local function makeEvent()
event = Event {
blockable = false,
blocking = false,
pause_force = true,
no_delete = true,
trigger = "after",
delay = .5,
timer = "UPTIME",
func = function()
if not running then
return true
end
loadFile()
event.start_timer = false
end
}
end
function global.startWatching(_file, _type)
if not _file then
return nil, "No file"
end
local info = love.filesystem.getInfo(_file)
if not info then
return nil, "File doesn't exist"
end
if not (info.type == "file") then
return nil, "Not a regular file"
end
if not event then makeEvent() end
if running and currentType and currentType.cleanup and type(currentType.cleanup) == "function" then
currentType.cleanup()
end
modtime = nil
file = _file
currentType = types[_type]
if not currentType then
return nil, "Shit's erroring (no type)"
end
if currentType.check and type(currentType.check) == "function" then
local res, msg = currentType.check();
if not res then
return nil, msg or "Pre-check failed!"
end
end
if not running then
running = true
loadFile()
G.E_MANAGER:add_event(event)
end
return true
end
function global.stopWatching()
running = false
if currentType.cleanup and type(currentType.cleanup) == "function" then
currentType.cleanup()
end
end
global.types = types;
global.subCommandDesc = ""
for k,v in pairs(types) do
global.subCommandDesc = global.subCommandDesc .. "watch " .. k .. " [file] - " .. (v.desc or "Wilson forgot to make a description for me.") .. "\n"
end
return global