Merge pull request #1369 from AverageConsumer/codex/mod-option-conditional-rows

This commit is contained in:
bryanthaboi
2026-08-15 20:20:26 -04:00
committed by GitHub
3 changed files with 73 additions and 5 deletions
+9 -4
View File
@@ -46,7 +46,11 @@ when the runtime schema is absent. The supported row types are `toggle`,
`choice`, `number`, and `text`. Their optional fields retain the meanings
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`.
provide `maxLen`. A row may also use
`visible_if = {key = "mode", equals = "compact"}` or replace `equals` with
`not_equals`. This only hides the in-game menu row; the schema and stored value
remain available, and consumers that do not implement conditions may ignore
the field.
Only mods that are enabled and successfully loaded in the current boot are
included. A disabled or failed mod must not contribute rows. If an older
@@ -69,9 +73,10 @@ 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.
the version whenever they change the document envelope or the meaning of an
existing field. New optional row fields that older consumers can safely ignore
do not require a bump. Version 1 is therefore the legacy unversioned format as
well as the explicitly versioned format shown above.
## Migration note
+43 -1
View File
@@ -986,12 +986,45 @@ end
function ManagerState:buildOptionRows(m, schema)
local rows = {}
local modId = m.id
local byKey, visibilityKeys = {}, {}
for _, row in ipairs(schema) do
if type(row) == "table" and type(row.key) == "string" then
byKey[row.key] = row
local condition = row.visible_if
if type(condition) == "table" and type(condition.key) == "string" then
visibilityKeys[condition.key] = true
end
end
end
local function visible(row)
local condition = row.visible_if
if condition == nil then return true end
if type(condition) ~= "table" or type(condition.key) ~= "string" then
return false
end
local dependency = byKey[condition.key] or { key = condition.key }
local value = self:optionValue(modId, dependency)
if condition.equals ~= nil then return value == condition.equals end
if condition.not_equals ~= nil then return value ~= condition.not_equals end
return false
end
local function refresh(key)
if not visibilityKeys[key] then return end
local preferred = rows[self.cursor] and rows[self.cursor].id
self.optionRows = self:buildOptionRows(m, schema)
for index, candidate in ipairs(self.optionRows) do
if candidate.id == preferred then self.cursor = index break end
end
self.cursor = clampIndex(self.cursor, #self.optionRows)
end
for _, row in ipairs(schema) do
if type(row) ~= "table" or type(row.key) ~= "string" or row.key == ""
or not OPTION_TYPES[row.type] then
-- malformed rows are skipped, reported where the errors screen reads
Runtime.reportError(modId, "options row skipped: "
.. tostring(type(row) == "table" and (row.key or row.type) or row))
elseif not visible(row) then
-- Keep the row in the schema and stored options, only hide its menu row.
elseif row.type == "toggle" then
rows[#rows + 1] = { id = row.key, label = row.label or row.key,
value = function()
@@ -999,6 +1032,7 @@ function ManagerState:buildOptionRows(m, schema)
end,
step = function()
self:setOption(modId, row.key, not self:optionValue(modId, row))
refresh(row.key)
return true
end }
elseif row.type == "choice" then
@@ -1021,6 +1055,7 @@ function ManagerState:buildOptionRows(m, schema)
end
index = clampIndex(index + dir, #choices)
self:setOption(modId, row.key, choices[index][2])
refresh(row.key)
return true
end }
elseif row.type == "number" then
@@ -1036,6 +1071,7 @@ function ManagerState:buildOptionRows(m, schema)
step = function(_, dir)
local cur = tonumber(self:optionValue(modId, row)) or 0
self:setOption(modId, row.key, clamp(cur + dir * (row.step or 1)))
refresh(row.key)
return true
end,
activate = function()
@@ -1044,7 +1080,10 @@ function ManagerState:buildOptionRows(m, schema)
max = row.max or 99,
start = math.max(1, tonumber(self:optionValue(modId, row)) or 1),
onDone = function(qty)
if qty then self:setOption(modId, row.key, clamp(qty)) end
if qty then
self:setOption(modId, row.key, clamp(qty))
refresh(row.key)
end
end,
}))
end }
@@ -1061,6 +1100,7 @@ function ManagerState:buildOptionRows(m, schema)
default = self:optionValue(modId, row),
onDone = function(name)
self:setOption(modId, row.key, name)
refresh(row.key)
end,
}))
end }
@@ -1075,6 +1115,8 @@ function ManagerState:buildOptionRows(m, schema)
self:setOption(modId, row.key, row.default)
end
end
self.optionRows = self:buildOptionRows(m, schema)
self.cursor = clampIndex(self.cursor, #self.optionRows)
self:notify("DEFAULTS RESTORED")
end }
return rows
+21
View File
@@ -1106,6 +1106,27 @@ check(loader.modOptions.okmod.hardcore == false
and loader.modOptions.okmod.startMoney == 3000
and loader.modOptions.okmod.tag == "BLUE",
"RESET DEFAULTS restores every schema default")
-- optional conditions keep mode-specific rows compact and refresh in place
local conditionalSchema = {
{ key = "mode", label = "MODE", type = "choice", default = "one",
choices = { { "ONE", "one" }, { "TWO", "two" } } },
{ key = "oneOnly", label = "ONE ONLY", type = "toggle", default = false,
visible_if = { key = "mode", equals = "one" } },
{ key = "twoOnly", label = "TWO ONLY", type = "toggle", default = false,
visible_if = { key = "mode", equals = "two" } },
{ key = "notOne", label = "NOT ONE", type = "toggle", default = false,
visible_if = { key = "mode", not_equals = "one" } },
}
loader.modOptions.condmod = {}
ms.cursor = 1
ms.optionRows = ms:buildOptionRows({ id = "condmod" }, conditionalSchema)
check(#ms.optionRows == 3 and ms.optionRows[2].id == "oneOnly",
"visible_if uses the controlling row default")
ms.optionRows[1].step(mgame, 1)
check(#ms.optionRows == 4 and ms.optionRows[2].id == "twoOnly"
and ms.optionRows[3].id == "notOne" and ms.cursor == 1,
"editing a controller refreshes conditions without moving the cursor")
press(ms, "b")
check(ms.screen == "list", "B leaves the options screen")