1664 lines
65 KiB
Lua
1664 lines
65 KiB
Lua
-- AnySCT core.lua -- targetSCT list, event sources, dispatch, saved variables, slash commands.
|
|
--
|
|
-- Everything that needs a frame lives at the bottom behind "if CreateFrame then", so the logic
|
|
-- above (targetSCT list, per-mob hit counters, dispatch) runs offline under Lua 5.0 with stubbed unit
|
|
-- functions (tools/luatests).
|
|
--
|
|
-- SOURCE LIST
|
|
-- An ordered list of targetSCTs, each an entry { name, src, unit, targetUnit, guid, ... }.
|
|
-- src "manual": /asct add <name>, saved per character.
|
|
-- src "main": CT_RA_MainTanks / oRA_MainTanks entries (option importMain), shown as "import".
|
|
-- Events of any listed targetSCT are shown over THAT targetSCT's target. The player's own events are never
|
|
-- shown (the player's own SCT addon stays the only one that shows their damage).
|
|
--
|
|
-- This addon never touches Blizzard's CombatText, SHOW_COMBAT_TEXT or any other SCT addon.
|
|
|
|
AnySCT_Core = {}
|
|
local C = AnySCT_Core
|
|
|
|
AnySCT_DEFAULTS = {
|
|
enabled = true,
|
|
direction = "up", -- "up" | "down"
|
|
sizeFactor = 1.0, -- text size = 0.018333 x screen diagonal x this (crits 0.0275)
|
|
wordFactor = 1.0, -- same for Miss / Dodge / Parry / ...
|
|
font = "CLIENT", -- "CLIENT" (DAMAGE_TEXT_FONT) | "FRIZQT" | "SKURRI" | "ARIALN"
|
|
riseHeight = 120, -- px a normal text rises (crits stay in place); the style table is authoritative
|
|
jitter = 0, -- random horizontal px per line
|
|
maxLines = 60,
|
|
maxLinesPerTarget = 16,
|
|
onlyWhileSourceFights = false, -- true: hide text while the listed source itself is not in combat (renamed from showOnlyInCombat, which gated on the PLAYER)
|
|
colourize = false, -- false: everything orange like the pet text; true: crit yellow, miss red, block blue, ...
|
|
mergeHits = true, -- identical attacks (same source, victim, attack, hit/crit) within a short window become ONE line with the running sum
|
|
mergeWindow = 0.4, -- seconds a line keeps absorbing identical hits
|
|
mergeCount = false, -- append " x5" to a merged line
|
|
markGlancing = false, -- true: add "glancing" / "crushing" after the number
|
|
guessTotemOwner = false, -- unknown totem owner = the only listed shaman (can misattribute enemy / own totems)
|
|
classColours = false, -- numbers / words take the source's class colour (pets: the owner's)
|
|
showHeals = true, -- heals received by a listed targetSCT float over it (green, "+N" and the healer)
|
|
showHots = false, -- also heal-over-time ticks (chat only, can be noisy)
|
|
showOwnHeals = true, -- include the player's own heals on the targetSCTs (the engine never floats heals)
|
|
healShowHealer = true, -- healer name under the number
|
|
showSpells = true, -- listen to special attack rows (Heroic Strike, ...); off unregisters the spell channels
|
|
includeTotems = true, -- also show totem damage whose owner is a listed targetSCT (needs an owner lookup)
|
|
showSpellName = false,
|
|
mobNameMode = "never", -- "never" | "anchor" (only at the fixed anchor) | "always"; renamed key so the old "anchor" default is not inherited
|
|
sourceNameMode = "multi", -- "never" | "multi" (only with more than one listed) | "always": the listed source's name in the suffix line
|
|
colorPerTarget = false, -- true: every listed source gets its own text colour (auto-picked, changeable in the list)
|
|
showLabel = false, -- "targetSCTs: ..." line above the anchor
|
|
threatGo = false, -- experimental GO / WAIT label from server threat data (TWThreat protocol)
|
|
threatTrace = false, -- record the received threat data to <WoW>\imports\anysct_threat.txt (bug reports; needs SuperWoW)
|
|
threatGoLead = 1500, -- threat buffer (points) the targetSCT needs over you before the label says GO
|
|
extendRange = true, -- combat log range 200: raid targetSCTs outside the party are silent otherwise
|
|
minimap = true,
|
|
mmAngle = 200,
|
|
importMain = true,
|
|
placement = "clamp", -- "clamp" | "anchorOff" | "anchor" | "both"
|
|
noPlate = "anchor", -- "anchor" | "hide"
|
|
edgeMargin = 40,
|
|
xOffset = 40, -- plate modes: shift right so targetSCT numbers sit beside the player's own
|
|
yOffset = 0,
|
|
plateRange = 0, -- 0 = leave the nameplate range CVars alone
|
|
pos = { point = "CENTER", x = 0, y = 120 },
|
|
}
|
|
|
|
local state = {
|
|
order = {}, -- targetSCT names in list order
|
|
byName = {}, -- name -> entry
|
|
byGuid = {}, -- normalised guid -> entry
|
|
petByName = {}, -- pet name -> owner entry
|
|
petByGuid = {}, -- normalised pet guid -> owner entry
|
|
roster = {}, -- name -> unit token
|
|
nampower = false,
|
|
npSpells = false,
|
|
npHeals = false,
|
|
needCode = 0,
|
|
loaded = false,
|
|
}
|
|
C.state = state
|
|
|
|
function C.Now()
|
|
if GetTime then return GetTime() end
|
|
return 0
|
|
end
|
|
|
|
-- ---- saved variables -------------------------------------------------------------------------
|
|
|
|
local function copyValue(v)
|
|
if type(v) ~= "table" then return v end
|
|
local t = {}
|
|
for k, x in pairs(v) do t[k] = x end
|
|
return t
|
|
end
|
|
|
|
-- One-time migration of saved variables written before the "tank" -> "targetSCT" rename: every
|
|
-- key whose name contains "tank" (any case) moves to the new spelling, in the root table, in the
|
|
-- options and in each character table. A value already present under the new key wins.
|
|
local RENAMED = { importMainTanks = "importMain" }
|
|
|
|
local function newKey(k)
|
|
if RENAMED[k] then return RENAMED[k] end
|
|
local n = string.gsub(k, "Tank", "Source")
|
|
n = string.gsub(n, "tank", "source")
|
|
return n
|
|
end
|
|
|
|
local function migrateKeys(t)
|
|
if type(t) ~= "table" then return end
|
|
local moves
|
|
for k in pairs(t) do
|
|
if type(k) == "string" and string.find(string.lower(k), "tank", 1, true) then
|
|
moves = moves or {}
|
|
table.insert(moves, k)
|
|
end
|
|
end
|
|
if not moves then return end
|
|
for i = 1, table.getn(moves) do
|
|
local k = moves[i]
|
|
local nk = newKey(k)
|
|
if t[nk] == nil then t[nk] = t[k] end
|
|
t[k] = nil
|
|
end
|
|
end
|
|
|
|
function C.MigrateDB()
|
|
local db = AnySCTDB
|
|
if type(db) ~= "table" or (db.dbVersion or 0) >= 4 then return end
|
|
if (db.dbVersion or 0) < 2 then
|
|
migrateKeys(db)
|
|
migrateKeys(db.opt)
|
|
if type(db.chars) == "table" then
|
|
for _, c in pairs(db.chars) do migrateKeys(c) end
|
|
end
|
|
end
|
|
-- v3: the hit-count GO / WAIT indicator is gone (threat based now)
|
|
if type(db.opt) == "table" then db.opt.goAfterHits = nil end
|
|
-- v4: line limits raised (16 / 4 -> 60 / 16); only values still equal to the old defaults are reset,
|
|
-- a limit the user chose deliberately stays
|
|
if type(db.opt) == "table" then
|
|
if db.opt.maxLines == 16 then db.opt.maxLines = nil end
|
|
if db.opt.maxLinesPerTarget == 4 then db.opt.maxLinesPerTarget = nil end
|
|
end
|
|
db.dbVersion = 4
|
|
end
|
|
|
|
function C.InitDB()
|
|
if type(AnySCTDB) ~= "table" then AnySCTDB = {} end
|
|
C.MigrateDB()
|
|
if type(AnySCTDB.opt) ~= "table" then AnySCTDB.opt = {} end
|
|
if type(AnySCTDB.chars) ~= "table" then AnySCTDB.chars = {} end
|
|
local o = AnySCTDB.opt
|
|
for k, v in pairs(AnySCT_DEFAULTS) do
|
|
if o[k] == nil then o[k] = copyValue(v) end
|
|
end
|
|
return o
|
|
end
|
|
|
|
function C.Opt(key)
|
|
if not AnySCTDB or not AnySCTDB.opt then C.InitDB() end
|
|
return AnySCTDB.opt[key]
|
|
end
|
|
|
|
-- The whole option table (for the display code).
|
|
function C.Opts()
|
|
if not AnySCTDB or not AnySCTDB.opt then C.InitDB() end
|
|
return AnySCTDB.opt
|
|
end
|
|
|
|
function C.SetOpt(key, value)
|
|
if not AnySCTDB or not AnySCTDB.opt then C.InitDB() end
|
|
AnySCTDB.opt[key] = value
|
|
if key == "extendRange" and value then C.ExtendRange() end
|
|
if key == "plateRange" then C.ApplyPlateRange() end
|
|
if key == "showSpells" or key == "showHeals" or key == "showHots" or key == "showOwnHeals" then C.UpdateEvents() end
|
|
if key == "enabled" then
|
|
C.UpdateEvents()
|
|
if value then
|
|
if UnitAffectingCombat("player") and C.StartTick then C.StartTick() end
|
|
elseif C.StopTick then
|
|
C.StopTick()
|
|
end
|
|
end
|
|
if key == "importMain" then C.SyncMain() end
|
|
if key == "threatGo" or key == "threatGoLead" then
|
|
C.UpdateEvents()
|
|
C.ThreatRefresh()
|
|
end
|
|
if AnySCT_SCT and AnySCT_SCT.OptionChanged then AnySCT_SCT.OptionChanged(key) end
|
|
if key == "minimap" and AnySCT_Minimap and AnySCT_Minimap.Update then AnySCT_Minimap.Update() end
|
|
end
|
|
|
|
function C.CharKey()
|
|
local realm = "?"
|
|
if GetRealmName then realm = GetRealmName() or "?" end
|
|
return realm .. "-" .. (UnitName("player") or "?")
|
|
end
|
|
|
|
-- Per-character table: { list = { names }, colours = { name = "RRGGBB" }, threat = { name = true|false } }.
|
|
function C.Char()
|
|
if not AnySCTDB or not AnySCTDB.chars then C.InitDB() end
|
|
local k = C.CharKey()
|
|
local c = AnySCTDB.chars[k]
|
|
if not c then
|
|
c = { list = {} }
|
|
AnySCTDB.chars[k] = c
|
|
end
|
|
if type(c.list) ~= "table" then c.list = {} end
|
|
if type(c.colours) ~= "table" then c.colours = {} end
|
|
if type(c.threat) ~= "table" then c.threat = {} end
|
|
return c
|
|
end
|
|
|
|
function C.Print(msg)
|
|
if C.out then C.out(msg) return end
|
|
if DEFAULT_CHAT_FRAME then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff55ccffAnySCT|r: " .. msg)
|
|
end
|
|
end
|
|
|
|
-- Debug trace (/asct debug). Callers test C.debug BEFORE building any string, so it costs nothing off.
|
|
C.debug = false
|
|
function C.Dbg(msg)
|
|
if C.debug then C.Print("|cff888888dbg|r " .. msg) end
|
|
end
|
|
|
|
-- ---- GUIDs -----------------------------------------------------------------------------------
|
|
|
|
-- SuperWoW: UnitExists returns (exists, guid). Without it the second value is nil -> no GUID.
|
|
function C.ResolveGuid(unit)
|
|
if not unit then return nil end
|
|
local ok, _, g = pcall(UnitExists, unit)
|
|
if ok and type(g) == "string" then return g end
|
|
if GetUnitGUID then
|
|
local ok2, g2 = pcall(GetUnitGUID, unit)
|
|
if ok2 and type(g2) == "string" then return g2 end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local normCache, normCount = {}, 0
|
|
|
|
-- Canonical form of a GUID so "0x000000000000ABCD", "0xabcd" and the number 43981 compare equal.
|
|
-- Memoised (events come in at raid rate); the memo is dropped when it grows past 300 entries.
|
|
function C.NormGuid(g)
|
|
if g == nil then return nil end
|
|
local hit = normCache[g]
|
|
if hit then return hit end
|
|
local s
|
|
if type(g) == "number" then
|
|
s = string.format("%x", g)
|
|
else
|
|
s = string.lower(tostring(g))
|
|
s = string.gsub(s, "^0x", "")
|
|
s = string.gsub(s, "^0+", "")
|
|
end
|
|
if s == "" then s = "0" end
|
|
if normCount >= 300 then normCache = {}; normCount = 0 end
|
|
normCache[g] = s
|
|
normCount = normCount + 1
|
|
return s
|
|
end
|
|
|
|
-- ---- roster ----------------------------------------------------------------------------------
|
|
|
|
-- Pet token of a group unit: pet, raidpetN, partypetN.
|
|
local function petToken(u)
|
|
if not u then return nil end
|
|
if u == "player" then return "pet" end
|
|
local _, _, n = string.find(u, "^raid(%d+)$")
|
|
if n then return "raidpet" .. n end
|
|
_, _, n = string.find(u, "^party(%d+)$")
|
|
if n then return "partypet" .. n end
|
|
return nil
|
|
end
|
|
|
|
function C.DropPet(e)
|
|
if e.petName and state.petByName[e.petName] == e then state.petByName[e.petName] = nil end
|
|
if e.petNorm and state.petByGuid[e.petNorm] == e then state.petByGuid[e.petNorm] = nil end
|
|
e.petName, e.petNorm = nil, nil
|
|
end
|
|
|
|
-- Cache the targetSCT's pet (name + GUID). Tokens are range limited, so the name cache keeps what it saw.
|
|
function C.RefreshPet(e)
|
|
C.ClearOwnerCache()
|
|
C.DropPet(e)
|
|
if not e.petUnit then return end
|
|
local ok, ex = pcall(UnitExists, e.petUnit)
|
|
if not (ok and ex) then return end
|
|
e.petName = UnitName(e.petUnit)
|
|
local g = C.ResolveGuid(e.petUnit)
|
|
e.petNorm = g and C.NormGuid(g) or nil
|
|
if e.petName then state.petByName[e.petName] = e end
|
|
if e.petNorm then state.petByGuid[e.petNorm] = e end
|
|
end
|
|
|
|
function C.RefreshEntry(e)
|
|
local u = state.roster[e.name]
|
|
e.unit = u
|
|
e.targetUnit = u and (u .. "target") or nil
|
|
e.petUnit = petToken(u)
|
|
e.class = nil
|
|
if u and UnitClass then
|
|
local _, cls = UnitClass(u)
|
|
e.class = cls
|
|
end
|
|
if u then
|
|
if e.guidNorm and state.byGuid[e.guidNorm] == e then state.byGuid[e.guidNorm] = nil end
|
|
local g = C.ResolveGuid(u)
|
|
e.guid = g
|
|
e.guidNorm = g and C.NormGuid(g) or nil
|
|
end
|
|
-- a source outside the group keeps the GUID it was last seen with
|
|
if e.guidNorm then state.byGuid[e.guidNorm] = e end
|
|
C.RefreshPet(e)
|
|
end
|
|
|
|
-- A listed source outside the group (hostile player, mob): learn its GUID when we see the unit.
|
|
function C.SeeUnit(name, unit)
|
|
local e = state.byName[name]
|
|
if not e or e.unit or UnitName(unit) ~= name then return end
|
|
local g = C.ResolveGuid(unit)
|
|
if not g then return end
|
|
if e.guidNorm and state.byGuid[e.guidNorm] == e then state.byGuid[e.guidNorm] = nil end
|
|
e.guid = g
|
|
e.guidNorm = C.NormGuid(g)
|
|
state.byGuid[e.guidNorm] = e
|
|
if UnitIsPlayer(unit) and UnitClass then
|
|
local _, cls = UnitClass(unit)
|
|
e.class = cls
|
|
end
|
|
end
|
|
|
|
function C.SeeLoose(unit)
|
|
local n = UnitName(unit)
|
|
if n and state.byName[n] and not state.byName[n].unit then C.SeeUnit(n, unit) end
|
|
end
|
|
|
|
local PROBE_UNITS = { "target", "targettarget", "mouseover" }
|
|
|
|
-- True when some listed source outside the group still has no GUID.
|
|
local function anyLooseWithoutGuid()
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
if not e.unit and not e.guidNorm then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Look at the units we can name (target, its target, mouseover) for listed sources without a GUID.
|
|
function C.ProbeLoose()
|
|
if not anyLooseWithoutGuid() then return false end
|
|
for i = 1, table.getn(PROBE_UNITS) do C.SeeLoose(PROBE_UNITS[i]) end
|
|
return not anyLooseWithoutGuid()
|
|
end
|
|
|
|
local lastRetry = -10
|
|
-- On-demand GUID retry when an event of a GUID-less source arrives: group sources via their token,
|
|
-- loose ones via ProbeLoose. Rate limited to 2 per second; no cost while every source has a GUID.
|
|
function C.RetryGuids()
|
|
local t = C.Now()
|
|
if t - lastRetry < 0.5 then return false end
|
|
lastRetry = t
|
|
local changed = C.FillGuids()
|
|
if C.ProbeLoose() then changed = true end
|
|
if changed and C.CheckCreature then C.CheckCreature() end
|
|
return changed
|
|
end
|
|
function C.ResetRetry() lastRetry = -10 end
|
|
|
|
function C.RebuildRoster()
|
|
local r = state.roster
|
|
for k in pairs(r) do r[k] = nil end
|
|
local nr = GetNumRaidMembers()
|
|
if nr > 0 then
|
|
for i = 1, nr do
|
|
local u = "raid" .. i
|
|
local n = UnitName(u)
|
|
if n then r[n] = u end
|
|
end
|
|
else
|
|
local np = GetNumPartyMembers()
|
|
for i = 1, np do
|
|
local u = "party" .. i
|
|
local n = UnitName(u)
|
|
if n then r[n] = u end
|
|
end
|
|
end
|
|
local pn = UnitName("player")
|
|
if pn then r[pn] = "player" end
|
|
-- every entry gets a fresh token
|
|
for i = 1, table.getn(state.order) do C.RefreshEntry(state.byName[state.order[i]]) end
|
|
C.SyncMain()
|
|
C.NotifyChanged()
|
|
end
|
|
|
|
-- ---- colours -----------------------------------------------------------------------------------
|
|
|
|
-- Auto palette: bright, saturated colours that read on game backgrounds. The first one is Blizzard's
|
|
-- pet orange, so a single listed unit looks as before. None is close to the heal green 19FF19.
|
|
C.PALETTE = { "FF8400", "00E5FF", "FF00FF", "FFFF00", "4D8DFF", "A6FF00", "FF6EB4", "9A4DFF",
|
|
"FF3030", "FFFFFF", "00C8A0" }
|
|
|
|
function C.HexToRgb(hex)
|
|
local r = tonumber(string.sub(hex, 1, 2), 16) or 255
|
|
local g = tonumber(string.sub(hex, 3, 4), 16) or 255
|
|
local b = tonumber(string.sub(hex, 5, 6), 16) or 255
|
|
return r / 255, g / 255, b / 255
|
|
end
|
|
|
|
function C.RgbToHex(r, g, b)
|
|
local function c(x)
|
|
x = math.floor(x * 255 + 0.5)
|
|
if x < 0 then x = 0 elseif x > 255 then x = 255 end
|
|
return x
|
|
end
|
|
return string.format("%02X%02X%02X", c(r), c(g), c(b))
|
|
end
|
|
|
|
-- Squared "redmean" colour distance of two hex colours (a cheap perceptual weighting of RGB).
|
|
function C.ColourDistance(h1, h2)
|
|
local r1, g1, b1 = C.HexToRgb(h1)
|
|
local r2, g2, b2 = C.HexToRgb(h2)
|
|
local rm = (r1 + r2) / 2
|
|
local dr, dg, db = r1 - r2, g1 - g2, b1 - b2
|
|
return (2 + rm) * dr * dr + 4 * dg * dg + (3 - rm) * db * db
|
|
end
|
|
|
|
-- Palette colour whose smallest distance to the colours in `used` (array of hex) is the largest.
|
|
-- Nothing used: the first palette colour. Ties go to the earlier palette entry (deterministic).
|
|
function C.PickAutoColour(used)
|
|
local pal = C.PALETTE
|
|
if not used or table.getn(used) == 0 then return pal[1] end
|
|
local best, bestD = pal[1], -1
|
|
for i = 1, table.getn(pal) do
|
|
local m = 1e9
|
|
for j = 1, table.getn(used) do
|
|
local d = C.ColourDistance(pal[i], used[j])
|
|
if d < m then m = d end
|
|
end
|
|
if m > bestD + 1e-9 then best, bestD = pal[i], m end
|
|
end
|
|
return best
|
|
end
|
|
|
|
local usedBuf = {}
|
|
local function usedColours(skip)
|
|
-- cleared from the end: table.getn on a table with leading holes is unreliable in Lua 5.0
|
|
for i = table.getn(usedBuf), 1, -1 do table.remove(usedBuf, i) end
|
|
for i = 1, table.getn(state.order) do
|
|
local o = state.byName[state.order[i]]
|
|
if o ~= skip and o.hex then table.insert(usedBuf, o.hex) end
|
|
end
|
|
return usedBuf
|
|
end
|
|
|
|
local function applyColour(e, hex)
|
|
e.hex = hex
|
|
local r, g, b = C.HexToRgb(hex)
|
|
e.rgb = { r, g, b }
|
|
C.Char().colours[e.name] = hex
|
|
end
|
|
|
|
-- Give a new entry its saved colour, else the best-separated palette colour.
|
|
function C.AssignColour(e)
|
|
local saved = C.Char().colours[e.name]
|
|
if type(saved) == "string" and string.len(saved) == 6 and tonumber(saved, 16) then
|
|
applyColour(e, string.upper(saved))
|
|
else
|
|
applyColour(e, C.PickAutoColour(usedColours(e)))
|
|
end
|
|
end
|
|
|
|
-- Manual colour (kept until AutoColours). hex "RRGGBB".
|
|
function C.SetColour(name, hex)
|
|
local e = state.byName[name]
|
|
if not e then return false end
|
|
applyColour(e, hex)
|
|
return true
|
|
end
|
|
|
|
-- Re-assign every colour by the auto rule, in list order.
|
|
function C.AutoColours()
|
|
for i = 1, table.getn(state.order) do state.byName[state.order[i]].hex = nil end
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
applyColour(e, C.PickAutoColour(usedColours(e)))
|
|
end
|
|
C.NotifyChanged()
|
|
end
|
|
|
|
-- ---- targetSCT list -------------------------------------------------------------------------------
|
|
|
|
function C.NotifyChanged()
|
|
C.CheckCreature()
|
|
C.ThreatRefresh()
|
|
if AnySCT_SCT and AnySCT_SCT.UpdateLabel then AnySCT_SCT.UpdateLabel() end
|
|
if AnySCT_Options and AnySCT_Options.refresh then AnySCT_Options.refresh() end
|
|
end
|
|
|
|
local function indexOf(list, name)
|
|
for i = 1, table.getn(list) do
|
|
if list[i] == name then return i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function C.Count(src)
|
|
local n = 0
|
|
for i = 1, table.getn(state.order) do
|
|
if not src or state.byName[state.order[i]].src == src then n = n + 1 end
|
|
end
|
|
return n
|
|
end
|
|
|
|
-- Add (or upgrade) a targetSCT. src: "manual" | "main". Manual entries are saved per character
|
|
-- unless noPersist (used when loading the saved list).
|
|
-- Default of the per-targetSCT "Threat" switch: on for friendly players (group members, imported main
|
|
-- tanks, names typed in blind), off for pets, totems, mobs and hostile players. info (optional, from the
|
|
-- right-click menu): { player = bool, friendly = bool } for a unit we could look at.
|
|
function C.DefaultThreatOn(name, src, info)
|
|
if src == "main" then return true end
|
|
if state.roster[name] then return true end
|
|
if string.find(name, "Totem", 1, true) then return false end
|
|
if info then
|
|
if info.player == false then return false end
|
|
if info.friendly == false then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function C.AssignThreat(e, info)
|
|
local saved = C.Char().threat[e.name]
|
|
if type(saved) == "boolean" then
|
|
e.threatOn = saved
|
|
else
|
|
e.threatOn = C.DefaultThreatOn(e.name, e.src, info)
|
|
C.Char().threat[e.name] = e.threatOn
|
|
end
|
|
end
|
|
|
|
function C.SetThreatOn(name, on)
|
|
local e = state.byName[name]
|
|
if not e then return false end
|
|
e.threatOn = on and true or false
|
|
C.Char().threat[name] = e.threatOn
|
|
C.ThreatRefresh()
|
|
return true
|
|
end
|
|
|
|
function C.AddSource(name, src, noPersist, info)
|
|
local e = state.byName[name]
|
|
if e then
|
|
if src == "manual" and e.src ~= "manual" then e.src = "manual" end
|
|
else
|
|
e = { name = name, src = src, paren = "(" .. name .. ")" }
|
|
state.byName[name] = e
|
|
table.insert(state.order, name)
|
|
C.AssignColour(e)
|
|
C.AssignThreat(e, info)
|
|
C.RefreshEntry(e)
|
|
end
|
|
if src == "manual" and not noPersist then
|
|
local list = C.Char().list
|
|
if not indexOf(list, name) then table.insert(list, name) end
|
|
end
|
|
C.NotifyChanged()
|
|
return e
|
|
end
|
|
|
|
function C.RemoveSource(name, noNotify)
|
|
local e = state.byName[name]
|
|
if not e then return false end
|
|
if e.guidNorm and state.byGuid[e.guidNorm] == e then state.byGuid[e.guidNorm] = nil end
|
|
C.DropPet(e)
|
|
state.byName[name] = nil
|
|
local i = indexOf(state.order, name)
|
|
if i then table.remove(state.order, i) end
|
|
local list = C.Char().list
|
|
local j = indexOf(list, name)
|
|
if j then table.remove(list, j) end
|
|
C.Char().colours[name] = nil -- the colour is free again
|
|
C.Char().threat[name] = nil
|
|
if not noNotify then C.NotifyChanged() end
|
|
return true
|
|
end
|
|
|
|
function C.ClearSources()
|
|
while table.getn(state.order) > 0 do C.RemoveSource(state.order[1], true) end
|
|
C.Char().list = {}
|
|
C.Char().colours = {}
|
|
C.Char().threat = {}
|
|
C.NotifyChanged()
|
|
end
|
|
|
|
-- ---- main tank lists (CT_RaidAssist / oRA) --------------------------------------------------------
|
|
|
|
local mainBuf, mainN = {}, 0
|
|
|
|
local function collectMain(t)
|
|
if type(t) ~= "table" then return end
|
|
for i = 1, 12 do
|
|
local n = t[i]
|
|
if type(n) == "string" and n ~= "" and not indexOf(mainBuf, n) then
|
|
mainN = mainN + 1
|
|
mainBuf[mainN] = n
|
|
end
|
|
end
|
|
end
|
|
|
|
function C.SyncMain()
|
|
for i = 1, mainN do mainBuf[i] = nil end
|
|
mainN = 0
|
|
if C.Opt("importMain") then
|
|
collectMain(CT_RA_MainTanks)
|
|
collectMain(oRA_MainTanks)
|
|
end
|
|
for i = 1, mainN do
|
|
local n = mainBuf[i]
|
|
if state.roster[n] then C.AddSource(n, "main", true) end
|
|
end
|
|
local i = 1
|
|
while i <= table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
if e.src == "main" and not indexOf(mainBuf, e.name) then
|
|
C.RemoveSource(e.name, true)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ---- targets of the listed sources -----------------------------------------------------------
|
|
|
|
-- Refresh every targetSCT's current target (used to place chat rows, which carry only a victim name).
|
|
function C.UpdateSourceTargets()
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
local tt = e.targetUnit
|
|
e.targetKey, e.targetName, e.targetGuid, e.targetHostile = nil, nil, nil, false
|
|
if tt then
|
|
local ok, ex, g = pcall(UnitExists, tt)
|
|
if ok and ex then
|
|
e.targetGuid = (type(g) == "string") and g or nil
|
|
e.targetName = UnitName(tt)
|
|
e.targetKey = e.targetGuid or e.targetName
|
|
e.targetHostile = (UnitCanAttack("player", tt) and not UnitIsDead(tt)) and true or false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ---- owner resolution (pets, totems) -------------------------------------------------------------
|
|
|
|
local ownerCache, ownerCount, cacheClearAt = {}, 0, nil
|
|
|
|
function C.ClearOwnerCache()
|
|
for k in pairs(ownerCache) do ownerCache[k] = nil end
|
|
ownerCount, cacheClearAt = 0, nil
|
|
end
|
|
|
|
-- After leaving combat the owner cache is dropped 60 s later (checked lazily, no timer).
|
|
function C.ScheduleCacheClear() cacheClearAt = C.Now() + 60 end
|
|
|
|
local function onlyShaman()
|
|
local found
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
if e.class == "SHAMAN" then
|
|
if found then return nil end
|
|
found = e
|
|
end
|
|
end
|
|
return found and found.name or nil
|
|
end
|
|
|
|
local function isGuid(x)
|
|
return type(x) == "number" or (type(x) == "string" and string.find(x, "^0x") ~= nil)
|
|
end
|
|
|
|
-- Owner name of a pet or totem given its GUID or its name, or nil. Pluggable: replace the global
|
|
-- to change the lookup. Cascade: cache -> SuperWoW owner suffix -> pet caches -> class fallback (a
|
|
-- "Totem" belongs to the only shaman among the listed targetSCTs) -> unknown. Cached per key.
|
|
function AnySCT_OwnerOf(x)
|
|
if x == nil then return nil end
|
|
if cacheClearAt and C.Now() >= cacheClearAt then C.ClearOwnerCache() end
|
|
local guid = isGuid(x)
|
|
local key = guid and C.NormGuid(x) or x
|
|
local hit = ownerCache[key]
|
|
if hit ~= nil then return hit or nil end
|
|
local owner
|
|
if guid then
|
|
local ok, n = pcall(UnitName, x .. "owner") -- SuperWoW; unverified for foreign totems
|
|
if ok and type(n) == "string" and state.byName[n] then owner = n end
|
|
local pe = state.petByGuid[key]
|
|
if not owner and pe then owner = pe.name end
|
|
if not owner and C.Opt("guessTotemOwner") then
|
|
local ok2, nm = pcall(UnitName, x)
|
|
if ok2 and type(nm) == "string" and string.find(nm, "Totem", 1, true) then owner = onlyShaman() end
|
|
end
|
|
else
|
|
local pe = state.petByName[x]
|
|
if pe then owner = pe.name end
|
|
if not owner and C.Opt("guessTotemOwner") and string.find(x, "Totem", 1, true) then owner = onlyShaman() end
|
|
end
|
|
if ownerCount >= 400 then C.ClearOwnerCache() end
|
|
ownerCache[key] = owner or false
|
|
ownerCount = ownerCount + 1
|
|
return owner
|
|
end
|
|
|
|
-- Entry (the listed targetSCT) that an event's attacker belongs to, by name or by GUID.
|
|
local function sourceFromName(name)
|
|
if not name then return nil end
|
|
local e = state.byName[name]
|
|
if e then return e end
|
|
local _, _, base, owner = string.find(name, "^(.-)%((.-)%)$") -- SuperWoW: "Petname(Owner)"
|
|
if owner then
|
|
e = state.byName[owner]
|
|
if e then return e end
|
|
name = base
|
|
end
|
|
if string.find(name, "Totem", 1, true) and not C.Opt("includeTotems") then return nil end
|
|
local o = AnySCT_OwnerOf(name)
|
|
return o and state.byName[o] or nil
|
|
end
|
|
|
|
local function sourceFromGuid(g)
|
|
if g == nil then return nil end
|
|
local n = C.NormGuid(g)
|
|
local e = state.byGuid[n] or state.petByGuid[n]
|
|
if e then return e end
|
|
local o = AnySCT_OwnerOf(g)
|
|
return o and state.byName[o] or nil
|
|
end
|
|
|
|
-- ---- dispatch --------------------------------------------------------------------------------
|
|
|
|
function C.IsSelf(e)
|
|
if e.unit == "player" then return true end
|
|
local pn = UnitName("player")
|
|
return pn ~= nil and e.name == pn
|
|
end
|
|
|
|
-- Combat state of a unit token or GUID: true / false, or nil when the unit cannot be looked up.
|
|
local function unitCombat(u)
|
|
local ok, ex = pcall(UnitExists, u)
|
|
if not (ok and ex) then return nil end
|
|
local ok2, r = pcall(UnitAffectingCombat, u)
|
|
if not ok2 then return nil end
|
|
return r and true or false
|
|
end
|
|
|
|
-- Is the listed source (or its pet) in combat? Unknown (no token, GUID not resolvable) counts as
|
|
-- yes: the option may only hide text it can prove the source is idle for.
|
|
function C.SourceFights(e)
|
|
local u = e.unit or e.guid
|
|
if not u then return true end
|
|
local r = unitCombat(u)
|
|
if r == nil then return true end
|
|
if r then return true end
|
|
if e.petUnit and unitCombat(e.petUnit) then return true end
|
|
return false
|
|
end
|
|
|
|
-- Show one result for a listed source e, over the VICTIM of the event (never over the targetSCT's
|
|
-- target as such: AoE and untargeted damage hits other mobs). vGuid: victim GUID when known.
|
|
function C.Emit(res, e, vGuid)
|
|
if not res or not e or not C.Opt("enabled") then return end
|
|
if C.IsSelf(e) then
|
|
if C.debug then C.Dbg("drop: " .. e.name .. " is the player (own events are never shown)") end
|
|
C.Ledger(res, "dropped:self")
|
|
return
|
|
end
|
|
C.UpdateSourceTargets()
|
|
local vName = res.victim
|
|
if vName == "you" then
|
|
vName = UnitName("player")
|
|
res.mobUnit = "player"
|
|
res.mobGuid = C.ResolveGuid("player")
|
|
elseif vGuid then
|
|
res.mobGuid = vGuid
|
|
if not vName then
|
|
local ok, n = pcall(UnitName, vGuid)
|
|
if ok then vName = n end
|
|
end
|
|
if e.targetGuid and C.NormGuid(e.targetGuid) == C.NormGuid(vGuid) then res.mobUnit = e.targetUnit end
|
|
elseif vName and vName == e.targetName then
|
|
-- chat rows carry only a name: if it is the targetSCT's target we know the GUID and the unit token
|
|
res.mobGuid, res.mobUnit = e.targetGuid, e.targetUnit
|
|
end
|
|
res.mobName = vName
|
|
res.source = e.name
|
|
res.srcRgb = e.rgb -- the listed source's own colour (used when colorPerTarget is on)
|
|
res.srcMulti = table.getn(state.order) > 1
|
|
res.class = e.class -- owner's class for pets and totems; nil for mobs
|
|
if C.Opt("onlyWhileSourceFights") and not C.SourceFights(e) then
|
|
if C.debug then C.Dbg("drop: " .. e.name .. " is not in combat (option: only while the source fights)") end
|
|
C.Ledger(res, "dropped:combat")
|
|
return
|
|
end
|
|
if AnySCT_SCT and AnySCT_SCT.Show then AnySCT_SCT.Show(res) end
|
|
end
|
|
|
|
-- nampower AUTO_ATTACK_OTHER (arg order unverified in-game, see README).
|
|
function C.OnAutoAttack(attackerGuid, targetGuid, damage, hitInfo, victimState, subCount, blocked, absorbed, resisted)
|
|
local e = sourceFromGuid(attackerGuid)
|
|
if not e then
|
|
if C.debug then C.Dbg("drop: AUTO_ATTACK_OTHER caster " .. tostring(attackerGuid) .. " not listed") end
|
|
return
|
|
end
|
|
local res = AnySCT_DecodeAutoAttack(damage, hitInfo, victimState, blocked, absorbed, resisted)
|
|
if res then
|
|
res.path = "np"
|
|
C.Emit(res, e, targetGuid)
|
|
end
|
|
end
|
|
|
|
local function spellName(spellId)
|
|
if not (C.Opt("showSpellName") and SpellInfo) then return nil end
|
|
local ok, n = pcall(SpellInfo, spellId)
|
|
if ok and type(n) == "string" then return n end
|
|
return nil
|
|
end
|
|
|
|
-- nampower SPELL_DAMAGE_EVENT_OTHER: target, caster, spellId, amount, mitigation, hitInfo, school, aura.
|
|
function C.OnSpellDamage(targetGuid, casterGuid, spellId, amount, mitigation, hitInfo)
|
|
local e = sourceFromGuid(casterGuid)
|
|
if not e then
|
|
if C.debug then C.Dbg("drop: SPELL_DAMAGE_EVENT_OTHER caster " .. tostring(casterGuid) .. " not listed") end
|
|
return
|
|
end
|
|
local res = AnySCT_DecodeSpellDamage(amount, mitigation, hitInfo)
|
|
res.spell = spellName(spellId)
|
|
res.spellId = spellId
|
|
res.path = "npspell"
|
|
C.Emit(res, e, targetGuid)
|
|
end
|
|
|
|
-- nampower SPELL_MISS_OTHER: caster, target, spellId, missType.
|
|
function C.OnSpellMiss(casterGuid, targetGuid, spellId, missType)
|
|
local e = sourceFromGuid(casterGuid)
|
|
if not e then
|
|
if C.debug then C.Dbg("drop: SPELL_MISS_OTHER caster " .. tostring(casterGuid) .. " not listed") end
|
|
return
|
|
end
|
|
local res = AnySCT_DecodeSpellMiss(missType)
|
|
if not res then return end
|
|
res.spell = spellName(spellId)
|
|
res.spellId = spellId
|
|
res.path = "npspell"
|
|
C.Emit(res, e, targetGuid)
|
|
end
|
|
|
|
local MELEE_EVENTS = {
|
|
CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS = true,
|
|
CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES = true,
|
|
CHAT_MSG_COMBAT_PARTY_HITS = true,
|
|
CHAT_MSG_COMBAT_PARTY_MISSES = true,
|
|
}
|
|
local SPELL_EVENTS = {
|
|
CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE = true,
|
|
CHAT_MSG_SPELL_PARTY_DAMAGE = true,
|
|
}
|
|
-- foreign pets and totems; registered only while a listed targetSCT has a pet or is a shaman
|
|
local CREATURE_MELEE = {
|
|
CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS = true,
|
|
CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES = true,
|
|
}
|
|
local CREATURE_SPELL = { CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE = true }
|
|
-- sources outside the group (hostile players, mobs): registered only while such a source is listed
|
|
local HOSTILE_MELEE = {
|
|
CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS = true,
|
|
CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES = true,
|
|
CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS = true,
|
|
CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES = true,
|
|
CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS = true,
|
|
CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES = true,
|
|
}
|
|
local HOSTILE_SPELL = {
|
|
CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE = true,
|
|
CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE = true,
|
|
CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE = true,
|
|
}
|
|
C.HOSTILE_MELEE, C.HOSTILE_SPELL = HOSTILE_MELEE, HOSTILE_SPELL
|
|
C.MELEE_EVENTS, C.SPELL_EVENTS = MELEE_EVENTS, SPELL_EVENTS
|
|
C.CREATURE_MELEE, C.CREATURE_SPELL = CREATURE_MELEE, CREATURE_SPELL
|
|
|
|
-- heal rows: direct heals (own heals come on SELF_BUFF) and heal-over-time ticks
|
|
local HEAL_EVENTS = {
|
|
CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF = true,
|
|
CHAT_MSG_SPELL_PARTY_BUFF = true,
|
|
CHAT_MSG_SPELL_SELF_BUFF = true,
|
|
}
|
|
local HOT_EVENTS = {
|
|
CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS = true,
|
|
CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS = true,
|
|
CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS = true,
|
|
}
|
|
C.HEAL_EVENTS, C.HOT_EVENTS = HEAL_EVENTS, HOT_EVENTS
|
|
|
|
-- Show a heal over the listed targetSCT e (the VICTIM of the heal). healer: name or nil.
|
|
function C.EmitHeal(res, e, healer)
|
|
if not res or not e or not C.Opt("enabled") or not C.Opt("showHeals") then return end
|
|
if C.IsSelf(e) then
|
|
if C.debug then C.Dbg("drop: heal on the player") end
|
|
return
|
|
end
|
|
if res.periodic and not C.Opt("showHots") then
|
|
if C.debug then C.Dbg("drop: HoT tick (option off)") end
|
|
return
|
|
end
|
|
local pn = UnitName("player")
|
|
if (healer == "you" or (healer ~= nil and healer == pn)) and not C.Opt("showOwnHeals") then
|
|
if C.debug then C.Dbg("drop: own heal (option off)") end
|
|
return
|
|
end
|
|
if healer == "you" then healer = pn end
|
|
if C.Opt("onlyWhileSourceFights") and not C.SourceFights(e) then
|
|
if C.debug then C.Dbg("drop: " .. e.name .. " is not in combat (option: only while the source fights)") end
|
|
return
|
|
end
|
|
res.healer = healer
|
|
res.mobGuid, res.mobUnit, res.mobName = e.guid, e.unit, e.name
|
|
res.source = e.name
|
|
res.class = e.class
|
|
if AnySCT_SCT and AnySCT_SCT.Show then AnySCT_SCT.Show(res) end
|
|
end
|
|
|
|
-- nampower SPELL_HEAL_BY_OTHER: target, caster, spellId, amount (a crit flag is not known).
|
|
function C.OnHealEvent(targetGuid, casterGuid, spellId, amount, crit)
|
|
local e = state.byGuid[C.NormGuid(targetGuid)]
|
|
if not e then
|
|
local ok, n = pcall(UnitName, targetGuid)
|
|
if ok and type(n) == "string" then e = state.byName[n] end
|
|
end
|
|
if not e then
|
|
if C.debug then C.Dbg("drop: heal target " .. tostring(targetGuid) .. " not listed") end
|
|
return
|
|
end
|
|
local healer
|
|
if casterGuid then
|
|
local ok, n = pcall(UnitName, casterGuid)
|
|
if ok and type(n) == "string" then healer = n end
|
|
end
|
|
local res = AnySCT_DecodeHeal(amount, crit)
|
|
res.spell = spellName(spellId)
|
|
C.EmitHeal(res, e, healer)
|
|
end
|
|
|
|
-- Chat rows of the heal channels: the VICTIM must be a listed source.
|
|
function C.OnHealChat(ev, msg)
|
|
if type(msg) ~= "string" or table.getn(state.order) == 0 then return end
|
|
local named = false
|
|
for i = 1, table.getn(state.order) do
|
|
if string.find(msg, state.order[i], 1, true) then named = true break end
|
|
end
|
|
if not named then return end
|
|
local res = AnySCT_ParseHeal(msg)
|
|
if not res then
|
|
if C.debug then C.Dbg("drop: heal row not parsed: " .. string.sub(msg, 1, 60)) end
|
|
return
|
|
end
|
|
local e = state.byName[res.victim]
|
|
if not e then
|
|
if C.debug then C.Dbg("drop: heal victim '" .. tostring(res.victim) .. "' not listed") end
|
|
return
|
|
end
|
|
if not e.guidNorm then C.RetryGuids() end
|
|
local healer = res.attacker
|
|
local own = (healer == "you") or healer == UnitName("player")
|
|
if e.guidNorm and state.npHeals and not res.periodic and not own then
|
|
if C.debug then C.Dbg("drop: " .. e.name .. " has a GUID, the nampower heal event handles it") end
|
|
return
|
|
end
|
|
C.EmitHeal(res, e, healer)
|
|
end
|
|
|
|
-- Cheap prefilter before any pattern work: the row must START with a targetSCT or pet name (attackers
|
|
-- come first in every row shape we parse), or mention a totem when totems are on.
|
|
local function mentionsSource(msg)
|
|
for i = 1, table.getn(state.order) do
|
|
local nm = state.order[i]
|
|
if string.find(msg, nm, 1, true) == 1 then return true end
|
|
if string.find(msg, state.byName[nm].paren, 1, true) then return true end -- "Petname(Owner)"
|
|
end
|
|
for name in pairs(state.petByName) do
|
|
if string.find(msg, name, 1, true) == 1 then return true end
|
|
end
|
|
if C.Opt("includeTotems") and string.find(msg, "Totem", 1, true) then return true end
|
|
return false
|
|
end
|
|
|
|
function C.OnChat(ev, msg)
|
|
if type(msg) ~= "string" or table.getn(state.order) == 0 then return end
|
|
if not mentionsSource(msg) then
|
|
if C.debug then C.Dbg("drop: no listed source at the start of: " .. string.sub(msg, 1, 60)) end
|
|
return
|
|
end
|
|
local res, spell
|
|
if MELEE_EVENTS[ev] or CREATURE_MELEE[ev] or HOSTILE_MELEE[ev] then
|
|
res = AnySCT_ParseMelee(msg)
|
|
-- a "Name's Spell hits ..." row that arrived on a melee channel is a spell row
|
|
if not res and C.Opt("showSpells") and string.find(msg, "'s ", 1, true) then
|
|
res = AnySCT_ParseSpell(msg)
|
|
spell = res and true or nil
|
|
end
|
|
elseif SPELL_EVENTS[ev] or CREATURE_SPELL[ev] or HOSTILE_SPELL[ev] then
|
|
spell = true
|
|
res = AnySCT_ParseSpell(msg)
|
|
end
|
|
if not res then
|
|
if C.debug then C.Dbg("drop: row not parsed: " .. string.sub(msg, 1, 60)) end
|
|
return
|
|
end
|
|
local e = sourceFromName(res.attacker)
|
|
if not e then
|
|
if C.debug then C.Dbg("drop: attacker '" .. tostring(res.attacker) .. "' not listed") end
|
|
return
|
|
end
|
|
-- a source without a GUID: try to learn it now (rate limited), outside the combat tick
|
|
if not e.guidNorm then C.RetryGuids() end
|
|
-- a nampower source with a known GUID already arrives through its own event; a source without
|
|
-- a GUID can never match those events, so its rows always come from chat
|
|
if e.guidNorm and ((spell and state.npSpells) or (not spell and state.nampower)) then
|
|
if C.debug then C.Dbg("drop: " .. e.name .. " has a GUID, the nampower event path handles it") end
|
|
return
|
|
end
|
|
-- a pet (or totem) of a listed source: remember its name for the optional "Owner (Pet)" suffix
|
|
if res.attacker and res.attacker ~= e.name then
|
|
local _, _, base = string.find(res.attacker, "^(.-)%(.-%)$")
|
|
res.srcPet = base or res.attacker
|
|
end
|
|
res.path = "chat"
|
|
C.Emit(res, e)
|
|
end
|
|
|
|
-- ---- tick ------------------------------------------------------------------------------------
|
|
|
|
-- Runs every 0.25 s, and only while the player is in combat (see the REGEN events below): idle cost
|
|
-- is zero.
|
|
-- Group sources whose GUID could not be read yet (range, nampower without SuperWoW): retry.
|
|
function C.FillGuids()
|
|
local changed = false
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
if e.unit and not e.guidNorm then
|
|
local g = C.ResolveGuid(e.unit)
|
|
if g then
|
|
e.guid = g
|
|
e.guidNorm = C.NormGuid(g)
|
|
state.byGuid[e.guidNorm] = e
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
return changed
|
|
end
|
|
|
|
function C.Tick()
|
|
if C.FillGuids() then C.CheckCreature() end
|
|
C.UpdateSourceTargets()
|
|
end
|
|
|
|
-- ---- sources & cvars -------------------------------------------------------------------------
|
|
|
|
function C.ExtendRange()
|
|
pcall(SetCVar, "CombatLogRangeFriendlyPlayers", "200")
|
|
pcall(SetCVar, "CombatLogRangeParty", "200")
|
|
end
|
|
|
|
function C.ApplyPlateRange()
|
|
local r = C.Opt("plateRange") or 0
|
|
if r > 0 then
|
|
pcall(SetCVar, "NameplateRange", tostring(r))
|
|
pcall(SetCVar, "NP_NameplateDistance", tostring(r))
|
|
end
|
|
end
|
|
|
|
local hinted = false
|
|
function C.PlateHint()
|
|
if hinted then return end
|
|
hinted = true
|
|
local ok, v = pcall(GetCVar, "nameplateShowEnemies")
|
|
if ok and v == "0" then
|
|
C.Print("enemy nameplates are off; turn them on (V key) to see the text float over the mob.")
|
|
end
|
|
end
|
|
|
|
local function setEv(f, ev, on)
|
|
if on then f:RegisterEvent(ev) else f:UnregisterEvent(ev) end
|
|
end
|
|
|
|
-- What the list needs: creature channels (a listed targetSCT, not the player, has a pet or is a
|
|
-- shaman / hunter / warlock), hostile channels (a listed source outside the group), and whether
|
|
-- any such source lacks a GUID (then its melee / spells must come from chat even with nampower).
|
|
function C.Needs()
|
|
local creature, loose, noGuid, friendly = false, false, false, false
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
if not C.IsSelf(e) then
|
|
if not e.unit then loose = true end
|
|
if not e.guidNorm then noGuid = true end
|
|
-- a heal target: a group member, or an outside unit not known to be hostile
|
|
if e.unit or not (e.guid and UnitCanAttack("player", e.guid)) then friendly = true end
|
|
if e.petName or e.class == "HUNTER" or e.class == "WARLOCK" then creature = true end
|
|
if e.class == "SHAMAN" and C.Opt("includeTotems") then creature = true end
|
|
end
|
|
end
|
|
return creature, loose, noGuid, friendly
|
|
end
|
|
|
|
-- Register exactly the events the active sources need and unregister the rest.
|
|
function C.UpdateEvents()
|
|
local f = C.frame
|
|
if not f then return end
|
|
local on = C.Opt("enabled") and true or false
|
|
local np, nps, spells = state.nampower, state.npSpells, C.Opt("showSpells")
|
|
local creature, loose, noGuid, friendly = C.Needs()
|
|
state.needCode = (creature and 1 or 0) + (loose and 2 or 0) + (noGuid and 4 or 0) + (friendly and 8 or 0)
|
|
local heals = on and C.Opt("showHeals") and friendly
|
|
local viaChat = heals and ((not state.npHeals) or noGuid)
|
|
local chatMelee = on and ((not np) or noGuid)
|
|
local chatSpell = on and spells and ((not nps) or noGuid)
|
|
setEv(f, "SPELL_HEAL_BY_OTHER", heals and state.npHeals)
|
|
for ev in pairs(HEAL_EVENTS) do
|
|
-- the player's own heals are not nampower "other" events: SELF_BUFF always comes from chat
|
|
if ev == "CHAT_MSG_SPELL_SELF_BUFF" then
|
|
setEv(f, ev, heals and C.Opt("showOwnHeals"))
|
|
else
|
|
setEv(f, ev, viaChat)
|
|
end
|
|
end
|
|
for ev in pairs(HOT_EVENTS) do setEv(f, ev, heals and C.Opt("showHots")) end
|
|
setEv(f, "AUTO_ATTACK_OTHER", on and np)
|
|
setEv(f, "SPELL_DAMAGE_EVENT_OTHER", on and spells and nps)
|
|
setEv(f, "SPELL_MISS_OTHER", on and spells and nps)
|
|
setEv(f, "UPDATE_MOUSEOVER_UNIT", on and loose)
|
|
setEv(f, "CHAT_MSG_ADDON", on and C.Opt("threatGo"))
|
|
setEv(f, "UNIT_TARGET", on and loose and noGuid) -- target-of-target changes carry no PLAYER_TARGET_CHANGED
|
|
for ev in pairs(MELEE_EVENTS) do setEv(f, ev, chatMelee) end
|
|
for ev in pairs(CREATURE_MELEE) do setEv(f, ev, chatMelee and (creature or loose)) end
|
|
for ev in pairs(HOSTILE_MELEE) do setEv(f, ev, chatMelee and loose) end
|
|
for ev in pairs(SPELL_EVENTS) do setEv(f, ev, chatSpell) end
|
|
for ev in pairs(CREATURE_SPELL) do setEv(f, ev, chatSpell and (creature or loose)) end
|
|
for ev in pairs(HOSTILE_SPELL) do setEv(f, ev, chatSpell and loose) end
|
|
end
|
|
|
|
function C.CheckCreature()
|
|
if not C.frame then return end
|
|
local creature, loose, noGuid, friendly = C.Needs()
|
|
local code = (creature and 1 or 0) + (loose and 2 or 0) + (noGuid and 4 or 0) + (friendly and 8 or 0)
|
|
if code ~= state.needCode then C.UpdateEvents() end
|
|
end
|
|
|
|
-- Detect nampower (CVars present). Auto attacks and spells each fall back to chat on their own.
|
|
function C.SetupSources()
|
|
local ok, v = pcall(GetCVar, "NP_EnableAutoAttackEvents")
|
|
state.nampower = (ok and v ~= nil) and true or false
|
|
if state.nampower then pcall(SetCVar, "NP_EnableAutoAttackEvents", "1") end
|
|
local ok2, v2 = pcall(GetCVar, "NP_EnableSpellDamageEvents")
|
|
state.npSpells = (ok2 and v2 ~= nil) and true or false
|
|
-- newer nampower builds (4.x) deliver SPELL_DAMAGE_EVENT_OTHER / SPELL_MISS_OTHER ungated: no CVar
|
|
-- exists to find, but the version call proves the events do
|
|
local hasVer = type(GetNampowerVersion) == "function"
|
|
if not state.npSpells and state.nampower and hasVer then state.npSpells = true end
|
|
if ok2 and v2 ~= nil then
|
|
pcall(SetCVar, "NP_EnableSpellDamageEvents", "1")
|
|
pcall(SetCVar, "NP_EnableSpellMissEvents", "1")
|
|
end
|
|
local ok3, v3 = pcall(GetCVar, "NP_EnableSpellHealEvents")
|
|
state.npHeals = (ok3 and v3 ~= nil) and true or false
|
|
if state.npHeals then pcall(SetCVar, "NP_EnableSpellHealEvents", "1") end
|
|
C.UpdateEvents()
|
|
return state.nampower
|
|
end
|
|
|
|
-- ---- threat based GO / WAIT (experimental) -------------------------------------------------------
|
|
-- Protocol of the server's threat answers (as used by TWThreat v1.2.3, unverified in this addon):
|
|
-- request SendAddonMessage("TWT_UDTSv4", "limit=10", "RAID" | "PARTY")
|
|
-- answer CHAT_MSG_ADDON arg1 "TWT" (maybe with a trailing space), arg2
|
|
-- "TWTv4=Name:tank:threat:pct:melee;Name:...[hash-separated TMTv1 tail]"
|
|
-- The server pulls aggro at > 110 % (melee) / > 130 % (ranged) of the tank's threat, so the buffer
|
|
-- before the player takes the mob is threat(targetSCT) * f - threat(player).
|
|
|
|
local TH = { lastPkt = -100, lastReq = -100, shown = false, status = nil, buffer = 0, name = nil, packets = 0, requests = 0 }
|
|
C.threat = TH
|
|
local pk = { n = 0, name = {}, tank = {}, threat = {}, melee = {} }
|
|
C.threatPk = pk
|
|
|
|
-- Parse one answer into pk (arrays are reused). True when it is a TWT v4 answer, even an empty one.
|
|
function C.ParseThreat(prefix, msg, out)
|
|
out = out or pk
|
|
out.n = 0
|
|
if type(prefix) ~= "string" or string.sub(prefix, 1, 3) ~= "TWT" then return false end
|
|
if type(msg) ~= "string" then return false end
|
|
local _, e = string.find(msg, "^TWTv4=")
|
|
if not e then return false end
|
|
local hash = string.find(msg, "#", 1, true)
|
|
local body
|
|
if hash then body = string.sub(msg, e + 1, hash - 1) else body = string.sub(msg, e + 1) end
|
|
local pos = 1
|
|
while true do
|
|
local _, f, name, tank, threat, _, melee = string.find(body, "^([^:;]+):(%d+):(%-?%d+):([%-%d%.]+):(%d+)", pos)
|
|
if not f then break end
|
|
local n = out.n + 1
|
|
out.n = n
|
|
out.name[n], out.tank[n] = name, (tank == "1")
|
|
out.threat[n], out.melee[n] = tonumber(threat), (melee == "1")
|
|
pos = f + 1
|
|
if string.sub(body, pos, pos) == ";" then pos = pos + 1 else break end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- status ("GO" | "WAIT"), buffer, targetSCT name for a parsed packet; nil when no enabled targetSCT
|
|
-- is in it. need = required buffer. Several enabled targetSCTs: the one holding aggro; if none does,
|
|
-- WAIT for the first one. A player missing from the packet has threat 0 and counts as melee (f 1.1).
|
|
function C.ThreatDecide(out, myName, need)
|
|
out = out or pk
|
|
local myT, myMelee = 0, nil
|
|
for i = 1, out.n do
|
|
if out.name[i] == myName then myT, myMelee = out.threat[i], out.melee[i] end
|
|
end
|
|
local f = 1.1
|
|
if myMelee == false then f = 1.3 end
|
|
local first, best
|
|
for i = 1, out.n do
|
|
local nm = out.name[i]
|
|
local e = state.byName[nm]
|
|
if e and e.threatOn and nm ~= myName then
|
|
if not first then first = i end
|
|
if out.tank[i] and not best then best = i end
|
|
end
|
|
end
|
|
if not first then return nil end
|
|
local i = best or first
|
|
local buffer = out.threat[i] * f - myT
|
|
if best and buffer >= (need or 0) then return "GO", buffer, out.name[i] end
|
|
return "WAIT", buffer, out.name[i]
|
|
end
|
|
|
|
-- Slider positions 0..75 <-> required buffer 0..7500 in steps of 100.
|
|
local LEAD_MAXPOS = 75
|
|
function C.LeadFromPos(p)
|
|
p = math.floor((p or 0) + 0.5)
|
|
if p < 0 then p = 0 elseif p > LEAD_MAXPOS then p = LEAD_MAXPOS end
|
|
return p * 100
|
|
end
|
|
|
|
-- Fixed-anchor geometry. Frame rects (GetCenter/GetLeft/...) and SetPoint offsets live in one
|
|
-- coordinate system; UIParent:GetWidth()/GetHeight() do NOT (1834x768 against a 2580x1080 rect on a
|
|
-- UI scale of 0.711), so the screen centre must come from UIParent's own rect, never from its size.
|
|
function C.AnchorPosFromCenter(cx, cy, ux, uy)
|
|
return { point = "CENTER", x = math.floor(cx - ux + 0.5), y = math.floor(cy - uy + 0.5) }
|
|
end
|
|
|
|
-- Half extents of the screen as slider limits: rounded up to a multiple of 5, at least 100.
|
|
function C.ScreenHalfRange(left, right, bottom, top)
|
|
local function half(a, b)
|
|
local h = math.ceil(((b or 0) - (a or 0)) / 2 / 5) * 5
|
|
if h < 100 then h = 100 end
|
|
return h
|
|
end
|
|
return half(left, right), half(bottom, top)
|
|
end
|
|
|
|
function C.PosFromLead(v)
|
|
v = v or 0
|
|
if v < 0 then v = 0 end
|
|
local p = math.floor(v / 100 + 0.5)
|
|
if p > LEAD_MAXPOS then p = LEAD_MAXPOS end
|
|
return p
|
|
end
|
|
|
|
-- Does the situation call for threat data at all?
|
|
function C.ThreatWanted()
|
|
if not (C.Opt("enabled") and C.Opt("threatGo")) then return false end
|
|
if GetNumRaidMembers() == 0 and GetNumPartyMembers() == 0 then return false end
|
|
local any = false
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
if e.threatOn and e.unit and not C.IsSelf(e) then any = true break end
|
|
end
|
|
if not any then return false end
|
|
if not UnitExists("target") or not UnitCanAttack("player", "target") or UnitIsDead("target") then return false end
|
|
local cl = UnitClassification and UnitClassification("target")
|
|
if cl ~= "elite" and cl ~= "worldboss" and cl ~= "rareelite" then return false end
|
|
return UnitAffectingCombat("target") and true or false
|
|
end
|
|
|
|
-- Send our own request only when TWThreat (or an earlier request of ours) has not fed us lately.
|
|
function C.ThreatShouldRequest(now)
|
|
if now - TH.lastPkt < 0.9 then return false end
|
|
if now - TH.lastReq < 1.0 then return false end
|
|
return true
|
|
end
|
|
|
|
-- Ring buffer of trace lines, flushed to <WoW>\imports\anysct_threat.txt (SuperWoW's ExportFile appends
|
|
-- .txt) at most every 2 s. Recorded only while /asct debug is on or the option "Record threat trace"
|
|
-- (threatTrace, default off) is set; callers test C.ThreatTracing() first. Nothing goes to chat, and
|
|
-- without ExportFile nothing is written.
|
|
function C.ThreatTracing() return C.debug or C.Opt("threatTrace") end
|
|
local RING = 200
|
|
local ring, ringN, ringHead, ringDirty, lastFlush = {}, 0, 0, false, -10
|
|
function C.ThreatFlush(force)
|
|
if not ringDirty or not ExportFile then return end
|
|
local now = C.Now()
|
|
if not force and now - lastFlush < 2 then return end
|
|
lastFlush, ringDirty = now, false
|
|
local parts, k = {}, 0
|
|
local start = (ringN < RING) and 1 or (math.mod(ringHead, RING) + 1)
|
|
for i = 0, ringN - 1 do
|
|
k = k + 1
|
|
parts[k] = ring[math.mod(start - 1 + i, RING) + 1]
|
|
end
|
|
pcall(ExportFile, "anysct_threat", table.concat(parts, "\n") .. "\n")
|
|
end
|
|
|
|
function C.ThreatLog(line)
|
|
ringHead = math.mod(ringHead, RING) + 1
|
|
ring[ringHead] = string.format("%.2f", C.Now()) .. " " .. line
|
|
if ringN < RING then ringN = ringN + 1 end
|
|
ringDirty = true
|
|
C.ThreatFlush(false)
|
|
end
|
|
|
|
-- Event ledger (same switch as the trace): one line per event of a listed source with the path it came
|
|
-- through (np / npspell / chat), what it was and what became of it (shown / shown+recycled /
|
|
-- dropped:<reason>), flushed to the WoW imports folder as anysct_events.txt. First line = outcome counters.
|
|
local LRING = 600
|
|
local lring, lN, lHead, lDirty, lLast = {}, 0, 0, false, -10
|
|
C.led = {}
|
|
function C.LedgerFlush(force)
|
|
if not lDirty or not ExportFile then return end
|
|
local now = C.Now()
|
|
if not force and now - lLast < 2 then return end
|
|
lLast, lDirty = now, false
|
|
local parts, k = {}, 1
|
|
local head = {}
|
|
for key, n in pairs(C.led) do head[table.getn(head) + 1] = key .. "=" .. n end
|
|
table.sort(head)
|
|
parts[1] = "# " .. table.concat(head, " ")
|
|
local start = (lN < LRING) and 1 or (math.mod(lHead, LRING) + 1)
|
|
for i = 0, lN - 1 do
|
|
k = k + 1
|
|
parts[k] = lring[math.mod(start - 1 + i, LRING) + 1]
|
|
end
|
|
pcall(ExportFile, "anysct_events", table.concat(parts, "\n") .. "\n")
|
|
end
|
|
|
|
function C.Ledger(res, outcome)
|
|
if not C.ThreatTracing() then return end
|
|
C.led[outcome] = (C.led[outcome] or 0) + 1
|
|
lHead = math.mod(lHead, LRING) + 1
|
|
lring[lHead] = string.format("%.2f", C.Now()) .. " " .. tostring(res.path or "?") .. " "
|
|
.. tostring(res.kind) .. " " .. tostring(res.amount or 0) .. " "
|
|
.. tostring(res.spell or res.spellId or "-") .. " " .. tostring(res.mobName or "?")
|
|
.. " " .. outcome
|
|
if lN < LRING then lN = lN + 1 end
|
|
lDirty = true
|
|
C.LedgerFlush(false)
|
|
end
|
|
|
|
-- Name prefix on the label: same never / multi / always rule as the number's suffix line.
|
|
local function threatSrcName(name)
|
|
local m = C.Opt("sourceNameMode")
|
|
if m == "always" or (m == "multi" and table.getn(state.order) > 1) then return name end
|
|
return nil
|
|
end
|
|
|
|
C.ThreatSrcName = threatSrcName
|
|
|
|
local function threatHide()
|
|
if TH.shown then
|
|
TH.shown = false
|
|
TH.status = nil
|
|
if AnySCT_SCT and AnySCT_SCT.SetThreat then AnySCT_SCT.SetThreat(nil) end
|
|
end
|
|
end
|
|
|
|
-- CHAT_MSG_ADDON: TWThreat's answers (and ours) arrive here.
|
|
function C.OnAddon(prefix, msg, channel, sender)
|
|
if not C.Opt("threatGo") then return end
|
|
local isTwt = type(prefix) == "string" and string.sub(prefix, 1, 3) == "TWT"
|
|
if isTwt and C.ThreatTracing() then
|
|
C.ThreatLog("pkt [" .. prefix .. "] " .. tostring(msg) .. " | " .. tostring(channel) .. " | " .. tostring(sender))
|
|
end
|
|
if not C.ParseThreat(prefix, msg, pk) then return end
|
|
local need = C.Opt("threatGoLead") or 1500
|
|
local status, buffer, name = C.ThreatDecide(pk, UnitName("player"), need)
|
|
if C.ThreatTracing() then
|
|
C.ThreatLog("decide " .. tostring(status) .. " buffer=" .. tostring(buffer) .. " need=" .. need
|
|
.. " by=" .. tostring(name) .. " n=" .. pk.n)
|
|
end
|
|
if not status then return end -- nobody of ours in it: not usable
|
|
TH.packets = TH.packets + 1
|
|
TH.lastPkt = C.Now()
|
|
TH.status, TH.buffer, TH.name = status, buffer, name
|
|
TH.shown = true
|
|
if AnySCT_SCT and AnySCT_SCT.SetThreat then AnySCT_SCT.SetThreat(status, buffer, need, threatSrcName(name)) end
|
|
end
|
|
|
|
function C.ThreatSend(now)
|
|
TH.lastReq = now
|
|
TH.requests = TH.requests + 1
|
|
local ch = (GetNumRaidMembers() > 0) and "RAID" or "PARTY"
|
|
local ok = pcall(SendAddonMessage, "TWT_UDTSv4", "limit=10", ch)
|
|
if C.ThreatTracing() then C.ThreatLog("req " .. ch .. " ok=" .. tostring(ok)) end
|
|
end
|
|
|
|
-- One step of the ticker (every ~0.2 s while threat data is wanted).
|
|
function C.ThreatTick()
|
|
if not C.ThreatWanted() then
|
|
threatHide()
|
|
if C.ThreatStop then C.ThreatStop() end
|
|
C.ThreatFlush(true) -- last lines of the fight reach the file
|
|
return
|
|
end
|
|
local now = C.Now()
|
|
if TH.shown and now - TH.lastPkt > 3 then threatHide() end
|
|
if C.ThreatShouldRequest(now) then C.ThreatSend(now) end
|
|
if C.ThreatTracing() then C.ThreatFlush(false) end
|
|
end
|
|
|
|
-- Start / stop the ticker to match the situation (zero idle OnUpdate otherwise).
|
|
function C.ThreatRefresh()
|
|
if C.ThreatWanted() then
|
|
if C.ThreatStart then C.ThreatStart() end
|
|
else
|
|
threatHide()
|
|
if C.ThreatStop then C.ThreatStop() end
|
|
end
|
|
end
|
|
|
|
function C.ThreatStatusLine()
|
|
local age = C.Now() - TH.lastPkt
|
|
return "threat: " .. (C.Opt("threatGo") and "on" or "off") .. ", wanted " .. tostring(C.ThreatWanted())
|
|
.. ", " .. TH.packets .. " usable packets, " .. TH.requests .. " requests, last "
|
|
.. (TH.lastPkt < 0 and "never" or string.format("%.1f s ago", age))
|
|
.. ", " .. tostring(TH.status) .. " " .. string.format("%d", TH.buffer or 0)
|
|
.. " (" .. tostring(TH.name) .. "), need " .. tostring(C.Opt("threatGoLead"))
|
|
end
|
|
|
|
-- ---- slash commands --------------------------------------------------------------------------
|
|
|
|
local HELP = {
|
|
"/anysct (or /asct) commands:",
|
|
" add <name> add a targetSCT (no name: use your target)",
|
|
" remove <name> remove a targetSCT",
|
|
" list show the targetSCT list",
|
|
" clear remove every targetSCT",
|
|
" colours re-assign the targetSCT colours automatically",
|
|
" threat state of the experimental threat GO / WAIT label",
|
|
" place clamp|anchoroff|anchor|both where the text appears",
|
|
" unlock | lock move the fixed anchor",
|
|
" demo show sample text",
|
|
" mem measure memory use (10 s idle, then a demo burst)",
|
|
" debug toggle the event / drop trace in the chat frame",
|
|
" options open the options window",
|
|
" reset reset all options to defaults",
|
|
}
|
|
|
|
-- The spelling an existing list entry or group member has for a typed name (compared lower case
|
|
-- only), or nil. Typed case is kept for names we do not know.
|
|
function C.Canon(n)
|
|
local ln = string.lower(n)
|
|
for i = 1, table.getn(state.order) do
|
|
if string.lower(state.order[i]) == ln then return state.order[i] end
|
|
end
|
|
for name in pairs(state.roster) do
|
|
if string.lower(name) == ln then return name end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local PLACE = { clamp = "clamp", anchoroff = "anchorOff", anchor = "anchor", both = "both" }
|
|
|
|
function C.Slash(msg)
|
|
msg = msg or ""
|
|
local _, _, cmd, rest = string.find(msg, "^%s*(%S*)%s*(.-)%s*$")
|
|
cmd = string.lower(cmd or "")
|
|
rest = rest or ""
|
|
if cmd == "add" or cmd == "tank" then -- "tank" is the old spelling, kept as a silent alias
|
|
local n = rest
|
|
if n == "" and UnitExists("target") and UnitIsPlayer("target") then n = UnitName("target") or "" end
|
|
if n == "" then C.Print("usage: /anysct add <name>") return end
|
|
n = C.Canon(n) or n
|
|
C.AddSource(n, "manual")
|
|
C.Print("targetSCT added: " .. n)
|
|
elseif cmd == "remove" or cmd == "rm" then
|
|
local n = C.Canon(rest) or rest
|
|
if rest ~= "" and C.RemoveSource(n) then C.Print("targetSCT removed: " .. n) else C.Print("not in the list: " .. rest) end
|
|
elseif cmd == "list" then
|
|
if table.getn(state.order) == 0 then C.Print("no targetSCTs") end
|
|
for i = 1, table.getn(state.order) do
|
|
local e = state.byName[state.order[i]]
|
|
C.Print(e.name .. " (" .. (e.src == "main" and "import" or e.src) .. ")")
|
|
end
|
|
elseif cmd == "clear" then
|
|
C.ClearSources()
|
|
C.Print("targetSCT list cleared")
|
|
elseif cmd == "threat" then
|
|
C.Print(C.ThreatStatusLine())
|
|
elseif cmd == "colours" or cmd == "colors" then
|
|
C.AutoColours()
|
|
C.Print("targetSCT colours re-assigned")
|
|
elseif cmd == "place" then
|
|
local p = PLACE[string.lower(rest)]
|
|
if p then C.SetOpt("placement", p) C.Print("placement: " .. p) else C.Print("usage: /anysct place clamp|anchoroff|anchor|both") end
|
|
elseif cmd == "unlock" or cmd == "lock" then
|
|
if AnySCT_SCT and AnySCT_SCT.SetMoveMode then AnySCT_SCT.SetMoveMode(cmd == "unlock") end
|
|
elseif cmd == "demo" then
|
|
if AnySCT_SCT and AnySCT_SCT.Demo then AnySCT_SCT.Demo() end
|
|
elseif cmd == "debug" then
|
|
C.debug = not C.debug
|
|
if not C.debug then C.ThreatFlush(true) end
|
|
C.Print("debug trace " .. (C.debug and "on" or "off"))
|
|
elseif cmd == "mem" then
|
|
if AnySCT_SCT and AnySCT_SCT.Mem then AnySCT_SCT.Mem() end
|
|
elseif cmd == "options" or cmd == "config" then
|
|
if AnySCT_Options and AnySCT_Options.toggle then AnySCT_Options.toggle() end
|
|
elseif cmd == "reset" then
|
|
AnySCTDB.opt = nil
|
|
C.InitDB()
|
|
if AnySCT_SCT and AnySCT_SCT.OptionChanged then AnySCT_SCT.OptionChanged("all") end
|
|
if AnySCT_Minimap and AnySCT_Minimap.Update then AnySCT_Minimap.Update() end
|
|
C.NotifyChanged()
|
|
C.Print("options reset")
|
|
else
|
|
for i = 1, table.getn(HELP) do C.Print(HELP[i]) end
|
|
end
|
|
end
|
|
|
|
-- ---- WoW wiring --------------------------------------------------------------------------------
|
|
|
|
if CreateFrame then
|
|
local f = CreateFrame("Frame", "AnySCTCoreFrame", UIParent)
|
|
C.frame = f
|
|
f:RegisterEvent("ADDON_LOADED")
|
|
f:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
f:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
f:RegisterEvent("RAID_ROSTER_UPDATE")
|
|
f:RegisterEvent("PARTY_MEMBERS_CHANGED")
|
|
f:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
f:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
f:RegisterEvent("UNIT_PET")
|
|
f:RegisterEvent("UNIT_FLAGS")
|
|
|
|
-- combat tick: attached on entering combat, detached on leaving it
|
|
local acc, tickOn = 0, false
|
|
local function tickFn()
|
|
acc = acc + arg1
|
|
if acc < 0.25 then return end
|
|
acc = 0
|
|
C.Tick()
|
|
end
|
|
function C.StartTick()
|
|
if tickOn then return end
|
|
tickOn = true
|
|
acc = 0
|
|
f:SetScript("OnUpdate", tickFn)
|
|
end
|
|
function C.StopTick()
|
|
tickOn = false
|
|
f:SetScript("OnUpdate", nil)
|
|
C.LedgerFlush(true)
|
|
end
|
|
|
|
-- threat ticker: attached only while threat data is wanted (see C.ThreatRefresh)
|
|
local tf = CreateFrame("Frame", "AnySCTThreatFrame", UIParent)
|
|
local tacc, tOn = 0, false
|
|
local function threatFn()
|
|
tacc = tacc + arg1
|
|
if tacc < 0.2 then return end
|
|
tacc = 0
|
|
C.ThreatTick()
|
|
end
|
|
function C.ThreatStart()
|
|
if tOn then return end
|
|
tOn, tacc = true, 0
|
|
tf:SetScript("OnUpdate", threatFn)
|
|
end
|
|
function C.ThreatStop()
|
|
if not tOn then return end
|
|
tOn = false
|
|
tf:SetScript("OnUpdate", nil)
|
|
end
|
|
|
|
local function enterWorld()
|
|
C.RebuildRoster()
|
|
if not state.loaded then
|
|
state.loaded = true
|
|
local list = C.Char().list
|
|
for i = 1, table.getn(list) do C.AddSource(list[i], "manual", true) end
|
|
C.PlateHint()
|
|
if AnySCT_Menu then AnySCT_Menu.Install() end
|
|
end
|
|
C.SetupSources()
|
|
if C.Opt("extendRange") then C.ExtendRange() end
|
|
C.ApplyPlateRange()
|
|
if AnySCT_SCT and AnySCT_SCT.Build then AnySCT_SCT.Build() end
|
|
if AnySCT_Minimap and AnySCT_Minimap.Update then AnySCT_Minimap.Update() end
|
|
if UnitAffectingCombat("player") then C.StartTick() end
|
|
C.NotifyChanged()
|
|
end
|
|
|
|
f:SetScript("OnEvent", function()
|
|
local ev = event
|
|
if C.debug and ev ~= "UNIT_TARGET" and ev ~= "ADDON_LOADED" then
|
|
local a = arg1
|
|
if type(a) == "string" then a = string.sub(a, 1, 80) end
|
|
if arg2 ~= nil and (ev == "AUTO_ATTACK_OTHER" or ev == "SPELL_DAMAGE_EVENT_OTHER" or ev == "SPELL_MISS_OTHER") then
|
|
a = tostring(a) .. " / " .. tostring(arg2)
|
|
end
|
|
C.Dbg("event " .. tostring(ev) .. " " .. tostring(a))
|
|
end
|
|
if ev == "AUTO_ATTACK_OTHER" then
|
|
C.OnAutoAttack(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
|
|
elseif ev == "SPELL_DAMAGE_EVENT_OTHER" then
|
|
C.OnSpellDamage(arg1, arg2, arg3, arg4, arg5, arg6)
|
|
elseif ev == "SPELL_MISS_OTHER" then
|
|
C.OnSpellMiss(arg1, arg2, arg3, arg4)
|
|
elseif ev == "SPELL_HEAL_BY_OTHER" then
|
|
C.OnHealEvent(arg1, arg2, arg3, arg4, arg5)
|
|
elseif HEAL_EVENTS[ev] or HOT_EVENTS[ev] then
|
|
C.OnHealChat(ev, arg1)
|
|
elseif MELEE_EVENTS[ev] or SPELL_EVENTS[ev] or CREATURE_MELEE[ev] or CREATURE_SPELL[ev]
|
|
or HOSTILE_MELEE[ev] or HOSTILE_SPELL[ev] then
|
|
C.OnChat(ev, arg1)
|
|
elseif ev == "CHAT_MSG_ADDON" then
|
|
C.OnAddon(arg1, arg2, arg3, arg4)
|
|
elseif ev == "UNIT_FLAGS" then
|
|
if arg1 == "target" and state.loaded then C.ThreatRefresh() end
|
|
elseif ev == "PLAYER_TARGET_CHANGED" then
|
|
if state.loaded then
|
|
C.ProbeLoose()
|
|
C.ThreatRefresh()
|
|
end
|
|
elseif ev == "UPDATE_MOUSEOVER_UNIT" then
|
|
C.SeeLoose("mouseover")
|
|
elseif ev == "UNIT_TARGET" then
|
|
-- only our own target's target matters, and only while a source still lacks a GUID
|
|
if arg1 == "target" and state.loaded then C.ProbeLoose() end
|
|
elseif ev == "PLAYER_REGEN_DISABLED" then
|
|
if state.loaded and C.Opt("enabled") then C.StartTick() end
|
|
if state.loaded then C.ThreatRefresh() end
|
|
elseif ev == "PLAYER_REGEN_ENABLED" then
|
|
C.StopTick()
|
|
if state.loaded then C.ThreatRefresh() end
|
|
C.ScheduleCacheClear()
|
|
elseif ev == "UNIT_PET" then
|
|
for i = 1, table.getn(state.order) do C.RefreshPet(state.byName[state.order[i]]) end
|
|
C.CheckCreature()
|
|
elseif ev == "RAID_ROSTER_UPDATE" or ev == "PARTY_MEMBERS_CHANGED" then
|
|
if state.loaded then C.RebuildRoster() end
|
|
elseif ev == "PLAYER_ENTERING_WORLD" then
|
|
enterWorld()
|
|
elseif ev == "ADDON_LOADED" and arg1 == "AnySCT" then
|
|
C.InitDB()
|
|
SLASH_ANYSCT1 = "/anysct"
|
|
SLASH_ANYSCT2 = "/asct"
|
|
SlashCmdList["ANYSCT"] = function(m) C.Slash(m) end
|
|
end
|
|
end)
|
|
end
|