DopingControl v0.6.1
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
-- 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
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
-- DopingControl core/const.lua
|
||||
-- Shared constants: roles, states, tabs, color palette, quality colors.
|
||||
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
|
||||
|
||||
DopingControl = DopingControl or {}
|
||||
local DC = DopingControl
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Roles (array order = display order) and labels
|
||||
-- ------------------------------------------------------------------
|
||||
DC.ROLES = { "TANK", "MELEE", "RANGED", "CASTER", "HEALER" }
|
||||
|
||||
DC.ROLE_LABEL = {
|
||||
TANK = "Tanks",
|
||||
MELEE = "Melee",
|
||||
RANGED = "Ranged",
|
||||
CASTER = "Casters",
|
||||
HEALER = "Healers",
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Cell/data states. Hard rule: UNKNOWN is NEVER condensed to MISSING --
|
||||
-- an unreadable player is "no data", not "has nothing".
|
||||
-- ------------------------------------------------------------------
|
||||
DC.STATE = {
|
||||
HAS = "HAS",
|
||||
MISSING = "MISSING",
|
||||
UNKNOWN = "UNKNOWN",
|
||||
NOTEXP = "NOTEXP",
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Tabs (order = display order). DEBUFFS' columns are built DYNAMICALLY
|
||||
-- from the store (no data/slot table); every other tab has a static slot
|
||||
-- table. DEBUFFS, RESIST and HIT are pure DISPLAY tabs -- no expectations,
|
||||
-- no gaps, no sums, no whisper (see the slot tables below and
|
||||
-- core/model.lua's per-tab branches). HIT sits between RESIST and
|
||||
-- EQUIPMENT because it answers the same "how is this player set up"
|
||||
-- question as the resistances, only for the attack side.
|
||||
-- ------------------------------------------------------------------
|
||||
DC.TABS = { "CONSUMABLES", "CLASSBUFFS", "DEBUFFS", "RESIST", "HIT", "EQUIPMENT" }
|
||||
|
||||
DC.TAB_LABEL = {
|
||||
CONSUMABLES = "Consumables",
|
||||
CLASSBUFFS = "Class buffs",
|
||||
DEBUFFS = "Debuffs",
|
||||
RESIST = "Resistances",
|
||||
HIT = "Hit",
|
||||
EQUIPMENT = "Equipment",
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- RESIST tab slot table (static, unlike DEBUFFS' dynamic columns --
|
||||
-- resistances are always the same five schools). All columns are ALWAYS
|
||||
-- active; expectations are never consulted on this tab (display only, see
|
||||
-- core/model.lua).
|
||||
--
|
||||
-- `school` is the UnitResistance() school index (0 = physical, 1 = holy --
|
||||
-- both never queried here; 2..6 below).
|
||||
-- ------------------------------------------------------------------
|
||||
DC.SLOTS_RESIST = {
|
||||
{ id = "RFIRE", label = "FIR", sub = "fire", full = "Fire resistance", kind = "resist", school = 2 },
|
||||
{ id = "RNAT", label = "NAT", sub = "nature", full = "Nature resistance", kind = "resist", school = 3 },
|
||||
{ id = "RFROST", label = "FRO", sub = "frost", full = "Frost resistance", kind = "resist", school = 4 },
|
||||
{ id = "RSHAD", label = "SHA", sub = "shadow", full = "Shadow resistance", kind = "resist", school = 5 },
|
||||
{ id = "RARC", label = "ARC", sub = "arcane", full = "Arcane resistance", kind = "resist", school = 6 },
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- HIT tab slot table (static). Same display-only rules as RESIST: every
|
||||
-- column is always active, expectations are never consulted, nothing is
|
||||
-- ever MISSING and nothing is whispered -- a hit number below its cap is
|
||||
-- information, not a gap to nag about.
|
||||
--
|
||||
-- The numbers behind these columns are read from item TOOLTIPS (per-item
|
||||
-- lines plus set bonuses, counted once per set), from hit-granting AURAS,
|
||||
-- and from TALENTS -- the own character's via GetTalentInfo, everyone
|
||||
-- else's via this server's inspect protocol (scan/talents.lua). There is
|
||||
-- no hit API on this client at all -- see data/hit.lua and
|
||||
-- data/talenthit.lua. `hitKind` selects which number a column shows:
|
||||
-- "melee" -- generic physical hit vs the yellow (special attack) cap
|
||||
-- "ranged" -- the same generic gear hit against the RANGED cap, and only
|
||||
-- for a player who actually carries a ranged attack weapon
|
||||
-- "spell" -- spell hit vs the spell cap
|
||||
--
|
||||
-- WHY SIX SPELL COLUMNS INSTEAD OF ONE: spell hit is not one number. A
|
||||
-- column's value is the GENERIC spell hit plus everything that only helps
|
||||
-- THAT school -- school-specific item affixes and school-specific talents
|
||||
-- (a mage's Arcane Focus, a priest's Shadow Focus, ...). Two casters of
|
||||
-- the same class routinely differ per school, which is exactly what the
|
||||
-- tab exists to show; folding them into one "spell hit" column would hide
|
||||
-- the difference that decides whether a caster is capped.
|
||||
--
|
||||
-- `school` on a spell column is the LOWERCASE school id, shared with the
|
||||
-- talent tables (data/talenthit.lua). Gear-parsed school extras are keyed
|
||||
-- by the CAPITALIZED tooltip spelling instead ("Fire"), because they come
|
||||
-- straight out of the tooltip text -- DC.HIT_SCHOOL_KEY (data/hit.lua)
|
||||
-- bridges the two and DC_Hit.SchoolHit does the lookup for callers.
|
||||
-- ------------------------------------------------------------------
|
||||
DC.SLOTS_HIT = {
|
||||
{ id = "MHIT", label = "MHT", sub = "melee", full = "Melee hit", kind = "hit", hitKind = "melee" },
|
||||
{ id = "RHIT", label = "RNG", sub = "ranged", full = "Ranged hit", kind = "hit", hitKind = "ranged" },
|
||||
{ id = "HARC", label = "ARC", sub = "arcane", full = "Arcane hit", kind = "hit", hitKind = "spell", school = "arcane" },
|
||||
{ id = "HFIR", label = "FIR", sub = "fire", full = "Fire hit", kind = "hit", hitKind = "spell", school = "fire" },
|
||||
{ id = "HFRO", label = "FRO", sub = "frost", full = "Frost hit", kind = "hit", hitKind = "spell", school = "frost" },
|
||||
{ id = "HHOL", label = "HOL", sub = "holy", full = "Holy hit", kind = "hit", hitKind = "spell", school = "holy" },
|
||||
{ id = "HNAT", label = "NAT", sub = "nature", full = "Nature hit", kind = "hit", hitKind = "spell", school = "nature" },
|
||||
{ id = "HSHA", label = "SHA", sub = "shadow", full = "Shadow hit", kind = "hit", hitKind = "spell", school = "shadow" },
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Out-of-range threshold in yards:
|
||||
-- a READABLE player with distance >= FAR_LIMIT gets rowVM.far = true and
|
||||
-- counts into vm.coverage.far. Purely diagnostic -- far never changes any
|
||||
-- cell state, sum, gap or pill (distance is never a filter).
|
||||
-- ------------------------------------------------------------------
|
||||
DC.FAR_LIMIT = 40
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Alternating-row wash ("zebra"): the alpha of DC.COLORS.zebra on every
|
||||
-- SECOND data row of a table, so the eye can hold a line across a wide
|
||||
-- grid. It has to stay well under the washes that carry MEANING -- the
|
||||
-- unreadable-row wash sits at 0.25 -- or a striped row would read as a
|
||||
-- state. One number, used by every table (matrix rows, both options
|
||||
-- grids), so the whole tool stripes at the same strength.
|
||||
-- ------------------------------------------------------------------
|
||||
DC.ZEBRA_ALPHA = 0.035
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Color palette
|
||||
-- Every entry is { r=<0..1>, g=<0..1>, b=<0..1> } (WoW SetTextColor /
|
||||
-- SetVertexColor convention). Hex source noted per line.
|
||||
-- ------------------------------------------------------------------
|
||||
local function hexrgb(hex)
|
||||
return {
|
||||
r = tonumber(string.sub(hex, 1, 2), 16) / 255,
|
||||
g = tonumber(string.sub(hex, 3, 4), 16) / 255,
|
||||
b = tonumber(string.sub(hex, 5, 6), 16) / 255,
|
||||
}
|
||||
end
|
||||
|
||||
DC.COLORS = {
|
||||
-- state colors (cell glyph / cell background / cell border)
|
||||
hat = hexrgb("3fae5c"), -- HAS green
|
||||
hatBg = hexrgb("152b1c"),
|
||||
hatBorder = hexrgb("2b5e3a"),
|
||||
fehlt = hexrgb("d05252"), -- MISSING red
|
||||
fehltBg = hexrgb("341a1a"),
|
||||
fehltBorder = hexrgb("6e3030"),
|
||||
unbek = hexrgb("8b93a0"), -- UNKNOWN gray
|
||||
unbekBg = hexrgb("22262e"),
|
||||
unbekBorder = hexrgb("3b424e"),
|
||||
skip = hexrgb("14181f"), -- not-expected cell bg
|
||||
skipBorder = hexrgb("1d222b"),
|
||||
skipGlyph = hexrgb("3a404c"),
|
||||
|
||||
-- gold family
|
||||
gold = hexrgb("8f7440"), -- hover border gold
|
||||
goldHi = hexrgb("e6c87e"), -- title / highlight gold
|
||||
goldSoft = hexrgb("54462a"), -- frame accent borders
|
||||
goldDim = hexrgb("a98f57"), -- secondary gold, links
|
||||
|
||||
-- panels / lines
|
||||
panel = hexrgb("11141a"), -- frame bg
|
||||
panel2 = hexrgb("171b23"), -- popover bg
|
||||
panel3 = hexrgb("1d222c"), -- reserved
|
||||
line = hexrgb("262c37"), -- standard borders
|
||||
theadBg = hexrgb("141821"), -- sticky head/foot bg
|
||||
void = hexrgb("0a0c10"), -- page background
|
||||
-- alternating-row wash ("zebra"): the COLOR is plain white, the
|
||||
-- subtlety lives entirely in DC.ZEBRA_ALPHA below -- a tinted stripe
|
||||
-- would shift the hue of every cell it sits under.
|
||||
zebra = hexrgb("ffffff"),
|
||||
|
||||
-- text
|
||||
ink = hexrgb("c9cdd4"), -- body text
|
||||
dim = hexrgb("868d99"), -- secondary text
|
||||
faint = hexrgb("5b6270"), -- tertiary / hints
|
||||
parch = hexrgb("d6cdb8"), -- parchment text
|
||||
parchDim = hexrgb("9d9482"), -- parchment dim (tooltip detail)
|
||||
|
||||
-- role group colors
|
||||
roleTANK = hexrgb("8fb3dd"),
|
||||
roleMELEE = hexrgb("e2a75e"),
|
||||
roleRANGED = hexrgb("7fcabe"),
|
||||
roleCASTER = hexrgb("bd9ce8"),
|
||||
roleHEALER = hexrgb("8fd6a0"),
|
||||
|
||||
-- role badge border colors
|
||||
roleBorderTANK = hexrgb("3c5877"),
|
||||
roleBorderMELEE = hexrgb("7a5c33"),
|
||||
roleBorderRANGED = hexrgb("3a6b63"),
|
||||
roleBorderCASTER = hexrgb("5e4a80"),
|
||||
roleBorderHEALER = hexrgb("3c6c4a"),
|
||||
|
||||
-- class colors: OWN table -- RAID_CLASS_COLORS does NOT exist on 1.12.
|
||||
-- Standard vanilla class colors, all 9 classes (the sim roster covers
|
||||
-- all of them).
|
||||
classWARRIOR = hexrgb("c79c6e"),
|
||||
classPALADIN = hexrgb("f58cba"),
|
||||
classHUNTER = hexrgb("abd473"),
|
||||
classROGUE = hexrgb("fff569"),
|
||||
classPRIEST = hexrgb("ffffff"),
|
||||
classSHAMAN = hexrgb("0070de"),
|
||||
classMAGE = hexrgb("69ccf0"),
|
||||
classWARLOCK = hexrgb("9482c9"),
|
||||
classDRUID = hexrgb("ff7d0a"),
|
||||
|
||||
-- trinket tile quality accents (WoW rare/3 and epic/4)
|
||||
rareText = hexrgb("4aa3ff"),
|
||||
rareBorder = hexrgb("1d5a9e"),
|
||||
epicText = hexrgb("b06cf0"),
|
||||
epicBorder = hexrgb("6a3f8f"),
|
||||
|
||||
-- green button / pill family + banners
|
||||
btnGreenText = hexrgb("cfe0a8"),
|
||||
btnGreenBg = hexrgb("26311e"),
|
||||
btnGreenBorder = hexrgb("4c6236"),
|
||||
bannerYellowBg = hexrgb("2b2413"),
|
||||
bannerYellowBorder = hexrgb("6e5a26"),
|
||||
bannerYellowText = hexrgb("e0c98a"),
|
||||
bannerGreenBg = hexrgb("16240f"),
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Item quality colors, indices 0..6 as returned by GetItemQualityColor.
|
||||
-- Own table so pure-Lua tests can run offline (GetItemQualityColor exists
|
||||
-- in-game but not under lua50). Values = 1.12 FrameXML ITEM_QUALITY_COLORS.
|
||||
-- ------------------------------------------------------------------
|
||||
DC.QUALITY = {
|
||||
[0] = { r = 0.65, g = 0.65, b = 0.65 }, -- poor (gray)
|
||||
[1] = { r = 1.00, g = 1.00, b = 1.00 }, -- common (white)
|
||||
[2] = { r = 0.10, g = 1.00, b = 0.00 }, -- uncommon (green)
|
||||
[3] = { r = 0.00, g = 0.44, b = 0.87 }, -- rare (blue)
|
||||
[4] = { r = 0.64, g = 0.21, b = 0.93 }, -- epic (purple)
|
||||
[5] = { r = 1.00, g = 0.50, b = 0.00 }, -- legendary (orange)
|
||||
[6] = { r = 0.90, g = 0.80, b = 0.50 }, -- artifact
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
-- 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()
|
||||
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 | 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
|
||||
if db and db.testMode then
|
||||
-- real data never beats the simulation while test mode is on
|
||||
DC.Print("test mode is on - /dc test 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")
|
||||
else
|
||||
DC.Print("test mode OFF - rescanning the real group")
|
||||
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 db.testMode 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
|
||||
+1740
File diff suppressed because it is too large
Load Diff
+534
@@ -0,0 +1,534 @@
|
||||
-- DopingControl core/probe.lua
|
||||
-- Feasibility probe: dumps RAW API returns to dc_probe.txt (in the WoW
|
||||
-- client's Imports folder) via SuperWoW ExportFile so raw facts can be read
|
||||
-- from disk instead of retyped from chat lines.
|
||||
--
|
||||
-- Entry points (core/init.lua wires the slash command):
|
||||
-- DC_Probe.HandleSlash(rest) -- rest = text after "probe":
|
||||
-- "" -> Battery() (part 2, near-range questions)
|
||||
-- "range <name>" -> RangeStart(name) / RangeStop() toggle (part 1)
|
||||
-- DC_Probe.Battery()
|
||||
-- DC_Probe.RangeStart(playerName) / DC_Probe.RangeStop()
|
||||
-- DC_Probe.IsRangeRunning()
|
||||
--
|
||||
-- Part 1 (range run): a raid member with known buffs walks away; a 1 Hz
|
||||
-- poller writes one line per tick:
|
||||
-- dist | UnitExists | UnitIsVisible | nA=count(UnitBuff) | 4th-return |
|
||||
-- nB=count(GetUnitField) | UnitPosition
|
||||
-- The unit token is RE-RESOLVED from the name every tick (raid indices are
|
||||
-- volatile after roster changes).
|
||||
--
|
||||
-- Part 2 (battery) answers five open client-behavior questions with raw data:
|
||||
-- Q1 does UnitBuff(unit, i) return a 4th value ~= nil (spell id)?
|
||||
-- Q2 does GetUnitField(guid,"aura") match the tooltip names; is the
|
||||
-- slot split really 1-32 buffs / 33-48 debuffs?
|
||||
-- Q3 does GetWeaponEnchantInfo(unit) name a foreign temp enchant?
|
||||
-- Q4 does GetInventoryItemLink(unit, slot) serve out-of-sight units?
|
||||
-- Q5 is the ENCHANT field in foreign item links filled (~= 0)?
|
||||
--
|
||||
-- File mechanics: ExportFile appends ".txt" ITSELF -- the name is passed
|
||||
-- WITHOUT extension ("dc_probe", never "dc_probe.txt"). ExportFile is a
|
||||
-- whole-file overwrite, so the probe keeps an in-memory ring of lines and
|
||||
-- rewrites the complete file on every flush.
|
||||
--
|
||||
-- Pure Lua 5.0, dofile-loadable offline (all WoW usage inside functions /
|
||||
-- existence-checked; the poller frame is created lazily in-game).
|
||||
|
||||
DopingControl = DopingControl or {}
|
||||
local DC = DopingControl
|
||||
|
||||
DC_Probe = DC_Probe or {}
|
||||
local P = DC_Probe
|
||||
|
||||
P.RING_CAP = 2000 -- max lines kept; oldest dropped first
|
||||
P.EXPORT_NAME = "dc_probe" -- NO extension (ExportFile appends .txt)
|
||||
P.BATTERY_UNIT_CAP = 8 -- full detail for at most N foreign units
|
||||
|
||||
local ring = {}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- ring + flush
|
||||
-- ------------------------------------------------------------------
|
||||
function P.Clear()
|
||||
ring = {}
|
||||
end
|
||||
|
||||
function P.Push(line)
|
||||
table.insert(ring, line)
|
||||
while table.getn(ring) > P.RING_CAP do
|
||||
table.remove(ring, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function P.Text()
|
||||
return table.concat(ring, "\n")
|
||||
end
|
||||
|
||||
local function say(msg)
|
||||
if DEFAULT_CHAT_FRAME then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffe6c87eDopingControl|r probe: " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
function P.Flush()
|
||||
if ExportFile then
|
||||
-- name WITHOUT extension -- ExportFile appends .txt itself.
|
||||
-- pcall: optional-DLL call -- a DLL-side I/O error
|
||||
-- must not escape into the slash handler or the 1 Hz range ticker.
|
||||
local ok = pcall(ExportFile, P.EXPORT_NAME, P.Text())
|
||||
if not ok then
|
||||
say("ExportFile failed - dump not written")
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
say("ExportFile missing (SuperWoW required) - dump not written")
|
||||
return false
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- formatting helpers
|
||||
-- ------------------------------------------------------------------
|
||||
local function fmt(v)
|
||||
if v == nil then
|
||||
return "nil"
|
||||
end
|
||||
if v == true then
|
||||
return "true"
|
||||
end
|
||||
if v == false then
|
||||
return "false"
|
||||
end
|
||||
if type(v) == "number" then
|
||||
return tostring(v)
|
||||
end
|
||||
if type(v) == "string" then
|
||||
return v
|
||||
end
|
||||
return type(v)
|
||||
end
|
||||
|
||||
local function gtype(name)
|
||||
local v = getglobal and getglobal(name)
|
||||
if v == nil then
|
||||
return "nil"
|
||||
end
|
||||
return type(v)
|
||||
end
|
||||
|
||||
local function nowStamp()
|
||||
if date then
|
||||
return date("%Y-%m-%d %H:%M:%S")
|
||||
end
|
||||
return "?"
|
||||
end
|
||||
|
||||
local function header(kind)
|
||||
P.Push("=== DopingControl probe " .. kind .. " " .. nowStamp() .. " ===")
|
||||
P.Push("client: TURTLE_WOW_VERSION=" .. fmt(TURTLE_WOW_VERSION)
|
||||
.. " SUPERWOW_VERSION=" .. fmt(SUPERWOW_VERSION))
|
||||
P.Push("dll-surface: UnitXP=" .. gtype("UnitXP")
|
||||
.. " GetUnitField=" .. gtype("GetUnitField")
|
||||
.. " ExportFile=" .. gtype("ExportFile")
|
||||
.. " UnitPosition=" .. gtype("UnitPosition"))
|
||||
end
|
||||
|
||||
-- resolve a roster unit token by player name; nil if not in group.
|
||||
-- Re-run every use: raid indices are volatile.
|
||||
local function resolveUnit(name)
|
||||
if not UnitName then
|
||||
return nil
|
||||
end
|
||||
local lower = string.lower(name)
|
||||
local nraid = (GetNumRaidMembers and GetNumRaidMembers()) or 0
|
||||
if nraid > 0 then
|
||||
for i = 1, nraid do
|
||||
local ok, nm = pcall(UnitName, "raid" .. i)
|
||||
if ok and nm and string.lower(nm) == lower then
|
||||
return "raid" .. i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
for i = 1, 4 do
|
||||
local ok, nm = pcall(UnitName, "party" .. i)
|
||||
if ok and nm and string.lower(nm) == lower then
|
||||
return "party" .. i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function unitGuid(unit)
|
||||
if not UnitExists then
|
||||
return nil
|
||||
end
|
||||
local ok, ex, g = pcall(UnitExists, unit)
|
||||
if ok and ex and g then
|
||||
return g
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- count UnitBuff entries + first non-nil 4th return
|
||||
local function pathACounts(unit)
|
||||
local n = 0
|
||||
local firstId = nil
|
||||
local idCount = 0
|
||||
if UnitBuff then
|
||||
for i = 1, 32 do
|
||||
local ok, tex, stacks, dtype, id4 = pcall(UnitBuff, unit, i)
|
||||
if not ok or not tex then
|
||||
break
|
||||
end
|
||||
n = n + 1
|
||||
if type(id4) == "number" and id4 > 0 then
|
||||
idCount = idCount + 1
|
||||
if not firstId then
|
||||
firstId = id4
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return n, firstId, idCount
|
||||
end
|
||||
|
||||
-- GetUnitField(guid,"aura") -> comma list for a slot range (sparse table)
|
||||
local function pathBList(guid, from, to)
|
||||
if not GetUnitField or not guid then
|
||||
return nil, 0
|
||||
end
|
||||
local ok, t = pcall(GetUnitField, guid, "aura")
|
||||
if not ok then
|
||||
return "ERR", 0
|
||||
end
|
||||
if type(t) ~= "table" then
|
||||
return "type=" .. type(t), 0
|
||||
end
|
||||
local parts = {}
|
||||
local n = 0
|
||||
for i = from, to do
|
||||
local v = t[i]
|
||||
if type(v) == "number" and v > 0 then
|
||||
n = n + 1
|
||||
table.insert(parts, i .. ":" .. v)
|
||||
end
|
||||
end
|
||||
if n == 0 then
|
||||
return "-", 0
|
||||
end
|
||||
return table.concat(parts, ","), n
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Part 2: the battery (/dc probe)
|
||||
-- ------------------------------------------------------------------
|
||||
function P.Battery()
|
||||
if not UnitName then
|
||||
say("no client APIs (offline?) - battery skipped")
|
||||
return false
|
||||
end
|
||||
P.Clear()
|
||||
header("battery")
|
||||
|
||||
local roster = (DC_Scan and DC_Scan.BuildRoster and DC_Scan.BuildRoster()) or {}
|
||||
local total = table.getn(roster)
|
||||
P.Push("roster: " .. total .. " units; full detail for the first "
|
||||
.. P.BATTERY_UNIT_CAP .. " foreign units")
|
||||
P.Push("")
|
||||
|
||||
-- self weapon enchant baseline (vanilla 6-return form)
|
||||
if GetWeaponEnchantInfo then
|
||||
local ok, r1, r2, r3, r4, r5, r6 = pcall(GetWeaponEnchantInfo)
|
||||
P.Push("self GetWeaponEnchantInfo(): ok=" .. fmt(ok)
|
||||
.. " hasMain=" .. fmt(r1) .. " mainExp=" .. fmt(r2)
|
||||
.. " mainCharges=" .. fmt(r3) .. " hasOff=" .. fmt(r4)
|
||||
.. " offExp=" .. fmt(r5) .. " offCharges=" .. fmt(r6))
|
||||
else
|
||||
P.Push("self GetWeaponEnchantInfo(): API missing")
|
||||
end
|
||||
P.Push("")
|
||||
|
||||
local detailed = 0
|
||||
for r = 1, total do
|
||||
local e = roster[r]
|
||||
local isSelf = false
|
||||
if UnitIsUnit then
|
||||
local ok, v = pcall(UnitIsUnit, e.unit, "player")
|
||||
isSelf = (ok and v) and true or false
|
||||
end
|
||||
if not isSelf and detailed < P.BATTERY_UNIT_CAP then
|
||||
detailed = detailed + 1
|
||||
local guid = unitGuid(e.unit)
|
||||
local online, dist = DC_Reach.ReadUnit(e.unit)
|
||||
P.Push("--- " .. e.unit .. " name=" .. fmt(e.name)
|
||||
.. " class=" .. fmt(e.class) .. " online=" .. fmt(online)
|
||||
.. " dist=" .. fmt(dist) .. " guid=" .. fmt(guid))
|
||||
|
||||
-- Q1: UnitBuff rows with 4th return + tooltip name
|
||||
if UnitBuff then
|
||||
local rows = 0
|
||||
for i = 1, 32 do
|
||||
local ok, tex, stacks, dtype, id4 = pcall(UnitBuff, e.unit, i)
|
||||
if not ok or not tex then
|
||||
break
|
||||
end
|
||||
rows = rows + 1
|
||||
local tipName = nil
|
||||
-- same hidden tooltip as scan/aura.lua (shared accessor,
|
||||
-- never a second frame under the same global name)
|
||||
local tip = DC_Aura and DC_Aura.GetScanTip
|
||||
and DC_Aura.GetScanTip()
|
||||
if tip then
|
||||
local ok2 = pcall(function()
|
||||
tip:ClearLines()
|
||||
tip:SetUnitBuff(e.unit, i)
|
||||
end)
|
||||
if ok2 then
|
||||
local obj = getglobal("DopingControlScanTipTextLeft1")
|
||||
if obj then
|
||||
tipName = obj:GetText()
|
||||
end
|
||||
end
|
||||
end
|
||||
P.Push("Q1 UnitBuff i=" .. i .. " tex=" .. fmt(tex)
|
||||
.. " stacks=" .. fmt(stacks) .. " dtype=" .. fmt(dtype)
|
||||
.. " id4=" .. fmt(id4) .. " tipName=" .. fmt(tipName))
|
||||
end
|
||||
local nA, firstId, idCount = pathACounts(e.unit)
|
||||
P.Push("Q1 summary: buffs=" .. nA .. " id4nonNil=" .. idCount
|
||||
.. " (id4 ~= nil answers Q1)")
|
||||
else
|
||||
P.Push("Q1 UnitBuff: API missing")
|
||||
end
|
||||
|
||||
-- Q2: descriptor list, buff and debuff halves separately
|
||||
local buffList = pathBList(guid, 1, 32)
|
||||
local debuffList = pathBList(guid, 33, 48)
|
||||
P.Push("Q2 GetUnitField(guid,\"aura\") slots1-32=" .. fmt(buffList))
|
||||
P.Push("Q2 GetUnitField(guid,\"aura\") slots33-48=" .. fmt(debuffList)
|
||||
.. " (compare vs UnitBuff names above -> slot-split check)")
|
||||
|
||||
-- Q3: foreign weapon enchant name
|
||||
if GetWeaponEnchantInfo then
|
||||
local ok, w1, w2 = pcall(GetWeaponEnchantInfo, e.unit)
|
||||
P.Push("Q3 GetWeaponEnchantInfo(unit): ok=" .. fmt(ok)
|
||||
.. " r1=" .. fmt(w1) .. " r2=" .. fmt(w2))
|
||||
end
|
||||
|
||||
-- Q4/Q5: inventory links, raw + parsed enchant field.
|
||||
-- subType (GetItemInfo return 6, de-shifted by DC_Gear.ItemInfo)
|
||||
-- is dumped per slot to verify the DC.ENCH_EXEMPT [ASSUMPTION]
|
||||
-- strings against reality (data/enchants.lua).
|
||||
local slots = DC_Gear.InvSlots()
|
||||
local linkCount = 0
|
||||
for s = 1, table.getn(slots) do
|
||||
local inv = slots[s]
|
||||
local ok, link = pcall(GetInventoryItemLink, e.unit, inv)
|
||||
if ok and link then
|
||||
linkCount = linkCount + 1
|
||||
local id, ench = DC_Gear.ParseLink(link)
|
||||
local subType = nil
|
||||
if id then
|
||||
local _, _, st = DC_Gear.ItemInfo(id)
|
||||
subType = st
|
||||
end
|
||||
P.Push("Q4/Q5 slot" .. inv .. " link=" .. fmt(link)
|
||||
.. " -> id=" .. fmt(id) .. " ench=" .. fmt(ench)
|
||||
.. " subType=" .. fmt(subType))
|
||||
end
|
||||
end
|
||||
P.Push("Q4 summary: " .. linkCount .. "/" .. table.getn(slots)
|
||||
.. " slots returned a link (0 for an out-of-sight unit"
|
||||
.. " answers Q4 negatively)")
|
||||
P.Push("Q5b: subType above checks the ENCH_EXEMPT assumption"
|
||||
.. " strings - expected \"Wands\"/\"Thrown\" (slot 18) and"
|
||||
.. " \"Miscellaneous\" (slot 17), data/enchants.lua")
|
||||
|
||||
-- Q6: resistance read paths (RESIST tab, scan/engine.lua
|
||||
-- readResists) -- school 2 (Fire) as a representative probe,
|
||||
-- all three paths dumped raw: does UnitResistance answer for a
|
||||
-- foreign unit token, does it also accept the GUID directly,
|
||||
-- and does GetUnitField(guid,"resistances") carry a usable
|
||||
-- table at all -- or does a foreign read silently return 0?
|
||||
if UnitResistance then
|
||||
local ok1, base1, total1 = pcall(UnitResistance, e.unit, 2)
|
||||
P.Push("Q6 UnitResistance(unit,2): ok=" .. fmt(ok1)
|
||||
.. " base=" .. fmt(base1) .. " total=" .. fmt(total1))
|
||||
if guid then
|
||||
local ok2, base2, total2 = pcall(UnitResistance, guid, 2)
|
||||
P.Push("Q6 UnitResistance(guid,2): ok=" .. fmt(ok2)
|
||||
.. " base=" .. fmt(base2) .. " total=" .. fmt(total2))
|
||||
end
|
||||
else
|
||||
P.Push("Q6 UnitResistance: API missing")
|
||||
end
|
||||
if guid and GetUnitField then
|
||||
local ok3, t = pcall(GetUnitField, guid, "resistances")
|
||||
if ok3 and type(t) == "table" then
|
||||
P.Push("Q6 GetUnitField(guid,\"resistances\"): [1]="
|
||||
.. fmt(t[1]) .. " [2]=" .. fmt(t[2]) .. " [3]="
|
||||
.. fmt(t[3]) .. " [4]=" .. fmt(t[4]) .. " [5]="
|
||||
.. fmt(t[5]) .. " [6]=" .. fmt(t[6]) .. " [7]="
|
||||
.. fmt(t[7]))
|
||||
else
|
||||
P.Push("Q6 GetUnitField(guid,\"resistances\"): ok="
|
||||
.. fmt(ok3) .. " type=" .. type(t))
|
||||
end
|
||||
end
|
||||
P.Push("")
|
||||
end
|
||||
end
|
||||
if detailed == 0 then
|
||||
P.Push("no foreign units in group - battery needs a party/raid")
|
||||
end
|
||||
|
||||
local written = P.Flush()
|
||||
if written then
|
||||
say("battery done (" .. detailed .. " foreign units) -> "
|
||||
.. P.EXPORT_NAME .. ".txt")
|
||||
end
|
||||
return written
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Part 1: the 1 Hz range run (/dc probe range <name>)
|
||||
-- ------------------------------------------------------------------
|
||||
local rangeFrame = nil
|
||||
local rangeName = nil
|
||||
local rangeRunning = false
|
||||
local rangeAcc = 0
|
||||
local rangeT0 = 0
|
||||
|
||||
local function rangeTick()
|
||||
local t = (GetTime and GetTime()) or 0
|
||||
local rel = math.floor(t - rangeT0 + 0.5)
|
||||
local unit = resolveUnit(rangeName)
|
||||
if not unit then
|
||||
P.Push("t=" .. rel .. "s target '" .. rangeName .. "' not in roster")
|
||||
P.Flush()
|
||||
return
|
||||
end
|
||||
local guid = unitGuid(unit)
|
||||
|
||||
local dist = nil
|
||||
if UnitXP then
|
||||
local ok, d = pcall(UnitXP, "distanceBetween", "player", unit)
|
||||
if ok and type(d) == "number" then
|
||||
dist = d
|
||||
end
|
||||
end
|
||||
local exists = nil
|
||||
if UnitExists then
|
||||
local ok, ex = pcall(UnitExists, unit)
|
||||
if ok then
|
||||
exists = ex
|
||||
end
|
||||
end
|
||||
local visible = nil
|
||||
if UnitIsVisible then
|
||||
local ok, v = pcall(UnitIsVisible, unit)
|
||||
if ok then
|
||||
visible = v
|
||||
end
|
||||
end
|
||||
local nA, firstId = pathACounts(unit)
|
||||
local _, nB = pathBList(guid, 1, 32)
|
||||
local pos = "nil"
|
||||
if UnitPosition and guid then
|
||||
local ok, x, y = pcall(UnitPosition, guid)
|
||||
if ok and type(x) == "number" then
|
||||
pos = "ok(" .. string.format("%.1f", x) .. ","
|
||||
.. string.format("%.1f", y or 0) .. ")"
|
||||
end
|
||||
end
|
||||
|
||||
-- part-1 column order (matches the header line written by RangeStart)
|
||||
P.Push("t=" .. rel .. "s unit=" .. unit
|
||||
.. " dist=" .. fmt(dist)
|
||||
.. " exists=" .. fmt(exists)
|
||||
.. " visible=" .. fmt(visible)
|
||||
.. " nA=" .. nA
|
||||
.. " id4first=" .. fmt(firstId)
|
||||
.. " nB=" .. nB
|
||||
.. " pos=" .. pos)
|
||||
P.Flush()
|
||||
end
|
||||
|
||||
function P.IsRangeRunning()
|
||||
return rangeRunning
|
||||
end
|
||||
|
||||
function P.RangeStart(name)
|
||||
if not CreateFrame then
|
||||
say("no client (offline) - range run unavailable")
|
||||
return false
|
||||
end
|
||||
if rangeRunning then
|
||||
say("range run already active for " .. fmt(rangeName)
|
||||
.. " - stop it first (/dc probe range)")
|
||||
return false
|
||||
end
|
||||
rangeName = name
|
||||
rangeRunning = true
|
||||
rangeAcc = 0
|
||||
rangeT0 = (GetTime and GetTime()) or 0
|
||||
P.Clear()
|
||||
header("range-run target=" .. name)
|
||||
P.Push("columns: t | unit | dist | exists | visible | nA=count(UnitBuff) |"
|
||||
.. " id4first | nB=count(GetUnitField slots 1-32) | pos=UnitPosition")
|
||||
if not rangeFrame then
|
||||
rangeFrame = CreateFrame("Frame", "DopingControlProbeFrame")
|
||||
end
|
||||
rangeFrame:SetScript("OnUpdate", function()
|
||||
rangeAcc = rangeAcc + (arg1 or 0)
|
||||
if rangeAcc >= 1.0 then
|
||||
rangeAcc = 0
|
||||
rangeTick()
|
||||
end
|
||||
end)
|
||||
say("range run started, target " .. name .. ", 1 Hz -> "
|
||||
.. P.EXPORT_NAME .. ".txt (stop: /dc probe range " .. name .. ")")
|
||||
return true
|
||||
end
|
||||
|
||||
function P.RangeStop()
|
||||
if rangeFrame then
|
||||
rangeFrame:SetScript("OnUpdate", nil)
|
||||
end
|
||||
if rangeRunning then
|
||||
rangeRunning = false
|
||||
P.Push("=== range run stopped " .. nowStamp() .. " ===")
|
||||
P.Flush()
|
||||
say("range run stopped, " .. table.getn(ring) .. " lines in "
|
||||
.. P.EXPORT_NAME .. ".txt")
|
||||
end
|
||||
rangeName = nil
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- slash glue: rest of the line after "probe"
|
||||
-- ------------------------------------------------------------------
|
||||
function P.HandleSlash(rest)
|
||||
rest = rest or ""
|
||||
rest = string.gsub(rest, "^%s+", "")
|
||||
rest = string.gsub(rest, "%s+$", "")
|
||||
if rest == "" then
|
||||
return P.Battery()
|
||||
end
|
||||
local _, _, word = string.find(rest, "^(%S+)")
|
||||
if word and string.lower(word) == "range" then
|
||||
if rangeRunning then
|
||||
P.RangeStop()
|
||||
return true
|
||||
end
|
||||
local _, _, name = string.find(rest, "^%S+%s+(.+)$")
|
||||
if name then
|
||||
return P.RangeStart(name)
|
||||
end
|
||||
say("usage: /dc probe range <playername>")
|
||||
return false
|
||||
end
|
||||
say("usage: /dc probe | /dc probe range <playername>")
|
||||
return false
|
||||
end
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
-- DopingControl core/roles.lua
|
||||
-- Role suggestion heuristic + confirmed-role resolution: five roles, the
|
||||
-- role hangs on the player NAME, and no heuristic ever decides silently --
|
||||
-- a suggestion is always flagged as such and carries a reason string.
|
||||
-- Badge UX: CLASS_ROLES = ordered plausible roles per class, cycle() =
|
||||
-- badge right-click (step + immediate confirm).
|
||||
-- Per-class expectations:
|
||||
-- ROLE_CLASSES = generated inversion of CLASS_ROLES (ordered classes per
|
||||
-- role) -- the domain of db.expectations[role][class].
|
||||
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
|
||||
|
||||
DopingControl = DopingControl or {}
|
||||
local DC = DopingControl
|
||||
|
||||
DC_Roles = DC_Roles or {}
|
||||
|
||||
-- Transient confirmations for the simulated raid (test mode). NEVER written
|
||||
-- to SavedVariables: synthetic names must not pollute db.roles.
|
||||
-- Reset by DC_Sim.buildStore() on every (re-)entry into test mode.
|
||||
DC.simRoles = DC.simRoles or {}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Plausible roles per class token, ORDERED (the order is
|
||||
-- the badge right-click cycle order). suggest() below must only ever
|
||||
-- return a role CONTAINED in the class's list -- property-tested over
|
||||
-- every class x fact combination in the offline test suite.
|
||||
-- ------------------------------------------------------------------
|
||||
DC_Roles.CLASS_ROLES = {
|
||||
WARRIOR = { "MELEE", "TANK" },
|
||||
ROGUE = { "MELEE" },
|
||||
HUNTER = { "RANGED", "MELEE" }, -- melee hunter is a real spec on TWoW
|
||||
MAGE = { "CASTER" },
|
||||
WARLOCK = { "CASTER" },
|
||||
PRIEST = { "HEALER", "CASTER" },
|
||||
DRUID = { "HEALER", "TANK", "MELEE", "CASTER" },
|
||||
SHAMAN = { "HEALER", "TANK", "MELEE", "CASTER" },
|
||||
PALADIN = { "HEALER", "TANK", "MELEE" },
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Per-class expectations:
|
||||
-- ROLE_CLASSES = the INVERSION of CLASS_ROLES -- for each role the
|
||||
-- ordered list of class tokens that can perform it, in the fixed
|
||||
-- class order below, filtered per role. GENERATED from CLASS_ROLES so
|
||||
-- the two tables can never drift apart.
|
||||
-- Consumers: core/config.lua materializes db.expectations[role][class]
|
||||
-- over exactly these lists, core/model.lua iterates them for the
|
||||
-- dynamic-column check, and the options expectation grid renders one
|
||||
-- indented class row per entry.
|
||||
-- ------------------------------------------------------------------
|
||||
DC_Roles.CLASS_ORDER = {
|
||||
"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST",
|
||||
"SHAMAN", "MAGE", "WARLOCK", "DRUID",
|
||||
}
|
||||
|
||||
DC_Roles.ROLE_CLASSES = {}
|
||||
do
|
||||
-- fresh tables on every (re-)load -- regeneration, not accretion
|
||||
local inv = DC_Roles.ROLE_CLASSES
|
||||
for i = 1, table.getn(DC_Roles.CLASS_ORDER) do
|
||||
local cls = DC_Roles.CLASS_ORDER[i]
|
||||
local list = DC_Roles.CLASS_ROLES[cls] or {}
|
||||
for j = 1, table.getn(list) do
|
||||
local role = list[j]
|
||||
if inv[role] == nil then
|
||||
inv[role] = {}
|
||||
end
|
||||
table.insert(inv[role], cls)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Facts extraction: derive the suggestion facts from a store player entry.
|
||||
-- Works on both aura paths: form NAMES from
|
||||
-- the tooltip path, form SPELL IDS from the GUID path (which delivers ids
|
||||
-- only).
|
||||
-- [ASSUMPTION] Form aura spell IDs (vanilla 1.12): Cat Form 768, Bear Form
|
||||
-- 5487, Dire Bear Form 9634, Moonkin Form 24858, Shadowform 15473.
|
||||
-- ------------------------------------------------------------------
|
||||
DC_Roles.FORM_NAMES = {
|
||||
["Cat Form"] = "catForm",
|
||||
["Bear Form"] = "bearForm",
|
||||
["Dire Bear Form"] = "bearForm",
|
||||
["Moonkin Form"] = "moonkin",
|
||||
["Shadowform"] = "shadowform",
|
||||
}
|
||||
|
||||
DC_Roles.FORM_IDS = {
|
||||
[768] = "catForm",
|
||||
[5487] = "bearForm",
|
||||
[9634] = "bearForm",
|
||||
[24858] = "moonkin",
|
||||
[15473] = "shadowform",
|
||||
}
|
||||
|
||||
function DC_Roles.factsFor(player)
|
||||
local facts = {
|
||||
hasShield = false, catForm = false, bearForm = false,
|
||||
shadowform = false, moonkin = false,
|
||||
}
|
||||
if not player then
|
||||
return facts
|
||||
end
|
||||
if player.aurasRead and player.auras then
|
||||
if player.auras.names then
|
||||
for name, key in pairs(DC_Roles.FORM_NAMES) do
|
||||
if player.auras.names[name] then
|
||||
facts[key] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if player.auras.ids then
|
||||
for id, key in pairs(DC_Roles.FORM_IDS) do
|
||||
if player.auras.ids[id] then
|
||||
facts[key] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if player.gearRead and player.gear then
|
||||
local off = player.gear[17] -- off hand inventory slot
|
||||
if type(off) == "table" and off.subType == "Shields" then
|
||||
facts.hasShield = true
|
||||
end
|
||||
end
|
||||
return facts
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Suggestion heuristic. ALWAYS returns a
|
||||
-- (role, reason) pair -- no suggestion without a stated reason (the badge
|
||||
-- tooltip shows it).
|
||||
-- ------------------------------------------------------------------
|
||||
function DC_Roles.suggest(class, facts)
|
||||
facts = facts or {}
|
||||
if class == "WARRIOR" then
|
||||
if facts.hasShield then
|
||||
return "TANK", "shield equipped"
|
||||
end
|
||||
return "MELEE", "class default"
|
||||
elseif class == "ROGUE" then
|
||||
return "MELEE", "class default"
|
||||
elseif class == "HUNTER" then
|
||||
return "RANGED", "class default"
|
||||
elseif class == "MAGE" then
|
||||
return "CASTER", "class default"
|
||||
elseif class == "WARLOCK" then
|
||||
return "CASTER", "class default"
|
||||
elseif class == "PRIEST" then
|
||||
if facts.shadowform then
|
||||
return "CASTER", "Shadowform at scan time"
|
||||
end
|
||||
return "HEALER", "class default"
|
||||
elseif class == "DRUID" then
|
||||
-- form precedence: bear > cat > moonkin > none (a druid can only be
|
||||
-- in one form at a time; the order only matters for garbage input)
|
||||
if facts.bearForm then
|
||||
return "TANK", "Bear Form at scan time"
|
||||
end
|
||||
if facts.catForm then
|
||||
return "MELEE", "Cat Form at scan time"
|
||||
end
|
||||
if facts.moonkin then
|
||||
return "CASTER", "Moonkin Form at scan time"
|
||||
end
|
||||
return "HEALER", "class default"
|
||||
elseif class == "SHAMAN" then
|
||||
-- deliberately NOT shield->MELEE: resto shamans
|
||||
-- carry shields too, so a shield is no enhancement signal here.
|
||||
-- Plain class default; enhancers get confirmed by hand once.
|
||||
return "HEALER", "class default"
|
||||
elseif class == "PALADIN" then
|
||||
if facts.hasShield then
|
||||
return "TANK", "shield equipped"
|
||||
end
|
||||
return "HEALER", "class default"
|
||||
end
|
||||
-- unknown class token: still return a pair, never nil
|
||||
return "MELEE", "unknown class"
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Resolution: confirmed role wins over suggestion.
|
||||
-- source "sim" -> confirmations live ONLY in DC.simRoles (transient);
|
||||
-- db.roles is never consulted and never written for
|
||||
-- simulated players (synthetic names must
|
||||
-- not end up in SavedVariables).
|
||||
-- source "scan" -> confirmations live in db.roles (persistent, keyed by
|
||||
-- player name).
|
||||
-- Returns role, suggested(bool), reason(string|nil):
|
||||
-- confirmed -> (role, false, nil)
|
||||
-- suggested -> (role, true, reason)
|
||||
-- ------------------------------------------------------------------
|
||||
function DC_Roles.resolve(db, storeSource, player)
|
||||
local name = player and player.name
|
||||
local confirmed
|
||||
if storeSource == "sim" then
|
||||
confirmed = name and DC.simRoles[name]
|
||||
else
|
||||
confirmed = name and db and db.roles and db.roles[name]
|
||||
end
|
||||
if confirmed then
|
||||
return confirmed, false, nil
|
||||
end
|
||||
local role, reason = DC_Roles.suggest(player and player.class,
|
||||
DC_Roles.factsFor(player))
|
||||
return role, true, reason
|
||||
end
|
||||
|
||||
-- Confirm a role (badge LEFT-click). Same routing rule as resolve().
|
||||
function DC_Roles.confirm(db, storeSource, name, role)
|
||||
if storeSource == "sim" then
|
||||
DC.simRoles[name] = role
|
||||
else
|
||||
db.roles[name] = role
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- cycle(db, storeSource, name, class, currentRole) -> nextRole
|
||||
-- Badge RIGHT-click: step to the entry AFTER currentRole
|
||||
-- in CLASS_ROLES[class] (wrapping; currentRole not in the list -> the
|
||||
-- FIRST entry) and CONFIRM it immediately -- same persistence routing as
|
||||
-- confirm() (sim source -> DC.simRoles, else db.roles).
|
||||
-- Single-entry classes wrap onto themselves (the UI treats right-click
|
||||
-- as a no-op there and never calls this; calling it anyway is harmless:
|
||||
-- the same role comes back and is re-confirmed).
|
||||
-- Unknown class token: no list -> currentRole returned unchanged and
|
||||
-- NOTHING confirmed (defensive; scan classes are always known tokens).
|
||||
-- ------------------------------------------------------------------
|
||||
function DC_Roles.cycle(db, storeSource, name, class, currentRole)
|
||||
local list = DC_Roles.CLASS_ROLES[class]
|
||||
if not list or table.getn(list) == 0 then
|
||||
return currentRole
|
||||
end
|
||||
local n = table.getn(list)
|
||||
local nextRole = list[1]
|
||||
for i = 1, n do
|
||||
if list[i] == currentRole then
|
||||
nextRole = list[math.mod(i, n) + 1]
|
||||
break
|
||||
end
|
||||
end
|
||||
DC_Roles.confirm(db, storeSource, name, nextRole)
|
||||
return nextRole
|
||||
end
|
||||
+1135
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user