mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 16:21:30 +02:00
@@ -138,6 +138,7 @@ function Loader.new(opts)
|
||||
events = Events.new(), hooks = Hooks.new(), content = {}, assets = {},
|
||||
exports = {}, migrations = {}, order = {},
|
||||
modSave = {}, modOptions = {}, optionSchemas = {}, imageCache = {},
|
||||
modInput = {},
|
||||
fs = (opts and opts.fs) or (love and love.filesystem),
|
||||
dev = dev,
|
||||
}, Loader)
|
||||
@@ -527,6 +528,44 @@ function Loader:_registerCommand(modId, verb, fn)
|
||||
return self.content.commands:register(verb, fn, modId)
|
||||
end
|
||||
|
||||
-- the GB buttons mod.input may drive (#807)
|
||||
local GB_BUTTONS = {
|
||||
up = true, down = true, left = true, right = true,
|
||||
a = true, b = true, start = true, select = true,
|
||||
}
|
||||
|
||||
-- per-mod mod.input ledger (#807): seq numbers this mod's Input sources,
|
||||
-- tokens maps each opaque press token to what release must undo. Living
|
||||
-- on the loader (not the api closure) is what lets rollback, hot reload
|
||||
-- and input recovery retire a mod's holds from outside the mod's own code.
|
||||
function Loader:_modInput(modId)
|
||||
local bucket = self.modInput[modId]
|
||||
if not bucket then
|
||||
bucket = { seq = 0, tokens = {} }
|
||||
self.modInput[modId] = bucket
|
||||
end
|
||||
return bucket
|
||||
end
|
||||
|
||||
-- Release every outstanding mod.input hold: one mod's on entry-chunk
|
||||
-- rollback, everyone's (no argument) on hot reload and input recovery
|
||||
-- (#807). When Input:reset already dropped the sources these releases
|
||||
-- are no-ops; the point is the stale tokens die with the code that took
|
||||
-- them, so a later mod.input:release on one is refused instead of
|
||||
-- touching a button someone else now holds.
|
||||
function Loader:releaseModInput(modId)
|
||||
if modId == nil then
|
||||
for id in pairs(self.modInput) do self:releaseModInput(id) end
|
||||
return
|
||||
end
|
||||
local bucket = self.modInput[modId]
|
||||
if not bucket then return end
|
||||
self.modInput[modId] = nil
|
||||
for _, rec in pairs(bucket.tokens) do
|
||||
rec.input:sourceRelease(rec.btn, rec.source)
|
||||
end
|
||||
end
|
||||
|
||||
function Loader:_api(mod)
|
||||
local loader = self
|
||||
local modId = mod.manifest.id
|
||||
@@ -559,6 +598,45 @@ function Loader:_api(mod)
|
||||
hooks = { wrap = function(_, name, callback, priority)
|
||||
return loader.hooks:wrap(name, callback, priority, modId)
|
||||
end },
|
||||
-- source-safe scripted GB input (#807): tap queues exactly one
|
||||
-- wasPressed edge for the next fixed step with no held state; press
|
||||
-- holds until release. Every call is its own "mod:<id>:<n>" source in
|
||||
-- game.input, so releasing a token can never drop a button the
|
||||
-- keyboard, a pad, the touch overlay, or another mod still holds.
|
||||
input = {
|
||||
tap = function(_, game, btn)
|
||||
local input = game and game.input
|
||||
assert(input, "mod.input needs the live game (see game.ready)")
|
||||
assert(GB_BUTTONS[btn], "unknown GB button: " .. tostring(btn))
|
||||
local bucket = loader:_modInput(modId)
|
||||
bucket.seq = bucket.seq + 1
|
||||
local source = "mod:" .. modId .. ":" .. bucket.seq
|
||||
input:sourcePress(btn, source)
|
||||
input:sourceRelease(btn, source)
|
||||
end,
|
||||
press = function(_, game, btn)
|
||||
local input = game and game.input
|
||||
assert(input, "mod.input needs the live game (see game.ready)")
|
||||
assert(GB_BUTTONS[btn], "unknown GB button: " .. tostring(btn))
|
||||
local bucket = loader:_modInput(modId)
|
||||
bucket.seq = bucket.seq + 1
|
||||
local source = "mod:" .. modId .. ":" .. bucket.seq
|
||||
input:sourcePress(btn, source)
|
||||
local token = {}
|
||||
bucket.tokens[token] = { input = input, btn = btn, source = source }
|
||||
return token
|
||||
end,
|
||||
-- idempotent, and a token another mod took is simply not in this
|
||||
-- ledger, so cross-mod release is refused by construction
|
||||
release = function(_, token)
|
||||
local bucket = loader.modInput[modId]
|
||||
local rec = bucket and bucket.tokens[token]
|
||||
if not rec then return false end
|
||||
bucket.tokens[token] = nil
|
||||
rec.input:sourceRelease(rec.btn, rec.source)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
-- the widget toolkit facade (12 4.5) is one shared surface, not
|
||||
-- per-mod state; each widget inside it loads on first touch
|
||||
ui = ModUI,
|
||||
@@ -714,6 +792,7 @@ function Loader:_rollback(modId)
|
||||
end
|
||||
self.events:removeOwner(modId)
|
||||
self.hooks:removeOwner(modId)
|
||||
self:releaseModInput(modId)
|
||||
self.exports[modId] = nil
|
||||
self.optionSchemas[modId] = nil
|
||||
self.migrations[modId] = nil
|
||||
|
||||
Reference in New Issue
Block a user