Files
coa-altoholic/DataStore_Characters/DataStore_Characters.lua
T
florian.berthold b332499098
release / release (push) Successful in 5s
coa.3: guard XP getters in DataStore_Characters for partial records
GetXPRate / GetRestXPRate did raw arithmetic on character.XP / .XPMax /
.lastLogoutTimestamp, which are nil for own alts seen via guild comm but
never fully scanned. Crashed AccountSummary on Vol'jin - CoA Beta.
2026-05-28 22:40:04 +02:00

361 lines
12 KiB
Lua

--[[ *** DataStore_Characters ***
Written by : Thaoky, EU-Marécages de Zangar
July 18th, 2009
--]]
if not DataStore then return end
local addonName = "DataStore_Characters"
_G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0")
local addon = _G[addonName]
local THIS_ACCOUNT = "Default"
local AddonDB_Defaults = {
global = {
Characters = {
['*'] = { -- ["Account.Realm.Name"]
-- ** General Stuff **
lastUpdate = nil, -- last time this char was updated. Set at logon & logout
name = nil, -- to simplify processing a bit, the name is saved in the table too, in addition to being part of the key
level = nil,
race = nil,
englishRace = nil,
class = nil,
englishClass = nil, -- "WARRIOR", "DRUID" .. english & caps, regardless of locale
faction = nil,
gender = nil, -- UnitSex
lastLogoutTimestamp = nil,
money = nil,
played = 0, -- /played, in seconds
zone = nil, -- character location
subZone = nil,
-- ** XP **
XP = nil, -- current level xp
XPMax = nil, -- max xp at current level
RestXP = nil,
isResting = nil, -- nil = out of an inn
-- ** Guild **
guildName = nil, -- nil = not in a guild, as returned by GetGuildInfo("player")
guildRankName = nil,
guildRankIndex = nil,
}
}
}
}
-- *** Scanning functions ***
local function ScanPlayerLocation()
local character = addon.ThisCharacter
character.zone = GetRealZoneText()
character.subZone = GetSubZoneText()
end
-- *** Event Handlers ***
local function OnPlayerGuildUpdate()
-- at login this event is called between OnEnable and PLAYER_ALIVE, where GetGuildInfo returns a wrong value
-- however, the value returned here is correct
if IsInGuild() then
-- find a way to improve this, it's minor, but it's called too often at login
local name, rank, index = GetGuildInfo("player")
if name and rank and index then
local character = addon.ThisCharacter
character.guildName = name
character.guildRankName = rank
character.guildRankIndex = index
end
end
end
local function OnPlayerUpdateResting()
addon.ThisCharacter.isResting = IsResting();
end
local function OnPlayerXPUpdate()
local character = addon.ThisCharacter
character.XP = UnitXP("player")
character.XPMax = UnitXPMax("player")
character.RestXP = GetXPExhaustion() or 0
end
local function OnPlayerMoney()
addon.ThisCharacter.money = GetMoney();
end
local function OnPlayerAlive()
-- print("DataStore_Characters.lua") -- DEBUG 2025 07 21
if not UnitIsGhost("player") then return end -- only scan if player released spirit and went to graveyard
local character = addon.ThisCharacter
character.name = UnitName("player") -- to simplify processing a bit, the name is saved in the table too, in addition to being part of the key
character.level = UnitLevel("player")
character.race, character.englishRace = UnitRace("player")
character.class, character.englishClass = UnitClass("player")
character.gender = UnitSex("player")
character.faction = UnitFactionGroup("player")
character.lastLogoutTimestamp = 0
character.lastUpdate = time()
OnPlayerMoney()
OnPlayerXPUpdate()
OnPlayerUpdateResting()
OnPlayerGuildUpdate()
end
local function OnPlayerLogout()
addon.ThisCharacter.lastLogoutTimestamp = time()
addon.ThisCharacter.lastUpdate = time()
end
-- ** Mixins **
local function _GetCharacterName(character)
return character.name
end
local function _GetCharacterLevel(character)
return character.level or 0
end
local function _GetCharacterRace(character)
return character.race or "", character.englishRace or ""
end
local function _GetCharacterClass(character)
return character.class or "", character.englishClass or ""
end
local WHITE = "|cFFFFFFFF"
local ClassColors = {
["MAGE"] = "|cFF69CCF0",
["WARRIOR"] = "|cFFC79C6E",
["HUNTER"] = "|cFFABD473",
["ROGUE"] = "|cFFFFF569",
["WARLOCK"] = "|cFF9482CA",
["DRUID"] = "|cFFFF7D0A",
["SHAMAN"] = "|cFF2459FF",
["PALADIN"] = "|cFFF58CBA",
["PRIEST"] = WHITE,
["DEATHKNIGHT"] = "|cFFC41F3B"
}
-- CoA: mirror _G.RAID_CLASS_COLORS (10 vanilla + HERO + 21 CoA tokens on
-- the Voljin/PTR realm) into ClassColors so guild/character rows for
-- BARBARIAN, WITCHDOCTOR, CHRONOMANCER, … render with the
-- realm-canonical palette instead of nil-concat-crashing in
-- _GetColoredCharacterName below. Source of truth is the client's
-- Interface/SharedXML/SharedConstants.lua; same pattern as
-- coa-omen/CoAClassColors.lua and the Altoholic UI's CoAClassColors.lua.
do
local source = _G.RAID_CLASS_COLORS
if type(source) == "table" then
for token, color in pairs(source) do
if ClassColors[token] == nil and type(color) == "table" then
local r, g, b
if color.GetRGB then r, g, b = color:GetRGB()
else r, g, b = color.r, color.g, color.b end
if r and g and b then
ClassColors[token] = string.format("|cFF%02X%02X%02X",
r * 255 + 0.5, g * 255 + 0.5, b * 255 + 0.5)
end
end
end
end
end
local function _GetColoredCharacterName(character)
return (ClassColors[character.englishClass] or WHITE) .. (character.name or "?") -- CoA: records seeded from guild comm before a full scan have no name yet
end
local function _GetClassColor(character)
-- return just the color of this character's class
return ClassColors[character.englishClass] or WHITE
end
local function _GetCharacterFaction(character)
return character.faction or ""
end
local function _GetColoredCharacterFaction(character)
if character.faction == "Alliance" then
return "|cFF2459FF" .. FACTION_ALLIANCE
else
return "|cFFFF0000" .. FACTION_HORDE
end
end
local function _GetCharacterGender(character)
return character.gender or ""
end
local function _GetLastLogout(character)
return character.lastLogoutTimestamp or 0
end
local function _GetMoney(character)
return character.money or 0
end
local function _GetXP(character)
return character.XP or 0
end
local function _GetXPRate(character)
local xpMax = character.XPMax or 0 -- CoA: comm-seeded / max-level / unscanned char has no XP data (also avoids /0)
if xpMax == 0 then return 0 end
return floor(((character.XP or 0) / xpMax) * 100)
end
local function _GetXPMax(character)
return character.XPMax or 0
end
local function _GetRestXP(character)
return character.RestXP or 0
end
local function _GetRestXPRate(character)
-- after extensive tests, it seems that the known formula to calculate rest xp is incorrect.
-- I believed that the maximum rest xp was exactly 1.5 level, and since 8 hours of rest = 5% of a level
-- being 100% rested would mean having 150% xp .. but there's a trick...
-- One would expect that 150% of rest xp would be split over the following levels, and that calculating the exact amount of rest
-- would require taking into account that 30% are over the current level, 100% over lv+1, and the remaining 20% over lv+2 ..
-- .. But that is not the case.Blizzard only takes into account 150% of rest xp AT THE CURRENT LEVEL RATE.
-- ex: at level 15, it takes 13600 xp to go to 16, therefore the maximum attainable rest xp is:
-- 136 (1% of the level) * 150 = 20400
-- thus, to calculate the exact rate (ex at level 15):
-- divide xptonext by 100 : 13600 / 100 = 136 ==> 1% of the level
-- multiply by 1.5 136 * 1.5 = 204
-- divide rest xp by this value 20400 / 204 = 100 ==> rest xp rate
local rate = 0
if character.RestXP and character.XPMax and character.XPMax > 0 then -- CoA: guard nil/zero XPMax
rate = (character.RestXP / ((character.XPMax / 100) * 1.5))
end
-- get the known rate of rest xp (the one saved at last logout) + the rate represented by the elapsed time since last logout
-- (elapsed time / 3600) * 0.625 * (2/3) simplifies to elapsed time / 8640
-- 0.625 comes from 8 hours rested = 5% of a level, *2/3 because 100% rested = 150% of xp (1.5 level)
if character.lastLogoutTimestamp and character.lastLogoutTimestamp ~= 0 then -- time since last logout, 0 for current char, <> for all others (CoA: nil for comm-seeded chars => "nil ~= 0" is true and crashed)
if character.isResting then
rate = rate + ((time() - character.lastLogoutTimestamp) / 8640)
else
rate = rate + ((time() - character.lastLogoutTimestamp) / 34560) -- 4 times less if not at an inn
end
end
return rate
end
local function _IsResting(character)
return character.isResting
end
local function _GetGuildInfo(character)
return character.guildName, character.guildRankName, character.guildRankIndex
end
local function _GetPlayTime(character)
return character.played
end
local function _GetLocation(character)
return character.zone, character.subZone
end
local PublicMethods = {
GetCharacterName = _GetCharacterName,
GetCharacterLevel = _GetCharacterLevel,
GetCharacterRace = _GetCharacterRace,
GetCharacterClass = _GetCharacterClass,
GetColoredCharacterName = _GetColoredCharacterName,
GetClassColor = _GetClassColor,
GetCharacterFaction = _GetCharacterFaction,
GetColoredCharacterFaction = _GetColoredCharacterFaction,
GetCharacterGender = _GetCharacterGender,
GetLastLogout = _GetLastLogout,
GetMoney = _GetMoney,
GetXP = _GetXP,
GetXPRate = _GetXPRate,
GetXPMax = _GetXPMax,
GetRestXP = _GetRestXP,
GetRestXPRate = _GetRestXPRate,
IsResting = _IsResting,
GetGuildInfo = _GetGuildInfo,
GetPlayTime = _GetPlayTime,
GetLocation = _GetLocation,
}
function addon:OnInitialize()
addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults)
DataStore:RegisterModule(addonName, addon, PublicMethods)
DataStore:SetCharacterBasedMethod("GetCharacterName")
DataStore:SetCharacterBasedMethod("GetCharacterLevel")
DataStore:SetCharacterBasedMethod("GetCharacterRace")
DataStore:SetCharacterBasedMethod("GetCharacterClass")
DataStore:SetCharacterBasedMethod("GetColoredCharacterName")
DataStore:SetCharacterBasedMethod("GetClassColor")
DataStore:SetCharacterBasedMethod("GetCharacterFaction")
DataStore:SetCharacterBasedMethod("GetColoredCharacterFaction")
DataStore:SetCharacterBasedMethod("GetCharacterGender")
DataStore:SetCharacterBasedMethod("GetLastLogout")
DataStore:SetCharacterBasedMethod("GetMoney")
DataStore:SetCharacterBasedMethod("GetXP")
DataStore:SetCharacterBasedMethod("GetXPRate")
DataStore:SetCharacterBasedMethod("GetXPMax")
DataStore:SetCharacterBasedMethod("GetRestXP")
DataStore:SetCharacterBasedMethod("GetRestXPRate")
DataStore:SetCharacterBasedMethod("IsResting")
DataStore:SetCharacterBasedMethod("GetGuildInfo")
DataStore:SetCharacterBasedMethod("GetPlayTime")
DataStore:SetCharacterBasedMethod("GetLocation")
end
function addon:OnEnable()
addon:RegisterEvent("PLAYER_ALIVE", OnPlayerAlive)
addon:RegisterEvent("PLAYER_LOGOUT", OnPlayerLogout)
addon:RegisterEvent("PLAYER_LEVEL_UP")
addon:RegisterEvent("PLAYER_MONEY", OnPlayerMoney)
addon:RegisterEvent("PLAYER_XP_UPDATE", OnPlayerXPUpdate)
addon:RegisterEvent("PLAYER_UPDATE_RESTING", OnPlayerUpdateResting)
addon:RegisterEvent("PLAYER_GUILD_UPDATE", OnPlayerGuildUpdate) -- for gkick, gquit, etc..
addon:RegisterEvent("ZONE_CHANGED", ScanPlayerLocation)
addon:RegisterEvent("ZONE_CHANGED_NEW_AREA", ScanPlayerLocation)
addon:RegisterEvent("ZONE_CHANGED_INDOORS", ScanPlayerLocation)
addon:RegisterEvent("TIME_PLAYED_MSG")
RequestTimePlayed() -- trigger a TIME_PLAYED_MSG event
end
function addon:OnDisable()
addon:UnregisterEvent("PLAYER_ALIVE")
addon:UnregisterEvent("PLAYER_LOGOUT")
addon:UnregisterEvent("PLAYER_LEVEL_UP")
addon:UnregisterEvent("PLAYER_MONEY")
addon:UnregisterEvent("PLAYER_XP_UPDATE")
addon:UnregisterEvent("PLAYER_UPDATE_RESTING")
addon:UnregisterEvent("PLAYER_GUILD_UPDATE")
addon:UnregisterEvent("ZONE_CHANGED")
addon:UnregisterEvent("ZONE_CHANGED_NEW_AREA")
addon:UnregisterEvent("ZONE_CHANGED_INDOORS")
addon:UnregisterEvent("TIME_PLAYED_MSG")
end
-- *** EVENT HANDLERS ***
function addon:PLAYER_LEVEL_UP(event, newLevel)
addon.ThisCharacter.level = newLevel
end
function addon:TIME_PLAYED_MSG(event, TotalTime, CurrentLevelTime)
addon.ThisCharacter.played = TotalTime
end