mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #882 from bryanthaboi/dev
This commit is contained in:
@@ -226,5 +226,12 @@ for driving a second physical display. This is what lets a mod lay the two
|
||||
passes out as two stacked Game Boy screens, or push one onto a second screen,
|
||||
without the engine knowing the layout.
|
||||
|
||||
`screen.render_visible` receives `(next, state)` while the main screen is being
|
||||
composed. Return `false` to omit that state from drawing, opacity selection and
|
||||
palette-zone ownership. The state remains on the stack and keeps its normal
|
||||
update and input ownership, so a mod can mirror a native menu on another
|
||||
display without reimplementing it. The default is `true`. Treat the wrapper as
|
||||
a pure predicate: the renderer may ask it more than once per frame.
|
||||
|
||||
Developer mode also arms the mod loader's dev tripwire, which flags mods
|
||||
that reach outside their permission set.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# RFC 0002 — Let mods hide an active screen state from the main render
|
||||
|
||||
## Status
|
||||
|
||||
Proposed. Engine: `StateStack.lua`, `Game.lua`. Tests:
|
||||
`screen_render_visible.lua`.
|
||||
|
||||
## Motivation
|
||||
|
||||
A mod can render a native menu on a companion display through
|
||||
`render.compose`, but it cannot remove that menu from the main display without
|
||||
also popping it. Popping transfers update and input ownership and forces the
|
||||
mod to reimplement native menu behavior.
|
||||
|
||||
## The decision it extends
|
||||
|
||||
No prior D-number. Extends the render-hook plan in `docs/modding.md` and the
|
||||
state-stack rendering contract in `docs/architecture.md`.
|
||||
|
||||
## The exact API delta
|
||||
|
||||
Backward-compatible, additive-only.
|
||||
|
||||
### `screen.render_visible`
|
||||
|
||||
New hook called with `(state) -> boolean` through the public wrapper signature
|
||||
`(next, state)`. Its vanilla result is `true`.
|
||||
|
||||
Returning `false` excludes the state from the main draw, from opaque-base
|
||||
selection and from palette-zone ownership. It does not remove the state or
|
||||
change update, input, push or pop behavior. The call sites are
|
||||
`StateStack:visibleBase`, `StateStack:draw` and the equivalent draw and palette
|
||||
walks in `Game:draw`.
|
||||
|
||||
The hook is guarded by `Runtime.wantsHook`, so the no-subscriber path allocates
|
||||
nothing. It is a pure render predicate and may be evaluated more than once per
|
||||
frame.
|
||||
|
||||
## Migration note for existing mods
|
||||
|
||||
**Nothing.** With no subscriber every state remains visible, and the existing
|
||||
state-stack, event and hook behavior is unchanged.
|
||||
|
||||
## Parity tests
|
||||
|
||||
- **No-mod:** the topmost opaque state still owns drawing and palette zones,
|
||||
and `Runtime.wantsHook("screen.render_visible")` stays false.
|
||||
- **Mod-API:** a fixture mod registers through `mod.hooks:wrap`, hides one
|
||||
opaque state and proves the state beneath draws and owns the palette while
|
||||
the hidden state remains topmost and continues updating.
|
||||
|
||||
## Deprecation etiquette
|
||||
|
||||
Nothing deprecated. This is one additive hook with a `true` vanilla default.
|
||||
+6
-2
@@ -16,6 +16,10 @@ local Screens = require("src.ui.Screens")
|
||||
|
||||
local Game = {}
|
||||
|
||||
local function renderVisible(stack, state)
|
||||
return state and (not stack.renderVisible or stack:renderVisible(state))
|
||||
end
|
||||
|
||||
-- dev-mode gate for the F5/backtick hotkeys; false keeps every src/dev
|
||||
-- module unloaded, so a player boot never touches a byte of dev code
|
||||
local devMode = os.getenv("POKEPORT_DEV") == "1" or _G.POKEPORT_DEV_MODE == true
|
||||
@@ -460,7 +464,7 @@ function Game:draw()
|
||||
local state = self.stack.states[i]
|
||||
local wideState = state and state.isWideBattleLayout
|
||||
and state:isWideBattleLayout()
|
||||
if state and state.draw then
|
||||
if renderVisible(self.stack, state) and state.draw then
|
||||
if classicOffset ~= 0 and not wideState then
|
||||
love.graphics.push()
|
||||
love.graphics.translate(classicOffset, 0)
|
||||
@@ -484,7 +488,7 @@ function Game:draw()
|
||||
local zones, worldZones, zoneOwner
|
||||
for i = #self.stack.states, 1, -1 do
|
||||
local s = self.stack.states[i]
|
||||
if s.sgbPalettes then
|
||||
if renderVisible(self.stack, s) and s.sgbPalettes then
|
||||
zones = s:sgbPalettes(self)
|
||||
zoneOwner = s
|
||||
break
|
||||
|
||||
+14
-2
@@ -39,17 +39,29 @@ function StateStack:update(dt)
|
||||
if top and top.update then top:update(dt) end
|
||||
end
|
||||
|
||||
local function visibleByDefault() return true end
|
||||
|
||||
-- A mod may mirror a state elsewhere and hide only its main-screen render.
|
||||
-- The state stays on the stack, so update and input ownership do not move.
|
||||
function StateStack:renderVisible(state)
|
||||
if not state then return false end
|
||||
if not Runtime.wantsHook("screen.render_visible") then return true end
|
||||
return Runtime.call("screen.render_visible", visibleByDefault, state) ~= false
|
||||
end
|
||||
|
||||
-- index of the lowest state drawn this frame (highest opaque, else 1)
|
||||
function StateStack:visibleBase()
|
||||
for i = #self.states, 1, -1 do
|
||||
if self.states[i].isOpaque then return i end
|
||||
local state = self.states[i]
|
||||
if self:renderVisible(state) and state.isOpaque then return i end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
function StateStack:draw()
|
||||
for i = self:visibleBase(), #self.states do
|
||||
if self.states[i].draw then self.states[i]:draw() end
|
||||
local state = self.states[i]
|
||||
if self:renderVisible(state) and state.draw then state:draw() end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
-- screen.render_visible through the public mod API: a mirrored native screen
|
||||
-- may leave the main render without leaving the active state stack.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Game = require("src.core.Game")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
|
||||
local FIXTURE = {
|
||||
["mods/fix_screen_mirror/manifest.json"] = [[{
|
||||
"id": "fix_screen_mirror",
|
||||
"name": "Fixture Screen Mirror",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"api": 2
|
||||
}]],
|
||||
["mods/fix_screen_mirror/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.hooks:wrap("screen.render_visible", function(nextFn, state)
|
||||
if state.screenId == "BagMenu" then return false end
|
||||
return nextFn(state)
|
||||
end)
|
||||
]],
|
||||
}
|
||||
|
||||
local savedSetUISize, savedBegin, savedEnd, savedTouch =
|
||||
Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame,
|
||||
TouchControls.draw
|
||||
local presentedZones
|
||||
Renderer.setUISize = function() end
|
||||
Renderer.beginFrame = function() end
|
||||
Renderer.endFrame = function(_, zones)
|
||||
presentedZones = zones
|
||||
return {}
|
||||
end
|
||||
TouchControls.draw = function() end
|
||||
|
||||
local function scene()
|
||||
local stack = setmetatable({}, { __index = StateStack })
|
||||
stack:init()
|
||||
local base = {
|
||||
isOpaque = true,
|
||||
draws = 0,
|
||||
draw = function(self) self.draws = self.draws + 1 end,
|
||||
sgbPalettes = function() return "base zones" end,
|
||||
}
|
||||
local menu = {
|
||||
screenId = "BagMenu",
|
||||
isOpaque = true,
|
||||
draws = 0,
|
||||
updates = 0,
|
||||
draw = function(self) self.draws = self.draws + 1 end,
|
||||
update = function(self) self.updates = self.updates + 1 end,
|
||||
sgbPalettes = function() return "menu zones" end,
|
||||
}
|
||||
stack:push(base)
|
||||
stack:push(menu)
|
||||
return { stack = stack, overworld = base, save = { options = {} } },
|
||||
base, menu
|
||||
end
|
||||
|
||||
-- no-mod parity
|
||||
do
|
||||
local run = T.sdk.loadNone({})
|
||||
local game, base, menu = scene()
|
||||
T.eq(Runtime.wantsHook("screen.render_visible"), false,
|
||||
"no subscriber leaves the render hook cold")
|
||||
Game.draw(game)
|
||||
T.eq(base.draws, 0, "the opaque menu still covers the state beneath")
|
||||
T.eq(menu.draws, 1, "the opaque menu still draws")
|
||||
T.eq(presentedZones, "menu zones", "the visible menu still owns palettes")
|
||||
run.release()
|
||||
end
|
||||
|
||||
-- subscribed path, registered by a real fixture mod
|
||||
do
|
||||
local run = T.sdk.loadMods({ "mods/fix_screen_mirror" },
|
||||
{ fs = T.sdk.memfs(FIXTURE) })
|
||||
T.eq(#run.errors, 0,
|
||||
"the fixture mod loads clean (" .. tostring(run.errors[1]) .. ")")
|
||||
local game, base, menu = scene()
|
||||
Game.draw(game)
|
||||
T.eq(base.draws, 1, "the state beneath the hidden menu draws")
|
||||
T.eq(menu.draws, 0, "the mirrored menu is omitted from the main draw")
|
||||
T.eq(presentedZones, "base zones",
|
||||
"a hidden state cannot own the main-screen palette")
|
||||
T.check(game.stack:top() == menu,
|
||||
"the hidden menu remains the active top state")
|
||||
game.stack:update(1 / 60)
|
||||
T.eq(menu.updates, 1, "the hidden menu keeps its update ownership")
|
||||
run.release()
|
||||
end
|
||||
|
||||
Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame,
|
||||
TouchControls.draw = savedSetUISize, savedBegin, savedEnd, savedTouch
|
||||
|
||||
T.finish("screen_render_visible")
|
||||
Reference in New Issue
Block a user