Files
DopingControl/core/init.lua
T
2026-08-30 15:37:17 +02:00

274 lines
11 KiB
Lua

-- DopingControl core/init.lua
-- Bootstrap: ADDON_LOADED wiring (SavedVariables defaults), event
-- registration (roster changes, READY_CHECK), test-mode switch, slash
-- commands /dc + /doping. Loads LAST in the TOC -- every module global
-- (DC_Scan, DC_Matrix, DC_Options, DC_Report, DC_Sim, DC_Probe) exists by
-- the time any function here runs.
--
-- Pure-Lua 5.0 dofile-loadable: all WoW wiring behind "if CreateFrame then".
-- The pure parts (DC.HandleSlash, DC.SetTestMode, DC.DumpUnknown) are
-- offline-tested with stubbed modules.
DopingControl = DopingControl or {}
local DC = DopingControl
-- Store (the shared data shape every consumer reads). Empty until the
-- first scan finishes or test mode injects the simulated store.
DC.store = DC.store or { players = {}, source = "scan" }
-- Unknown-aura tally, filled by scan/aura.lua (DC.unknownSeen[name] = count);
-- read here by /dc unknown (unknown buff names are never silently discarded).
DC.unknownSeen = DC.unknownSeen or {}
local CHAT_PREFIX = "|cff3fae5cDopingControl|r: "
function DC.Print(msg)
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage(CHAT_PREFIX .. tostring(msg))
else
print(CHAT_PREFIX .. tostring(msg)) -- offline Lua 5.0
end
end
-- Called by scan/engine.lua whenever the store changed (scan finished).
function DC.OnStoreUpdated()
DC_Matrix.Refresh()
end
-- Matrix visibility, used to decide whether a roster change should trigger
-- an immediate rescan. DC_Matrix.IsShown is not pinned in the plan -- if
-- ui/matrix.lua does not provide it, we conservatively treat the window as
-- hidden (next manual scan / ready check rescans anyway).
local function MatrixIsShown()
if DC_Matrix and DC_Matrix.IsShown then
return DC_Matrix.IsShown()
end
return nil
end
-- ------------------------------------------------------------------
-- Test mode: the simulator fully replaces the data source.
-- ------------------------------------------------------------------
function DC.SetTestMode(v)
local db = DC.db or DopingControlDB
db.testMode = v
if v then
-- buildStore() itself resets and refills DC.simRoles with the
-- built-in confirmations (DC_Sim.ROLES, core/sim.lua) -- do NOT wipe
-- DC.simRoles afterwards, that would erase them and turn every row
-- into a suggestion badge (the sim roster deliberately contains
-- exactly two suggestion-badge cases).
DC.store = DC_Sim.buildStore()
elseif DC.demoMode then
-- Demo mode owns the simulated store on its own account
-- (DC.SimOwnsStore) -- the mirror image of core/demo.lua's
-- "leaving demo mode must not kick test mode out of the sim
-- store". Without this the showcase would be emptied here and the
-- rescan below would be dropped by the scan gate anyway, leaving a
-- blank window. /dc demo is what ends demo mode.
DC.store = DC_Sim.buildStore()
else
DC.store = { players = {}, source = "scan" }
DC_Scan.Start(true) -- leaving test mode: immediate real rescan
end
DC_Matrix.Refresh()
end
-- ------------------------------------------------------------------
-- /dc unknown -- dump DC.unknownSeen via SuperWoW ExportFile.
-- ------------------------------------------------------------------
function DC.DumpUnknown()
local names = {}
local distinct = 0
for name in pairs(DC.unknownSeen) do
table.insert(names, name)
distinct = distinct + 1
end
if distinct == 0 then
DC.Print("no unknown buffs recorded")
return
end
table.sort(names, function(a, b)
local ca, cb = DC.unknownSeen[a], DC.unknownSeen[b]
if ca ~= cb then return ca > cb end
return a < b
end)
local lines = { "DopingControl unknown buffs (" .. distinct .. " names)" }
for i = 1, table.getn(names) do
table.insert(lines, string.format("%dx %s", DC.unknownSeen[names[i]], names[i]))
end
local text = table.concat(lines, "\n")
if ExportFile then
-- SuperWoW appends ".txt" itself -- pass the name WITHOUT extension.
-- pcall: optional-DLL call -- an I/O error must not
-- escape into the slash handler.
local okE = pcall(ExportFile, "dc_unknown", text)
if okE then
DC.Print(distinct .. " unknown buff names dumped to dc_unknown.txt (WoW Imports folder)")
else
DC.Print("ExportFile failed - no dump written")
end
else
DC.Print("ExportFile unavailable (SuperWoW required) - no dump written")
end
end
-- ------------------------------------------------------------------
-- Slash commands: /dc, /doping
-- ------------------------------------------------------------------
local HELP_TEXT = "commands: /dc (window) | scan | options | report | test | demo | probe [range <name>] | unknown | talents [on|off|clear]"
function DC.HandleSlash(msg)
msg = msg or ""
local _, _, word, rest = string.find(msg, "^%s*(%S*)%s*(.-)%s*$")
local cmd = string.lower(word or "")
local db = DC.db or DopingControlDB
if cmd == "" then
DC_Matrix.Toggle()
elseif cmd == "scan" then
-- Real data never beats the simulation while EITHER simulated mode
-- owns the store (DC.SimOwnsStore, core/config.lua). Test mode is
-- named first because it is the persisted one -- but the refusal
-- must name the mode that is actually on, or the user toggles the
-- wrong switch and the scan keeps refusing.
if db and db.testMode then
DC.Print("test mode is on - /dc test to switch back before scanning")
elseif DC.SimOwnsStore(db) then
DC.Print("demo mode is on - /dc demo to switch back before scanning")
else
DC_Scan.Start()
end
elseif cmd == "options" then
DC_Options.Toggle()
elseif cmd == "report" then
DC_Report.Open()
elseif cmd == "test" then
DC.SetTestMode(not (db and db.testMode))
if db and db.testMode then
DC.Print("test mode ON - simulated raid, chat output disabled")
elseif DC.demoMode then
-- SetTestMode kept the sim store for demo mode and started no
-- rescan; a "rescanning" message here would be false.
DC.Print("test mode OFF - demo mode still on, still showing the simulated raid (/dc demo to leave)")
else
DC.Print("test mode OFF - rescanning the real group")
end
elseif cmd == "demo" then
-- Showcase view: the simulated raid plus a TRANSIENT expectation
-- layer that shows all six school protection columns at once
-- (core/demo.lua). Nothing about it is saved -- unlike /dc test,
-- which persists db.testMode.
if not DC.SetDemoMode then
DC.Print("demo module not loaded")
else
DC.SetDemoMode(not DC.demoMode)
if DC.demoMode then
DC.Print("demo mode ON - simulated raid, all six protection columns shown")
else
DC.Print("demo mode OFF - back to the normal view")
end
end
elseif cmd == "probe" then
-- core/probe.lua owns the probe subcommand grammar:
-- DC_Probe.HandleSlash(rest) handles "" (battery),
-- "range <name>" (1 Hz run) and the range-run stop toggle.
DC_Probe.HandleSlash(rest)
elseif cmd == "unknown" then
DC.DumpUnknown()
elseif cmd == "talents" then
-- foreign talents come in over this server's inspect protocol
-- (scan/talents.lua). "on"/"off" is a real switch, not cosmetic:
-- asking makes OTHER players' clients send their tree back, so it
-- must be possible to stop doing that.
if not DC_Talents then
DC.Print("talent module not loaded")
elseif rest == "on" or rest == "off" then
DopingControlDB.readTalents = (rest == "on")
DC.Print("talent reading " .. rest)
if rest == "off" and DC_Talents.Clear then
DC_Talents.Clear()
end
elseif rest == "clear" then
DC_Talents.Clear()
DC.Print("talent cache cleared - they will be asked again")
else
local s = DC_Talents.Stats()
DC.Print("talents: " .. s.known .. " known, " .. s.pending
.. " mid-reply, " .. s.queued .. " queued, "
.. (s.enabled and "ON" or "OFF")
.. " (/dc talents on|off|clear)")
end
else
DC.Print(HELP_TEXT)
end
end
-- ------------------------------------------------------------------
-- WoW wiring
-- ------------------------------------------------------------------
if CreateFrame then
SLASH_DOPINGCONTROL1 = "/dc"
SLASH_DOPINGCONTROL2 = "/doping"
SlashCmdList["DOPINGCONTROL"] = DC.HandleSlash
-- Matrix toolbar hooks (ui/matrix.lua fires MX.hooks.*): the Options
-- and Report buttons fall back to DC_Options/DC_Report directly, but
-- the Scan button ONLY fires MX.hooks.scan -- wire it here through the
-- slash path so it gets the same test-mode gate as "/dc scan".
if DC_Matrix and DC_Matrix.hooks then
DC_Matrix.hooks.scan = function()
DC.HandleSlash("scan")
end
end
local ev = CreateFrame("Frame", "DopingControlInitEventFrame", UIParent)
ev:RegisterEvent("ADDON_LOADED")
ev:SetScript("OnEvent", function()
if event == "ADDON_LOADED" then
if arg1 ~= "DopingControl" then return end
ev:UnregisterEvent("ADDON_LOADED")
-- SavedVariables are in place now.
DopingControlDB = DopingControlDB or {}
DC.EnsureDefaults(DopingControlDB) -- also deep-copies
-- DC.DEFAULT_EXPECT into db.expectations on first load
-- (core/config.lua handles that inside EnsureDefaults).
DC.db = DopingControlDB
-- Roster changes invalidate the store; rescan only when the
-- window is up (otherwise the next scan re-reads the roster
-- anyway -- scan/engine.lua re-walks it on every Start()).
ev:RegisterEvent("RAID_ROSTER_UPDATE")
ev:RegisterEvent("PARTY_MEMBERS_CHANGED")
-- READY_CHECK: registered defensively -- it is unverified
-- whether this event fires natively on 1.12/TurtleWoW, so a
-- rejecting client must not break the bootstrap.
pcall(ev.RegisterEvent, ev, "READY_CHECK")
-- Persisted test mode survives reloads: restore the simulated
-- store so window content and db.testMode stay consistent.
if DC.db.testMode then
DC.store = DC_Sim.buildStore()
end
elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
local db = DC.db
if db and not DC.SimOwnsStore(db) and MatrixIsShown() then
DC_Scan.Start()
end
elseif event == "READY_CHECK" then
-- The auto-SCAN on ready check is owned by scan/engine.lua
-- (single owner) -- registering the Start
-- here too fired a duplicate full scan per ready check. This
-- handler only does the auto-OPEN half.
local db = DC.db
if db and db.readyCheckOpen then
DC_Matrix.Show()
end
end
end)
end