mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
Export legacy mod option schemas
This commit is contained in:
+70
-15
@@ -1,9 +1,26 @@
|
|||||||
# Mod option schema export
|
# RFC 0008 — Runtime mod option schema export
|
||||||
|
|
||||||
`mod_option_schemas.json` is an optional runtime snapshot written beside
|
## Status
|
||||||
`options.lua` after the mod loader finishes. It gives a native launcher a
|
|
||||||
data-only description of mod settings without requiring the launcher to run
|
Proposed. Engine: `src/mods/Loader.lua`. Tests:
|
||||||
untrusted mod entry code before boot.
|
`tests/mod_loader_tests.lua`. This RFC defines an optional filesystem
|
||||||
|
contract; it does not require a native launcher or any other consumer.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
A native launcher may want to present settings for installed mods before it
|
||||||
|
starts the game. Running every mod's entry chunk in that launcher just to
|
||||||
|
discover its settings would duplicate engine behavior and give the launcher
|
||||||
|
an unnecessary code-execution surface. The engine already has the authoritative
|
||||||
|
runtime schemas after mod loading, so it can publish a data-only snapshot for
|
||||||
|
platform shells that want one.
|
||||||
|
|
||||||
|
## The exact contract
|
||||||
|
|
||||||
|
After the mod loader has finished running entry chunks, it may write
|
||||||
|
`mod_option_schemas.json` beside `options.lua` in the same filesystem. The
|
||||||
|
document is a snapshot of the current boot; it is not a second settings store
|
||||||
|
and does not change how option values are read or written.
|
||||||
|
|
||||||
Version 1 has this shape:
|
Version 1 has this shape:
|
||||||
|
|
||||||
@@ -23,14 +40,52 @@ Version 1 has this shape:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Only enabled, successfully loaded mods are included. A boot with no schemas
|
`mods` is keyed by mod id. Its rows come from the runtime
|
||||||
writes `{"schema_version":1,"mods":{}}` when an older snapshot exists, so a
|
`mod.options:define` schema, or from the legacy manifest `options_schema` file
|
||||||
disabled or failed mod cannot leave stale settings rows behind. A filesystem
|
when the runtime schema is absent. The supported row types are `toggle`,
|
||||||
that cannot write is tolerated, and a fresh mod-free boot does not create the
|
`choice`, `number`, and `text`. Their optional fields retain the meanings
|
||||||
file.
|
established by the existing in-game option UI: choices are `[label, value]`
|
||||||
|
pairs, numeric rows may provide `min`, `max`, and `step`, and text rows may
|
||||||
|
provide `maxLen`.
|
||||||
|
|
||||||
The supported row types are `toggle`, `choice`, `number`, and `text`. Native
|
Only mods that are enabled and successfully loaded in the current boot are
|
||||||
consumers may ignore unknown future row types. Consumers must accept a
|
included. A disabled or failed mod must not contribute rows. If an older
|
||||||
missing `schema_version` as legacy version 1 and ignore newer versions rather
|
snapshot exists and the current boot has no schema-bearing mods, the producer
|
||||||
than guessing at their shape. Producers must bump the version when changing
|
overwrites it with `{"schema_version":1,"mods":{}}`; this prevents stale
|
||||||
the document shape.
|
settings rows from surviving a disable or load failure. A fresh mod-free boot
|
||||||
|
does not create the file, and a filesystem without write support is tolerated.
|
||||||
|
|
||||||
|
The producer writes the snapshot after entry chunks and the final load set
|
||||||
|
have been established. Consumers must treat the file as untrusted input and
|
||||||
|
must not execute anything from it.
|
||||||
|
|
||||||
|
## Compatibility and versioning
|
||||||
|
|
||||||
|
The contract is optional on both sides. A native consumer may be absent, and
|
||||||
|
the engine continues normally if the file cannot be written. A native
|
||||||
|
consumer is not required to render, validate, or persist every supported row;
|
||||||
|
it may ignore an unknown row type or optional field.
|
||||||
|
|
||||||
|
For compatibility with files produced by the original unversioned prototype,
|
||||||
|
a missing `schema_version` means version 1. Consumers must ignore documents
|
||||||
|
with a newer version rather than guessing at their shape. Producers must bump
|
||||||
|
the version whenever they change the document shape or the meaning of an
|
||||||
|
existing field. Version 1 is therefore the legacy unversioned format as well
|
||||||
|
as the explicitly versioned format shown above.
|
||||||
|
|
||||||
|
## Migration note
|
||||||
|
|
||||||
|
Nothing. Existing mods, option values, and the in-game options UI are
|
||||||
|
unchanged. Platforms that do not consume `mod_option_schemas.json` have no
|
||||||
|
new integration requirement.
|
||||||
|
|
||||||
|
## Parity tests
|
||||||
|
|
||||||
|
`tests/mod_loader_tests.lua` verifies the explicit version, runtime and legacy
|
||||||
|
row round-tripping, enabled/disabled filtering, failed-mod filtering,
|
||||||
|
stale-snapshot clearing, and tolerance of a read-only filesystem.
|
||||||
|
|
||||||
|
## Deprecation etiquette
|
||||||
|
|
||||||
|
Nothing is deprecated. The unversioned file form remains readable as legacy
|
||||||
|
version 1; new producers write the explicit `schema_version` field.
|
||||||
|
|||||||
+17
-3
@@ -202,9 +202,23 @@ function Loader:_writeOptionSchemas()
|
|||||||
if not self.fs.write then return end
|
if not self.fs.write then return end
|
||||||
|
|
||||||
local mods = {}
|
local mods = {}
|
||||||
for id, schema in pairs(self.optionSchemas) do
|
for id, mod in pairs(self.mods) do
|
||||||
if self.mods[id] and self.mods[id].enabled and not self.mods[id].failed then
|
if mod.enabled and not mod.failed then
|
||||||
mods[id] = schema
|
local schema = self.optionSchemas[id]
|
||||||
|
-- Keep the legacy manifest options_schema path visible to native
|
||||||
|
-- consumers too. ManagerState loads this same data-only chunk on
|
||||||
|
-- demand; using it here means older mods do not need to migrate to
|
||||||
|
-- mod.options:define just to appear in a launcher settings screen.
|
||||||
|
if schema == nil and mod.manifest.options_schema and self.fs.load then
|
||||||
|
local chunk = self.fs.load(mod.path .. "/" .. mod.manifest.options_schema)
|
||||||
|
if chunk then
|
||||||
|
local ok, rows = pcall(chunk)
|
||||||
|
if ok and type(rows) == "table" then schema = rows end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if schema ~= nil then
|
||||||
|
mods[id] = schema
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,16 @@ end
|
|||||||
return function(mod)
|
return function(mod)
|
||||||
mod.options:define({ { key = "shh", type = "toggle", default = true } })
|
mod.options:define({ { key = "shh", type = "toggle", default = true } })
|
||||||
end
|
end
|
||||||
|
]],
|
||||||
|
["mods/legacy/manifest.json"] = [[
|
||||||
|
{"id":"legacy","name":"legacy","version":"1.0.0","entry":"main.lua",
|
||||||
|
"options_schema":"options.lua"}
|
||||||
|
]],
|
||||||
|
["mods/legacy/main.lua"] = "return function(mod) end",
|
||||||
|
["mods/legacy/options.lua"] = [[
|
||||||
|
return {
|
||||||
|
{ key = "legacy_toggle", type = "toggle", label = "Legacy", default = true },
|
||||||
|
}
|
||||||
]],
|
]],
|
||||||
}
|
}
|
||||||
local writes = {}
|
local writes = {}
|
||||||
@@ -350,6 +360,9 @@ end
|
|||||||
"enabled mod schema is exported")
|
"enabled mod schema is exported")
|
||||||
check(decoded and decoded.mods and decoded.mods.quiet == nil,
|
check(decoded and decoded.mods and decoded.mods.quiet == nil,
|
||||||
"disabled mod schema is not exported")
|
"disabled mod schema is not exported")
|
||||||
|
check(decoded and decoded.mods and decoded.mods.legacy
|
||||||
|
and decoded.mods.legacy[1].key == "legacy_toggle",
|
||||||
|
"manifest options_schema is exported")
|
||||||
local rows = decoded and decoded.mods.loud or {}
|
local rows = decoded and decoded.mods.loud or {}
|
||||||
local byKey = {}
|
local byKey = {}
|
||||||
for _, row in ipairs(rows) do byKey[row.key] = row end
|
for _, row in ipairs(rows) do byKey[row.key] = row end
|
||||||
@@ -385,6 +398,7 @@ end
|
|||||||
"a failed mod schema is not exported")
|
"a failed mod schema is not exported")
|
||||||
|
|
||||||
loader:setEnabled("loud", false)
|
loader:setEnabled("loud", false)
|
||||||
|
loader:setEnabled("legacy", false)
|
||||||
loader:_writeOptionSchemas()
|
loader:_writeOptionSchemas()
|
||||||
local cleared = Json.decode(writes["mod_option_schemas.json"])
|
local cleared = Json.decode(writes["mod_option_schemas.json"])
|
||||||
check(cleared and cleared.schema_version == 1 and next(cleared.mods) == nil,
|
check(cleared and cleared.schema_version == 1 and next(cleared.mods) == nil,
|
||||||
|
|||||||
Reference in New Issue
Block a user