DramaticShapeVoxelMod archival mirror · read-only
lib/ModSetting.lua

ModSetting.lua

lib/ModSetting.lua7.6 KB100644d87112b717
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
-- 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