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..ba8c0c29 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 @@ -84,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 @@ -140,10 +141,12 @@ 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.stacks = {} @@ -189,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() @@ -559,7 +569,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 +608,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..06d51d55 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 = {} @@ -65,6 +66,7 @@ function developerTools.wrapConfig() config.name = realName end write() + config.name = CUSTOM_USERNAME or realName end end 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..cb7433fb 100644 --- a/common/engine/Match.lua +++ b/common/engine/Match.lua @@ -32,6 +32,10 @@ local MatchRules = require("common.data.MatchRules") ---@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 @@ -64,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 ) @@ -615,10 +624,10 @@ 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 + 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 + config.debug_vsFramesBehind >= self.garbageTargets[2][1].clock then + if stack.clock + self.debug.vsFramesBehind >= 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()