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 `choice`, `number`, and `text`. Their optional fields retain the meanings
established by the existing in-game option UI: choices are `[label, value]` 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 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 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 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, For compatibility with files produced by the original unversioned prototype,
a missing `schema_version` means version 1. Consumers must ignore documents a missing `schema_version` means version 1. Consumers must ignore documents
with a newer version rather than guessing at their shape. Producers must bump 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 the version whenever they change the document envelope or the meaning of an
existing field. Version 1 is therefore the legacy unversioned format as well existing field. New optional row fields that older consumers can safely ignore
as the explicitly versioned format shown above. do not require a bump. Version 1 is therefore the legacy unversioned format as
well as the explicitly versioned format shown above.
## Migration note ## Migration note
+43 -1
View File
@@ -986,12 +986,45 @@ end
function ManagerState:buildOptionRows(m, schema) function ManagerState:buildOptionRows(m, schema)
local rows = {} local rows = {}
local modId = m.id 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 for _, row in ipairs(schema) do
if type(row) ~= "table" or type(row.key) ~= "string" or row.key == "" if type(row) ~= "table" or type(row.key) ~= "string" or row.key == ""
or not OPTION_TYPES[row.type] then or not OPTION_TYPES[row.type] then
-- malformed rows are skipped, reported where the errors screen reads -- malformed rows are skipped, reported where the errors screen reads
Runtime.reportError(modId, "options row skipped: " Runtime.reportError(modId, "options row skipped: "
.. tostring(type(row) == "table" and (row.key or row.type) or row)) .. 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 elseif row.type == "toggle" then
rows[#rows + 1] = { id = row.key, label = row.label or row.key, rows[#rows + 1] = { id = row.key, label = row.label or row.key,
value = function() value = function()
@@ -999,6 +1032,7 @@ function ManagerState:buildOptionRows(m, schema)
end, end,
step = function() step = function()
self:setOption(modId, row.key, not self:optionValue(modId, row)) self:setOption(modId, row.key, not self:optionValue(modId, row))
refresh(row.key)
return true return true
end } end }
elseif row.type == "choice" then elseif row.type == "choice" then
@@ -1021,6 +1055,7 @@ function ManagerState:buildOptionRows(m, schema)
end end
index = clampIndex(index + dir, #choices) index = clampIndex(index + dir, #choices)
self:setOption(modId, row.key, choices[index][2]) self:setOption(modId, row.key, choices[index][2])
refresh(row.key)
return true return true
end } end }
elseif row.type == "number" then elseif row.type == "number" then
@@ -1036,6 +1071,7 @@ function ManagerState:buildOptionRows(m, schema)
step = function(_, dir) step = function(_, dir)
local cur = tonumber(self:optionValue(modId, row)) or 0 local cur = tonumber(self:optionValue(modId, row)) or 0
self:setOption(modId, row.key, clamp(cur + dir * (row.step or 1))) self:setOption(modId, row.key, clamp(cur + dir * (row.step or 1)))
refresh(row.key)
return true return true
end, end,
activate = function() activate = function()
@@ -1044,7 +1080,10 @@ function ManagerState:buildOptionRows(m, schema)
max = row.max or 99, max = row.max or 99,
start = math.max(1, tonumber(self:optionValue(modId, row)) or 1), start = math.max(1, tonumber(self:optionValue(modId, row)) or 1),
onDone = function(qty) 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,
})) }))
end } end }
@@ -1061,6 +1100,7 @@ function ManagerState:buildOptionRows(m, schema)
default = self:optionValue(modId, row), default = self:optionValue(modId, row),
onDone = function(name) onDone = function(name)
self:setOption(modId, row.key, name) self:setOption(modId, row.key, name)
refresh(row.key)
end, end,
})) }))
end } end }
@@ -1075,6 +1115,8 @@ function ManagerState:buildOptionRows(m, schema)
self:setOption(modId, row.key, row.default) self:setOption(modId, row.key, row.default)
end end
end end
self.optionRows = self:buildOptionRows(m, schema)
self.cursor = clampIndex(self.cursor, #self.optionRows)
self:notify("DEFAULTS RESTORED") self:notify("DEFAULTS RESTORED")
end } end }
return rows 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.startMoney == 3000
and loader.modOptions.okmod.tag == "BLUE", and loader.modOptions.okmod.tag == "BLUE",
"RESET DEFAULTS restores every schema default") "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") press(ms, "b")
check(ms.screen == "list", "B leaves the options screen") check(ms.screen == "list", "B leaves the options screen")