initial commit v.2.1.0

This commit is contained in:
Torsten Brendgen
2026-04-24 23:43:55 +02:00
parent 258cadeba5
commit f1d2a761e4
17 changed files with 3252 additions and 3891 deletions

View File

@@ -0,0 +1,524 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
HMGT.TrackerDetection = HMGT.TrackerDetection or {}
local internals = HMGT.TrackerInternals or {}
local GetSpellChargesInfo = internals.GetSpellChargesInfo
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
local GetGlobalCooldownInfo = internals.GetGlobalCooldownInfo
local GetSpellDebugLabel = internals.GetSpellDebugLabel
local BuildCooldownStateFingerprint = internals.BuildCooldownStateFingerprint
local ApplyOwnCooldownReducers = internals.ApplyOwnCooldownReducers
local ApplyObservedCooldownReducers = internals.ApplyObservedCooldownReducers
function HMGT:HandleOwnSpellCast(spellId)
local isInterrupt = HMGT_SpellData.InterruptLookup[spellId] ~= nil
local isCooldown = HMGT_SpellData.CooldownLookup[spellId] ~= nil
if not isInterrupt and not isCooldown then return end
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
or HMGT_SpellData.CooldownLookup[spellId]
spellId = tonumber(spellEntry and spellEntry.spellId) or spellId
local name = self:NormalizePlayerName(UnitName("player"))
local pData = self.playerData[name]
local talents = pData and pData.talents or {}
if self:IsAvailabilitySpell(spellEntry) then
self:LogTrackedSpellCast(name, spellEntry, {
stateKind = "availability",
required = HMGT_SpellData.GetEffectiveAvailabilityRequired(spellEntry, talents),
})
if self:RefreshOwnAvailabilitySpell(spellEntry) then
self:PublishOwnSpellState(spellId, { sendLegacy = true })
end
self:TriggerTrackerUpdate()
return
end
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
local now = GetTime()
local inCombat = InCombatLockdown and InCombatLockdown()
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
if not inCombat and GetSpellChargesInfo then
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
end
local cachedMaxCharges, cachedChargeDuration = self:GetKnownChargeInfo(
spellEntry,
talents,
spellId,
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
)
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
spellEntry,
talents,
(not inCombat and tonumber(max) and tonumber(max) > 0) and tonumber(max) or ((cachedMaxCharges > 0) and cachedMaxCharges or nil),
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration)
or ((cachedChargeDuration > 0) and cachedChargeDuration or effectiveCd)
)
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
local currentCharges = 0
local maxCharges = 0
local chargeDur = 0
local chargeStartTime = nil
local startTime = now
local duration = effectiveCd
local expiresIn = effectiveCd
local existingCd = self:GetActiveCooldown(name, spellId)
if existingCd and (tonumber(existingCd.maxCharges) or 0) > 0 then
self:ResolveChargeState(existingCd, now)
end
if hasCharges then
maxCharges = math.max(1, tonumber(max) or cachedMaxCharges or tonumber(inferredMaxCharges) or 1)
currentCharges = tonumber(cur)
if currentCharges == nil then
local prevCharges = existingCd and tonumber(existingCd.currentCharges)
local prevMax = existingCd and tonumber(existingCd.maxCharges)
if prevCharges and prevMax and prevMax == maxCharges then
currentCharges = math.max(0, prevCharges - 1)
else
currentCharges = math.max(0, maxCharges - 1)
end
end
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
chargeDur = tonumber(chargeDuration)
or cachedChargeDuration
or tonumber(inferredChargeDuration)
or tonumber(effectiveCd)
or 0
chargeDur = math.max(0, chargeDur)
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
if currentCharges < maxCharges and chargeDur > 0 then
chargeStartTime = tonumber(chargeStart) or now
local missing = maxCharges - currentCharges
startTime = chargeStartTime
duration = missing * chargeDur
expiresIn = math.max(0, duration - (now - startTime))
else
startTime = now
duration = 0
expiresIn = 0
end
end
self:Debug(
"verbose",
"HandleOwnSpellCast name=%s spellId=%s cd=%.2f charges=%s/%s",
tostring(name),
tostring(spellId),
tonumber(effectiveCd) or 0,
hasCharges and tostring(currentCharges) or "-",
hasCharges and tostring(maxCharges) or "-"
)
self._cdNonce = (self._cdNonce or 0) + 1
local nonce = self._cdNonce
self:SetActiveCooldown(name, spellId, {
startTime = startTime,
duration = duration,
spellEntry = spellEntry,
currentCharges = hasCharges and currentCharges or nil,
maxCharges = hasCharges and maxCharges or nil,
chargeStart = hasCharges and chargeStartTime or nil,
chargeDuration = hasCharges and chargeDur or nil,
_nonce = nonce,
})
self:LogTrackedSpellCast(name, spellEntry, {
cooldown = effectiveCd,
currentCharges = hasCharges and currentCharges or nil,
maxCharges = hasCharges and maxCharges or nil,
chargeCooldown = hasCharges and chargeDur or nil,
})
if expiresIn > 0 then
self:ScheduleTimer(function()
local current = self:GetActiveCooldown(name, spellId)
if current and current._nonce == nonce then
self:ClearActiveCooldown(name, spellId)
self:PublishOwnSpellState(spellId)
self:TriggerTrackerUpdate()
end
end, expiresIn)
end
self:PublishOwnSpellState(spellId, { sendLegacy = true })
self:TriggerTrackerUpdate()
end
function HMGT:RefreshOwnCooldownStateFromGame(spellId)
local sid = tonumber(spellId)
if not sid then return false end
if InCombatLockdown and InCombatLockdown() then
return false
end
local ownName = self:NormalizePlayerName(UnitName("player"))
if not ownName then return false end
local spellEntry = HMGT_SpellData.InterruptLookup[sid]
or HMGT_SpellData.CooldownLookup[sid]
if not spellEntry or self:IsAvailabilitySpell(spellEntry) then
return false
end
sid = tonumber(spellEntry.spellId) or sid
local existing = self:GetActiveCooldown(ownName, sid)
local before = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(existing) or "nil"
local now = GetTime()
local pData = self.playerData[ownName]
local talents = pData and pData.talents or {}
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
if GetSpellChargesInfo then
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(sid)
end
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
spellEntry,
talents,
(tonumber(max) and tonumber(max) > 0) and tonumber(max) or nil,
(tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
)
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
if hasCharges then
local maxCharges = math.max(1, tonumber(max) or tonumber(inferredMaxCharges) or 1)
local currentCharges = tonumber(cur)
if currentCharges == nil then
currentCharges = maxCharges
end
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
local chargeDur = tonumber(chargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
chargeDur = math.max(0, chargeDur)
if currentCharges < maxCharges and chargeDur > 0 then
local chargeStartTime = tonumber(chargeStart) or now
local missing = maxCharges - currentCharges
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
startTime = chargeStartTime,
duration = missing * chargeDur,
spellEntry = spellEntry,
currentCharges = currentCharges,
maxCharges = maxCharges,
chargeStart = chargeStartTime,
chargeDuration = chargeDur,
})
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
else
self:ClearActiveCooldown(ownName, sid)
end
else
local cooldownStart, cooldownDuration = 0, 0
if GetSpellCooldownInfo then
cooldownStart, cooldownDuration = GetSpellCooldownInfo(sid)
end
cooldownStart = tonumber(cooldownStart) or 0
cooldownDuration = tonumber(cooldownDuration) or 0
local gcdStart, gcdDuration = 0, 0
if GetGlobalCooldownInfo then
gcdStart, gcdDuration = GetGlobalCooldownInfo()
end
gcdStart = tonumber(gcdStart) or 0
gcdDuration = tonumber(gcdDuration) or 0
local existingDuration = tonumber(existing and existing.duration) or 0
local existingStart = tonumber(existing and existing.startTime) or now
local existingRemaining = math.max(0, existingDuration - (now - existingStart))
local isLikelyGlobalCooldown = cooldownDuration > 0
and gcdDuration > 0
and math.abs(cooldownDuration - gcdDuration) <= 0.15
and (tonumber(effectiveCd) or 0) > (gcdDuration + 1.0)
local isSuspiciousShortRefresh = cooldownDuration > 0
and existingRemaining > 2.0
and existingDuration > 2.0
and cooldownDuration < math.max(2.0, existingDuration * 0.35)
and cooldownDuration < math.max(2.0, (tonumber(effectiveCd) or 0) * 0.35)
if isLikelyGlobalCooldown or isSuspiciousShortRefresh then
self:DebugScoped(
"verbose",
"TrackedSpells",
"Ignore suspicious refresh for %s: spellCD=%.3f gcd=%.3f existing=%.3f remaining=%.3f effective=%.3f",
GetSpellDebugLabel and GetSpellDebugLabel(sid) or tostring(sid),
cooldownDuration,
gcdDuration,
existingDuration,
existingRemaining,
tonumber(effectiveCd) or 0
)
return false
end
if cooldownDuration > 0 then
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
startTime = cooldownStart,
duration = cooldownDuration,
spellEntry = spellEntry,
})
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
else
self:ClearActiveCooldown(ownName, sid)
end
end
local after = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(self:GetActiveCooldown(ownName, sid)) or "nil"
return before ~= after
end
function HMGT:DidOwnInterruptSucceed(triggerSpellId, talents)
local sid = tonumber(triggerSpellId)
if not sid then return false end
local spellEntry = HMGT_SpellData and HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
if not spellEntry then return false end
sid = tonumber(spellEntry.spellId) or sid
local observedDuration = 0
if GetSpellCooldownInfo then
local _, duration = GetSpellCooldownInfo(sid)
observedDuration = duration
end
observedDuration = tonumber(observedDuration) or 0
if observedDuration <= 0 then return false end
local expectedDuration = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
expectedDuration = tonumber(expectedDuration) or 0
if expectedDuration <= 0 then return false end
return observedDuration < (expectedDuration - 0.05)
end
function HMGT:HandleOwnCooldownReductionTrigger(triggerSpellId)
local ownName = self:NormalizePlayerName(UnitName("player"))
if not ownName then return end
local pData = self.playerData[ownName]
local classToken = pData and pData.class or select(2, UnitClass("player"))
local specIndex = pData and pData.specIndex or GetSpecialization()
local talents = pData and pData.talents or {}
if not classToken or not specIndex then return end
local reducers = HMGT_SpellData.GetCooldownReducersForCast(classToken, specIndex, triggerSpellId, talents)
if not reducers or #reducers == 0 then return end
local instantReducers = {}
local observedInstantReducers = {}
local successReducers = {}
local observedSuccessReducers = {}
for _, reducer in ipairs(reducers) do
local observed = type(reducer.observe) == "table"
if reducer.requireInterruptSuccess then
if observed then
observedSuccessReducers[#observedSuccessReducers + 1] = reducer
else
successReducers[#successReducers + 1] = reducer
end
else
if observed then
observedInstantReducers[#observedInstantReducers + 1] = reducer
else
instantReducers[#instantReducers + 1] = reducer
end
end
end
local castTs = GetServerTime()
if #instantReducers > 0 and ApplyOwnCooldownReducers then
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, instantReducers, castTs)
end
if #observedInstantReducers > 0 and ApplyObservedCooldownReducers then
ApplyObservedCooldownReducers(self, ownName, observedInstantReducers)
end
if #successReducers > 0 or #observedSuccessReducers > 0 then
local function ApplySuccessReducers()
if not self:DidOwnInterruptSucceed(triggerSpellId, talents) then
return false
end
if #successReducers > 0 and ApplyOwnCooldownReducers then
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, successReducers, castTs)
end
if #observedSuccessReducers > 0 and ApplyObservedCooldownReducers then
ApplyObservedCooldownReducers(self, ownName, observedSuccessReducers)
end
return true
end
if not ApplySuccessReducers() then
C_Timer.After(0.12, function()
if not self or not self.playerData or not self.playerData[ownName] then
return
end
ApplySuccessReducers()
end)
end
end
end
function HMGT:HandleRemoteSpellCast(playerName, spellId, castTimestamp, curCharges, maxCharges, chargeRemaining, chargeDuration)
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
or HMGT_SpellData.CooldownLookup[spellId]
if not spellEntry then return end
spellId = tonumber(spellEntry.spellId) or spellId
if self:IsAvailabilitySpell(spellEntry) then return end
local pData = self.playerData[playerName]
local talents = pData and pData.talents or {}
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
castTimestamp = tonumber(castTimestamp) or GetServerTime()
local existingEntry = self:GetActiveCooldown(playerName, spellId)
if (tonumber(maxCharges) or 0) <= 0 and existingEntry and existingEntry.lastCastTimestamp then
local prevTs = tonumber(existingEntry.lastCastTimestamp) or 0
if math.abs(prevTs - castTimestamp) <= 1 then
return
end
end
local now = GetTime()
local elapsed = math.max(0, GetServerTime() - castTimestamp)
local incomingCur = tonumber(curCharges) or 0
local incomingMax = tonumber(maxCharges) or 0
local incomingChargeRemaining = tonumber(chargeRemaining) or 0
local incomingChargeDuration = tonumber(chargeDuration) or 0
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
spellEntry,
talents,
(incomingMax > 0) and incomingMax or nil,
(incomingChargeDuration > 0) and incomingChargeDuration or effectiveCd
)
local hasCharges = (incomingMax > 1) or (tonumber(inferredMaxCharges) or 0) > 1
local currentCharges = 0
local maxChargeCount = 0
local chargeDur = 0
local nextChargeRemaining = 0
local chargeStartTime = nil
local startTime, duration, expiresIn
if hasCharges then
maxChargeCount = math.max(1, (incomingMax > 0 and incomingMax) or tonumber(inferredMaxCharges) or 1)
chargeDur = tonumber(incomingChargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
chargeDur = math.max(0, chargeDur)
if chargeDur <= 0 then
chargeDur = math.max(0, tonumber(effectiveCd) or 0)
end
if incomingMax > 0 then
currentCharges = math.max(0, math.min(maxChargeCount, incomingCur))
nextChargeRemaining = math.max(0, math.min(chargeDur, incomingChargeRemaining - elapsed))
if currentCharges < maxChargeCount and chargeDur > 0 then
chargeStartTime = now - math.max(0, chargeDur - nextChargeRemaining)
end
else
local existing = self:GetActiveCooldown(playerName, spellId)
if existing and (tonumber(existing.maxCharges) or 0) == maxChargeCount then
self:ResolveChargeState(existing, now)
local prevCharges = tonumber(existing.currentCharges) or maxChargeCount
local prevStart = tonumber(existing.chargeStart)
local prevDur = tonumber(existing.chargeDuration) or chargeDur
if prevDur > 0 then
chargeDur = prevDur
end
currentCharges = math.max(0, prevCharges - 1)
if currentCharges < maxChargeCount and chargeDur > 0 then
if prevCharges >= maxChargeCount then
chargeStartTime = now
else
chargeStartTime = prevStart or now
end
nextChargeRemaining = math.max(0, chargeDur - (now - chargeStartTime))
end
else
currentCharges = math.max(0, maxChargeCount - 1)
if currentCharges < maxChargeCount and chargeDur > 0 then
chargeStartTime = now
nextChargeRemaining = chargeDur
end
end
end
if currentCharges >= maxChargeCount and maxChargeCount > 0 then
currentCharges = math.max(0, maxChargeCount - 1)
if chargeDur > 0 then
chargeStartTime = now
nextChargeRemaining = chargeDur
end
end
if currentCharges < maxChargeCount and chargeDur > 0 then
chargeStartTime = chargeStartTime or now
local missing = maxChargeCount - currentCharges
startTime = chargeStartTime
duration = missing * chargeDur
expiresIn = math.max(0, duration - (now - startTime))
else
startTime = now
duration = 0
expiresIn = 0
end
else
local remaining = effectiveCd - elapsed
if remaining <= 0 then return end
startTime = now - elapsed
duration = effectiveCd
expiresIn = remaining
end
self:Debug(
"verbose",
"HandleRemoteSpellCast name=%s spellId=%s elapsed=%.2f expiresIn=%.2f charges=%s/%s",
tostring(playerName),
tostring(spellId),
tonumber(elapsed) or 0,
tonumber(expiresIn) or 0,
hasCharges and tostring(currentCharges) or "-",
hasCharges and tostring(maxChargeCount) or "-"
)
self._cdNonce = (self._cdNonce or 0) + 1
local nonce = self._cdNonce
self:SetActiveCooldown(playerName, spellId, {
startTime = startTime,
duration = duration,
spellEntry = spellEntry,
currentCharges = hasCharges and currentCharges or nil,
maxCharges = hasCharges and maxChargeCount or nil,
chargeStart = hasCharges and chargeStartTime or nil,
chargeDuration = hasCharges and chargeDur or nil,
lastCastTimestamp = castTimestamp,
_nonce = nonce,
})
self:LogTrackedSpellCast(playerName, spellEntry, {
cooldown = effectiveCd,
currentCharges = hasCharges and currentCharges or nil,
maxCharges = hasCharges and maxChargeCount or nil,
chargeCooldown = hasCharges and chargeDur or nil,
})
if expiresIn > 0 then
self:ScheduleTimer(function()
local current = self:GetActiveCooldown(playerName, spellId)
if current and current._nonce == nonce then
self:ClearActiveCooldown(playerName, spellId)
self:TriggerTrackerUpdate()
end
end, expiresIn)
end
self:TriggerTrackerUpdate()
end