Expose window margins to tool HUDs

This commit is contained in:
Kevin Jacobson
2026-07-28 14:09:46 -07:00
parent d09cf80da5
commit fb58fa788b
4 changed files with 37 additions and 21 deletions
+5 -4
View File
@@ -164,10 +164,11 @@ 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.
`render.hud` receives `(next, game, viewport)` after the finished game frame is
composited and before touch controls draw. The window-space viewport contains
`width`, `height`, `gameX`, `gameY`, `gameWidth`, `gameHeight`, `scale`, `dpiX`,
and `dpiY`, so a tool can use the letterbox margins without drawing over the
playfield or pushing an updating game state.
Developer mode also arms the mod loader's dev tripwire, which flags mods
that reach outside their permission set.
+7 -9
View File
@@ -255,14 +255,6 @@ 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
@@ -282,7 +274,13 @@ function Game:draw()
if worldBelow and self.overworld.sgbWorldZones then
worldZones = self.overworld:sgbWorldZones()
end
Renderer:endFrame(zones, worldZones)
local viewport = Renderer:endFrame(zones, worldZones)
-- Persistent tool status is screen-space UI: draw it over the completed
-- render pipeline with exact playfield/margin geometry, but below mobile
-- controls. It never becomes an updating game state.
if ModRuntime.wantsHook("render.hud") then
ModRuntime.call("render.hud", function() end, self, viewport)
end
-- on-screen mobile controls: pure screen-space, over the finished frame
TouchControls:draw()
end
+7
View File
@@ -706,6 +706,13 @@ function Renderer:endFrame(zones, worldZones)
self.uprightActive = false
self.worldOverride = nil
PaletteFX.setPass(nil)
return {
width = ww, height = wh,
gameX = ox, gameY = oy,
gameWidth = vpw, gameHeight = vph,
scale = Sp,
dpiX = dpiX, dpiY = dpiY,
}
end
return Renderer
+18 -8
View File
@@ -86,9 +86,9 @@ 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.
-- A tool status indicator draws in window space after the renderer composites
-- the game. This gives it exact playfield/margin geometry and keeps it crisp
-- over compatible render pipelines without entering the game canvas.
do
local Renderer = require("src.render.Renderer")
local TouchControls = require("src.core.TouchControls")
@@ -98,7 +98,15 @@ do
local order = {}
Renderer.setUISize = function() end
Renderer.beginFrame = function() end
Renderer.endFrame = function() order[#order + 1] = "present" end
Renderer.endFrame = function()
order[#order + 1] = "present"
return {
width = 1024, height = 768,
gameX = 112, gameY = 24,
gameWidth = 800, gameHeight = 720,
scale = 5,
}
end
TouchControls.draw = function() order[#order + 1] = "touch" end
local fake = { overworld = {}, stack = { states = {} } }
@@ -108,15 +116,17 @@ do
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")
T.eq(viewport.width, 1024, "render.hud receives the window width")
T.eq(viewport.height, 768, "render.hud receives the window height")
T.eq(viewport.gameX, 112, "render.hud receives the playfield origin")
T.eq(viewport.gameWidth, 800, "render.hud receives the playfield width")
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")
T.eq(table.concat(order, ","), "states,present,hud,touch",
"render.hud draws after frame composition and before touch controls")
hooks:removeOwner("tool_fixture")
Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame,
TouchControls.draw = savedSetUISize, savedBegin, savedEnd, savedTouch