Files
HailMaryGuildTools/Modules/Tracker/TrackerState.lua
2026-04-24 23:43:55 +02:00

411 lines
13 KiB
Lua

local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
HMGT.TrackerState = HMGT.TrackerState or {}
function HMGT:EnsureTrackerStateTables()
self.playerData = self.playerData or {}
self.activeCDs = self.activeCDs or {}
self.availabilityStates = self.availabilityStates or {}
self.localSpellStateRevisions = self.localSpellStateRevisions or {}
self.remoteSpellStateRevisions = self.remoteSpellStateRevisions or {}
self.knownChargeInfo = self.knownChargeInfo or {}
end
function HMGT:ResetTrackerState()
self.playerData = {}
self.activeCDs = {}
self.availabilityStates = {}
self.localSpellStateRevisions = {}
self.remoteSpellStateRevisions = {}
self.knownChargeInfo = {}
end
function HMGT:GetPlayerCooldownMap(playerName, create)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName then
return nil
end
self:EnsureTrackerStateTables()
if create then
self.activeCDs[normalizedName] = self.activeCDs[normalizedName] or {}
end
return self.activeCDs[normalizedName]
end
function HMGT:GetAvailabilityStateMap(playerName, create)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName then
return nil
end
self:EnsureTrackerStateTables()
if create then
self.availabilityStates[normalizedName] = self.availabilityStates[normalizedName] or {}
end
return self.availabilityStates[normalizedName]
end
function HMGT:GetAvailabilityStateEntry(playerName, spellId)
local sid = tonumber(spellId)
local states = self:GetAvailabilityStateMap(playerName, false)
return states and sid and states[sid] or nil
end
function HMGT:SetAvailabilityStateEntry(playerName, spellId, stateData)
local sid = tonumber(spellId)
if not sid or sid <= 0 or type(stateData) ~= "table" then
return nil
end
local states = self:GetAvailabilityStateMap(playerName, true)
if not states then
return nil
end
states[sid] = stateData
return stateData
end
function HMGT:ClearAvailabilityState(playerName, spellId)
local sid = tonumber(spellId)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName or not sid or sid <= 0 then
return false
end
local states = self.availabilityStates and self.availabilityStates[normalizedName]
if not states or not states[sid] then
return false
end
states[sid] = nil
if not next(states) then
self.availabilityStates[normalizedName] = nil
end
return true
end
function HMGT:GetActiveCooldown(playerName, spellId)
local sid = tonumber(spellId)
local cooldowns = self:GetPlayerCooldownMap(playerName, false)
return cooldowns and sid and cooldowns[sid] or nil
end
function HMGT:SetActiveCooldown(playerName, spellId, cdData)
local sid = tonumber(spellId)
if not sid or sid <= 0 or type(cdData) ~= "table" then
return nil
end
local cooldowns = self:GetPlayerCooldownMap(playerName, true)
if not cooldowns then
return nil
end
cooldowns[sid] = cdData
return cdData
end
function HMGT:ClearActiveCooldown(playerName, spellId)
local sid = tonumber(spellId)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName or not sid or sid <= 0 then
return false
end
local cooldowns = self.activeCDs and self.activeCDs[normalizedName]
if not cooldowns or not cooldowns[sid] then
return false
end
cooldowns[sid] = nil
if not next(cooldowns) then
self.activeCDs[normalizedName] = nil
end
return true
end
function HMGT:ClearPlayerCooldowns(playerName)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName then
return false
end
if self.activeCDs and self.activeCDs[normalizedName] then
self.activeCDs[normalizedName] = nil
return true
end
return false
end
function HMGT:GetLocalSpellStateRevision(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then
return 0
end
self:EnsureTrackerStateTables()
return tonumber(self.localSpellStateRevisions[sid]) or 0
end
function HMGT:EnsureLocalSpellStateRevision(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then
return 0
end
self:EnsureTrackerStateTables()
local current = tonumber(self.localSpellStateRevisions[sid]) or 0
if current <= 0 then
current = 1
self.localSpellStateRevisions[sid] = current
end
return current
end
function HMGT:NextLocalSpellStateRevision(spellId)
local sid = tonumber(spellId)
if not sid or sid <= 0 then
return 0
end
self:EnsureTrackerStateTables()
local nextRevision = (tonumber(self.localSpellStateRevisions[sid]) or 0) + 1
self.localSpellStateRevisions[sid] = nextRevision
return nextRevision
end
function HMGT:GetRemoteSpellStateRevision(playerName, spellId)
local normalizedName = self:NormalizePlayerName(playerName)
local sid = tonumber(spellId)
local bySpell = normalizedName and self.remoteSpellStateRevisions[normalizedName]
return tonumber(bySpell and bySpell[sid]) or 0
end
function HMGT:SetRemoteSpellStateRevision(playerName, spellId, revision)
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
end
self:EnsureTrackerStateTables()
self.remoteSpellStateRevisions[normalizedName] = self.remoteSpellStateRevisions[normalizedName] or {}
self.remoteSpellStateRevisions[normalizedName][sid] = rev
end
function HMGT:ClearRemoteSpellStateRevisions(playerName)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName then
return false
end
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
self.remoteSpellStateRevisions[normalizedName] = nil
return true
end
return false
end
function HMGT:ClearTrackerStateForPlayer(playerName)
local normalizedName = self:NormalizePlayerName(playerName)
if not normalizedName then
return false
end
local changed = false
if self.activeCDs and self.activeCDs[normalizedName] then
self.activeCDs[normalizedName] = nil
changed = true
end
if self.availabilityStates and self.availabilityStates[normalizedName] then
self.availabilityStates[normalizedName] = nil
changed = true
end
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
self.remoteSpellStateRevisions[normalizedName] = nil
changed = true
end
return changed
end
function HMGT:StoreKnownChargeInfo(spellId, maxCharges, chargeDuration)
local sid = tonumber(spellId)
local maxCount = tonumber(maxCharges)
if not sid or sid <= 0 or not maxCount or maxCount <= 1 then
return
end
self:EnsureTrackerStateTables()
self.knownChargeInfo[sid] = {
maxCharges = math.max(1, math.floor(maxCount + 0.5)),
chargeDuration = math.max(0, tonumber(chargeDuration) or 0),
updatedAt = GetTime(),
}
end
function HMGT:GetKnownChargeInfo(spellEntry, talents, spellId, fallbackChargeDuration)
local sid = tonumber(spellId or (spellEntry and spellEntry.spellId))
if not sid or sid <= 0 then
return 0, 0
end
local cached = self.knownChargeInfo and self.knownChargeInfo[sid]
local cachedMax = tonumber(cached and cached.maxCharges) or 0
local cachedDuration = tonumber(cached and cached.chargeDuration) or 0
local inferredMax, inferredDuration = HMGT_SpellData.GetEffectiveChargeInfo(
spellEntry,
talents or {},
(cachedMax > 0) and cachedMax or nil,
(cachedDuration > 0) and cachedDuration or fallbackChargeDuration
)
local maxCharges = math.max(cachedMax, tonumber(inferredMax) or 0)
local chargeDuration = math.max(
tonumber(inferredDuration) or 0,
cachedDuration,
tonumber(fallbackChargeDuration) or 0
)
if maxCharges > 1 then
self:StoreKnownChargeInfo(sid, maxCharges, chargeDuration)
end
return maxCharges, chargeDuration
end
function HMGT:PruneAvailabilityStates(playerName, knownSpells)
local normalizedName = self:NormalizePlayerName(playerName)
local states = normalizedName and self.availabilityStates[normalizedName]
if not states or type(knownSpells) ~= "table" then
return false
end
local changed = false
for sid in pairs(states) do
if not knownSpells[tonumber(sid)] then
states[sid] = nil
changed = true
end
end
if not next(states) then
self.availabilityStates[normalizedName] = nil
end
return changed
end
function HMGT:ResolveChargeState(cdData, now)
if type(cdData) ~= "table" then
return 0, 0, 0, 0
end
now = tonumber(now) or GetTime()
local maxCharges = math.max(0, tonumber(cdData.maxCharges) or 0)
local currentCharges = math.max(0, tonumber(cdData.currentCharges) or 0)
local chargeDuration = math.max(0, tonumber(cdData.chargeDuration) or 0)
local chargeStart = tonumber(cdData.chargeStart)
if maxCharges <= 0 then
return 0, chargeDuration, currentCharges, maxCharges
end
if currentCharges >= maxCharges or chargeDuration <= 0 or not chargeStart then
return 0, chargeDuration, math.min(currentCharges, maxCharges), maxCharges
end
local elapsed = math.max(0, now - chargeStart)
local gainedCharges = math.floor(elapsed / chargeDuration)
local remaining = chargeDuration - (elapsed % chargeDuration)
if gainedCharges > 0 then
currentCharges = math.min(maxCharges, currentCharges + gainedCharges)
if currentCharges >= maxCharges then
currentCharges = maxCharges
chargeStart = nil
remaining = 0
else
chargeStart = now - (elapsed % chargeDuration)
end
cdData.currentCharges = currentCharges
cdData.chargeStart = chargeStart
if currentCharges >= maxCharges then
cdData.startTime = now
cdData.duration = 0
else
local missing = maxCharges - currentCharges
cdData.startTime = chargeStart
cdData.duration = missing * chargeDuration
end
end
if currentCharges >= maxCharges then
return 0, chargeDuration, currentCharges, maxCharges
end
return math.max(0, remaining), chargeDuration, currentCharges, maxCharges
end
function HMGT:RefreshCooldownExpiryTimer(playerName, spellId, cdData)
if not cdData then return 0 end
local now = GetTime()
local duration = tonumber(cdData.duration) or 0
local startTime = tonumber(cdData.startTime) or now
local expiresIn = math.max(0, duration - (now - startTime))
self._cdNonce = (self._cdNonce or 0) + 1
local nonce = self._cdNonce
cdData._nonce = nonce
if expiresIn > 0 then
self:ScheduleTimer(function()
local current = self:GetActiveCooldown(playerName, spellId)
if current and current._nonce == nonce then
self:ClearActiveCooldown(playerName, spellId)
if playerName == self:NormalizePlayerName(UnitName("player")) then
self:PublishOwnSpellState(spellId)
end
self:TriggerTrackerUpdate()
end
end, expiresIn)
end
return expiresIn
end
function HMGT:CleanupStaleCooldowns()
local now = GetTime()
local ownName = self:NormalizePlayerName(UnitName("player"))
local removed = 0
for playerName, spells in pairs(self.activeCDs) do
for spellId, cdInfo in pairs(spells) do
local duration = tonumber(cdInfo.duration) or 0
local startTime = tonumber(cdInfo.startTime) or now
local rem = duration - (now - startTime)
local hasCharges = (tonumber(cdInfo.maxCharges) or 0) > 0
local currentCharges = tonumber(cdInfo.currentCharges) or 0
local maxCharges = tonumber(cdInfo.maxCharges) or 0
if hasCharges then
local _, _, cur, max = self:ResolveChargeState(cdInfo, now)
currentCharges = cur
maxCharges = max
end
local shouldDrop = false
if hasCharges then
if currentCharges >= maxCharges then
shouldDrop = true
elseif (tonumber(cdInfo.chargeDuration) or 0) <= 0 and rem <= -2 then
shouldDrop = true
end
elseif rem <= -2 then
shouldDrop = true
end
if shouldDrop then
spells[spellId] = nil
if playerName == ownName then
self:PublishOwnSpellState(spellId)
end
removed = removed + 1
end
end
if not next(spells) then
self.activeCDs[playerName] = nil
end
end
if removed > 0 then
self:Debug("verbose", "CleanupStaleCooldowns removed=%d", removed)
end
end