From d09cf80da597a82ec95bf75a63a2944fed72cea0 Mon Sep 17 00:00:00 2001 From: Kevin Jacobson Date: Tue, 28 Jul 2026 13:58:42 -0700 Subject: [PATCH] Add persistent HUD hook for tool mods --- docs/modding.md | 5 +++++ src/core/Game.lua | 8 ++++++++ tests/engine/tool_mod_hooks.lua | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/docs/modding.md b/docs/modding.md index a9d0d38d..7ffa7b13 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -164,5 +164,10 @@ for a tool to offer a fresh-session action before gameplay begins. Ephemeral tools can wrap `save.write(next, game)` and return `false` to veto a progress write before world state is captured or any bytes reach disk. +`render.hud` receives `(next, game, viewport)` after visible states draw and +before the UI canvas is presented. `viewport.width` and `viewport.height` are +the active UI dimensions, so status indicators can stay visible over normal +and widescreen screens without pushing an updating game state. + Developer mode also arms the mod loader's dev tripwire, which flags mods that reach outside their permission set. diff --git a/src/core/Game.lua b/src/core/Game.lua index a2709bb6..9a447adb 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -255,6 +255,14 @@ function Game:draw() end Renderer:beginFrame(worldBelow) self.stack:draw() + -- Persistent tool status belongs above every game state but inside the + -- active UI canvas, so it composes with palette/render mods and survives + -- menus, battles, and transitions without becoming an updating state. + if ModRuntime.wantsHook("render.hud") then + local width, height = Renderer:uiSize() + ModRuntime.call("render.hud", function() end, self, + { width = width, height = height }) + end -- SGB colorization: the topmost state that knows its palette owns the -- screen (overlays like text boxes inherit from what's beneath them); -- the overworld's world pass colors each visible map area separately diff --git a/tests/engine/tool_mod_hooks.lua b/tests/engine/tool_mod_hooks.lua index d53ffb2e..4e0f5e52 100644 --- a/tests/engine/tool_mod_hooks.lua +++ b/tests/engine/tool_mod_hooks.lua @@ -86,6 +86,42 @@ do hooks:removeOwner("tool_fixture") end +-- A tool status indicator must draw after every visible game state but before +-- the renderer presents that frame. This keeps the HUD visible over the +-- overworld, menus, battles, and compatible render pipelines. +do + local Renderer = require("src.render.Renderer") + local TouchControls = require("src.core.TouchControls") + local savedSetUISize, savedBegin, savedEnd, savedTouch = + Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame, + TouchControls.draw + local order = {} + Renderer.setUISize = function() end + Renderer.beginFrame = function() end + Renderer.endFrame = function() order[#order + 1] = "present" end + TouchControls.draw = function() order[#order + 1] = "touch" end + + local fake = { overworld = {}, stack = { states = {} } } + function fake.stack:visibleBase() return 1 end + function fake.stack:top() return {} end + function fake.stack:draw() order[#order + 1] = "states" end + + hooks:wrap("render.hud", function(nextFn, game, viewport) + T.check(game == fake, "render.hud receives the live Game object") + T.eq(viewport.width, 160, "render.hud receives the UI width") + T.eq(viewport.height, 144, "render.hud receives the UI height") + order[#order + 1] = "hud" + return nextFn(game, viewport) + end, 0, "tool_fixture") + + require("src.core.Game").draw(fake) + T.eq(table.concat(order, ","), "states,hud,present,touch", + "render.hud draws over states before frame presentation") + hooks:removeOwner("tool_fixture") + Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame, + TouchControls.draw = savedSetUISize, savedBegin, savedEnd, savedTouch +end + Runtime.events, Runtime.hooks, Runtime.errors = savedEvents, savedHooks, savedErrors