Files
HailMaryGuildTools/Modules/Tracker/TrackerSync.lua

1042 lines
40 KiB
Lua

local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
HMGT.TrackerSync = HMGT.TrackerSync or {}
local internals = HMGT.TrackerInternals or {}
local GetSpellChargesInfo = internals.GetSpellChargesInfo
local GetSpellDebugLabel = internals.GetSpellDebugLabel
local MSG_SPELL_CAST = HMGT.MSG_SPELL_CAST
local MSG_CD_REDUCE = HMGT.MSG_CD_REDUCE
local MSG_SPELL_STATE = HMGT.MSG_SPELL_STATE
local MSG_HELLO = HMGT.MSG_HELLO
local MSG_PLAYER_INFO = HMGT.MSG_PLAYER_INFO
local MSG_SYNC_REQUEST = HMGT.MSG_SYNC_REQUEST
local MSG_SYNC_RESPONSE = HMGT.MSG_SYNC_RESPONSE
local MSG_RELIABLE = HMGT.MSG_RELIABLE
local MSG_ACK = HMGT.MSG_ACK
local COMM_PREFIX = HMGT.COMM_PREFIX
local ADDON_VERSION = HMGT.ADDON_VERSION or "dev"
local PROTOCOL_VERSION = HMGT.PROTOCOL_VERSION or 0
function HMGT:SuppressRemoteTrackedSpellLogs(playerName, duration)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName then
return
end
self._suppressTrackedSpellLogUntil = self._suppressTrackedSpellLogUntil or {}
self._suppressTrackedSpellLogUntil[normalizedName] = GetTime() + math.max(0, tonumber(duration) or 0)
end
function HMGT:IsRemoteTrackedSpellLogSuppressed(playerName)
local normalizedName = self:NormalizePlayerName(playerName)
local suppression = self._suppressTrackedSpellLogUntil
local untilTime = suppression and suppression[normalizedName]
if not untilTime then
return false
end
if untilTime <= GetTime() then
suppression[normalizedName] = nil
return false
end
return true
end
function HMGT:BuildClearSpellStateSnapshot(spellId, spellEntry)
return {
spellId = tonumber(spellId),
spellEntry = spellEntry,
kind = "clear",
a = 0,
b = 0,
c = 0,
d = 0,
}
end
function HMGT:GetOwnSpellStateSnapshot(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then return nil end
local spellEntry = HMGT_SpellData.InterruptLookup[sid]
or HMGT_SpellData.CooldownLookup[sid]
if not spellEntry then return nil end
if self:IsAvailabilitySpell(spellEntry) then
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
if (tonumber(max) or 0) > 0 then
self:StoreAvailabilityState(self:NormalizePlayerName(UnitName("player")), sid, current, max, spellEntry)
return {
spellId = sid,
spellEntry = spellEntry,
kind = "availability",
a = tonumber(current) or 0,
b = tonumber(max) or 0,
c = 0,
d = 0,
}
end
return self:BuildClearSpellStateSnapshot(sid, spellEntry)
end
local ownName = self:NormalizePlayerName(UnitName("player"))
local pData = ownName and self.playerData and self.playerData[ownName]
local talents = pData and pData.talents or {}
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
local knownMaxCharges, knownChargeDuration = self:GetKnownChargeInfo(spellEntry, talents, sid, effectiveCd)
local cdData = ownName and self:GetActiveCooldown(ownName, sid)
if cdData then
if (tonumber(cdData.maxCharges) or 0) > 0 then
local nextRemaining, chargeDuration, charges, maxCharges = self:ResolveChargeState(cdData)
self:StoreKnownChargeInfo(sid, maxCharges, chargeDuration)
if (tonumber(maxCharges) or 0) > 0 and (tonumber(charges) or 0) < (tonumber(maxCharges) or 0) then
return {
spellId = sid,
spellEntry = spellEntry,
kind = "charges",
a = tonumber(charges) or 0,
b = tonumber(maxCharges) or 0,
c = tonumber(nextRemaining) or 0,
d = tonumber(chargeDuration) or 0,
}
end
elseif knownMaxCharges > 1 then
local duration = tonumber(cdData.duration) or 0
local startTime = tonumber(cdData.startTime) or GetTime()
local remaining = math.max(0, duration - (GetTime() - startTime))
local currentCharges = knownMaxCharges
if remaining > 0 then
currentCharges = math.max(0, knownMaxCharges - 1)
return {
spellId = sid,
spellEntry = spellEntry,
kind = "charges",
a = tonumber(currentCharges) or 0,
b = tonumber(knownMaxCharges) or 0,
c = tonumber(remaining) or 0,
d = tonumber(knownChargeDuration) or tonumber(effectiveCd) or duration,
}
end
else
local duration = tonumber(cdData.duration) or 0
local startTime = tonumber(cdData.startTime) or GetTime()
local remaining = math.max(0, duration - (GetTime() - startTime))
if duration > 0 and remaining > 0 then
return {
spellId = sid,
spellEntry = spellEntry,
kind = "cooldown",
a = remaining,
b = duration,
c = 0,
d = 0,
}
end
end
end
if InCombatLockdown and InCombatLockdown() then
if knownMaxCharges > 1 then
return {
spellId = sid,
spellEntry = spellEntry,
kind = "charges",
a = tonumber(knownMaxCharges) or 0,
b = tonumber(knownMaxCharges) or 0,
c = 0,
d = tonumber(knownChargeDuration) or tonumber(effectiveCd) or 0,
}
end
return self:BuildClearSpellStateSnapshot(sid, spellEntry)
end
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(ownName, sid)
if (tonumber(maxCharges) or 0) > 0 then
local cur = math.max(0, math.floor((tonumber(currentCharges) or 0) + 0.5))
local max = math.max(0, math.floor((tonumber(maxCharges) or 0) + 0.5))
local nextRemaining = math.max(0, tonumber(remaining) or 0)
local chargeDuration = math.max(0, tonumber(total) or 0)
if max <= 0 or cur >= max then
return self:BuildClearSpellStateSnapshot(sid, spellEntry)
end
return {
spellId = sid,
spellEntry = spellEntry,
kind = "charges",
a = cur,
b = max,
c = nextRemaining,
d = chargeDuration,
}
end
local duration = math.max(0, tonumber(total) or 0)
local cooldownRemaining = math.max(0, tonumber(remaining) or 0)
if duration <= 0 or cooldownRemaining <= 0 then
return self:BuildClearSpellStateSnapshot(sid, spellEntry)
end
return {
spellId = sid,
spellEntry = spellEntry,
kind = "cooldown",
a = cooldownRemaining,
b = duration,
c = 0,
d = 0,
}
end
function HMGT:SendSpellStateSnapshot(snapshot, target, revision)
if type(snapshot) ~= "table" then return false end
local sid = tonumber(snapshot.spellId)
local kind = tostring(snapshot.kind or "")
local rev = tonumber(revision) or 0
if not sid or sid <= 0 or kind == "" or rev <= 0 then
return false
end
self:DebugScoped(
"verbose",
"TrackerSync",
"SendSpellStateSnapshot target=%s spell=%s kind=%s rev=%d a=%.3f b=%.3f c=%.3f d=%.3f",
tostring(target and target ~= "" and target or "GROUP"),
GetSpellDebugLabel and GetSpellDebugLabel(sid) or tostring(sid),
tostring(kind),
rev,
tonumber(snapshot.a) or 0,
tonumber(snapshot.b) or 0,
tonumber(snapshot.c) or 0,
tonumber(snapshot.d) or 0
)
local payload = string.format(
"%s|%d|%s|%d|%.3f|%.3f|%.3f|%.3f|%s|%d",
MSG_SPELL_STATE,
sid,
kind,
rev,
tonumber(snapshot.a) or 0,
tonumber(snapshot.b) or 0,
tonumber(snapshot.c) or 0,
tonumber(snapshot.d) or 0,
ADDON_VERSION,
PROTOCOL_VERSION
)
if target and target ~= "" then
self:SendDirectMessage(payload, target, "ALERT")
else
self:SendGroupMessage(payload, "ALERT")
end
return true
end
function HMGT:PublishOwnSpellState(spellId, opts)
opts = opts or {}
local sid = tonumber(spellId)
if not sid or sid <= 0 then return false end
local snapshot = opts.snapshot or self:GetOwnSpellStateSnapshot(sid)
if not snapshot then return false end
local revision = tonumber(opts.revision) or self:NextLocalSpellStateRevision(sid)
local sent = self:SendSpellStateSnapshot(snapshot, opts.target, revision)
if not sent then
return false
end
if opts.sendLegacy then
if snapshot.kind == "availability" then
self:BroadcastAvailabilityState(sid, snapshot.a, snapshot.b, opts.target)
elseif snapshot.kind ~= "clear" then
self:BroadcastSpellCast(sid, snapshot)
end
end
return true
end
function HMGT:SendOwnTrackedSpellStates(target)
local ownName = self:NormalizePlayerName(UnitName("player"))
if not ownName then return 0 end
self:RefreshOwnAvailabilityStates()
local sent = 0
local sentBySpell = {}
local activeStates = self:GetPlayerCooldownMap(ownName, false)
if type(activeStates) == "table" then
for sid in pairs(activeStates) do
sid = tonumber(sid)
if sid and sid > 0 and not sentBySpell[sid] then
local revision = self:EnsureLocalSpellStateRevision(sid)
if revision > 0 and self:SendSpellStateSnapshot(self:GetOwnSpellStateSnapshot(sid), target, revision) then
sent = sent + 1
sentBySpell[sid] = true
end
end
end
end
local availabilityStates = self:GetAvailabilityStateMap(ownName, false)
if type(availabilityStates) == "table" then
for sid in pairs(availabilityStates) do
sid = tonumber(sid)
if sid and sid > 0 and not sentBySpell[sid] then
local revision = self:EnsureLocalSpellStateRevision(sid)
if revision > 0 and self:SendSpellStateSnapshot(self:GetOwnSpellStateSnapshot(sid), target, revision) then
sent = sent + 1
sentBySpell[sid] = true
end
end
end
end
return sent
end
function HMGT:BroadcastRepairSpellStates()
if not self:IsEnabled() then return end
local sent = self:SendOwnTrackedSpellStates()
if sent > 0 then
self:DebugScoped("verbose", "TrackerSync", "RepairSpellStates sent=%d", sent)
end
end
function HMGT:ReconcileOwnTrackedSpellStatesFromGame(publishChanges)
if InCombatLockdown and InCombatLockdown() then
return 0
end
local ownName = self:NormalizePlayerName(UnitName("player"))
local pData = ownName and self.playerData and self.playerData[ownName]
if not ownName or not pData or not pData.class or not pData.specIndex then
return 0
end
pData.knownSpells = self:CollectOwnAvailableTrackerSpells(pData.class, pData.specIndex)
local changed = 0
for sid in pairs(pData.knownSpells or {}) do
local spellEntry = HMGT_SpellData.InterruptLookup[sid]
or HMGT_SpellData.CooldownLookup[sid]
if spellEntry and not self:IsAvailabilitySpell(spellEntry) then
if self:RefreshOwnCooldownStateFromGame(sid) then
changed = changed + 1
if publishChanges then
self:PublishOwnSpellState(sid, { sendLegacy = true })
end
end
end
end
if changed > 0 then
self:TriggerTrackerUpdate()
end
return changed
end
function HMGT:SendHello(target)
local name = self:NormalizePlayerName(UnitName("player"))
local pData = self.playerData[name]
if not pData or not pData.class or not pData.specIndex then return end
pData.knownSpells = self:CollectOwnAvailableTrackerSpells(pData.class, pData.specIndex)
self:RefreshOwnAvailabilityStates()
local knownSpellList = self:SerializeKnownSpellList(pData.knownSpells)
local knownCount = 0
for _ in pairs(pData.knownSpells or {}) do
knownCount = knownCount + 1
end
local payload = string.format("%s|%s|%d|%s|%d|%s|%s",
MSG_HELLO,
ADDON_VERSION,
PROTOCOL_VERSION,
pData.class,
pData.specIndex,
pData.talentHash or "",
knownSpellList
)
if target and target ~= "" then
self:DebugScoped("verbose", "Comm", "SendHello whisper target=%s class=%s spec=%s spells=%d",
tostring(target), tostring(pData.class), tostring(pData.specIndex), knownCount)
self:SendDirectMessage(payload, target)
self:SendOwnTrackedSpellStates(target)
self:SendOwnAvailabilityStates(target)
return
end
self:DebugScoped("verbose", "Comm", "SendHello group class=%s spec=%s spells=%d",
tostring(pData.class), tostring(pData.specIndex), knownCount)
self:SendGroupMessage(payload)
self:SendOwnTrackedSpellStates()
self:SendOwnAvailabilityStates()
end
function HMGT:BroadcastSpellCast(spellId, snapshot)
local cur, max, chargeRemaining, chargeDuration = 0, 0, 0, 0
if type(snapshot) == "table" and tostring(snapshot.kind) == "charges" then
cur = math.max(0, math.floor((tonumber(snapshot.a) or 0) + 0.5))
max = math.max(0, math.floor((tonumber(snapshot.b) or 0) + 0.5))
chargeRemaining = math.max(0, tonumber(snapshot.c) or 0)
chargeDuration = math.max(0, tonumber(snapshot.d) or 0)
elseif not (InCombatLockdown and InCombatLockdown()) and GetSpellChargesInfo then
local c, m, cs, cd = GetSpellChargesInfo(spellId)
cur = tonumber(c) or 0
max = tonumber(m) or 0
chargeDuration = tonumber(cd) or 0
if max > 0 and cur < max and cs and chargeDuration > 0 then
chargeRemaining = math.max(0, chargeDuration - (GetTime() - cs))
end
else
local ownName = self:NormalizePlayerName(UnitName("player"))
local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(ownName, spellId, {
deferChargeCooldownUntilEmpty = false,
})
cur = math.max(0, math.floor((tonumber(currentCharges) or 0) + 0.5))
max = math.max(0, math.floor((tonumber(maxCharges) or 0) + 0.5))
chargeRemaining = math.max(0, tonumber(remaining) or 0)
chargeDuration = math.max(0, tonumber(total) or 0)
end
self:DebugScoped("verbose", "TrackerSync", "BroadcastSpellCast spell=%s serverTime=%s charges=%d/%d",
GetSpellDebugLabel and GetSpellDebugLabel(spellId) or tostring(spellId),
tostring(GetServerTime()),
cur,
max)
self:SendGroupMessage(string.format("%s|%d|%d|%d|%d|%.3f|%.3f|%s|%d",
MSG_SPELL_CAST, spellId, GetServerTime(), cur, max, chargeRemaining, chargeDuration, ADDON_VERSION, PROTOCOL_VERSION))
end
function HMGT:BroadcastCooldownReduce(targetSpellId, amount, castTimestamp, triggerSpellId)
local sid = tonumber(targetSpellId)
local value = tonumber(amount) or 0
if not sid or sid <= 0 or value <= 0 then return end
local ts = tonumber(castTimestamp) or GetServerTime()
local triggerId = tonumber(triggerSpellId) or 0
self:Debug(
"verbose",
"BroadcastCooldownReduce target=%s amount=%.2f ts=%s trigger=%s",
tostring(sid),
value,
tostring(ts),
tostring(triggerId)
)
self:SendGroupMessage(string.format(
"%s|%d|%.3f|%d|%d|%s|%d",
MSG_CD_REDUCE,
sid,
value,
ts,
triggerId,
ADDON_VERSION,
PROTOCOL_VERSION
))
end
function HMGT:RequestSync(reason)
self:DebugScoped("info", "Comm", "RequestSync(%s)", tostring(reason or "Hello"))
self:SendHello()
end
function HMGT:QueueSyncRequest(delay, reason)
local wait = tonumber(delay) or 0.2
if wait < 0 then wait = 0 end
if self._syncRequestTimer then
return
end
self._syncRequestTimer = self:ScheduleTimer(function()
self._syncRequestTimer = nil
self:RequestSync(reason or "Hello")
end, wait)
end
function HMGT:QueueDeltaSyncBurst(reason, delays)
if not (IsInGroup() or IsInRaid()) then
return
end
local now = GetTime()
local normalizedReason = tostring(reason or "delta")
self._deltaSyncBurstAt = self._deltaSyncBurstAt or {}
if (tonumber(self._deltaSyncBurstAt[normalizedReason]) or 0) > now - 2.5 then
return
end
self._deltaSyncBurstAt[normalizedReason] = now
delays = type(delays) == "table" and delays or { 0.35, 1.25, 2.75 }
self._syncBurstTimers = self._syncBurstTimers or {}
for _, wait in ipairs(delays) do
local delay = math.max(0, tonumber(wait) or 0)
local timerHandle
timerHandle = self:ScheduleTimer(function()
if self._syncBurstTimers then
for index, handle in ipairs(self._syncBurstTimers) do
if handle == timerHandle then
table.remove(self._syncBurstTimers, index)
break
end
end
end
self:RequestSync(normalizedReason)
end, delay)
self._syncBurstTimers[#self._syncBurstTimers + 1] = timerHandle
end
self:DebugScoped("info", "Comm", "QueueDeltaSyncBurst reason=%s count=%d", normalizedReason, #delays)
end
function HMGT:SendSyncResponse(target)
local name = self:NormalizePlayerName(UnitName("player"))
local pData = self.playerData[name]
if not pData then return end
pData.knownSpells = self:CollectOwnAvailableTrackerSpells(pData.class, pData.specIndex)
self:RefreshOwnAvailabilityStates()
local knownSpellList = self:SerializeKnownSpellList(pData.knownSpells)
local cdList = {}
local now = GetTime()
local ownCooldowns = self:GetPlayerCooldownMap(name, false)
if ownCooldowns then
for spellId, cdInfo in pairs(ownCooldowns) do
if (tonumber(cdInfo.maxCharges) or 0) > 0 then
self:ResolveChargeState(cdInfo, now)
end
local remaining = cdInfo.duration - (now - cdInfo.startTime)
remaining = math.max(0, math.min(cdInfo.duration, remaining))
if remaining > 0 then
table.insert(cdList, string.format("%d:%.3f:%.3f:%d:%d",
spellId, remaining, cdInfo.duration, cdInfo.currentCharges or 0, cdInfo.maxCharges or 0))
end
end
end
self:SendDirectMessage(
string.format("%s|%s|%d|%s|%d|%s|%s|%s",
MSG_SYNC_RESPONSE,
ADDON_VERSION,
PROTOCOL_VERSION,
pData.class,
pData.specIndex,
pData.talentHash or "",
knownSpellList,
table.concat(cdList, ";")),
target)
local stateCount = self:SendOwnTrackedSpellStates(target)
local availabilityCount = self:SendOwnAvailabilityStates(target)
self:DebugScoped("verbose", "Comm", "SendSyncResponse target=%s entries=%d state=%d availability=%d", tostring(target), #cdList, stateCount, availabilityCount)
end
function HMGT:StoreRemotePlayerInfo(playerName, class, specIndex, talentHash, knownSpellList)
if not playerName or not class then return end
local previous = self.playerData[playerName]
local knownSpells = previous and previous.knownSpells
if knownSpellList ~= nil then
knownSpells = self:ParseKnownSpellList(knownSpellList)
end
self.playerData[playerName] = {
class = class,
specIndex = tonumber(specIndex),
talentHash = talentHash,
talents = self:ParseTalentHash(talentHash),
knownSpells = knownSpells,
}
if type(knownSpells) == "table" then
self:PruneAvailabilityStates(playerName, knownSpells)
end
local knownCount = 0
if type(knownSpells) == "table" then
for _ in pairs(knownSpells) do
knownCount = knownCount + 1
end
end
self:DebugScoped(
"info",
"TrackerSync",
"Spielerinfo von %s: class=%s spec=%s bekannteSpells=%d",
tostring(playerName),
tostring(class),
tostring(specIndex),
knownCount
)
end
function HMGT:GetClassTokenForSpecId(specId)
local sid = tonumber(specId)
if not sid or sid <= 0 then
return nil
end
if type(GetSpecializationInfoByID) == "function" then
local returns = { pcall(GetSpecializationInfoByID, sid) }
local ok = returns[1]
local classToken = returns[7]
if ok and type(classToken) == "string" and classToken ~= "" then
return classToken
end
end
if type(GetSpecializationInfoForClassID) ~= "function" then
return nil
end
for classID = 1, 20 do
local _, token = GetClassInfo(classID)
if token then
local count = 4
if type(GetNumSpecializationsForClassID) == "function" then
count = tonumber(GetNumSpecializationsForClassID(classID)) or 4
end
for index = 1, math.max(1, count) do
local foundSpecId = GetSpecializationInfoForClassID(classID, index)
if tonumber(foundSpecId) == sid then
return token
end
end
end
end
return nil
end
function HMGT:ClearRemoteSpellState(playerName, spellId)
local normalizedName = self:NormalizePlayerName(playerName)
local sid = tonumber(spellId)
if not normalizedName or not sid or sid <= 0 then
return false
end
local changed = false
if self:ClearActiveCooldown(normalizedName, sid) then
changed = true
end
if self:ClearAvailabilityState(normalizedName, sid) then
changed = true
end
return changed
end
function HMGT:ApplyRemoteSpellState(playerName, spellId, kind, revision, a, b, c, d)
local normalizedName = self:NormalizePlayerName(playerName)
local sid = tonumber(spellId)
local rev = tonumber(revision) or 0
if not normalizedName or not sid or sid <= 0 or rev <= 0 then
return false
end
if not self:IsPlayerInCurrentGroup(normalizedName) then
return false
end
local currentRevision = self:GetRemoteSpellStateRevision(normalizedName, sid)
if currentRevision >= rev then
return false
end
local spellEntry = HMGT_SpellData.CooldownLookup[sid]
or HMGT_SpellData.InterruptLookup[sid]
if not spellEntry then
return false
end
sid = tonumber(spellEntry.spellId) or sid
local now = GetTime()
local stateKind = tostring(kind or "")
local changed = false
local shouldLogCast = false
local logDetails = nil
local previousEntry = self:GetActiveCooldown(normalizedName, sid)
local isSuppressed = self:IsRemoteTrackedSpellLogSuppressed(normalizedName)
if stateKind == "clear" then
changed = self:ClearRemoteSpellState(normalizedName, sid)
elseif stateKind == "availability" then
changed = self:StoreAvailabilityState(normalizedName, sid, tonumber(a) or 0, tonumber(b) or 0, spellEntry)
if self:ClearActiveCooldown(normalizedName, sid) then
changed = true
end
elseif stateKind == "cooldown" then
local duration = math.max(0, tonumber(b) or 0)
local remaining = math.max(0, math.min(duration, tonumber(a) or 0))
if duration <= 0 or remaining <= 0 then
changed = self:ClearRemoteSpellState(normalizedName, sid)
else
local previousRemaining = 0
if previousEntry then
previousRemaining = math.max(
0,
(tonumber(previousEntry.duration) or 0) - (now - (tonumber(previousEntry.startTime) or now))
)
end
self:SetActiveCooldown(normalizedName, sid, {
startTime = now - (duration - remaining),
duration = duration,
spellEntry = spellEntry,
_stateRevision = rev,
_stateKind = stateKind,
})
changed = true
shouldLogCast = (not isSuppressed) and previousRemaining <= 0.05
if shouldLogCast then
logDetails = {
cooldown = duration,
}
end
end
elseif stateKind == "charges" then
local maxCharges = math.max(0, math.floor((tonumber(b) or 0) + 0.5))
local currentCharges = math.max(0, math.min(maxCharges, math.floor((tonumber(a) or 0) + 0.5)))
local nextRemaining = math.max(0, tonumber(c) or 0)
local chargeDuration = math.max(0, tonumber(d) or 0)
if maxCharges <= 0 or currentCharges >= maxCharges then
changed = self:ClearRemoteSpellState(normalizedName, sid)
else
local previousCharges = nil
if previousEntry and (tonumber(previousEntry.maxCharges) or 0) > 0 then
self:ResolveChargeState(previousEntry, now)
previousCharges = tonumber(previousEntry.currentCharges)
end
local chargeStart = nil
local duration = 0
local startTime = now
if chargeDuration > 0 then
nextRemaining = math.min(chargeDuration, nextRemaining)
chargeStart = now - math.max(0, chargeDuration - nextRemaining)
duration = (maxCharges - currentCharges) * chargeDuration
startTime = chargeStart
end
self:SetActiveCooldown(normalizedName, sid, {
startTime = startTime,
duration = duration,
spellEntry = spellEntry,
currentCharges = currentCharges,
maxCharges = maxCharges,
chargeStart = chargeStart,
chargeDuration = chargeDuration,
_stateRevision = rev,
_stateKind = stateKind,
})
changed = true
shouldLogCast = (not isSuppressed)
and (
(previousCharges ~= nil and currentCharges < previousCharges)
or (previousCharges == nil)
)
if shouldLogCast then
logDetails = {
cooldown = chargeDuration,
currentCharges = currentCharges,
maxCharges = maxCharges,
chargeCooldown = chargeDuration,
}
end
end
else
return false
end
self:SetRemoteSpellStateRevision(normalizedName, sid, rev)
if changed then
self:DebugScoped(
"info",
"TrackerSync",
"Sync von %s: %s -> %s (rev=%d)",
tostring(normalizedName),
GetSpellDebugLabel and GetSpellDebugLabel(sid) or tostring(sid),
tostring(stateKind),
rev
)
end
if changed and shouldLogCast and logDetails then
self:LogTrackedSpellCast(normalizedName, spellEntry, logDetails)
end
return changed
end
function HMGT:OnCommReceived(prefix, message, distribution, sender)
if prefix ~= COMM_PREFIX then return end
local senderName = self:NormalizePlayerName(sender)
if senderName == self:NormalizePlayerName(UnitName("player")) then return end
local msgType = message:match("^(%a+)")
self:DebugScoped("verbose", "Comm", "OnCommReceived type=%s from=%s dist=%s", tostring(msgType), tostring(senderName), tostring(distribution))
if msgType == MSG_ACK then
local messageId = message:match("^%a+|(.+)$")
if messageId then
self:HandleReliableAck(senderName, messageId)
end
return
elseif msgType == MSG_RELIABLE then
local messageId, innerPayload = message:match("^%a+|([^|]+)|(.+)$")
if not messageId or not innerPayload then
return
end
local dedupeKey = string.format("%s|%s", tostring(senderName or ""), tostring(messageId))
self.receivedReliableMessages = self.receivedReliableMessages or {}
self:SendReliableAck(sender, messageId)
if self.receivedReliableMessages[dedupeKey] then
self:DebugScoped("verbose", "Comm", "Reliable duplicate sender=%s id=%s", tostring(senderName), tostring(messageId))
return
end
self.receivedReliableMessages[dedupeKey] = GetTime() + 30
message = innerPayload
msgType = message:match("^(%a+)")
self:DebugScoped("verbose", "Comm", "Reliable recv sender=%s id=%s inner=%s", tostring(senderName), tostring(messageId), tostring(msgType))
end
if msgType == MSG_SPELL_CAST then
local spellId, timestamp, cur, max, chargeRemaining, chargeDuration, version, protocol =
message:match("^%a+|(%d+)|([%d%.]+)|(%d+)|(%d+)|([%d%.]+)|([%d%.]+)|([^|]+)|(%d+)$")
if not spellId then
spellId, timestamp, version = message:match("^%a+|(%d+)|([%d%.]+)|(.+)$")
if not spellId then
spellId, timestamp = message:match("^%a+|(%d+)|([%d%.]+)$")
end
end
if spellId then
self:RegisterPeerVersion(senderName, version, protocol, "SC")
self:RememberPeerProtocolVersion(senderName, protocol)
if (tonumber(protocol) or 0) >= 5 then
return
end
self:DebugScoped("verbose", "TrackerSync", "Legacy cast von %s: %s ts=%s",
tostring(senderName),
GetSpellDebugLabel and GetSpellDebugLabel(spellId) or tostring(spellId),
tostring(timestamp))
self:HandleRemoteSpellCast(
senderName,
tonumber(spellId),
tonumber(timestamp),
tonumber(cur) or 0,
tonumber(max) or 0,
tonumber(chargeRemaining) or 0,
tonumber(chargeDuration) or 0
)
end
elseif msgType == MSG_CD_REDUCE then
local targetSpellId, amount, timestamp, triggerSpellId, version, protocol =
message:match("^%a+|(%d+)|([%d%.]+)|([%d%.]+)|(%d+)|([^|]+)|(%d+)$")
if not targetSpellId then
targetSpellId, amount, timestamp, triggerSpellId =
message:match("^%a+|(%d+)|([%d%.]+)|([%d%.]+)|(%d+)$")
end
if targetSpellId then
self:RegisterPeerVersion(senderName, version, protocol, "CR")
self:RememberPeerProtocolVersion(senderName, protocol)
if (tonumber(protocol) or 0) >= 5 then
return
end
self:HandleRemoteCooldownReduce(
senderName,
tonumber(targetSpellId),
tonumber(amount) or 0,
tonumber(timestamp),
tonumber(triggerSpellId) or 0
)
end
elseif msgType == MSG_SPELL_STATE then
local spellId, stateKind, revision, a, b, c, d, version, protocol =
message:match("^%a+|(%d+)|(%a+)|(%d+)|([%d%.%-]+)|([%d%.%-]+)|([%d%.%-]+)|([%d%.%-]+)|([^|]+)|(%d+)$")
if spellId then
self:RegisterPeerVersion(senderName, version, protocol, "STA")
self:RememberPeerProtocolVersion(senderName, protocol)
if self:ApplyRemoteSpellState(senderName, spellId, stateKind, revision, a, b, c, d) then
self:TriggerTrackerUpdate()
end
else
local current, max
spellId, current, max, version, protocol =
message:match("^%a+|(%d+)|(%d+)|(%d+)|([^|]+)|(%d+)$")
if not spellId then
spellId, current, max = message:match("^%a+|(%d+)|(%d+)|(%d+)$")
end
if spellId then
self:RegisterPeerVersion(senderName, version, protocol, "STA")
self:RememberPeerProtocolVersion(senderName, protocol)
if (tonumber(protocol) or 0) >= 5 then
return
end
local sid = tonumber(spellId)
local spellEntry = HMGT_SpellData.CooldownLookup[sid]
or HMGT_SpellData.InterruptLookup[sid]
if self:IsAvailabilitySpell(spellEntry) then
if self:StoreAvailabilityState(senderName, sid, tonumber(current) or 0, tonumber(max) or 0, spellEntry) then
self:TriggerTrackerUpdate()
end
end
end
end
elseif msgType == MSG_HELLO then
local version, protocol, class, specIndex, talentHash, knownSpellList =
message:match("^%a+|([^|]+)|(%d+)|(%u+)|(%d+)|(.-)|(.-)$")
if class then
self:RegisterPeerVersion(senderName, version, protocol, "HEL")
self:RememberPeerProtocolVersion(senderName, protocol)
self:ClearRemoteSpellStateRevisions(senderName)
self:StoreRemotePlayerInfo(senderName, class, specIndex, talentHash, knownSpellList)
self:DebugScoped("info", "TrackerSync", "Hello von %s: class=%s spec=%s spells=%s",
tostring(senderName), tostring(class), tostring(specIndex), tostring(knownSpellList or ""))
self:SendSyncResponse(sender)
self:TriggerTrackerUpdate()
end
elseif msgType == MSG_PLAYER_INFO then
local class, specIndex, talentHash, version, protocol =
message:match("^%a+|(%u+)|(%d+)|(.-)|([^|]+)|(%d+)$")
if not class then
class, specIndex, talentHash = message:match("^%a+|(%u+)|(%d+)|(.*)")
end
if class then
self:RegisterPeerVersion(senderName, version, protocol, "PI")
self:RememberPeerProtocolVersion(senderName, protocol)
self:StoreRemotePlayerInfo(senderName, class, specIndex, talentHash, nil)
self:TriggerTrackerUpdate()
end
elseif msgType == MSG_SYNC_REQUEST then
local version, protocol, class, specIndex, talentHash, knownSpellList =
message:match("^%a+|([^|]+)|(%d+)|(%u+)|(%d+)|(.-)|(.-)$")
if class then
self:StoreRemotePlayerInfo(senderName, class, specIndex, talentHash, knownSpellList)
end
if not version then
version, protocol = message:match("^%a+|([^|]+)|(%d+)$")
end
if not version then
version = message:match("^%a+|(.+)$")
end
self:RegisterPeerVersion(senderName, version, protocol, "SRQ")
self:RememberPeerProtocolVersion(senderName, protocol)
self:DebugScoped("info", "Comm", "SyncRequest von %s", tostring(senderName))
self:SendSyncResponse(sender)
self:TriggerTrackerUpdate()
elseif msgType == MSG_SYNC_RESPONSE then
local version, protocol, class, specIndex, talentHash, knownSpellList, cdListStr =
message:match("^%a+|([^|]+)|(%d+)|(%u+)|(%d+)|(.-)|(.-)|(.-)$")
if not class then
version, protocol, class, specIndex, talentHash, cdListStr =
message:match("^%a+|([^|]+)|(%d+)|(%u+)|(%d+)|(.-)|(.-)$")
end
if not class then
class, specIndex, talentHash, cdListStr =
message:match("^%a+|(%u+)|(%d+)|(.-)|(.-)$")
end
if class then
self:RegisterPeerVersion(senderName, version, protocol, "SRS")
self:RememberPeerProtocolVersion(senderName, protocol)
self:SuppressRemoteTrackedSpellLogs(senderName, 1.5)
self:StoreRemotePlayerInfo(senderName, class, specIndex, talentHash, knownSpellList)
if cdListStr and cdListStr ~= "" then
local knownTalents = self.playerData[senderName] and self.playerData[senderName].talents or {}
local applied = 0
for entry in cdListStr:gmatch("([^;]+)") do
local sid, rem, dur, cur, max = entry:match("(%d+):([%d%.]+):([%d%.]+):(%d+):(%d+)")
if not sid then
sid, rem, dur = entry:match("(%d+):([%d%.]+):([%d%.]+)")
end
if sid then
sid, rem, dur = tonumber(sid), tonumber(rem), tonumber(dur)
rem = math.max(0, math.min(dur, rem))
local remaining = rem
if remaining > 0 then
local spellEntry = HMGT_SpellData.CooldownLookup[sid]
or HMGT_SpellData.InterruptLookup[sid]
if spellEntry then
local localStartTime = GetTime() - (dur - remaining)
local curCharges = tonumber(cur) or 0
local maxChargeCount = tonumber(max) or 0
local chargeStart = nil
local chargeDur = nil
if maxChargeCount > 0 then
curCharges = math.max(0, math.min(maxChargeCount, curCharges))
local missing = maxChargeCount - curCharges
if missing > 0 and dur > 0 then
chargeDur = dur / missing
chargeStart = localStartTime
end
else
local inferredMax, inferredDur = HMGT_SpellData.GetEffectiveChargeInfo(
spellEntry,
knownTalents,
nil,
HMGT_SpellData.GetEffectiveCooldown(spellEntry, knownTalents)
)
if (tonumber(inferredMax) or 0) > 1 then
maxChargeCount = inferredMax
curCharges = math.max(0, inferredMax - 1)
chargeDur = inferredDur
chargeStart = localStartTime
end
end
self:SetActiveCooldown(senderName, sid, {
startTime = localStartTime,
duration = dur,
spellEntry = spellEntry,
currentCharges = (maxChargeCount > 0) and curCharges or nil,
maxCharges = (maxChargeCount > 0) and maxChargeCount or nil,
chargeStart = chargeStart,
chargeDuration = chargeDur,
})
applied = applied + 1
end
end
end
end
self:DebugScoped("info", "TrackerSync", "SyncResponse von %s: cdsApplied=%d", tostring(senderName), applied)
end
self:TriggerTrackerUpdate()
end
elseif msgType == HMGT.MSG_RAID_TIMELINE then
local encounterId, timeSec, spellId, leadTime, alertText =
message:match("^%a+|(%d+)|(%d+)|([%-]?%d+)|(%d+)|(.*)$")
if not encounterId then
encounterId, timeSec, spellId, leadTime =
message:match("^%a+|(%d+)|(%d+)|([%-]?%d+)|(%d+)$")
alertText = ""
end
if encounterId and HMGT.RaidTimeline and HMGT.RaidTimeline.HandleAssignmentComm then
HMGT.RaidTimeline:HandleAssignmentComm(
senderName,
tonumber(encounterId),
tonumber(timeSec),
tonumber(spellId),
tonumber(leadTime),
alertText
)
end
elseif msgType == HMGT.MSG_RAID_TIMELINE_TEST then
local encounterId, difficultyId, serverStartTime, duration =
message:match("^%a+|(%d+)|(%d+)|(%d+)|(%d+)$")
if encounterId and HMGT.RaidTimeline and HMGT.RaidTimeline.HandleTestStartComm then
HMGT.RaidTimeline:HandleTestStartComm(
senderName,
tonumber(encounterId),
tonumber(difficultyId),
tonumber(serverStartTime),
tonumber(duration)
)
end
end
end