mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
feat(mods): support conditional option rows
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user