411 lines
13 KiB
Lua
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
|