-- DopingControl core/config.lua -- SavedVariables defaults + fill-in: single DEFAULTS table, per-field fill-in -- with nil-checks (NEVER 'or' for booleans -- would flip saved false), -- copy-on-assign for table values so editing a live value can never mutate -- the defaults. -- Pure Lua 5.0, no WoW API -- dofile-loadable offline. DopingControl = DopingControl or {} local DC = DopingControl -- SavedVariables root (the client replaces this global at ADDON_LOADED; -- core/init.lua then calls DC.EnsureDefaults(DopingControlDB)). DopingControlDB = DopingControlDB or {} -- ------------------------------------------------------------------ -- Defaults -- -- Two keys intentionally have NO entry here (a nil value cannot live in a -- Lua table): -- framePos -- default nil = "no saved position, use the anchor -- default"; written by the UI on drag as -- { point=..., relPoint=..., x=..., y=... }. -- expectations -- default nil = "not initialized yet"; EnsureDefaults -- special-cases it below: the saved shape -- is PER CLASS (db.expectations[role][class][slotId]), -- materialized/migrated from the DC.DEFAULT_EXPECT seed -- by DC.EnsureExpectations. -- ------------------------------------------------------------------ DC.DEFAULTS = { testMode = false, -- /dc test: simulated raid showMinimapButton = true, minimapAngle = 215, -- degrees around the minimap rim readyCheckScan = true, -- auto-scan on READY_CHECK readyCheckOpen = true, -- auto-open window on READY_CHECK collapse = {}, -- per-role collapsed flags, e.g. collapse.RANGED = true filterGapsOnly = false, -- toolbar filter All | Gaps only roles = {}, -- confirmed roles keyed by player NAME -- Weapon skill books: db.skillBooks[playerName][weaponSkillName] = -- true|false. A quest weapon-skill book (+3 skill) is INVISIBLE on -- other players -- foreign weapon skill cannot be read at all (see -- data/hit.lua) -- so it is an opt-in the user ticks per player and -- per weapon skill; it only shifts that player's melee/ranged hit CAP. -- Unchecking stores an explicit false (never nil), exactly like the -- expectation grid, so "deliberately off" survives any later fill-in. -- The own character ignores these flags: its weapon skill is measured -- and a measured value already contains the book. skillBooks = {}, } -- Generic deep copy (tables only; keys are copied by reference, values -- recursively). No cycle handling -- config/expectation shapes are trees. function DC.DeepCopy(src) if type(src) ~= "table" then return src end local out = {} for k, v in pairs(src) do if type(v) == "table" then out[k] = DC.DeepCopy(v) else out[k] = v end end return out end -- ------------------------------------------------------------------ -- Expectations: the saved shape is -- PER CLASS -- -- db.expectations[role][class][slotId] = true|nil -- with one class line for every class that can perform the role -- (DC_Roles.ROLE_CLASSES, the ordered inversion of CLASS_ROLES). -- DC.DEFAULT_EXPECT (data/expectations.lua) STAYS the role x slot SEED. -- -- EnsureExpectations guarantees the full structure: -- * first load (expectations == nil): every (role, performing class) -- gets a deep copy of the seed's role line. -- * legacy saved tables (one line per role) -- detected by BOOLEANS directly under -- [role] (new-shape values are class tables) -- are MIGRATED: the -- old role line is copied to every performing class, then the -- structure is replaced. User edits survive on every class where -- they are representable. -- * new-shape tables: existing class lines are NEVER touched; missing -- (role,class) combos are filled from the seed. The fill is -- behavior-neutral -- the model resolves a missing combo to the -- DC.DEFAULT_EXPECT role line anyway (runtime fallback) -- it only -- hands the options grid a complete structure. -- Known ambiguity (accepted): an old-shape role line with EVERY slot -- unchecked is {} and indistinguishable from an untouched new-shape -- role table (the boolean marker is gone); it is re-seeded per class. -- -- Resolved at CALL time, not file-load time: data/expectations.lua and -- core/roles.lua load after this file (TOC order const -> config -> -- data/* -> model -> roles), but EnsureDefaults only runs at -- ADDON_LOADED when everything is in. Offline callers without DC_Roles -- loaded simply skip the expectations block. -- ------------------------------------------------------------------ local function isOldShapeLine(line) for _, v in pairs(line) do if type(v) == "boolean" then return true end end return false end -- Top up ONE class line in place: slots the seed marks true and the line -- has NEVER SEEN (nil) become true. Deliberate unchecks survive (the -- options grid stores those as explicit false, never nil). Shared by both -- EnsureExpectations branches below so a freshly migrated line is already -- complete -- not just on the SECOND call (idempotency: a migration must -- converge in one pass, not two). local function topUpLine(cl, seedLine) for slotId, v in pairs(seedLine) do if v == true and cl[slotId] == nil then cl[slotId] = true end end end function DC.EnsureExpectations(db) if DC.DEFAULT_EXPECT == nil or DC_Roles == nil or DC_Roles.ROLE_CLASSES == nil then return end if db.expectations == nil then db.expectations = {} end local exp = db.expectations local nRoles = table.getn(DC.ROLES) for r = 1, nRoles do local role = DC.ROLES[r] local classes = DC_Roles.ROLE_CLASSES[role] or {} local nClasses = table.getn(classes) local line = exp[role] local seedLine = DC.DEFAULT_EXPECT[role] or {} if line ~= nil and isOldShapeLine(line) then -- migration: copy the old role line to every performing -- class, then top up each new class line from the CURRENT -- seed (a slot added since the old line was saved must reach -- it immediately, not only on a later call -- see topUpLine), -- then replace the structure (never lose user edits where -- representable) local byClass = {} for c = 1, nClasses do local cl = DC.DeepCopy(line) topUpLine(cl, seedLine) byClass[classes[c]] = cl end exp[role] = byClass else if line == nil then line = {} exp[role] = line end -- fill missing (role,class) combos from the seed; existing -- class lines keep their values, but slots the seed knows -- and the line has NEVER SEEN (nil) are topped up -- without -- this, a slot added in a later version never reaches saved -- lines (field report: neck/ring columns arrived as "not -- expected" for everyone). Deliberate unchecks survive: the -- options grid stores them as explicit false, never nil. for c = 1, nClasses do local cls = classes[c] if line[cls] == nil then line[cls] = DC.DeepCopy(seedLine) else topUpLine(line[cls], seedLine) end end end end end -- Fill missing fields in db from DC.DEFAULTS. Existing values -- including -- saved booleans that are false -- are never touched. Table defaults are -- deep-copied on assign. function DC.EnsureDefaults(db) for k, v in pairs(DC.DEFAULTS) do if db[k] == nil then if type(v) == "table" then db[k] = DC.DeepCopy(v) else db[k] = v end end end -- expectations: per-(role,class) shape, materialized/migrated from -- the DC.DEFAULT_EXPECT seed (see EnsureExpectations above) DC.EnsureExpectations(db) return db end -- Reset the whole config IN PLACE (keeps the table reference -- the client -- saves the global DopingControlDB, so the reference must survive). function DC.ResetConfig(db) for k in pairs(db) do db[k] = nil end return DC.EnsureDefaults(db) end -- Reset only the expectation matrix to the DC.DEFAULT_EXPECT seed, -- materialized per (role, performing class) -- options button -- "Reset expectations" (reseeds ALL classes). function DC.ResetExpectations(db) db.expectations = nil DC.EnsureDefaults(db) return db.expectations end