--[[ *** 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 hasScannedThisSession local function OnPlayerAlive() -- CoA: scan once at login. PLAYER_ALIVE also fires on resurrect / Feign-Death cancel -- (unchanged data), so skip those. The previous "only when ghost" gate skipped LOGIN -- too, so name/level/class/money/XP never populated on a normal login - the root of -- "no character data". (Same trap as DataStore_Inventory / _Skills; see commit fdcb25a.) if hasScannedThisSession then return end hasScannedThisSession = true 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 or 0 -- CoA: nil on partial-data alt; callers do arithmetic (AccountSummary) 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