mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
e12cda633a
mod.content.encounters:patch on a Gold boot keys by encounter KIND (encounters.grass.ROUTE_29), but the id space was open: a key the catalog did not describe was treated as a mod's own data and merged as-is. A Gen 1 encounters mod ported unchanged writes the MAP where Gold wants the kind, so the call was accepted, merged into data.gen2Encounters.ROUTE_29 and read by nothing -- vanilla game, no error, nothing in the Mod Manager (#2369). That key cannot be a mod's own data the way an extra palette id can: src/battle/gen2/Encounter.lua, src/core/gen2/Roamers.lua, src/core/gen2/BugContest.lua and src/world/gen2/World.lua read this table by name and the set of names is fixed, so an unknown id is a write nothing reads. Add an opt-in `keysClosed` shape slot (folded by Schemas.shapeFor like the other gen2* slots) that Schemas.check honours in its keys/keyValue branch, and set it on encounters' Gen 2 spec. An unknown id now names the ids that do exist instead of silently no-opping; api 2 fails the mod, api 1 keeps the warning. Also catalogue roamMons, which the extractor has always emitted (RomExtractorGen2:readRoamMons) and Roamers.roster has always read, but which was missing from gen2Keys. Only encounters opts in: palettes, battle_anims and constants keep their open id spaces, and Red's encounters id space is untouched. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
239 lines
9.2 KiB
Lua
239 lines
9.2 KiB
Lua
-- Renders the registry reference from Schemas.REGISTRIES so the page cannot
|
|
-- drift from the engine. Run from the repo root:
|
|
-- luajit tools/gen_registry_docs.lua -- in-repo, the default
|
|
-- luajit tools/gen_registry_docs.lua ../gen1recomp.wiki
|
|
-- POKEPORT_DOCS_DIR=../project.wiki luajit tools/gen_registry_docs.lua
|
|
-- The full doc pipeline moves into the modkit CLI later; this is the
|
|
-- Schemas -> markdown seed it will absorb.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local Schemas = require("src.mods.Schemas")
|
|
|
|
-- No argument writes INSIDE the repo (a default landing in a sibling dir
|
|
-- creates one on the first run); `modkit docs` reads this exact path
|
|
local DEFAULT_DIR = "docs/modding/reference"
|
|
local DEFAULT_FILE = "registries.md"
|
|
-- an explicit target is the GitHub wiki checkout, whose page names are flat
|
|
local WIKI_FILE = "Reference-Registries.md"
|
|
|
|
-- precedence: argv, env, in-repo default -- so a wiki checkout is one flag
|
|
-- away and a CI job can set it once for every generator on this convention
|
|
local outDir = (... or nil)
|
|
if outDir == nil or outDir == "" then outDir = os.getenv("POKEPORT_DOCS_DIR") end
|
|
local explicit = outDir ~= nil and outDir ~= ""
|
|
if not explicit then outDir = DEFAULT_DIR end
|
|
outDir = outDir:gsub("/+$", "")
|
|
|
|
local OUT = outDir .. "/" .. (explicit and WIKI_FILE or DEFAULT_FILE)
|
|
|
|
local names = {}
|
|
for name in pairs(Schemas.REGISTRIES) do names[#names + 1] = name end
|
|
table.sort(names)
|
|
|
|
local out = {}
|
|
local function line(fmt, ...)
|
|
if select("#", ...) > 0 then
|
|
out[#out + 1] = fmt:format(...)
|
|
else
|
|
out[#out + 1] = fmt
|
|
end
|
|
end
|
|
|
|
line("<!-- Generated by tools/gen_registry_docs.lua from src/mods/Schemas.lua.")
|
|
line(" Do not edit by hand; regenerate after any schema change. -->")
|
|
line("")
|
|
line("# Registry reference")
|
|
line("")
|
|
line("One section per registry: merge semantics, the `Data` table the merge")
|
|
line("writes, and the value schema. Where Gold differs -- a different table, a")
|
|
line("different record, or no home at all -- the registry carries a Gen 2")
|
|
line("subsection built from the same catalog entry. Concepts and verbs:")
|
|
line("[Concepts: Registries](Concepts-Registries).")
|
|
line("")
|
|
line("## Reading this reference")
|
|
line("")
|
|
if explicit then
|
|
line("Start with [Choose a Registry](Choose-A-Registry) when you know what")
|
|
line("you want to make but not the registry name. This page is the precise")
|
|
line("schema catalog once you have chosen one.")
|
|
else
|
|
line("Use the wiki's `Choose-A-Registry` guide when you know what you want")
|
|
line("to make but not the registry name. This page is the precise schema")
|
|
line("catalog once you have chosen one.")
|
|
end
|
|
line("")
|
|
line("### Type notation")
|
|
line("")
|
|
line("- `string`, `number`, `boolean`, and `function` name the Lua value to")
|
|
line(" supply. A function is code the engine calls later; see the linked")
|
|
line(" concept/reference page for its arguments and return value.")
|
|
line("- `{ field, otherField? }` is a record (a Lua table with named fields).")
|
|
line(" A `?` means that field is optional. `list of T` is an ordered Lua")
|
|
line(" table of values shaped like `T`; `map of K -> V` maps each key to a")
|
|
line(" value shaped like `V`.")
|
|
line("- `A | B` means either shape is accepted. `moves id`, `items id`, and")
|
|
line(" similar phrases mean the internal id of a record in that registry,")
|
|
line(" not its displayed name.")
|
|
line("")
|
|
line("### Example status")
|
|
line("")
|
|
line("Every example below is **shape only**. Put an adapted call in your mod")
|
|
line("entry file. Replace `...` and names such as `fn` with real values, and")
|
|
line("supply every field marked required when registering a new record.")
|
|
|
|
local function tableCell(value)
|
|
return tostring(value):gsub("|", "\\|")
|
|
end
|
|
|
|
-- the value schema of one spec, whichever generation's shape it carries.
|
|
-- Schemas.check reads keys/keyValue, then value, then fields in that order,
|
|
-- so this renders them in the same order or the page would describe a branch
|
|
-- that never runs.
|
|
local function renderSchema(spec)
|
|
if spec.keys then
|
|
line("")
|
|
if spec.keysClosed then
|
|
line("Id = a top-level key of the target table. The set below is **closed**:")
|
|
line("an id that is not one of these is rejected rather than merged, because")
|
|
line("the engine reads this table by name and a key it does not name is a")
|
|
line("write nothing reads.")
|
|
else
|
|
line("Id = a top-level key of the target table. Keys not listed here are")
|
|
line("accepted and merged as-is.")
|
|
end
|
|
line("")
|
|
line("| key | type |")
|
|
line("|---|---|")
|
|
local keyNames = {}
|
|
for keyName in pairs(spec.keys) do keyNames[#keyNames + 1] = keyName end
|
|
table.sort(keyNames)
|
|
for _, keyName in ipairs(keyNames) do
|
|
line("| `%s` | %s |", keyName, tableCell(spec.keys[keyName].desc))
|
|
end
|
|
elseif spec.fields then
|
|
line("")
|
|
line("| field | type | required |")
|
|
line("|---|---|---|")
|
|
local fieldNames = {}
|
|
for fieldName in pairs(spec.fields) do fieldNames[#fieldNames + 1] = fieldName end
|
|
table.sort(fieldNames)
|
|
for _, fieldName in ipairs(fieldNames) do
|
|
local ft = spec.fields[fieldName]
|
|
line("| `%s` | %s | %s |", fieldName, tableCell(ft.desc),
|
|
ft.kind == "opt" and "no" or "yes")
|
|
end
|
|
elseif spec.keyValue then
|
|
line("")
|
|
line("Id = a top-level key of the target table; every key carries the same")
|
|
line("shape.")
|
|
line("")
|
|
line("- value: %s", spec.keyValue.desc)
|
|
elseif spec.value then
|
|
line("- value: %s", spec.value.desc)
|
|
end
|
|
end
|
|
|
|
-- prose a registry needs beyond its schema (resolution order, the guarantees
|
|
-- a value carries). It lives on the spec rather than in the page because
|
|
-- this file is regenerated: hand-written paragraphs in the output are deleted
|
|
-- by the next run.
|
|
local function renderExample(example, notes)
|
|
if example then
|
|
line("")
|
|
line("<!-- snippet: illustrative -->")
|
|
line("```lua")
|
|
line("%s", example)
|
|
line("```")
|
|
end
|
|
if notes then
|
|
line("")
|
|
line("%s", notes)
|
|
end
|
|
end
|
|
|
|
for _, name in ipairs(names) do
|
|
local spec = Schemas.REGISTRIES[name]
|
|
line("")
|
|
line("## %s", name)
|
|
line("")
|
|
line("- semantics: `%s`", spec.semantics)
|
|
line("- target: %s", spec.target and ("`Data." .. spec.target .. "`") or "none")
|
|
if spec.deprecated then
|
|
line("- **deprecated** -- use %s", spec.deprecated.useInstead)
|
|
end
|
|
if name == "link_fields" then
|
|
line("")
|
|
line("**Cart protocol only:** this registry does not make a freely enabled")
|
|
line("loose mod eligible for player link play. Player links are set up in the")
|
|
line("launcher with a vanilla cart or a sealed custom cart.")
|
|
end
|
|
-- The mirror of the Gen 2 gating below. Six registries exist because GOLD
|
|
-- does (the phone book, the decorations, the radio dial), so they carry no
|
|
-- Gen 1 target and "- target: none" on its own reads as a broken entry
|
|
-- rather than as the deliberate one-generation registry it is.
|
|
if Schemas.gatedFor(name, 1) then
|
|
line("")
|
|
line("Gen 2 only: Red, Blue and Yellow have no such system, so there is no")
|
|
line("Gen 1 table to merge into and a write here on a Gen 1 boot is dropped")
|
|
line("and reported. See the Gold subsection below for where it does land.")
|
|
end
|
|
renderSchema(spec)
|
|
renderExample(spec.example, spec.notes)
|
|
|
|
-- Gold. The registry NAME and the verbs are shared across generations, so
|
|
-- what a Gen 2 subsection says is only ever WHERE the merge lands and what
|
|
-- a record there looks like -- or that there is nowhere to land, which is a
|
|
-- write the loader drops and reports rather than a name a mod may not use.
|
|
local gen2Target = Schemas.targetFor(name, spec, 2)
|
|
local shaped = Schemas.hasGen2Shape(spec)
|
|
if Schemas.gatedFor(name, 2) then
|
|
line("")
|
|
line("### On Gold (Gen 2)")
|
|
line("")
|
|
line("No Gen 2 home: Gold reimplements this system without reading the")
|
|
line("registry, so a write here is dropped and reported on a Gold boot.")
|
|
line("`docs/mod-api-gen2-compat.md` in the engine repo lists what is left")
|
|
line("to do for each one.")
|
|
elseif gen2Target ~= spec.target or shaped then
|
|
local gen2 = Schemas.shapeFor(name, spec, 2)
|
|
line("")
|
|
line("### On Gold (Gen 2)")
|
|
line("")
|
|
line("- semantics: `%s`", gen2.semantics)
|
|
line("- target: `Data.%s`", gen2Target)
|
|
if shaped then
|
|
line("")
|
|
line("The record differs; the registry name, the verbs and the id space")
|
|
line("do not.")
|
|
renderSchema(gen2)
|
|
renderExample(spec.gen2Example, spec.gen2Notes)
|
|
else
|
|
renderExample(spec.gen2Example, spec.gen2Notes)
|
|
end
|
|
end
|
|
end
|
|
|
|
line("")
|
|
line("## v1 aliases")
|
|
line("")
|
|
line("| alias | canonical |")
|
|
line("|---|---|")
|
|
local aliases = {}
|
|
for alias in pairs(Schemas.ALIASES) do aliases[#aliases + 1] = alias end
|
|
table.sort(aliases)
|
|
for _, alias in ipairs(aliases) do
|
|
line("| `%s` | `%s` |", alias, Schemas.ALIASES[alias])
|
|
end
|
|
|
|
-- a wiki checkout may not have the directory yet; create it before the
|
|
-- open so pointing at a fresh clone is not a two-step
|
|
local file = io.open(OUT, "w")
|
|
if not file then
|
|
os.execute('mkdir -p "' .. outDir:gsub('"', '\\"') .. '"')
|
|
file = assert(io.open(OUT, "w"),
|
|
"cannot write " .. OUT .. " (is the output directory reachable?)")
|
|
end
|
|
file:write(table.concat(out, "\n") .. "\n")
|
|
file:close()
|
|
print("wrote " .. OUT)
|