-- One of this mod's own settings: a ladder of values, where it persists, -- and the row the player cycles it on. -- -- The engine gives a render pipeline all of this for free -- ladder, -- options row, hotkey, persistence -- but only to something that OWNS a -- pass of the frame. The voxel wireframe and the world curve do not: they -- parameterise the voxel pass, so they have nothing to put in drawWorld or -- present and the registry rightly rejects them. What is left is a plain -- mod setting, and this is the boilerplate two of them would otherwise -- each carry a copy of: -- -- options:define a home in options.modOptions.DRAMATIC_SHAPE, plus a row -- on this mod's page in the mod manager. -- ui.options.rows the same setting on the OPTIONS menu, where the -- player already goes for VOXEL and T-SHIFT. -- -- Both rows read and write the one stored value, so they cannot disagree. -- Writing mirrors what the manager's own page does (ManagerState:setOption): -- the live save's options table, the loader's copy that mod.options:get -- reads, and then the file. -- the mod namespace (see main.lua): V.require loads a sibling module local V = ... local ModSetting = {} ModSetting.__index = ModSetting local function modId() local mod = V.mod return (mod and mod.id) or "DRAMATIC_SHAPE" end -- `values` are the stored values in ladder order and `labels` what the row -- shows for each; values[1] is the default, and the one an unreadable or -- unrecognised stored value falls back to. function ModSetting.new(key, label, values, labels) return setmetatable({ key = key, label = label, values = values, labels = labels, index = nil, -- nil = not yet read back from the persisted options }, ModSetting) end local function indexOf(self, value) for i, v in ipairs(self.values) do if v == value then return i end end return 1 end -- ------- rungs that are not always there -- -- A ladder may carry a rung that cannot be selected right now -- STADIUM -- needs models built out of a ROM the player supplies, and until that has -- happened there is nothing behind the option. `gate` is asked per rung and -- decides whether it exists at all this frame. -- -- Skipped rather than shown-and-refused, deliberately. A row that can be -- cycled onto and then does nothing is indistinguishable from a broken mod; -- a row that simply has fewer stops reads as the mod not offering something, -- which is the truth. What the player is missing, and how to get it, is said -- once in the row's help text instead of implied by a dead setting. -- -- values[1] is never gated: it is the default and the fallback, so there is -- always at least one rung to land on. function ModSetting:setGate(gate) self.gate = gate return self end function ModSetting:allows(i) if i == 1 or not self.gate then return true end local ok, allowed = pcall(self.gate, self.values[i], i) return (not ok) or allowed and true or false end -- How many rungs are live, for a caller that wants to know whether a row is -- worth showing at all. function ModSetting:rungs() local n = 0 for i = 1, #self.values do if self:allows(i) then n = n + 1 end end return n end -- What the player left it at last session. Read lazily rather than at load -- time: the loader fills modOptions before a mod runs, but reading through -- the API keeps this honest about where the value lives. function ModSetting:read() if self.index then return self.index end local mod = V.mod local value if mod and mod.options then local ok, got = pcall(mod.options.get, mod.options, self.key) if ok then value = got end end self.index = indexOf(self, value) return self.index end function ModSetting:get() local i = self:read() -- a rung that was live when it was stored and is not now -- the player -- moved the ROM, or opened the same save on another machine -- reads as -- the default rather than as a mode with nothing behind it. The stored -- value is left alone, so putting the ROM back restores their choice. if not self:allows(i) then return self.values[1] end return self.values[i] end function ModSetting:level() return self:read() - 1 end function ModSetting:setIndex(i, game) local n = #self.values i = ((i - 1) % n + n) % n + 1 self.index = i local value, id = self.values[i], modId() local opts = game and game.save and game.save.options if opts then opts.modOptions = opts.modOptions or {} opts.modOptions[id] = opts.modOptions[id] or {} opts.modOptions[id][self.key] = value end local loader = game and game.mods if loader then loader.modOptions = loader.modOptions or {} loader.modOptions[id] = loader.modOptions[id] or {} loader.modOptions[id][self.key] = value end if game and game.writeOptions then pcall(game.writeOptions, game) end return value end -- Set by the STORED VALUE rather than by its place on the ladder, for a -- caller that knows which setting it wants and not where it sits -- a -- preset, or an assertion. An unrecognised value lands on values[1], the -- same default indexOf answers everywhere else, so this can never leave a -- setting holding something the row cannot display. -- -- Worth having as its own entry point because a ladder's ORDER is not a -- promise: 3D-BTL grew a third rung in the middle of itself (see -- OverworldBattle), and every caller that had counted to two would have -- silently meant something else afterwards. function ModSetting:setValue(value, game) return self:setIndex(indexOf(self, value), game) end -- Step to the next rung that is actually live, in `dir`. Bounded by the -- ladder's length so a gate that refuses everything still terminates on -- values[1], which allows() never gates. function ModSetting:cycle(game, dir) dir = dir or 1 local n = #self.values local i = self:read() for _ = 1, n do i = ((i + dir - 1) % n + n) % n + 1 if self:allows(i) then break end end return self:setIndex(i, game) end -- Adopt a value set from somewhere else (the mod manager's settings page, -- which writes and persists on its own). Nothing to store: just move the -- cached index so the next read agrees with it. function ModSetting:sync(value) self.index = indexOf(self, value) end -- The label of the rung actually in force, which is not the stored one when -- that rung has been gated away (see get). Its own entry point because a -- caller can want the label without wanting a row: SettingsMenu puts one -- setting's rung on the second line of the CATEGORY that contains it. function ModSetting:valueLabel() local i = self:read() return self.labels[self:allows(i) and i or 1] end -- The descriptor src/ui/OptionRows.lua renders, in the shape the -- ui.options.rows hook appends. function ModSetting:row() local self_ = self return { id = "DRAMATIC_SHAPE:" .. self.key, label = self.label, value = function() return self_:valueLabel() end, step = function(game, dir) self_:cycle(game, dir) return true end, } end -- The row the mod manager's own settings page builds for this mod. function ModSetting:schema(help) local choices = {} -- gated rungs are left off the manager's page too, so the two rows agree -- about what can be chosen for i, v in ipairs(self.values) do if self:allows(i) then choices[#choices + 1] = { self.labels[i], v } end end if #self.values == 2 and self.values[1] == false then return { key = self.key, type = "toggle", label = self.label, default = self.values[1], help = help } end return { key = self.key, type = "choice", label = self.label, choices = choices, default = self.values[1], help = help } end return ModSetting