From ea5675f1a93a42a1afa8b6b405803f78871c2abf Mon Sep 17 00:00:00 2001 From: JamBox <8935453+JamesVanBoxtel@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:24:16 -0700 Subject: [PATCH 1/4] Add Debug Settings Architecture Repost of this commit since it was reverted. The Debug Settings system provides runtime-configurable debug features through a UI overlay. Settings are persisted to config.debug and can be accessed anywhere via the DebugSettings singleton. Also provide a way to generate the debug menu and surface it from a debug button. To support this new button and future overlays, add a root element to the game so we can order overlays Updated Bool Selector to size correctly Updated Style Selector creation to make sure sizing and layout is right Reload main menu when you come back to it Make navigation stack a UIElement so it works with touch handling Fixes #697 --- client/src/BattleRoom.lua | 3 +- client/src/ChallengeModePlayerStack.lua | 3 +- client/src/ClientMatch.lua | 5 +- client/src/Game.lua | 69 ++- client/src/NavigationStack.lua | 36 +- client/src/PlayerStack.lua | 7 +- client/src/Shortcuts.lua | 8 +- client/src/config.lua | 31 +- client/src/debug/DebugMenu.lua | 90 ++++ client/src/debug/DebugSettings.lua | 418 ++++++++++++++++++ client/src/developer.lua | 3 +- client/src/scenes/CharacterSelect.lua | 77 ++-- client/src/scenes/DesignHelper.lua | 2 +- client/src/scenes/EndlessMenu.lua | 4 +- client/src/scenes/GameBase.lua | 5 +- client/src/scenes/MainMenu.lua | 15 +- client/src/scenes/ModManagement.lua | 8 +- client/src/scenes/OptionsMenu.lua | 34 +- client/src/scenes/PortraitGame.lua | 5 +- client/src/scenes/ReplayGame.lua | 3 +- client/src/scenes/Scene.lua | 3 +- client/src/scenes/TimeAttackMenu.lua | 4 +- client/src/ui/BoolSelector.lua | 81 ++-- client/src/ui/Carousel.lua | 3 +- client/src/ui/Grid.lua | 3 +- client/src/ui/GridElement.lua | 3 +- client/src/ui/MenuItem.lua | 19 +- client/src/ui/MultiPlayerSelectionWrapper.lua | 1 + client/src/ui/OverlayContainer.lua | 124 ++++++ client/src/ui/PagedUniGrid.lua | 3 +- client/src/ui/StackPanel.lua | 49 +- client/src/ui/Stepper.lua | 3 +- client/src/ui/touchHandler.lua | 14 +- common/engine/Match.lua | 6 +- docs/DebugSettings.md | 117 +++++ main.lua | 9 +- 36 files changed, 1083 insertions(+), 185 deletions(-) create mode 100644 client/src/debug/DebugMenu.lua create mode 100644 client/src/debug/DebugSettings.lua create mode 100644 client/src/ui/OverlayContainer.lua create mode 100644 docs/DebugSettings.md diff --git a/client/src/BattleRoom.lua b/client/src/BattleRoom.lua index 5d1f1643..0beeb535 100644 --- a/client/src/BattleRoom.lua +++ b/client/src/BattleRoom.lua @@ -12,6 +12,7 @@ local BlackFadeTransition = require("client.src.scenes.Transitions.BlackFadeTran local Easings = require("client.src.Easings") local system = require("client.src.system") local GeneratorSource = require("common.engine.GeneratorSource") +local DebugSettings = require("client.src.debug.DebugSettings") -- A Battle Room is a session of matches, keeping track of the room number, player settings, wins / losses etc ---@class BattleRoom : Signal @@ -383,7 +384,7 @@ function BattleRoom:createScene(match) end -- for touch android players load a different scene - if (system.isMobileOS() or DEBUG_ENABLED) and self.gameScene.name ~= "PuzzleGame" and + if (system.isMobileOS() or DebugSettings.simulateMobileOS()) and self.gameScene.name ~= "PuzzleGame" and --but only if they are the only local player cause for 2p vs local using portrait mode would be bad tableUtils.count(self.players, function(p) return p.isLocal and p.human end) == 1 then for _, player in ipairs(self.players) do diff --git a/client/src/ChallengeModePlayerStack.lua b/client/src/ChallengeModePlayerStack.lua index 91d3bb19..eea1a3be 100644 --- a/client/src/ChallengeModePlayerStack.lua +++ b/client/src/ChallengeModePlayerStack.lua @@ -1,6 +1,7 @@ local class = require("common.lib.class") local ClientStack = require("client.src.ClientStack") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") ---@class ChallengeModePlayerStack : ClientStack ---@field engine SimulatedStack @@ -198,7 +199,7 @@ function ChallengeModePlayerStack:drawMultibar() end function ChallengeModePlayerStack:drawDebug() - if config.debug_mode then + if DebugSettings.showStackDebugInfo() then local drawX = self.frameOriginX + self:canvasWidth() / 2 local drawY = 10 local padding = 14 diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index bf71af74..1574a3b0 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -16,6 +16,7 @@ local Telegraph = require("client.src.graphics.Telegraph") local MatchParticipant = require("client.src.MatchParticipant") local ChallengeModePlayerStack = require("client.src.ChallengeModePlayerStack") local NetworkProtocol = require("common.network.NetworkProtocol") +local DebugSettings = require("client.src.debug.DebugSettings") ---@module "client.src.ChallengeModePlayerStack" ---@class ClientMatch @@ -559,7 +560,7 @@ end function ClientMatch:drawCommunityMessage() -- Draw the community message - if not config.debug_mode then + if not DebugSettings.showStackDebugInfo() then GraphicsUtil.printf(join_community_msg or "", 0, 668, consts.CANVAS_WIDTH, "center") end end @@ -598,7 +599,7 @@ function ClientMatch:render() end end - if config.debug_mode then + if DebugSettings.showStackDebugInfo() then local padding = 14 local drawX = 500 local drawY = -4 diff --git a/client/src/Game.lua b/client/src/Game.lua index 1952ff0c..22f765e5 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -33,6 +33,14 @@ local system = require("client.src.system") local ModController = require("client.src.mods.ModController") local RichPresence = require("client.lib.rich_presence.RichPresence") +local DebugSettings = require("client.src.debug.DebugSettings") +local Button = require("client.src.ui.Button") +local TextButton = require("client.src.ui.TextButton") +local OverlayContainer = require("client.src.ui.OverlayContainer") +local DebugMenu = require("client.src.debug.DebugMenu") +local Label = require("client.src.ui.Label") +local UIElement = require("client.src.ui.UIElement") +local NavigationStack = require("client.src.NavigationStack") -- Provides a scale that is on .5 boundary to make sure it renders well. -- Useful for creating new canvas with a solid DPI @@ -106,12 +114,19 @@ local Game = class( -- time in seconds, can be used by other elements to track the passing of time beyond dt self.timer = love.timer.getTime() + + self.debugOverlay = nil + self.debugButton = nil + + -- Root UI element that contains all UI (scenes + overlays + debug) + self.uiRoot = UIElement({x = 0, y = 0, width = consts.CANVAS_WIDTH, height = consts.CANVAS_HEIGHT}) end ) Game.newCanvasSnappedScale = newCanvasSnappedScale function Game:load() + DebugSettings.init() PuzzleLibrary.cleanupDefaultPuzzles(consts.PUZZLES_SAVE_DIRECTORY) -- move to constructor @@ -131,8 +146,11 @@ function Game:load() self.input:importConfigurations(user_input_conf) end - self.navigationStack = require("client.src.NavigationStack") + self.navigationStack = NavigationStack({}) self.navigationStack:push(StartUp({setupRoutine = self.setupRoutine})) + + -- Add navigation stack to root UI + self.uiRoot:addChild(self.navigationStack) self.globalCanvas = love.graphics.newCanvas(consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT, {dpiscale=GAME:newCanvasSnappedScale()}) end @@ -253,6 +271,8 @@ function Game:setupRoutine() self:initializeLocalPlayer() ModController:loadModFor(characters[GAME.localPlayer.settings.characterId], GAME.localPlayer, true) + + self:initializeDebugOverlay() end -- GAME.localPlayer is the standard player for battleRooms that don't get started from replays/spectate @@ -366,9 +386,9 @@ function Game:update(dt) handleShortcuts() - prof.push("navigationStack update") - self.navigationStack:update(dt) - prof.pop("navigationStack update") + prof.push("uiRoot update") + self.uiRoot:update(dt) + prof.pop("uiRoot update") if self.backgroundImage then self.backgroundImage:update(dt) @@ -386,7 +406,7 @@ function Game:draw() love.graphics.clear() -- With this, self.globalCanvas is clear and set as our active canvas everything is being drawn to - self.navigationStack:draw() + self.uiRoot:draw() self:drawFPS() self:drawScaleInfo() @@ -402,8 +422,7 @@ function Game:draw() end function Game:drawFPS() - -- Draw the FPS if enabled - if self.config.show_fps then + if self.config.show_fps or DebugSettings.forceFPS() then love.graphics.print("FPS: " .. love.timer.getFPS(), 1, 1) end end @@ -666,4 +685,40 @@ function Game:setLanguage(lang_code) Localization:refresh_global_strings() end +function Game:initializeDebugOverlay() + if not DEBUG_ENABLED then + return + end + + self.debugButton = TextButton({ + x = consts.CANVAS_WIDTH - 50, + y = consts.CANVAS_HEIGHT - 50, + label = Label({ + text = "Debug", + translate = false, + hAlign = "center", + vAlign = "center" + }), + width = 40, + height = 40, + onClick = function() + if self.debugOverlay then + if not self.debugOverlay:isActive() then + self.debugOverlay:open() + end + end + end + }) + + local debugMenu = DebugMenu.makeDebugMenu({height = consts.CANVAS_HEIGHT - 40}) + self.debugOverlay = OverlayContainer({ + content = debugMenu + }) + + -- Add debug UI to root + self.uiRoot:addChild(self.debugButton) + self.uiRoot:addChild(self.debugOverlay) +end + + return Game diff --git a/client/src/NavigationStack.lua b/client/src/NavigationStack.lua index e9ef669c..89ff75a4 100644 --- a/client/src/NavigationStack.lua +++ b/client/src/NavigationStack.lua @@ -1,11 +1,23 @@ local DirectTransition = require("client.src.scenes.Transitions.DirectTransition") local logger = require("common.lib.logger") - -local NavigationStack = { - scenes = {}, - transition = nil, - callback = nil, -} +local UIElement = require("client.src.ui.UIElement") +local class = require("common.lib.class") +local consts = require("common.engine.consts") + +---@class NavigationStack : UiElement +---@field scenes Scene[] +---@field transition table? +---@field callback function? +local NavigationStack = class( + function(self) + self.scenes = {} + self.transition = nil + self.callback = nil + self.width = consts.CANVAS_WIDTH + self.height = consts.CANVAS_HEIGHT + end, + UIElement +) function NavigationStack:push(newScene, transition) local activeScene = self.scenes[#self.scenes] @@ -142,7 +154,7 @@ function NavigationStack:getActiveScene() end end -function NavigationStack:update(dt) +function NavigationStack:updateSelf(dt) if self.transition then self.transition:update(dt) @@ -163,7 +175,7 @@ function NavigationStack:update(dt) end end -function NavigationStack:draw() +function NavigationStack:drawSelf() if self.transition then self.transition:draw() else @@ -174,4 +186,12 @@ function NavigationStack:draw() end end +function NavigationStack:getTouchedElement(x, y) + local activeScene = self:getActiveScene() + if activeScene and activeScene.uiRoot then + return activeScene.uiRoot:getTouchedElement(x, y) + end + return nil +end + return NavigationStack \ No newline at end of file diff --git a/client/src/PlayerStack.lua b/client/src/PlayerStack.lua index 4b325209..81f41022 100644 --- a/client/src/PlayerStack.lua +++ b/client/src/PlayerStack.lua @@ -15,6 +15,7 @@ local logger = require("common.lib.logger") require("client.src.analytics") local KeyDataEncoding = require("common.data.KeyDataEncoding") local MatchRules = require("common.data.MatchRules") +local DebugSettings = require("client.src.debug.DebugSettings") ---@module "common.data.LevelData" local floor, min, max = math.floor, math.min, math.max @@ -767,7 +768,7 @@ function PlayerStack:drawPopBurstParticle(atlas, quad, frameIndex, atlasDimensio end function PlayerStack:drawDebug() - if config.debug_mode then + if DebugSettings.showStackDebugInfo() then local engine = self.engine local x = self.origin_x + 480 @@ -863,7 +864,7 @@ function PlayerStack:drawDebug() end function PlayerStack:drawDebugPanels(shakeOffset) - if not config.debug_mode then + if not DebugSettings.showStackDebugInfo() then return end @@ -952,7 +953,7 @@ function PlayerStack:drawRating() local rating if self.player.rating and tonumber(self.player.rating) then rating = self.player.rating - elseif config.debug_mode then + elseif DebugSettings.showStackDebugInfo() then rating = 1544 + self.player.playerNumber end diff --git a/client/src/Shortcuts.lua b/client/src/Shortcuts.lua index 2d428a57..7c009e3f 100644 --- a/client/src/Shortcuts.lua +++ b/client/src/Shortcuts.lua @@ -7,7 +7,13 @@ local logger = require("common.lib.logger") local function runSystemCommands() -- toggle debug mode if input.allKeys.isDown["d"] then - config.debug_mode = not config.debug_mode + if GAME.debugOverlay then + if GAME.debugOverlay.active then + GAME.debugOverlay:close() + else + GAME.debugOverlay:open() + end + end -- reload characters elseif input.allKeys.isDown["c"] then characters_reload_graphics() diff --git a/client/src/config.lua b/client/src/config.lua index c83d8b2e..93c69a3f 100644 --- a/client/src/config.lua +++ b/client/src/config.lua @@ -3,6 +3,7 @@ json = require("common.lib.dkjson") local util = require("common.lib.util") local fileUtils = require("client.src.FileUtils") local consts = require("common.engine.consts") +local DebugSettings = require("client.src.debug.DebugSettings") require("client.src.globals") -- Default configuration values @@ -27,12 +28,7 @@ require("client.src.globals") ---@field SFX_volume number ---@field music_volume number ---@field enableMenuMusic boolean ----@field debug_mode boolean ----@field debugShowServers boolean ----@field debugShowDesignHelper boolean ----@field debugProfile boolean ----@field debugProfileThreshold integer ----@field debug_vsFramesBehind integer +---@field debug DebugConfig? ---@field show_fps boolean ---@field show_ingame_infos boolean ---@field danger_music_changeback_delay boolean @@ -93,12 +89,8 @@ config = { SFX_volume = 50, music_volume = 50, enableMenuMusic = true, - -- Debug mode flag - debug_mode = false, - debugShowServers = false, - debugShowDesignHelper = false, - debugProfile = false, - debugProfileThreshold = 50, + -- Debug settings persisted separately + debug = DebugSettings.getDefaultConfigValues(), -- Show FPS in the top-left corner of the screen show_fps = false, @@ -226,19 +218,6 @@ config = { if type(read_data.music_volume) == "number" then configTable.music_volume = util.bound(0, read_data.music_volume, 100) end - if type(read_data.debug_mode) == "boolean" then - configTable.debug_mode = read_data.debug_mode - end - if type(read_data.debugShowServers) == "boolean" then - configTable.debugShowServers = read_data.debugShowServers - end - if type(read_data.debugShowDesignHelper) == "boolean" then - configTable.debugShowDesignHelper = read_data.debugShowDesignHelper - end - if type(read_data.debugProfile) == "boolean" then - configTable.debugProfile = read_data.debugProfile - end - -- debugProfileThreshold is not saved to prevent accidental dense profiling if type(read_data.show_fps) == "boolean" then configTable.show_fps = read_data.show_fps end @@ -310,6 +289,8 @@ config = { if type(read_data.enableMenuMusic) == "boolean" then configTable.enableMenuMusic = read_data.enableMenuMusic end + + configTable.debug = DebugSettings.normalizeConfigValues(read_data.debug) end end diff --git a/client/src/debug/DebugMenu.lua b/client/src/debug/DebugMenu.lua new file mode 100644 index 00000000..311e352f --- /dev/null +++ b/client/src/debug/DebugMenu.lua @@ -0,0 +1,90 @@ +local class = require("common.lib.class") +local ui = require("client.src.ui") +local DebugSettings = require("client.src.debug.DebugSettings") +local GraphicsUtil = require("client.src.graphics.graphics_util") + +local function createDebugBoolSelector(debugKey, onChangeFn) + return ui.BoolSelector({ + startValue = DebugSettings.get(debugKey) --[[@as boolean]], + onValueChange = function(selfElement, value) + GAME.theme:playMoveSfx() + DebugSettings.set(debugKey, value) + if onChangeFn then + onChangeFn() + end + end + }) +end + +local function createDebugSlider(debugKey, min, max, onValueChangeFn) + return ui.Slider({ + min = min, + max = max, + value = DebugSettings.get(debugKey) --[[@as number]], + tickLength = math.ceil(100 / max), + onValueChange = function(slider) + DebugSettings.set(debugKey, slider.value) + if onValueChangeFn then + onValueChangeFn(slider) + end + end + }) +end + +local function buildDebugMenuItems(options) + local debugMenuOptions = {} + + for _, def in ipairs(DebugSettings.getDefinitions()) do + if def.type == "boolean" then + debugMenuOptions[#debugMenuOptions + 1] = ui.MenuItem.createBoolSelectorMenuItem( + def.label, + nil, + false, + createDebugBoolSelector(def.key) + ) + elseif def.type == "number" then + debugMenuOptions[#debugMenuOptions + 1] = ui.MenuItem.createSliderMenuItem( + def.label, + nil, + false, + createDebugSlider(def.key, def.min or 0, def.max or 100) + ) + end + end + + if DEBUG_ENABLED then + debugMenuOptions[#debugMenuOptions + 1] = ui.MenuItem.createButtonMenuItem("Window Size Tester", nil, false, function() + GAME.navigationStack:push(require("client.src.scenes.WindowSizeTester")()) + end) + end + + if options.showBackButton then + debugMenuOptions[#debugMenuOptions + 1] = ui.MenuItem.createButtonMenuItem("back", nil, nil, function() + GAME.theme:playCancelSfx() + if options.onBack then + options.onBack() + end + end) + end + + return debugMenuOptions +end + +-- Menu for configuring debug settings +---@class DebugMenu : Menu +local DebugMenu = class(function(self, options) + +end, ui.Menu) + +-- We need a factory because menu items must be passed into the base class init +function DebugMenu.makeDebugMenu(options) + options = options or {} + options.x = 0 + options.y = 0 + options.hAlign = "center" + options.vAlign = "center" + options.menuItems = buildDebugMenuItems(options) + return DebugMenu(options) +end + +return DebugMenu diff --git a/client/src/debug/DebugSettings.lua b/client/src/debug/DebugSettings.lua new file mode 100644 index 00000000..43b02a2d --- /dev/null +++ b/client/src/debug/DebugSettings.lua @@ -0,0 +1,418 @@ +local logger = require("common.lib.logger") + +-- Singleton class for managing runtime debug settings +-- Settings are persisted to the config file under config.debug +---@class DebugSettings +local DebugSettings = {} + +---@class DebugConfig +---@field showStackDebugInfo boolean +---@field showUIElementBorders boolean +---@field simulateMobileOS boolean +---@field forceFPS boolean +---@field drawGraphicsStats boolean +---@field showRuntimeGraph boolean +---@field vsFramesBehind number +---@field showDebugServers boolean +---@field showDesignHelper boolean +---@field profileFrameTimes boolean +---@field profileThreshold number + +---@class DebugSettingDefinition +---@field key string The key used in config.debug table +---@field type "boolean"|"number" The type of the setting +---@field default boolean|number The default value +---@field label string The display label for the UI +---@field min number? Minimum value for number types +---@field max number? Maximum value for number types +---@field debugBuildOnly boolean? Forces the setting off and hides it in non-debug builds + +-- All debug settings defined in one place with their metadata +local settingDefinitions = { + { + key = "showStackDebugInfo", + type = "boolean", + default = false, + label = "Show Stack Debug Info", + debugBuildOnly = false + }, + { + key = "showUIElementBorders", + type = "boolean", + default = false, + label = "Show UI Element Borders", + debugBuildOnly = true + }, + { + key = "simulateMobileOS", + type = "boolean", + default = false, + label = "Simulate Mobile OS", + debugBuildOnly = true + }, + { + key = "forceFPS", + type = "boolean", + default = false, + label = "Force FPS Display", + debugBuildOnly = true + }, + { + key = "drawGraphicsStats", + type = "boolean", + default = false, + label = "Draw Graphics Stats", + debugBuildOnly = true + }, + { + key = "showRuntimeGraph", + type = "boolean", + default = false, + label = "Show Runtime Graph", + debugBuildOnly = true + }, + { + key = "vsFramesBehind", + type = "number", + default = 0, + label = "VS Frames Behind", + min = 0, + max = 200, + debugBuildOnly = true + }, + { + key = "showDebugServers", + type = "boolean", + default = false, + label = "Show Debug Servers", + debugBuildOnly = false + }, + { + key = "showDesignHelper", + type = "boolean", + default = false, + label = "Show Design Helper", + debugBuildOnly = true + }, + { + key = "profileFrameTimes", + type = "boolean", + default = false, + label = "Profile frame times", + debugBuildOnly = false + }, + { + key = "profileThreshold", + type = "number", + default = 50, + label = "Discard frames below duration (ms)", + min = 0, + max = 100, + debugBuildOnly = false + } +} + +local function isNonDebugBuild() + return not DEBUG_ENABLED +end + +local function shouldLockToDefault(def) + return isNonDebugBuild() and def.debugBuildOnly +end + +---Creates a DebugConfig table populated with default values. +---@return DebugConfig +local function createDefaultConfig() + local defaults = {} + for _, def in ipairs(settingDefinitions) do + local value = def.default + defaults[def.key] = value + end + ---@type DebugConfig + return defaults +end + +--- Current settings values (loaded from config.debug) +---@type DebugConfig +local settings = createDefaultConfig() + +local function clampNumber(value, minValue, maxValue) + if minValue then + value = math.max(minValue, value) + end + if maxValue then + value = math.min(maxValue, value) + end + return value +end + +local function findDefinition(key) + for _, def in ipairs(settingDefinitions) do + if def.key == key then + return def + end + end +end + +---Returns default debug configuration values keyed by setting. +---@return DebugConfig +function DebugSettings.getDefaultConfigValues() + return createDefaultConfig() +end + +---Normalizes persisted debug configuration values using definitions. +---@param persisted table|nil +---@return DebugConfig +function DebugSettings.normalizeConfigValues(persisted) + local normalized = createDefaultConfig() + local source = persisted or {} + + for _, def in ipairs(settingDefinitions) do + local value = source[def.key] + if def.type == "boolean" then + if type(value) == "boolean" then + normalized[def.key] = value + end + elseif def.type == "number" then + if type(value) ~= "number" then + value = def.default + end + normalized[def.key] = clampNumber(value, def.min, def.max) + end + end + + return normalized +end + +-- Initializes debug settings from config +function DebugSettings.init() + config.debug = config.debug or {} + + config.debug = DebugSettings.normalizeConfigValues(config.debug) + + for _, def in ipairs(settingDefinitions) do + local value = config.debug[def.key] + if shouldLockToDefault(def) then + settings[def.key] = def.default + else + settings[def.key] = value + end + end +end + +-- Saves current settings to config +local function saveSettings() + config.debug = config.debug or {} + + for _, def in ipairs(settingDefinitions) do + config.debug[def.key] = settings[def.key] + end + + write_conf_file() +end + +local releaseDefinitionCache +-- Returns all setting definitions for UI generation +---@return DebugSettingDefinition[] +function DebugSettings.getDefinitions() + if not isNonDebugBuild() then + return settingDefinitions + end + + if not releaseDefinitionCache then + releaseDefinitionCache = {} + for _, def in ipairs(settingDefinitions) do + if not def.debugBuildOnly then + releaseDefinitionCache[#releaseDefinitionCache + 1] = def + end + end + end + + return releaseDefinitionCache +end + +-- Gets the value of a setting by key +---@param key string +---@return boolean|number +function DebugSettings.get(key) + local def = findDefinition(key) + + if isNonDebugBuild() then + if def then + if shouldLockToDefault(def) then + return def.default + end + + if def.type == "boolean" then + return settings[key] or false + elseif def.type == "number" then + return settings[key] or 0 + end + end + return false + end + + if not def then + return false + end + + return settings[key] +end + +-- Sets the value of a setting by key +---@param key string +---@param value boolean|number +function DebugSettings.set(key, value) + local def = findDefinition(key) + if not def then + logger.warn("Attempted to set unknown debug setting: " .. key) + return + end + + if def.type == "boolean" then + settings[key] = value and true or false + else + local numericValue = type(value) == "number" and value or def.default + settings[key] = clampNumber(numericValue, def.min, def.max) + end + saveSettings() +end + +-- Returns whether to show stack debug information +---@return boolean +function DebugSettings.showStackDebugInfo() + return DebugSettings.get("showStackDebugInfo") --[[@as boolean]] +end + +-- Returns whether to show UI element borders +---@return boolean +function DebugSettings.showUIElementBorders() + return DebugSettings.get("showUIElementBorders") --[[@as boolean]] +end + +-- Returns whether to simulate mobile OS on desktop +---@return boolean +function DebugSettings.simulateMobileOS() + return DebugSettings.get("simulateMobileOS") --[[@as boolean]] +end + +-- Returns whether to force FPS display in debug builds +---@return boolean +function DebugSettings.forceFPS() + return DebugSettings.get("forceFPS") --[[@as boolean]] +end + +-- Returns whether to draw graphics stats (draw calls, texture memory, etc.) +---@return boolean +function DebugSettings.drawGraphicsStats() + return DebugSettings.get("drawGraphicsStats") --[[@as boolean]] +end + +-- Returns whether to show the runtime graph +---@return boolean +function DebugSettings.showRuntimeGraph() + return DebugSettings.get("showRuntimeGraph") --[[@as boolean]] +end + +-- Returns the VS frames behind value (only applicable when showStackDebugInfo is true) +---@return number +function DebugSettings.getVSFramesBehind() + return DebugSettings.get("vsFramesBehind") --[[@as number]] +end + +-- Sets whether to show stack debug information +---@param value boolean +function DebugSettings.setShowStackDebugInfo(value) + DebugSettings.set("showStackDebugInfo", value) +end + +-- Sets whether to show UI element borders +---@param value boolean +function DebugSettings.setShowUIElementBorders(value) + DebugSettings.set("showUIElementBorders", value) +end + +-- Sets whether to simulate mobile OS on desktop +---@param value boolean +function DebugSettings.setSimulateMobileOS(value) + DebugSettings.set("simulateMobileOS", value) +end + +-- Sets whether to force FPS display in debug builds +---@param value boolean +function DebugSettings.setForceFPS(value) + DebugSettings.set("forceFPS", value) +end + +-- Sets whether to draw graphics stats +---@param value boolean +function DebugSettings.setDrawGraphicsStats(value) + DebugSettings.set("drawGraphicsStats", value) +end + +-- Sets whether to show the runtime graph +---@param value boolean +function DebugSettings.setShowRuntimeGraph(value) + DebugSettings.set("showRuntimeGraph", value) +end + +-- Sets the VS frames behind value +---@param value number +function DebugSettings.setVSFramesBehind(value) + DebugSettings.set("vsFramesBehind", value) +end + +-- Returns whether to show debug servers in main menu +---@return boolean +function DebugSettings.showDebugServers() + return DebugSettings.get("showDebugServers") --[[@as boolean]] +end + +-- Sets whether to show debug servers in main menu +---@param value boolean +function DebugSettings.setShowDebugServers(value) + DebugSettings.set("showDebugServers", value) +end + +-- Returns whether to show design helper in main menu +---@return boolean +function DebugSettings.showDesignHelper() + return DebugSettings.get("showDesignHelper") --[[@as boolean]] +end + +-- Sets whether to show design helper in main menu +---@param value boolean +function DebugSettings.setShowDesignHelper(value) + DebugSettings.set("showDesignHelper", value) +end + +-- Returns whether to profile frame times +---@return boolean +function DebugSettings.getProfileFrameTimes() + return DebugSettings.get("profileFrameTimes") --[[@as boolean]] +end + +-- Sets whether to profile frame times +---@param value boolean +function DebugSettings.setProfileFrameTimes(value) + DebugSettings.set("profileFrameTimes", value) + local prof = require("common.lib.zoneProfiler") + prof.enable(value) + prof.setDurationFilter(DebugSettings.getProfileThreshold() / 1000) +end + +-- Returns the profile threshold in milliseconds +---@return number +function DebugSettings.getProfileThreshold() + return DebugSettings.get("profileThreshold") --[[@as number]] +end + +-- Sets the profile threshold in milliseconds +---@param value number +function DebugSettings.setProfileThreshold(value) + DebugSettings.set("profileThreshold", value) + local prof = require("common.lib.zoneProfiler") + prof.setDurationFilter(value / 1000) +end + +return DebugSettings diff --git a/client/src/developer.lua b/client/src/developer.lua index ff4cc477..22f52222 100644 --- a/client/src/developer.lua +++ b/client/src/developer.lua @@ -1,4 +1,5 @@ local system = require("client.src.system") +local DebugSettings = require("client.src.debug.DebugSettings") ---@diagnostic disable: duplicate-set-field -- Put any local development changes you need in here that you don't want commited. @@ -7,7 +8,7 @@ local system = require("client.src.system") local function enableProfiler(threshold) local prof = require("common.lib.zoneProfiler") prof.enable(true) - prof.setDurationFilter((threshold or config.debugProfileThreshold) / 1000) + prof.setDurationFilter((threshold or DebugSettings.getProfileThreshold()) / 1000) end local developerTools = {} diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index 8ceac080..eb29eb46 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -676,9 +676,18 @@ end ---@param player Player ---@param width number ----@return BoolSelector rankedSelector +---@return StackPanel rankedSelectionContainer function CharacterSelect:createRankedSelection(player, width) - local rankedSelector = ui.BoolSelector({startValue = player.settings.wantsRanked, isEnabled = player.isLocal, vFill = true, width = width, vAlign = "center", hAlign = "center"}) + + -- player number icon + local playerIndex = tableUtils.indexOf(self.players, player) + local playerNumberIcon = ui.ImageContainer({ + image = themes[config.theme].images.IMG_players[playerIndex], + scale = 2, + vAlign = "center" + }) + + local rankedSelector = ui.BoolSelector({startValue = player.settings.wantsRanked, isEnabled = player.isLocal, vAlign = "center"}) rankedSelector.onValueChange = function(boolSelector, value) GAME.theme:playValidationSfx() player:setWantsRanked(value) @@ -688,31 +697,40 @@ function CharacterSelect:createRankedSelection(player, width) player:connectSignal("wantsRankedChanged", rankedSelector, rankedSelector.setValue) + local container = ui.StackPanel( + { + alignment = "left", + height = rankedSelector.height, + hAlign = "center", + vAlign = "center", + } + ) + container.playerNumberIcon = playerNumberIcon + container.rankedSelector = rankedSelector + container:addElement(playerNumberIcon) + container:addElement(ui.UiElement({width = 8, height = 8})) + container:addElement(rankedSelector) + container:addElement(ui.UiElement({width = 8, height = 8})) + + return container +end + +---@param player Player +---@param width number +---@return StackPanel styleSelectionContainer +---@return BoolSelector styleSelector +function CharacterSelect:createStyleSelection(player, width) -- player number icon local playerIndex = tableUtils.indexOf(self.players, player) local playerNumberIcon = ui.ImageContainer({ image = themes[config.theme].images.IMG_players[playerIndex], - hAlign = "left", - vAlign = "center", - x = 2, scale = 2, + vAlign = "center" }) - rankedSelector.playerNumberIcon = playerNumberIcon - rankedSelector:addChild(rankedSelector.playerNumberIcon) - return rankedSelector -end - ----@param player Player ----@param width number ----@return BoolSelector styleSelector -function CharacterSelect:createStyleSelection(player, width) local styleSelector = ui.BoolSelector({ startValue = (player.settings.style == GameModes.Styles.MODERN), - vFill = true, - width = width, - vAlign = "center", - hAlign = "center", + isEnabled = player.isLocal }) -- onValueChange should get implemented by the caller @@ -729,19 +747,20 @@ function CharacterSelect:createStyleSelection(player, width) end ) - -- player number icon - local playerIndex = tableUtils.indexOf(self.players, player) - local playerNumberIcon = ui.ImageContainer({ - image = themes[config.theme].images.IMG_players[playerIndex], - hAlign = "left", + local container = ui.StackPanel({ + alignment = "left", + height = styleSelector.height, + hAlign = "center", vAlign = "center", - x = 8, - scale = 2, }) - styleSelector.playerNumberIcon = playerNumberIcon - styleSelector:addChild(styleSelector.playerNumberIcon) - - return styleSelector + container.playerNumberIcon = playerNumberIcon + container.styleSelector = styleSelector + container:addElement(playerNumberIcon) + container:addElement(ui.UiElement({width = 8, height = 8})) + container:addElement(styleSelector) + container:addElement(ui.UiElement({width = 8, height = 8})) + + return container, styleSelector end function CharacterSelect:createRecordsBox(lastText) diff --git a/client/src/scenes/DesignHelper.lua b/client/src/scenes/DesignHelper.lua index ac962771..2fc41930 100644 --- a/client/src/scenes/DesignHelper.lua +++ b/client/src/scenes/DesignHelper.lua @@ -41,7 +41,7 @@ function DesignHelper:loadGrid() end function DesignHelper:loadRankedSelection(width) - local rankedSelector = ui.BoolSelector({startValue = true, vFill = true, width = width, vAlign = "center", hAlign = "center"}) + local rankedSelector = ui.BoolSelector({startValue = true, width = width, vAlign = "center", hAlign = "center"}) return rankedSelector end diff --git a/client/src/scenes/EndlessMenu.lua b/client/src/scenes/EndlessMenu.lua index 92bd9d96..a0a00b27 100644 --- a/client/src/scenes/EndlessMenu.lua +++ b/client/src/scenes/EndlessMenu.lua @@ -49,8 +49,8 @@ function EndlessMenu:loadUserInterface() self.ui.styleSelection = ui.MultiPlayerSelectionWrapper({vFill = true, alignment = "left", hAlign = "center", vAlign = "center"}) self.ui.styleSelection:setTitle("endless_modern") - local styleSelector = self:createStyleSelection(player, unitSize) - self.ui.styleSelection:addElement(styleSelector, player) + local styleContainer, styleSelector = self:createStyleSelection(player, unitSize) + self.ui.styleSelection:addElement(styleContainer, player) self.ui.grid:createElementAt(5, 2, 1, 1, "styleSelection", self.ui.styleSelection, nil, true) diff --git a/client/src/scenes/GameBase.lua b/client/src/scenes/GameBase.lua index 8c860557..bf1f793e 100644 --- a/client/src/scenes/GameBase.lua +++ b/client/src/scenes/GameBase.lua @@ -15,6 +15,7 @@ local ui = require("client.src.ui") local FileUtils = require("client.src.FileUtils") local ClientStack = require("client.src.ClientStack") local MatchRules = require("common.data.MatchRules") +local DebugSettings = require("client.src.debug.DebugSettings") -- Scene template for running any type of game instance (endless, vs-self, replays, etc.) ---@class GameBase : Scene @@ -444,14 +445,14 @@ function GameBase:drawHUD() end stack:drawLevel() - if stack.analytic and not config.debug_mode then + if stack.analytic and not DebugSettings.showStackDebugInfo() then --prof.push("Stack:drawAnalyticData") stack:drawAnalyticData() --prof.pop("Stack:drawAnalyticData") end end - if not config.debug_mode and GAME.battleRoom and GAME.battleRoom.spectatorString then -- this is printed in the same space as the debug details + if not DebugSettings.showStackDebugInfo() and GAME.battleRoom and GAME.battleRoom.spectatorString then -- this is printed in the same space as the debug details GraphicsUtil.print(GAME.battleRoom.spectatorString, themes[config.theme].spectators_Pos[1], themes[config.theme].spectators_Pos[2]) end diff --git a/client/src/scenes/MainMenu.lua b/client/src/scenes/MainMenu.lua index 8054187c..ffb9f72c 100644 --- a/client/src/scenes/MainMenu.lua +++ b/client/src/scenes/MainMenu.lua @@ -4,6 +4,7 @@ local ui = require("client.src.ui") local GraphicsUtil = require("client.src.graphics.graphics_util") local class = require("common.lib.class") local GameModes = require("common.data.GameModes") +local DebugSettings = require("client.src.debug.DebugSettings") local EndlessMenu = require("client.src.scenes.EndlessMenu") local PuzzleMenu = require("client.src.scenes.PuzzleMenu") local TimeAttackMenu = require("client.src.scenes.TimeAttackMenu") @@ -40,6 +41,16 @@ local function switchToScene(scene, transition) GAME.navigationStack:push(scene, transition) end +function MainMenu:refresh() + if self.menu then + self.menu:detach() + self.menu = nil + end + + self.menu = self:createMainMenu() + self.uiRoot:addChild(self.menu) +end + function MainMenu:createMainMenu() local menuItems = {ui.MenuItem.createButtonMenuItem("mm_1_endless", nil, nil, function() @@ -104,12 +115,12 @@ function MainMenu:createMainMenu() } local function addDebugMenuItems() - if config.debugShowServers then + if DebugSettings.showDebugServers() then for i, menuItem in ipairs(debugMenuItems) do menu:addMenuItem(i + 7, menuItem) end end - if config.debugShowDesignHelper then + if DebugSettings.showDesignHelper() then menu:addMenuItem(#menu.menuItems, ui.MenuItem.createButtonMenuItem("Design Helper", nil, nil, function() switchToScene(DesignHelper()) end)) diff --git a/client/src/scenes/ModManagement.lua b/client/src/scenes/ModManagement.lua index 95761b77..5b5bac0d 100644 --- a/client/src/scenes/ModManagement.lua +++ b/client/src/scenes/ModManagement.lua @@ -167,7 +167,7 @@ function ModManagement:loadStageGrid() if stageId ~= consts.RANDOM_STAGE_SPECIAL_VALUE then local stage = allStages[stageId] local icon = ui.ImageContainer({drawBorders = true, image = stage.images.thumbnail, hFill = true, vFill = true, hAlign = "center", vAlign = "center"}) - local enableSelector = ui.BoolSelector({startValue = not not stages[stage.id], hAlign = "center", vAlign = "center", hFill = true, vFill = true}) + local enableSelector = ui.BoolSelector({startValue = not not stages[stage.id], hAlign = "center", vAlign = "center"}) enableSelector.onValueChange = function(boolSelector, value) GAME.theme:playValidationSfx() stage:enable(boolSelector.value) @@ -181,7 +181,7 @@ function ModManagement:loadStageGrid() GAME.localPlayer:setStage(stages[consts.RANDOM_STAGE_SPECIAL_VALUE]) end end - local visibilitySelector = ui.BoolSelector({startValue = stage.isVisible, hAlign = "center", vAlign = "center", hFill = true, vFill = true}) + local visibilitySelector = ui.BoolSelector({startValue = stage.isVisible, hAlign = "center", vAlign = "center"}) visibilitySelector.onValueChange = function(boolSelector, value) end local name = ui.Label({text = stage.display_name, translate = false, hAlign = "center", vAlign = "center"}) @@ -241,7 +241,7 @@ function ModManagement:loadCharacterGrid() if characterId ~= consts.RANDOM_CHARACTER_SPECIAL_VALUE then local character = allCharacters[characterId] local icon = ui.ImageContainer({drawBorders = true, image = character.images.icon, hFill = true, vFill = true}) - local enableSelector = ui.BoolSelector({startValue = not not characters[character.id], hAlign = "center", vAlign = "center", hFill = true, vFill = true}) + local enableSelector = ui.BoolSelector({startValue = not not characters[character.id], hAlign = "center", vAlign = "center"}) enableSelector.onValueChange = function(boolSelector, value) GAME.theme:playValidationSfx() character:enable(boolSelector.value) @@ -255,7 +255,7 @@ function ModManagement:loadCharacterGrid() GAME.localPlayer:setCharacter(characters[consts.RANDOM_CHARACTER_SPECIAL_VALUE]) end end - local visibilitySelector = ui.BoolSelector({startValue = character.isVisible, hAlign = "center", vAlign = "center", hFill = true, vFill = true}) + local visibilitySelector = ui.BoolSelector({startValue = character.isVisible, hAlign = "center", vAlign = "center"}) visibilitySelector.onValueChange = function(boolSelector, value) end local displayName = ui.Label({text = character.display_name, translate = false, hAlign = "center", vAlign = "center"}) diff --git a/client/src/scenes/OptionsMenu.lua b/client/src/scenes/OptionsMenu.lua index daf25251..7b7477f3 100644 --- a/client/src/scenes/OptionsMenu.lua +++ b/client/src/scenes/OptionsMenu.lua @@ -2,6 +2,7 @@ local Scene = require("client.src.scenes.Scene") local ui = require("client.src.ui") local inputManager = require("client.src.inputManager") local save = require("client.src.save") +local DebugMenu = require("client.src.debug.DebugMenu") local consts = require("common.engine.consts") local fileUtils = require("client.src.FileUtils") local analytics = require("client.src.analytics") @@ -15,7 +16,6 @@ local ModManagement = require("client.src.scenes.ModManagement") local system = require("client.src.system") local JsonSafePrecision = require("common.data.JsonSafePrecision") local logger = require("common.lib.logger") -local prof = require("common.lib.zoneProfiler") -- Scene for the options menu local OptionsMenu = class(function(self, sceneParams) @@ -486,30 +486,14 @@ function OptionsMenu:loadSoundMenu() end function OptionsMenu:loadDebugMenu() - local debugMenuOptions = { - ui.MenuItem.createToggleButtonGroupMenuItem("op_debug_mode", nil, nil, createToggleButtonGroup("debug_mode")), - ui.MenuItem.createSliderMenuItem("VS Frames Behind", nil, false, createConfigSlider("debug_vsFramesBehind", 0, 200)), - ui.MenuItem.createToggleButtonGroupMenuItem("Show Debug Servers", nil, false, createToggleButtonGroup("debugShowServers")), - ui.MenuItem.createToggleButtonGroupMenuItem("Show Design Helper", nil, false, createToggleButtonGroup("debugShowDesignHelper")), - ui.MenuItem.createButtonMenuItem("Window Size Tester", nil, false, function() - GAME.navigationStack:push(require("client.src.scenes.WindowSizeTester")()) - end), - ui.MenuItem.createToggleButtonGroupMenuItem("Profile frame times", nil, false, createToggleButtonGroup("debugProfile", - function() - prof.enable(config.debugProfile) - prof.setDurationFilter(config.debugProfileThreshold / 1000) - end)), - ui.MenuItem.createSliderMenuItem("Discard frames below duration (ms)", nil, false, createConfigSlider("debugProfileThreshold", 0, 100, - function() - prof.setDurationFilter(config.debugProfileThreshold / 1000) - end)), - ui.MenuItem.createButtonMenuItem("back", nil, nil, function() - GAME.theme:playCancelSfx() - self:switchToScreen("baseMenu") - end), - } - - return ui.Menu.createCenteredMenu(debugMenuOptions) + local debugMenu = DebugMenu.makeDebugMenu({ + showBackButton = true, + onBack = function() + self:switchToScreen("baseMenu") + end, + height = themes[config.theme].main_menu_max_height + }) + return debugMenu end function OptionsMenu:loadAboutMenu() diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua index 3f05ab16..d95886cb 100644 --- a/client/src/scenes/PortraitGame.lua +++ b/client/src/scenes/PortraitGame.lua @@ -6,6 +6,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local ui = require("client.src.ui") local input = require("client.src.inputManager") local system = require("client.src.system") +local DebugSettings = require("client.src.debug.DebugSettings") local PortraitGame = class(function(self, sceneParams) end, @@ -193,7 +194,7 @@ function PortraitGame:flipToPortrait() GAME.globalCanvas = love.graphics.newCanvas(consts.CANVAS_HEIGHT, consts.CANVAS_WIDTH, {dpiscale=GAME:newCanvasSnappedScale()}) local width, height, _ = love.window.getMode() - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() or DebugSettings.simulateMobileOS() then -- flip the window dimensions to portrait love.window.updateMode(height, width, {}) love.window.setFullscreen(true) @@ -245,7 +246,7 @@ function PortraitGame:returnToLandscape() GAME.globalCanvas = love.graphics.newCanvas(consts.CANVAS_WIDTH, consts.CANVAS_HEIGHT, {dpiscale=GAME:newCanvasSnappedScale()}) -- flip the window dimensions to landscape local width, height, _ = love.window.getMode() - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() or DebugSettings.simulateMobileOS() then love.window.updateMode(height, width, {}) love.window.setFullscreen(false) --GAME:updateCanvasPositionAndScale(width, height) diff --git a/client/src/scenes/ReplayGame.lua b/client/src/scenes/ReplayGame.lua index f2e306a7..911cc0a9 100644 --- a/client/src/scenes/ReplayGame.lua +++ b/client/src/scenes/ReplayGame.lua @@ -5,6 +5,7 @@ local util = require("common.lib.util") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local prof = require("common.lib.zoneProfiler") +local DebugSettings = require("client.src.debug.DebugSettings") local ReplayGame = class( function (self, sceneParams) @@ -141,7 +142,7 @@ function ReplayGame:drawHUD() end stack:drawLevel() - if stack.analytic and not DEBUG_ENABLED then + if stack.analytic and not DebugSettings.showStackDebugInfo() then prof.push("Stack:drawAnalyticData") stack:drawAnalyticData() prof.pop("Stack:drawAnalyticData") diff --git a/client/src/scenes/Scene.lua b/client/src/scenes/Scene.lua index ea9f9adc..0270b7cc 100644 --- a/client/src/scenes/Scene.lua +++ b/client/src/scenes/Scene.lua @@ -5,6 +5,7 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") local tableUtils = require("common.lib.tableUtils") local SoundController = require("client.src.music.SoundController") local directsFocus = require("client.src.ui.FocusDirector") +local DebugSettings = require("client.src.debug.DebugSettings") ---@alias sceneMusic ("none" | "main" | "title_screen" | "select_screen") @@ -83,7 +84,7 @@ end function Scene:drawCommunityMessage() -- Draw the community message - if not config.debug_mode then + if not DebugSettings.showStackDebugInfo() then GraphicsUtil.printf(join_community_msg or "", 0, (668 / 720) * GAME.globalCanvas:getHeight(), GAME.globalCanvas:getWidth(), "center") end end diff --git a/client/src/scenes/TimeAttackMenu.lua b/client/src/scenes/TimeAttackMenu.lua index 1ab39f81..c846420e 100644 --- a/client/src/scenes/TimeAttackMenu.lua +++ b/client/src/scenes/TimeAttackMenu.lua @@ -48,8 +48,8 @@ function TimeAttackMenu:loadUserInterface() self.ui.styleSelection = ui.MultiPlayerSelectionWrapper({vFill = true, alignment = "left", hAlign = "center", vAlign = "center"}) self.ui.styleSelection:setTitle("endless_modern") - local styleSelector = self:createStyleSelection(player, unitSize) - self.ui.styleSelection:addElement(styleSelector, player) + local styleContainer, styleSelector = self:createStyleSelection(player, unitSize) + self.ui.styleSelection:addElement(styleContainer, player) self.ui.grid:createElementAt(5, 2, 1, 1, "styleSelection", self.ui.styleSelection, nil, true) diff --git a/client/src/ui/BoolSelector.lua b/client/src/ui/BoolSelector.lua index 5d4d7e83..2827daeb 100644 --- a/client/src/ui/BoolSelector.lua +++ b/client/src/ui/BoolSelector.lua @@ -3,6 +3,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") ---@class BoolSelectorOptions : UiElementOptions ---@field startValue boolean? @@ -11,9 +12,22 @@ local GraphicsUtil = require("client.src.graphics.graphics_util") ---@class BoolSelector : UiElement ---@field value boolean ---@field vertical boolean -local BoolSelector = class(function(boolSelector, options) - boolSelector.value = options.startValue or false - boolSelector.vertical = false +---@field circleRadius number +---@field extraDistance number +---@field lengthPadding number +---@field widthPadding number +local BoolSelector = class(function(self, options) + self.value = options.startValue or false + self.vertical = false + self.circleRadius = 10 + self.extraDistance = 16 + self.lengthPadding = 2 + self.widthPadding = 2 + self.onValueChange = options.onValueChange or function() end + + -- Calculate initial dimensions + self.width = self:calculateWidth() + self.height = self:calculateHeight() end, UiElement) @@ -62,57 +76,60 @@ function BoolSelector:setValue(value) end end +---@return number +function BoolSelector:calculateWidth() + local width = self.circleRadius * 2 + 2 * self.widthPadding + if not self.vertical then + width = width + self.extraDistance + end + return width +end + +---@return number +function BoolSelector:calculateHeight() + local height = self.circleRadius * 2 + 2 * self.lengthPadding + if self.vertical then + height = height + self.extraDistance + end + return height +end + -- other code may implement a callback here -- function BoolSelector.onValueChange() end -local circleRadius = 10 -local extraDistance = 16 -local lengthPadding = 2 -local widthPadding = 2 -local totalWidth = 0 -local totalLength = 0 -local fakeCenteredChild = {hAlign = "center", vAlign = "center", width = totalWidth, height = totalLength} - function BoolSelector:drawSelf() - if DEBUG_ENABLED then + if DebugSettings.showUIElementBorders() then GraphicsUtil.setColor(0, 0, 1, 1) GraphicsUtil.drawRectangle("line", self.x + 1, self.y + 1, self.width - 2, self.height - 2) GraphicsUtil.setColor(1, 1, 1, 1) end - local circleX = circleRadius + widthPadding - local circleY = circleRadius + lengthPadding - totalWidth = circleRadius * 2 + 2 * widthPadding - totalLength = circleRadius * 2 + 2 * lengthPadding + local drawX = self.x + self.widthPadding + local drawY = self.y + self.lengthPadding + local drawWidth = self.width - 2 * self.widthPadding + local drawHeight = self.height - 2 * self.lengthPadding + + local circleX = self.circleRadius + local circleY = self.circleRadius + if self.vertical then - totalLength = totalLength + extraDistance if self.value == false then - circleY = circleY + extraDistance + circleY = circleY + self.extraDistance end else - totalWidth = totalWidth + extraDistance if self.value then - circleX = circleX + extraDistance + circleX = circleX + self.extraDistance end end - fakeCenteredChild.width = totalWidth - fakeCenteredChild.height = totalLength - - -- we want these to be centered but creating a Rectangle / Circle ui element is maybe a bit too much? - -- so just apply the translation via a fake element with all necessary props - GraphicsUtil.applyAlignment(self, fakeCenteredChild) - love.graphics.translate(self.x, self.y) if self.value then GraphicsUtil.setColor(30/255, 190/255, 67/255, 1) - GraphicsUtil.drawRectangle("fill", 0, 0, totalWidth, totalLength, nil, nil, nil, nil, circleRadius, circleRadius) + GraphicsUtil.drawRectangle("fill", drawX, drawY, drawWidth, drawHeight, nil, nil, nil, nil, self.circleRadius, self.circleRadius) GraphicsUtil.setColor(1, 1, 1, 1) end - GraphicsUtil.drawRectangle("line", 0, 0, totalWidth, totalLength, nil, nil, nil, nil, circleRadius, circleRadius) - love.graphics.circle("fill", circleX, circleY, circleRadius) - - GraphicsUtil.resetAlignment() + GraphicsUtil.drawRectangle("line", drawX, drawY, drawWidth, drawHeight, nil, nil, nil, nil, self.circleRadius, self.circleRadius) + love.graphics.circle("fill", drawX + circleX, drawY + circleY, self.circleRadius) end return BoolSelector \ No newline at end of file diff --git a/client/src/ui/Carousel.lua b/client/src/ui/Carousel.lua index 4215d017..bfd71988 100644 --- a/client/src/ui/Carousel.lua +++ b/client/src/ui/Carousel.lua @@ -4,6 +4,7 @@ local Focusable = require(PATH .. ".Focusable") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local tableUtils = require("common.lib.tableUtils") +local DebugSettings = require("client.src.debug.DebugSettings") local function calculateFontSize(height) return math.floor(height / 2) + 1 @@ -87,7 +88,7 @@ function Carousel.setPassengerByIndex(self, index) end function Carousel:drawSelf() - if DEBUG_ENABLED then + if DebugSettings.showUIElementBorders() then GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) end end diff --git a/client/src/ui/Grid.lua b/client/src/ui/Grid.lua index 2dfc7a2b..aedf54c0 100644 --- a/client/src/ui/Grid.lua +++ b/client/src/ui/Grid.lua @@ -3,6 +3,7 @@ local UiElement = require(PATH .. ".UIElement") local GridElement = require(PATH .. ".GridElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") local Grid = class(function(self, options) self.unitSize = options.unitSize @@ -73,7 +74,7 @@ function Grid:createElementAt(x, y, width, height, description, uiElement, noPad end function Grid:drawSelf() - if DEBUG_ENABLED then + if DebugSettings.showUIElementBorders() then GraphicsUtil.setColor(1, 1, 1, 0.5) GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) diff --git a/client/src/ui/GridElement.lua b/client/src/ui/GridElement.lua index d0725fdf..ff916567 100644 --- a/client/src/ui/GridElement.lua +++ b/client/src/ui/GridElement.lua @@ -2,6 +2,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") local GridElement = class(function(gridElement, options) if options.content then @@ -16,7 +17,7 @@ local GridElement = class(function(gridElement, options) gridElement.gridHeight = options.gridHeight if options.drawBorders ~= nil then gridElement.drawBorders = options.drawBorders - elseif DEBUG_ENABLED then + elseif DebugSettings.showUIElementBorders() then gridElement.drawBorders = true else gridElement.drawBorders = false diff --git a/client/src/ui/MenuItem.lua b/client/src/ui/MenuItem.lua index 7931404d..400192c1 100644 --- a/client/src/ui/MenuItem.lua +++ b/client/src/ui/MenuItem.lua @@ -5,6 +5,7 @@ local TextButton = require(PATH .. ".TextButton") local class = require("common.lib.class") local GraphicsUtil = require("client.src.graphics.graphics_util") local system = require("client.src.system") +local DebugSettings = require("client.src.debug.DebugSettings") -- MenuItem is a specific UIElement that all children of Menu should be local MenuItem = class(function(self, options) @@ -27,7 +28,7 @@ function MenuItem.createMenuItem(label, item) menuItem.width = label.width + (2 * MenuItem.PADDING) - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() or DebugSettings.simulateMobileOS() then label.height = math.max(30, label.height + (2 * MenuItem.PADDING)) menuItem.height = math.max(30, label.height, item and item.height or 0) else @@ -38,7 +39,7 @@ function MenuItem.createMenuItem(label, item) local spaceBetween = 16 item.x = label.width + spaceBetween item.vAlign = "center" - if system.isMobileOS() or DEBUG_ENABLED then + if system.isMobileOS() or DebugSettings.simulateMobileOS() then item.height = math.max(30, item.height) end menuItem.width = item.x + item.width + MenuItem.PADDING @@ -128,7 +129,19 @@ function MenuItem.createSliderMenuItem(text, replacements, translate, slider) end local label = Label({text = text, replacements = replacements, translate = translate, vAlign = "center"}) local menuItem = MenuItem.createMenuItem(label, slider) - + + return menuItem +end + +function MenuItem.createBoolSelectorMenuItem(text, replacements, translate, boolSelector) + assert(text ~= nil) + assert(boolSelector ~= nil) + if translate == nil then + translate = true + end + local label = Label({text = text, replacements = replacements, translate = translate, vAlign = "center"}) + local menuItem = MenuItem.createMenuItem(label, boolSelector) + return menuItem end diff --git a/client/src/ui/MultiPlayerSelectionWrapper.lua b/client/src/ui/MultiPlayerSelectionWrapper.lua index c0802d71..969dba23 100644 --- a/client/src/ui/MultiPlayerSelectionWrapper.lua +++ b/client/src/ui/MultiPlayerSelectionWrapper.lua @@ -18,6 +18,7 @@ end, StackPanel) function MultiPlayerSelectionWrapper:addElement(uiElement, player) + assert(uiElement.receiveInputs) self.wrappedElements[player] = uiElement uiElement.yieldFocus = function() self.yieldFocus() diff --git a/client/src/ui/OverlayContainer.lua b/client/src/ui/OverlayContainer.lua new file mode 100644 index 00000000..e3a41467 --- /dev/null +++ b/client/src/ui/OverlayContainer.lua @@ -0,0 +1,124 @@ +local class = require("common.lib.class") +local UiElement = require("client.src.ui.UIElement") +local GraphicsUtil = require("client.src.graphics.graphics_util") +local consts = require("common.engine.consts") + +-- Full-screen semi-transparent overlay that holds a centered UI element +-- Closes on click outside content area +---@class OverlayContainer : UiElement +---@field content UiElement? The centered content element +---@field onClose fun()? Callback invoked when overlay closes +---@field active boolean True when overlay is open and processing input +local OverlayContainer = class( + function(self, options) + options = options or {} + self.content = options.content + self.onClose = options.onClose + self.active = false + + self.width = consts.CANVAS_WIDTH + self.height = consts.CANVAS_HEIGHT + self:setVisibility(false) + + if self.content then + self:addChild(self.content) + self.content.hAlign = "center" + self.content.vAlign = "center" + end + end, + UiElement +) + +-- Opens the overlay +function OverlayContainer:open() + self.active = true + self:setVisibility(true) +end + +-- Closes the overlay +function OverlayContainer:close() + if not self.active then + return + end + + self.active = false + self:setVisibility(false) + + if self.onClose then + self.onClose() + end +end + +-- Checks if the overlay is currently active +---@return boolean +function OverlayContainer:isActive() + return self.active +end + +-- Sets the content element for the overlay +---@param content UiElement The content to display in the center +function OverlayContainer:setContent(content) + if self.content then + self.content:detach() + end + + self.content = content + if self.content then + self:addChild(self.content) + self.content.hAlign = "center" + self.content.vAlign = "center" + end +end + +-- Draws the semi-transparent background +function OverlayContainer:drawSelf() + if not self.active then + return + end + + GraphicsUtil.setColor(0, 0, 0, 0.75) + GraphicsUtil.drawRectangle("fill", 0, 0, self.width, self.height) + GraphicsUtil.setColor(1, 1, 1, 1) +end + +-- Touch handler - closes overlay if clicking outside content +---@return boolean? True to block touch event propagation +function OverlayContainer:onTouch(x, y) + if not self.active then + return false + end + + if self.content then + local contentX, contentY = self.content:getScreenPos() + local inContentBounds = x >= contentX and x < contentX + self.content.width and + y >= contentY and y < contentY + self.content.height + + if not inContentBounds then + self:close() + return true + end + end + + return true +end + +-- Release handler - blocks event propagation +---@return boolean? True to block release event propagation +function OverlayContainer:onRelease() + if self.active then + return true + end +end + +-- Input handler - closes overlay on ESC key +function OverlayContainer:receiveInputs(inputs, dt) + if not self.active then + return + end + + if inputs.isDown["MenuEsc"] then + self:close() + end +end + +return OverlayContainer diff --git a/client/src/ui/PagedUniGrid.lua b/client/src/ui/PagedUniGrid.lua index 2dfe1bc4..a9645894 100644 --- a/client/src/ui/PagedUniGrid.lua +++ b/client/src/ui/PagedUniGrid.lua @@ -6,6 +6,7 @@ local Grid = require(PATH .. ".Grid") local class = require("common.lib.class") local Signal = require("common.lib.signal") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") local function addNewPage(pagedUniGrid) local grid = Grid({ @@ -101,7 +102,7 @@ function PagedUniGrid:refreshPageTurnButtonVisibility() end function PagedUniGrid:drawSelf() - if DEBUG_ENABLED then + if DebugSettings.showUIElementBorders() then GraphicsUtil.setColor(1, 0, 0, 1) GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) diff --git a/client/src/ui/StackPanel.lua b/client/src/ui/StackPanel.lua index e1b22697..a1018b35 100644 --- a/client/src/ui/StackPanel.lua +++ b/client/src/ui/StackPanel.lua @@ -3,23 +3,26 @@ local UiElement = require(PATH .. ".UIElement") local class = require("common.lib.class") local tableUtils = require("common.lib.tableUtils") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") --- StackPanel is a layouting element that stacks up all its children in one direction based on an alignment setting --- Useful for auto-aligning multiple ui elements that only know one of their dimensions +---@class StackPanel : UiElement +---StackPanel is a layouting element that stacks up all its children in one direction based on an alignment setting. +---Useful for auto-aligning multiple ui elements that only know one of their dimensions. +---@field alignment "left"|"right"|"top"|"bottom" Direction in which children are stacked +---@field pixelsTaken number Tracks how many pixels are already taken in the stacking direction +---@field TYPE string Class type identifier local StackPanel = class(function(stackPanel, options) - -- all children are aligned automatically towards that option inside the StackPanel - -- possible values: "left", "right", "top", "bottom" + ---@type "left"|"right"|"top"|"bottom" stackPanel.alignment = options.alignment - - -- StackPanels are unidirectional but can go into either direction - -- pixelsTaken tracks how many pixels are already taken in the direction the StackPanel propagates towards + ---@type number stackPanel.pixelsTaken = 0 - -- a stack panel does not have a size limit it's alignment dimension grows with its content end, UiElement) StackPanel.TYPE = "StackPanel" +---Applies positioning and sizing settings to a UI element based on the StackPanel's alignment +---@param uiElement UiElement The element to apply settings to function StackPanel:applyStackPanelSettings(uiElement) if self.alignment == "left" then uiElement.hFill = false @@ -48,19 +51,29 @@ function StackPanel:applyStackPanelSettings(uiElement) end end +---Adds a UI element to the StackPanel, applying proper positioning and resizing +---@param uiElement UiElement The element to add function StackPanel:addElement(uiElement) self:applyStackPanelSettings(uiElement) self:addChild(uiElement) self:resize() + uiElement.yieldFocus = function() + self.yieldFocus() + end end - +---Inserts a UI element at a specific index in the StackPanel +---@param uiElement UiElement The element to insert +---@param index number The position to insert at (1-based) function StackPanel:insertElementAtIndex(uiElement, index) -- add it at the end StackPanel.addElement(self, uiElement) StackPanel.shiftTo(self, uiElement, index) end +---Shifts an element to a specific index by swapping positions with preceding elements +---@param uiElement UiElement The element to shift +---@param index number The target position (1-based) function StackPanel:shiftTo(uiElement, index) -- swap the previous element with it while updating values until it reached the desired index for i = #self.children - 1, index, -1 do @@ -82,6 +95,9 @@ function StackPanel:shiftTo(uiElement, index) end end +---Removes an element from the StackPanel, updating positions and pixel tracking +---IMPORTANT: Use this method instead of element:detach() to maintain proper layout state +---@param uiElement UiElement The element to remove function StackPanel:remove(uiElement) local index = tableUtils.indexOf(self.children, uiElement) @@ -114,8 +130,21 @@ function StackPanel:remove(uiElement) uiElement:detach() end +---Processes user input and forwards it to child elements +---@param input table Input state +---@param dt number Delta time since last frame +function StackPanel:receiveInputs(input, dt) + for _, child in ipairs(self.children) do + if child.receiveInputs then + child:receiveInputs(input, dt) + return + end + end +end + +---Draws the StackPanel's debug borders if enabled in debug settings function StackPanel:drawSelf() - if DEBUG_ENABLED then + if DebugSettings.showUIElementBorders() then GraphicsUtil.setColor(1, 0, 0, 0.7) GraphicsUtil.drawRectangle("line", self.x, self.y, self.width, self.height) GraphicsUtil.setColor(1, 1, 1, 1) diff --git a/client/src/ui/Stepper.lua b/client/src/ui/Stepper.lua index a342bb64..9aeba5b2 100644 --- a/client/src/ui/Stepper.lua +++ b/client/src/ui/Stepper.lua @@ -5,6 +5,7 @@ local Label = require(PATH .. ".Label") local class = require("common.lib.class") local util = require("common.lib.util") local GraphicsUtil = require("client.src.graphics.graphics_util") +local DebugSettings = require("client.src.debug.DebugSettings") local NAV_BUTTON_WIDTH = 25 local EMPTY_STEPPER_WIDTH = 160 @@ -110,7 +111,7 @@ function Stepper:refreshLocalization() end function Stepper:drawSelf() - if config.debug_mode then + if DebugSettings.showUIElementBorders() then GraphicsUtil.setColor(self.color) GraphicsUtil.drawRectangle("fill", self.x, self.y, self.width, self.height) GraphicsUtil.setColor(self.borderColor) diff --git a/client/src/ui/touchHandler.lua b/client/src/ui/touchHandler.lua index e297c264..f4470052 100644 --- a/client/src/ui/touchHandler.lua +++ b/client/src/ui/touchHandler.lua @@ -10,15 +10,11 @@ local touchHandler = { } function touchHandler:touch(x, y) - local activeScene = GAME.navigationStack:getActiveScene() - -- if there is no active scene that implies an on-going scene switch, no interactions should be possible - if activeScene then - -- prevent multitouch - if not self.touchedElement then - self.touchedElement = activeScene.uiRoot:getTouchedElement(x, y) - if self.touchedElement and self.touchedElement.onTouch then - self.touchedElement:onTouch(x, y) - end + -- prevent multitouch + if not self.touchedElement then + self.touchedElement = GAME.uiRoot:getTouchedElement(x, y) + if self.touchedElement and self.touchedElement.onTouch then + self.touchedElement:onTouch(x, y) end end end diff --git a/common/engine/Match.lua b/common/engine/Match.lua index 18172e4c..2efa110a 100644 --- a/common/engine/Match.lua +++ b/common/engine/Match.lua @@ -12,6 +12,7 @@ local LegacyPanelSource = require("common.compatibility.LegacyPanelSource") local InputCompression = require("common.data.InputCompression") local ReplayV3 = require("common.data.ReplayV3") local MatchRules = require("common.data.MatchRules") +local DebugSettings = require("client.src.debug.DebugSettings") ---@class Match ---@field stacks (Stack | SimulatedStack)[] The stacks to run as part of the match @@ -615,10 +616,11 @@ function Match:shouldRun(stack, runsSoFar) end -- In debug mode allow non-local player 2 to fall a certain number of frames behind - if config and config.debug_mode and not stack.is_local and config.debug_vsFramesBehind and config.debug_vsFramesBehind > 0 and tableUtils.indexOf(self.stacks, stack) == 2 then + local framesBehind = DebugSettings.getVSFramesBehind() + if not stack.is_local and framesBehind > 0 and tableUtils.indexOf(self.stacks, stack) == 2 then -- Only stay behind if the game isn't over for the local player (=garbageTarget) yet if self.garbageTargets[2][1] and self.garbageTargets[2][1].game_ended and self.garbageTargets[2][1]:game_ended() == false then - if stack.clock + config.debug_vsFramesBehind >= self.garbageTargets[2][1].clock then + if stack.clock + framesBehind >= self.garbageTargets[2][1].clock then return false end end diff --git a/docs/DebugSettings.md b/docs/DebugSettings.md new file mode 100644 index 00000000..36dc2a74 --- /dev/null +++ b/docs/DebugSettings.md @@ -0,0 +1,117 @@ +# Debug Settings System Documentation + +## Overview +The Debug Settings system provides runtime-configurable debug features through a UI overlay. Settings are persisted to `config.debug` and can be accessed anywhere via the `DebugSettings` singleton. + +## Architecture + +### Components +- **DebugSettings** (`client/src/debug/DebugSettings.lua`) - Singleton managing all debug settings +- **OverlayContainer** (`client/src/ui/OverlayContainer.lua`) - Full-screen overlay UI for settings menu +- **Debug Button** (`client/src/Game.lua`) - Bottom-right button that opens the overlay (only visible when `DEBUG_ENABLED`) + +## How Debug-Only Settings Work + +Settings can be marked as `debugBuildOnly = true` to control their availability: + +### Debug Builds (`DEBUG_ENABLED = true`) +- Setting appears in the UI overlay +- Can be toggled on/off by the user +- Value persists to config file +- Returns actual user-configured value + +### Release Builds (`DEBUG_ENABLED = false`) +- Setting is hidden from the UI overlay +- Always returns the default value (typically `false`) +- Persisted value is ignored +- Cannot be changed at runtime + +This is implemented through two mechanisms: + +**1. UI Filtering** - `DebugSettings.getDefinitions()` filters out `debugBuildOnly` settings in release builds + +**2. Value Locking** - `DebugSettings.get()` returns the default value for debug-only settings in release builds + +## Adding New Debug Settings + +### Step 1: Add Setting Definition + +Add a new entry to the `settingDefinitions` table in `DebugSettings.lua`: + +```lua +{ + key = "myNewSetting", + type = "boolean", -- or "number" + default = false, + label = "My New Feature", + debugBuildOnly = true -- false if should be available in release builds +} +``` + +For number settings, add `min` and `max`: +```lua +{ + key = "myNumberSetting", + type = "number", + default = 0, + label = "My Number Setting", + min = 0, + max = 100, + debugBuildOnly = true +} +``` + +### Step 2: Add Accessor Methods + +Add getter and optional setter methods following the naming convention: + +```lua +-- Getter +function DebugSettings.myNewSetting() + return DebugSettings.get("myNewSetting") --[[@as boolean]] +end + +-- Setter (if needed for programmatic access) +function DebugSettings.setMyNewSetting(value) + DebugSettings.set("myNewSetting", value) +end +``` + +### Step 3: Use in Code + +Replace existing debug checks with the new method: + +```lua + +if DebugSettings.myNewSetting() then + -- debug behavior +end +``` + +## Setting Definition Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `key` | string | Yes | Unique key used in config.debug | +| `type` | "boolean" \| "number" | Yes | Data type of the setting | +| `default` | boolean \| number | Yes | Default value when not configured | +| `label` | string | Yes | Display label shown in UI overlay | +| `min` | number | No | Minimum value (number types only) | +| `max` | number | No | Maximum value (number types only) | +| `debugBuildOnly` | boolean | No | If true, only available in DEBUG_ENABLED builds (defaults to false) | + +## Persistence + +Settings are automatically saved to `config.debug` in the user's config file: + +```lua +config.debug = { + showStackDebugInfo = false, + showUIElementBorders = true, + vsFramesBehind = 0, + -- ... other settings +} +``` + +- Settings load on startup via `DebugSettings.init()` +- Settings save immediately when changed via `DebugSettings.set()` diff --git a/main.lua b/main.lua index 952272a3..bcd2dcb9 100644 --- a/main.lua +++ b/main.lua @@ -1,6 +1,7 @@ local logger = require("common.lib.logger") require("common.lib.mathExtensions") local utf8 = require("common.lib.utf8Additions") +local DebugSettings = require("client.src.debug.DebugSettings") local inputManager = require("client.src.inputManager") require("client.src.globals") local touchHandler = require("client.src.ui.touchHandler") @@ -62,8 +63,8 @@ function love.load(args, rawArgs) GAME:load() if not PROFILE_MEMORY then - prof.enable(config.debugProfile) - prof.setDurationFilter(config.debugProfileThreshold / 1000) + prof.enable(DebugSettings.getProfileFrameTimes()) + prof.setDurationFilter(DebugSettings.getProfileThreshold() / 1000) end end @@ -78,7 +79,7 @@ end -- Intentional override ---@diagnostic disable-next-line: duplicate-set-field function love.update(dt) - if config.show_fps and config.debug_mode then + if DebugSettings.showRuntimeGraph() then if CustomRun.runTimeGraph == nil then CustomRun.runTimeGraph = RunTimeGraph() end @@ -126,7 +127,7 @@ end function love.draw() GAME:draw() - if DEBUG_ENABLED then + if DebugSettings.drawGraphicsStats() then local stats = love.graphics.getStats() local width, height = love.graphics.getDimensions() From 948bf6d5baa00adfda51580ae9eaa7e31029f585 Mon Sep 17 00:00:00 2001 From: JamBox <8935453+JamesVanBoxtel@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:05:43 -0800 Subject: [PATCH 2/4] Move client debug settings out of Match --- client/src/ClientMatch.lua | 2 ++ common/engine/Match.lua | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index 1574a3b0..4e0113ba 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -147,6 +147,8 @@ end function ClientMatch:setup() self.engine = Match(self.panelSource, self.matchRules) + self.engine.debug.vsFramesBehind = DebugSettings.getVSFramesBehind() + self.stacks = {} for i, player in ipairs(self.players) do diff --git a/common/engine/Match.lua b/common/engine/Match.lua index 2efa110a..cb7433fb 100644 --- a/common/engine/Match.lua +++ b/common/engine/Match.lua @@ -12,7 +12,6 @@ local LegacyPanelSource = require("common.compatibility.LegacyPanelSource") local InputCompression = require("common.data.InputCompression") local ReplayV3 = require("common.data.ReplayV3") local MatchRules = require("common.data.MatchRules") -local DebugSettings = require("client.src.debug.DebugSettings") ---@class Match ---@field stacks (Stack | SimulatedStack)[] The stacks to run as part of the match @@ -33,6 +32,10 @@ local DebugSettings = require("client.src.debug.DebugSettings") ---@field gameOverClock integer? ---@field aborted boolean the game stopped in the middle because of crash, desync, game leave, online player left, etc. ---@field desyncError boolean? the match stopped because the other stack became too out of sync +---@field debug MatchDebugConfig internal debug configuration that defaults to non-debug values + +---@class MatchDebugConfig +---@field vsFramesBehind integer -- A match is a particular instance of the game, for example 1 time attack round, or 1 vs match ---@class Match @@ -65,6 +68,11 @@ function(self, panelSource, matchRules) self.clock = 0 self.ended = false self.aborted = false + + -- Initialize internal debug configuration with non-debug defaults + self.debug = { + vsFramesBehind = 0 + } end ) @@ -616,11 +624,10 @@ function Match:shouldRun(stack, runsSoFar) end -- In debug mode allow non-local player 2 to fall a certain number of frames behind - local framesBehind = DebugSettings.getVSFramesBehind() - if not stack.is_local and framesBehind > 0 and tableUtils.indexOf(self.stacks, stack) == 2 then + if not stack.is_local and self.debug.vsFramesBehind > 0 and tableUtils.indexOf(self.stacks, stack) == 2 then -- Only stay behind if the game isn't over for the local player (=garbageTarget) yet if self.garbageTargets[2][1] and self.garbageTargets[2][1].game_ended and self.garbageTargets[2][1]:game_ended() == false then - if stack.clock + framesBehind >= self.garbageTargets[2][1].clock then + if stack.clock + self.debug.vsFramesBehind >= self.garbageTargets[2][1].clock then return false end end From d633322da9e9c6ab5b2e212d4ea059ea69ccb18a Mon Sep 17 00:00:00 2001 From: JamBox <8935453+JamesVanBoxtel@users.noreply.github.com> Date: Sat, 8 Nov 2025 09:53:13 -0800 Subject: [PATCH 3/4] Restore the custom name after saving --- client/src/developer.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/developer.lua b/client/src/developer.lua index 22f52222..06d51d55 100644 --- a/client/src/developer.lua +++ b/client/src/developer.lua @@ -66,6 +66,7 @@ function developerTools.wrapConfig() config.name = realName end write() + config.name = CUSTOM_USERNAME or realName end end From 5c74b9984cc25a68ddd9fd4f2ff93e76815e3c2b Mon Sep 17 00:00:00 2001 From: JamBox <8935453+JamesVanBoxtel@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:10:15 -0800 Subject: [PATCH 4/4] setup for both code paths of match --- client/src/ClientMatch.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index 4e0113ba..ba8c0c29 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -85,7 +85,7 @@ function ClientMatch.createFromGameMode(players, gameMode, panelSource, ranked, clientMatch.panelSource = panelSource clientMatch.supportsPause = #players == 1 and players[1].isLocal - clientMatch:setup() + clientMatch:setupFromGameMode() return clientMatch end @@ -141,14 +141,14 @@ function ClientMatch.createFromReplay(replay, players) clientMatch.stacks[i] = clientStack end + clientMatch:sharedSetup() + return clientMatch end -function ClientMatch:setup() +function ClientMatch:setupFromGameMode() self.engine = Match(self.panelSource, self.matchRules) - self.engine.debug.vsFramesBehind = DebugSettings.getVSFramesBehind() - self.stacks = {} for i, player in ipairs(self.players) do @@ -192,9 +192,16 @@ function ClientMatch:setup() end end + self:sharedSetup() + self.replay = self.engine:createNewReplay() end + +function ClientMatch:sharedSetup() + self.engine.debug.vsFramesBehind = DebugSettings.getVSFramesBehind() +end + function ClientMatch:run() if self.isPaused or self.engine:hasEnded() then self:runGameOver()