DopingControl v0.6.4
This commit is contained in:
+36
-13
@@ -19,6 +19,12 @@
|
||||
-- resolved via SuperWoW SpellInfo(id) (return 3 = icon) when the function
|
||||
-- exists. textures is OPTIONAL/partial by design -- every consumer
|
||||
-- nil-guards, nothing downstream may require it.
|
||||
-- Buff EFFECT lines: auras.effects[name] = the tooltip's line 2 text
|
||||
-- (same hidden-tooltip read as the name, TextLeft2) for every path-A buff
|
||||
-- that yielded both a name and a non-empty line 2. This feeds the item
|
||||
-- identification ladder for generic buff names ("Well Fed" -> which food,
|
||||
-- via DC.ResolveItem's discrim patterns). effects is OPTIONAL/partial
|
||||
-- exactly like textures -- every consumer nil-guards.
|
||||
-- Disagreements between the two ID sets are counted (symmetric difference,
|
||||
-- only when BOTH paths yielded at least one ID -- an absent path is missing
|
||||
-- data, not a disagreement) and accumulated by the engine into
|
||||
@@ -68,8 +74,9 @@ local A = DC_Aura
|
||||
|
||||
-- Assemble(rawA, rawB, idNames) -> auras, aurasRead, disagreements, unknown
|
||||
-- rawA : array of { name = string|nil, id = number|nil,
|
||||
-- texture = string|nil } (path A; texture is the 1st
|
||||
-- UnitBuff return, optional)
|
||||
-- texture = string|nil, effect = string|nil }
|
||||
-- (path A; texture is the 1st UnitBuff return,
|
||||
-- effect the tooltip's line 2 -- both optional)
|
||||
-- rawB : array of spell ids (numbers, path B)
|
||||
-- idNames : OPTIONAL { [spellId] = "Buff Name" } for path-B ids,
|
||||
-- filled by the WoW-side caller (SuperWoW SpellInfo lookup --
|
||||
@@ -78,9 +85,10 @@ local A = DC_Aura
|
||||
-- as before (no behavior change without it).
|
||||
-- Returns:
|
||||
-- auras : { names = { [name]=true }, ids = { [id]=true },
|
||||
-- textures = { [name]=iconPath } }
|
||||
-- (store shape; textures may be empty/partial --
|
||||
-- consumers nil-guard)
|
||||
-- textures = { [name]=iconPath },
|
||||
-- effects = { [name]=effectLine } }
|
||||
-- (store shape; textures AND effects may be empty/
|
||||
-- partial -- consumers nil-guard)
|
||||
-- aurasRead : bool -- true iff at least one USABLE aura arrived: a
|
||||
-- path-A entry carrying a name or an id, or a path-B id.
|
||||
-- Entries with NEITHER (UnitBuff texture present but the
|
||||
@@ -131,6 +139,7 @@ function A.Assemble(rawA, rawB, idNames)
|
||||
local idsA = {}
|
||||
local idsB = {}
|
||||
local textures = {} -- [buffName] = icon path
|
||||
local effects = {} -- [buffName] = tooltip effect line (line 2)
|
||||
local namedIds = {} -- ids that arrived WITH a name (path A rows)
|
||||
local usable = 0 -- entries that carry actual evidence (see header)
|
||||
|
||||
@@ -144,6 +153,9 @@ function A.Assemble(rawA, rawB, idNames)
|
||||
if e.texture then
|
||||
textures[e.name] = e.texture
|
||||
end
|
||||
if type(e.effect) == "string" and e.effect ~= "" then
|
||||
effects[e.name] = e.effect
|
||||
end
|
||||
end
|
||||
if type(e.id) == "number" and e.id > 0 then
|
||||
idsA[e.id] = true
|
||||
@@ -259,7 +271,8 @@ function A.Assemble(rawA, rawB, idNames)
|
||||
end
|
||||
end
|
||||
|
||||
return { names = names, ids = ids, textures = textures },
|
||||
return { names = names, ids = ids, textures = textures,
|
||||
effects = effects },
|
||||
aurasRead, disagreements, unknown
|
||||
end
|
||||
|
||||
@@ -376,15 +389,22 @@ function A.GetScanTip()
|
||||
return ensureTip()
|
||||
end
|
||||
|
||||
-- Returns name (tooltip line 1) AND effect (tooltip line 2 -- the buff's
|
||||
-- effect text, feeds the item identification ladder for generic names).
|
||||
-- raises an error on bad units -> always called through pcall
|
||||
local function tipBuffName(unit, buffIndex)
|
||||
scanTip:ClearLines()
|
||||
scanTip:SetUnitBuff(unit, buffIndex)
|
||||
local name, effect = nil, nil
|
||||
local textObj = getglobal("DopingControlScanTipTextLeft1")
|
||||
if textObj then
|
||||
return textObj:GetText()
|
||||
name = textObj:GetText()
|
||||
end
|
||||
return nil
|
||||
local effObj = getglobal("DopingControlScanTipTextLeft2")
|
||||
if effObj then
|
||||
effect = effObj:GetText()
|
||||
end
|
||||
return name, effect
|
||||
end
|
||||
|
||||
-- debuff twin of tipBuffName (same hidden tooltip, SetUnitDebuff);
|
||||
@@ -420,20 +440,23 @@ function A.ReadUnit(unit, guid)
|
||||
if not ok or not texture then
|
||||
break
|
||||
end
|
||||
local name = nil
|
||||
local name, effect = nil, nil
|
||||
if tip then
|
||||
local ok2, nm = pcall(tipBuffName, unit, i)
|
||||
local ok2, nm, eff = pcall(tipBuffName, unit, i)
|
||||
if ok2 then
|
||||
name = nm
|
||||
effect = eff
|
||||
end
|
||||
end
|
||||
local id = nil
|
||||
if type(auraID) == "number" and auraID > 0 then
|
||||
id = auraID
|
||||
end
|
||||
-- texture (UnitBuff return 1) rides along so Assemble can key
|
||||
-- it under the tooltip name (auras.textures)
|
||||
table.insert(rawA, { name = name, id = id, texture = texture })
|
||||
-- texture (UnitBuff return 1) and the tooltip effect line ride
|
||||
-- along so Assemble can key them under the tooltip name
|
||||
-- (auras.textures / auras.effects)
|
||||
table.insert(rawA, { name = name, id = id, texture = texture,
|
||||
effect = effect })
|
||||
end
|
||||
end
|
||||
if UnitDebuff then
|
||||
|
||||
+21
-7
@@ -120,6 +120,15 @@ function E.NormalizeWeapon(isSelf, v, readable)
|
||||
end
|
||||
return false
|
||||
end
|
||||
-- Stock 1.12 GetWeaponEnchantInfo takes NO arguments: on a client without
|
||||
-- SuperWoW the extra unit is ignored and the first return is the PLAYER's
|
||||
-- hasMainHandEnchant (1|nil). Only the SuperWoW name channel answers about
|
||||
-- the foreign unit, and it is always string-or-nil (measured: 11 names,
|
||||
-- 14 nils, "" never). A non-string therefore says nothing about this unit
|
||||
-- => not readable, never "has an imbue".
|
||||
if v ~= nil and type(v) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
if v == nil then
|
||||
if readable == true then
|
||||
return false
|
||||
@@ -998,10 +1007,12 @@ if CreateFrame then
|
||||
end
|
||||
E.SweepLastKnown(DC.lastKnown, rosterSet, E.CACHE_CAP)
|
||||
local db = DopingControlDB
|
||||
if db and db.testMode then
|
||||
-- Test mode was switched ON while this scan was in flight: the
|
||||
-- simulator fully replaces the data source ("real
|
||||
-- data never beats the simulation") -- drop the result.
|
||||
if DC.SimOwnsStore(db) then
|
||||
-- Test mode OR demo mode was on (or switched on while this scan
|
||||
-- was in flight): the simulator fully replaces the data source
|
||||
-- ("real data never beats the simulation") -- drop the result.
|
||||
-- This is the load-bearing gate: it is the only thing between a
|
||||
-- finished scan and DC.store = E.NewStore(...) below.
|
||||
results = {}
|
||||
return
|
||||
end
|
||||
@@ -1055,8 +1066,11 @@ if CreateFrame then
|
||||
|
||||
-- Start(force) -> bool (scan started/restarted).
|
||||
-- Already running: no-op unless force, which restarts with a fresh
|
||||
-- roster snapshot. Test mode is NOT checked here -- an explicit Start
|
||||
-- is user intent; only the READY_CHECK auto-trigger honors testMode.
|
||||
-- roster snapshot. DC.SimOwnsStore is NOT checked here -- an explicit
|
||||
-- Start is user intent, and the callers that are NOT user intent
|
||||
-- (READY_CHECK, the roster-change and window-open auto-scans) check it
|
||||
-- themselves. The scan may therefore run in test/demo mode; what it may
|
||||
-- never do is publish, so finish() drops the result instead.
|
||||
function E.Start(force)
|
||||
if running then
|
||||
if not force then
|
||||
@@ -1087,7 +1101,7 @@ if CreateFrame then
|
||||
eventFrame:SetScript("OnEvent", function()
|
||||
if event == "READY_CHECK" then
|
||||
local db = DopingControlDB
|
||||
if db and db.readyCheckScan and not db.testMode then
|
||||
if db and db.readyCheckScan and not DC.SimOwnsStore(db) then
|
||||
E.Start()
|
||||
end
|
||||
return
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@ local G = DC_Gear
|
||||
|
||||
-- Fallback inventory-slot list (= invSlot column of DC.SLOTS_EQUIPMENT,
|
||||
-- data/enchants.lua) so this file works standalone in tests.
|
||||
local FALLBACK_INV_SLOTS = { 1, 2, 3, 15, 5, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 }
|
||||
local FALLBACK_INV_SLOTS = { 1, 2, 3, 15, 5, 6, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 }
|
||||
|
||||
-- InvSlots() -> array of the 16 inventory slot numbers, in column order.
|
||||
-- InvSlots() -> array of the 17 inventory slot numbers, in column order.
|
||||
-- Prefers DC.SLOTS_EQUIPMENT (single source of truth once data/ is loaded).
|
||||
function G.InvSlots()
|
||||
if DC.SLOTS_EQUIPMENT then
|
||||
|
||||
+7
-6
@@ -122,9 +122,11 @@ DC_Hit = DC_Hit or {}
|
||||
local H = DC_Hit
|
||||
|
||||
-- Equipment slots that can carry +hit, in inventory-slot order. This is
|
||||
-- deliberately its OWN list and not DC_Gear.InvSlots(): the enchant tab
|
||||
-- has no waist column, but a belt most certainly can carry hit. Shirt (4)
|
||||
-- and tabard (19) never carry stats and are left out.
|
||||
-- deliberately its OWN list and not DC_Gear.InvSlots(): the two lists
|
||||
-- happen to agree today (both cover the waist, invSlot 6 -- see
|
||||
-- data/enchants.lua's bug-history note), but this file must not assume
|
||||
-- that stays true, so it keeps every hit-bearing slot spelled out here.
|
||||
-- Shirt (4) and tabard (19) never carry stats and are left out.
|
||||
H.SLOTS = { 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
|
||||
|
||||
H.SLOT_MAIN = 16
|
||||
@@ -888,9 +890,8 @@ end
|
||||
-- ReadUnit(unit, rawLinks) -> hit, race
|
||||
-- rawLinks : OPTIONAL { [invSlot] = link } that scan/gear.lua already
|
||||
-- fetched for this unit. Slots it covers are taken from it
|
||||
-- (no second GetInventoryItemLink pass); slots outside its
|
||||
-- coverage -- the waist, which the enchant tab has no column
|
||||
-- for -- are fetched here.
|
||||
-- (no second GetInventoryItemLink pass); any slot H.SLOTS
|
||||
-- needs that rawLinks did not cover is fetched here instead.
|
||||
-- hit : the store shape documented in the file header
|
||||
-- race : race token from UnitRace (2nd return, e.g. "NightElf");
|
||||
-- needed for the racial weapon-skill estimate of foreign
|
||||
|
||||
+62
-6
@@ -67,6 +67,11 @@ T.ASK_GAP = 5
|
||||
-- do not ask the same player again for this long. Talents change on
|
||||
-- respec, which is rare and never mid-raid.
|
||||
T.REASK_AFTER = 1800
|
||||
-- how long an unfinished reply stays open. A whole reply lands within a
|
||||
-- fraction of a second, so a line arriving this much later cannot belong to
|
||||
-- it -- it is the start of a NEW reply and must not be folded into the old
|
||||
-- accumulator (see T.AccStale).
|
||||
T.ACC_TTL = 10
|
||||
|
||||
-- ==================================================================
|
||||
-- PURE SECTION (offline-testable)
|
||||
@@ -147,9 +152,24 @@ end
|
||||
-- spent = <sum of pointsSpent over the trees that reported>,
|
||||
-- sumRank = <sum of every rank we stored>,
|
||||
-- trees = <how many tab lines arrived>,
|
||||
-- complete = <INSTalentEND seen> }
|
||||
function T.NewAcc()
|
||||
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false }
|
||||
-- complete = <INSTalentEND seen>,
|
||||
-- at = <when the first line arrived, nil offline> }
|
||||
function T.NewAcc(now)
|
||||
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false,
|
||||
at = now }
|
||||
end
|
||||
|
||||
-- Has this accumulator been waiting so long that the next line must belong to
|
||||
-- a different reply? Only the END marker drops an accumulator, so a reply
|
||||
-- whose tail was lost would otherwise stay open forever and swallow the next
|
||||
-- one: tab lines add up twice while ranks are deduplicated by name, so
|
||||
-- T.Plausible rejects a perfectly good retry. Without a clock (offline) or an
|
||||
-- opening stamp nothing expires -- the old behaviour, unchanged.
|
||||
function T.AccStale(acc, now)
|
||||
if not acc or not acc.at or not now then
|
||||
return false
|
||||
end
|
||||
return (now - acc.at) > T.ACC_TTL
|
||||
end
|
||||
|
||||
-- Fold one parsed line into the accumulator. Returns the accumulator so
|
||||
@@ -281,6 +301,9 @@ if CreateFrame then
|
||||
return ok
|
||||
end
|
||||
|
||||
-- set below, once the frame exists: arms the paced sender
|
||||
local arm
|
||||
|
||||
-- Called by the scan with the players it can see. Nothing is sent here
|
||||
-- -- names are only lined up; the timer below paces them out.
|
||||
function T.Request(names)
|
||||
@@ -296,6 +319,9 @@ if CreateFrame then
|
||||
table.insert(queue, n)
|
||||
end
|
||||
end
|
||||
if arm and table.getn(queue) > 0 then
|
||||
arm()
|
||||
end
|
||||
end
|
||||
|
||||
local f = CreateFrame("Frame", "DopingControlTalentFrame")
|
||||
@@ -313,9 +339,14 @@ if CreateFrame then
|
||||
if not kind then
|
||||
return
|
||||
end
|
||||
local now = GetTime()
|
||||
local a = acc[sender]
|
||||
if a and T.AccStale(a, now) then
|
||||
-- the previous reply lost its END line; this one starts fresh
|
||||
a = nil
|
||||
end
|
||||
if not a then
|
||||
a = T.NewAcc()
|
||||
a = T.NewAcc(now)
|
||||
acc[sender] = a
|
||||
end
|
||||
T.Feed(a, kind, data)
|
||||
@@ -338,14 +369,26 @@ if CreateFrame then
|
||||
-- queue the scan filled. Deliberately a plain timer rather than a burst
|
||||
-- after each scan -- 40 requests in one frame is what a rate limit
|
||||
-- would punish, and nothing documents where that limit sits.
|
||||
--
|
||||
-- It is installed only while there is something to send and takes itself
|
||||
-- off again the moment the queue runs dry (or the feature is switched
|
||||
-- off): an OnUpdate runs 60+ times a second for the whole session, and a
|
||||
-- player who never scans must not pay for a queue that is always empty.
|
||||
-- Re-arming needs no extra wiring -- every scan calls T.Request.
|
||||
local elapsed = 0
|
||||
f:SetScript("OnUpdate", function()
|
||||
|
||||
local function disarm()
|
||||
f:SetScript("OnUpdate", nil)
|
||||
end
|
||||
|
||||
local function drain()
|
||||
elapsed = elapsed + (arg1 or 0)
|
||||
if elapsed < 1 then
|
||||
return
|
||||
end
|
||||
elapsed = 0
|
||||
if not enabled() or table.getn(queue) == 0 then
|
||||
disarm()
|
||||
return
|
||||
end
|
||||
local now = GetTime()
|
||||
@@ -359,10 +402,23 @@ if CreateFrame then
|
||||
T.Ask(name)
|
||||
end
|
||||
end
|
||||
end)
|
||||
if table.getn(queue) == 0 then
|
||||
disarm()
|
||||
end
|
||||
end
|
||||
|
||||
arm = function()
|
||||
if f:GetScript("OnUpdate") then
|
||||
return
|
||||
end
|
||||
-- a long idle gap must not fire an immediate burst
|
||||
elapsed = 0
|
||||
f:SetScript("OnUpdate", drain)
|
||||
end
|
||||
|
||||
-- forget everything (respec, or a deliberate re-read)
|
||||
function T.Clear()
|
||||
disarm()
|
||||
acc = {}
|
||||
ranks = {}
|
||||
readAt = {}
|
||||
|
||||
Reference in New Issue
Block a user