--[[ *** DataStore *** Written by : Thaoky, EU-Marécages de Zangar July 15th, 2009 This is the main DataStore module, its purpose is to be a single point of contact for common operations between client addons and other DataStore modules. For instance, it prevents client addons from calling a different :GetCharacter() in each module, as the value returned by the main module can be passed to the other ones. Other services offered by DataStore: - DataStore Events ; possibility to trigger and respond to DataStore's own events (see the respective modules for details) - Tracks guild members status in a slightly more accurate way than with GUILD_ROSTER_UPDATE alone. - Guild member info can be requested by character name (DataStore:GetGuildMemberInfo(member)) rather than by index (GetGuildRosterInfo) - Tracks online guild members' alts, used mostly by other DataStore modules, but can also be used by client addons. Note: a "main" is the currently connected player, "alts" are all his other characters in the same guild. The notions of "main" & "alts" are thus only valid for live data, nothing else. --]] DataStore = LibStub("AceAddon-3.0"):NewAddon("DataStore", "AceConsole-3.0", "AceEvent-3.0", "AceComm-3.0", "AceSerializer-3.0") local addon = DataStore addon.Version = "v3.3.001" local THIS_ACCOUNT = "Default" local commPrefix = "DataStore" local Characters, Guilds -- pointers to the parts of the DB that contain character, guild data local RegisteredModules = {} local RegisteredMethods = {} local guildMembersIndexes = {} -- hash table containing guild member info local onlineMembers = {} -- simple hash table to track online members: ["member"] = true (or nil) local onlineMembersAlts = {} -- simple hash table to track online members' alts: ["member"] = "alt1|alt2|alt3..." -- Message types local MSG_ANNOUNCELOGIN = 1 -- broacast at login local MSG_LOGINREPLY = 2 -- reply to MSG_ANNOUNCELOGIN local AddonDB_Defaults = { global = { Options = { HideStartGuilds = 0, -- Hide Rising-Gods Starter Guilds }, Guilds = { ['*'] = { -- ["Account.Realm.Name"] faction = nil, } }, Characters = { ['*'] = { -- ["Account.Realm.Name"] faction = nil, guildName = nil, -- nil = not in a guild, as returned by GetGuildInfo("player") } }, SharedContent = { -- lists the shared content -- ["Account.Realm.Name"] = true means the char is shared, -- ["Account.Realm.Name.Module"] = true means the module is shared for that char }, } } local function GetKey(name, realm, account) -- default values name = name or UnitName("player") realm = realm or GetRealmName() account = account or THIS_ACCOUNT return format("%s.%s.%s", account, realm, name) end local function GetDBVersion() return addon.db.global.Version or 0 end local function SetDBVersion(version) addon.db.global.Version = version end local DBUpdaters = { -- Table of functions, each one updates to its index's version -- ex: [3] = the function that upgrades from v2 to v3 [1] = function(self) -- This function moves character keys from the "global" level to the "Characters" sub-table -- keys are also changed from a simple boolean (previously set to true) to a table. Only faction & guildname are tracked (for later use) for k, v in pairs(addon.db.global) do if type(v) == "boolean" then if not addon.db.global.Characters[k].faction then -- for characters other than the current one .. addon.db.global.Characters[k].faction = "" -- set the faction field to create the table. end addon.db.global[k] = nil -- kill the key at the old location end end end, } local function UpdateDB() local version = GetDBVersion() for i = (version+1), #DBUpdaters do -- start from latest version +1 to the very last DBUpdaters[i]() SetDBVersion(i) end DBUpdaters = nil GetDBVersion = nil SetDBVersion = nil end local function GetAlts(guild) -- returns a | delimited string containing the list of alts in the same guild guild = guild or GetGuildInfo("player") if not guild then return end local out = {} for k, v in pairs(Characters) do local accountKey, realmKey, charKey = strsplit(".", k) if accountKey and accountKey == THIS_ACCOUNT then -- same account if realmKey and realmKey == GetRealmName() then -- same realm if charKey and charKey ~= UnitName("player") then -- skip current char if v.guildName and v.guildName == guild then -- same guild (to send only guilded alts, privacy concern, do not change this) table.insert(out, charKey) end end end end end return table.concat(out, "|") end local function SaveAlts(sender, alts) if alts then if strlen(alts) > 0 then -- sender has no alts onlineMembersAlts[sender] = alts -- "alt1|alt2|alt3..." end addon:SendMessage("DATASTORE_GUILD_ALTS_RECEIVED", sender, alts) end end local function GuildBroadcast(messageType, ...) local serializedData = addon:Serialize(messageType, ...) addon:SendCommMessage(commPrefix, serializedData, "GUILD") end local function GuildWhisper(player, messageType, ...) if addon:IsGuildMemberOnline(player) then local serializedData = addon:Serialize(messageType, ...) addon:SendCommMessage(commPrefix, serializedData, "WHISPER", player) end end local function CopyTable(src, dest) for k, v in pairs (src) do if type(v) == "table" then dest[k] = {} CopyTable(v, dest[k]) else dest[k] = v end end end -- *** Event Handlers *** local currentGuildName 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() and not currentGuildName then -- the event may be triggered multiple times, and GetGuildInfo may return incoherent values in subsequent calls, so only save if we have no value. currentGuildName = GetGuildInfo("player") if currentGuildName then Guilds[GetKey(currentGuildName)].faction = UnitFactionGroup("player") -- the first time a valid value is found, broadcast to guild, it must happen here for a standard login, but won't work here after a reloadui since this event is not triggered GuildBroadcast(MSG_ANNOUNCELOGIN, GetAlts(currentGuildName)) addon:SendMessage("DATASTORE_ANNOUNCELOGIN", currentGuildName) end end Characters[GetKey()].guildName = currentGuildName end local function OnPlayerAlive() -- print("DataStore.lua") -- DEBUG 2025 07 21 if not UnitIsGhost("player") then return end -- only scan if player released spirit and went to graveyard Characters[GetKey()].faction = UnitFactionGroup("player") OnPlayerGuildUpdate() end local function OnGuildRosterUpdate() wipe(guildMembersIndexes) for i=1, GetNumGuildMembers(true) do -- browse all players (online & offline) local name, _, _, _, _, _, _, _, onlineStatus = GetGuildRosterInfo(i) if name then guildMembersIndexes[name] = i if onlineMembers[name] and not onlineStatus then -- if a player was online but has now gone offline, trigger a message addon:SendMessage("DATASTORE_GUILD_MEMBER_OFFLINE", name) end onlineMembers[name] = onlineStatus end end end local msgOffline = gsub(ERR_FRIEND_OFFLINE_S, "%%s", "(.+)") -- this turns "%s has gone offline." into "(.+) has gone offline." local function OnChatMsgSystem(event, arg) if arg then local member = arg:match(msgOffline) if member then -- guild roster update can be triggered every 10 secs max, so if a players logs in & out right after, sending him message will result in "No player named xx" -- marking him as offline prevents this onlineMembers[member] = nil onlineMembersAlts[member] = nil addon:SendMessage("DATASTORE_GUILD_MEMBER_OFFLINE", member) end end end -- *** Guild Comm *** local GuildCommCallbacks = { [commPrefix] = { [MSG_ANNOUNCELOGIN] = function(sender, alts) onlineMembers[sender] = true -- sender is obviously online if sender ~= UnitName("player") then -- don't send back to self GuildWhisper(sender, MSG_LOGINREPLY, GetAlts()) -- reply by sending my own alts .. end SaveAlts(sender, alts) -- .. and save received data end, [MSG_LOGINREPLY] = function(sender, alts) SaveAlts(sender, alts) end, }, } local function GuildCommHandler(prefix, message, distribution, sender) -- This handler will be used by other modules as well local guild = GetGuildInfo("player") if guild and addon:GetOption("DataStore", "HideStartGuilds") == 1 and (guild == "Community Horde" or guild == "Community Allianz") then return -- block if ignore starter guilds end local success, msgType, arg1, arg2, arg3 = addon:Deserialize(message) if success and msgType and GuildCommCallbacks[prefix] then local func = GuildCommCallbacks[prefix][msgType] if func then func(sender, arg1, arg2, arg3) end end end -- Explanation of this piece of code -- Whenever DataStore:MethodXXX(arg1, arg2, etc..) is called, this attempts to find the method in the registered list -- If this method is character related, we intercept the string (ex: ["Default.RealmZZZ.CharYYY") and get the associated character table in the module that owns these data -- since we actually pass a table to registered methods, the "conversion" is done here. --[[ *** Sample code *** local character = DataStore:GetCharacter() -- while the implementation of GetNumSpells in DataStore_Spells expects a table as first parameter, the string value returned by GetCharacter is converted on the fly -- this service prevents having to maintain a separate pointer to each character table in the respective DataStore_* modules. local n = DataStore:GetNumSpells(character, "Fire") print(n) --]] local lookupMethods = { __index = function(self, key) return function(self, arg1, ...) if not RegisteredMethods[key] then -- print(format("DataStore : method <%s> is missing.", key)) -- enable this in Debug only, there's a risk that this function gets called unexpectedly return end if RegisteredMethods[key].isCharBased then -- if this method is character related, the first expected parameter is the character local owner = RegisteredMethods[key].owner arg1 = owner.Characters[arg1] -- turns a "string" parameter into a table, fully intended. if not arg1.lastUpdate then return end -- lastUpdate must be present in the Character part of a db, if not, data is unavailable elseif RegisteredMethods[key].isGuildBased then -- if this method is guild related, the first expected parameter is the guild local owner = RegisteredMethods[key].owner arg1 = owner.Guilds[arg1] -- turns a "string" parameter into a table, fully intended. if not arg1 then return end end return RegisteredMethods[key].func(arg1, ...) end end } function addon:OnInitialize() addon.db = LibStub("AceDB-3.0"):New("DataStoreDB", AddonDB_Defaults) Characters = addon.db.global.Characters Guilds = addon.db.global.Guilds UpdateDB() setmetatable(addon, lookupMethods) addon:SetupOptions() -- See Options.lua end function addon:OnEnable() addon:RegisterEvent("PLAYER_ALIVE", OnPlayerAlive) addon:RegisterEvent("PLAYER_GUILD_UPDATE", OnPlayerGuildUpdate) -- for gkick, gquit, etc.. if IsInGuild() then addon:RegisterEvent("GUILD_ROSTER_UPDATE", OnGuildRosterUpdate) -- we only care about "%s has come online" or "%s has gone offline", so register only if player is in a guild addon:RegisterEvent("CHAT_MSG_SYSTEM", OnChatMsgSystem) addon:RegisterComm(commPrefix, GuildCommHandler) local guild = GetGuildInfo("player") -- will be nil in a standard login (called too soon), but ok for a reloadui. if guild then GuildBroadcast(MSG_ANNOUNCELOGIN, GetAlts(guild)) addon:SendMessage("DATASTORE_ANNOUNCELOGIN", guild) end end end function addon:OnDisable() addon:UnregisterEvent("PLAYER_ALIVE") addon:UnregisterEvent("PLAYER_GUILD_UPDATE") addon:UnregisterEvent("GUILD_ROSTER_UPDATE") addon:UnregisterEvent("CHAT_MSG_SYSTEM") end -- *** DB functions *** function addon:RegisterModule(moduleName, module, publicMethods) assert(type(moduleName) == "string") assert(type(module) == "table") if not RegisteredModules[moduleName] then -- add the module's database address (addon.db.global) to the list of known modules RegisteredModules[moduleName] = module local db = module.db.global -- simplifies the life of child modules, and prepares a few pointers for them module.ThisCharacter = db.Characters[GetKey()] module.Characters = db.Characters module.Guilds = db.Guilds -- register module's public method for methodName, method in pairs(publicMethods) do if RegisteredMethods[methodName] then print(format("DataStore:RegisterMethod() : adding method for module <%s> failed.", moduleName)) print(format("DataStore:RegisterMethod() : method <%s> already exists !", methodName)) return end RegisteredMethods[methodName] = { func = method, owner = module, -- module that owns this method & associated data } end end end function addon:SetCharacterBasedMethod(methodName) -- flags a given method as character based if RegisteredMethods[methodName] then -- this will take care of error checking before calling the registered method, and pass the appropriate character table as argument RegisteredMethods[methodName].isCharBased = true end end function addon:SetGuildBasedMethod(methodName) if RegisteredMethods[methodName] then RegisteredMethods[methodName].isGuildBased = true -- same as above for guilds end end function addon:GetGuildCommHandler() return GuildCommHandler end function addon:SetGuildCommCallbacks(prefix, callbacks) GuildCommCallbacks[prefix] = callbacks -- no need to create a new table, it exists already as a local table in the calling module end function addon:IsModuleEnabled(name) assert(type(name) == "string") if RegisteredModules[name] then return true end end function addon:GetCharacter(name, realm, account) local key = GetKey(name, realm, account) if Characters[key] then -- if the key is known, return it to caller, it can be passed to other modules return key end end function addon:GetCharacters(realm, account) -- get a list of characters on a given realm/account realm = realm or GetRealmName() account = account or THIS_ACCOUNT local out = {} local accountKey, realmKey, charKey for k, v in pairs(Characters) do if v.faction and v.faction == "" then -- this is an integrity check, may happen after a failed account sync. Characters[k] = nil -- kill the key, don't add it to the list. else accountKey, realmKey, charKey = strsplit(".", k) if accountKey and realmKey then if accountKey == account and realmKey == realm then out[charKey] = k -- allows this kind of iteration: -- for characterName, character in pairs(DS:GetCharacters(realm, account)) do -- do stuff with characterName only -- or do stuff with the "character" key to pass to other DataStore functions -- end end end end end return out end function addon:DeleteCharacter(name, realm, account) local key = GetKey(name, realm, account) if not Characters[key] then return end -- delete the character in all modules for moduleName, moduleDB in pairs(RegisteredModules) do if moduleDB.Characters then moduleDB.Characters[key] = nil end end -- delete the key in DataStore Characters[key] = nil end function addon:GetNumCharactersInDB() -- a simple count of the number of character entries in the db local count = 0 for _, _ in pairs(Characters) do count = count + 1 end return count end function addon:GetGuild(name, realm, account) name = name or GetGuildInfo("player") local key = GetKey(name, realm, account) if Guilds[key] then -- if the key is known, return it to caller, it can be passed to other modules return key end end function addon:GetGuilds(realm, account) -- get a list of guilds on a given realm/account realm = realm or GetRealmName() account = account or THIS_ACCOUNT local out = {} local accountKey, realmKey, guildKey for k, _ in pairs(Guilds) do accountKey, realmKey, guildKey = strsplit(".", k) if accountKey and realmKey then if accountKey == account and realmKey == realm then out[guildKey] = k -- this allows to iterate with this kind of loop: -- for guildName, guild in pairs(DS:GetGuilds(realm, account)) do -- do stuff with guildName only -- or do stuff with the "guild" key to pass to other DataStore functions -- end end end end return out end function addon:DeleteRealm(realm, account) for name, _ in pairs(addon:GetCharacters(realm, account)) do addon:DeleteCharacter(name, realm, account) end end function addon:GetRealms(account) account = account or THIS_ACCOUNT local out = {} local accountKey, realmKey for k, _ in pairs(Characters) do accountKey, realmKey = strsplit(".", k) if accountKey and realmKey then if accountKey == account then out[realmKey] = true -- allows this kind of iteration: -- for realmName in pairs(DS:GetRealms( account)) do -- end end end end return out end function addon:GetAccounts() local out = {} local accountKey for k, _ in pairs(Characters) do accountKey = strsplit(".", k) if accountKey then out[accountKey] = true -- allows this kind of iteration: -- for accountName in pairs(DS:GetAccounts()) do -- end end end return out end function addon:GetModules() return RegisteredModules -- for moduleName, module in pairs(DS:GetModules()) do -- end end function addon:GetCharacterTable(module, name, realm, account) -- module can be either the module name (string) or the module table -- ex: DS:GetCharacterTable("DataStore_Containers", ...) or DS:GetCharacterTable(DataStore_Containers, ...) if type(module) == "string" then module = RegisteredModules[module] end assert(type(module) == "table") return module.Characters[GetKey(name, realm, account)] end function addon:GetModuleLastUpdate(module, name, realm, account) -- module can be either the module name (string) or the module table -- ex: DS:GetModuleLastUpdate("DataStore_Containers", ...) or DS:GetModuleLastUpdate(DataStore_Containers, ...) if type(module) == "string" then module = RegisteredModules[module] end assert(type(module) == "table") local key = GetKey(name, realm, account) return module.Characters[key].lastUpdate end function addon:ImportData(module, data, name, realm, account) -- module can be either the module name (string) or the module table -- ex: DS:ImportData("DataStore_Containers", ...) or DS:ImportData(DataStore_Containers, ...) if type(module) == "string" then module = RegisteredModules[module] end assert(type(module) == "table") -- change this, it shoudl be a COPYTABLE instead of an assignation, otherwise, ace DB wildcards are not applied -- module.Characters[GetKey(name, realm, account)] = data CopyTable(data, module.Characters[GetKey(name, realm, account)]) end function addon:ImportCharacter(key, faction, guild) -- after data has been imported, add a player entry to the DB, so that it becomes "visible" to the outside world. -- in other words, the correct sequence of operations should be something like: -- DataStore:ImportData(DataStore_Talents) -- DataStore:ImportData(DataStore_Spells) -- DataStore:ImportCharacter(key, faction, guild) Characters[key].faction = faction Characters[key].guildName = guild end function addon:SetOption(module, option, value) -- module can be either the module name (string) or the module table -- ex: DS:SetOption("DataStore_Containers", ...) or DS:SetOption(DataStore_Containers, ...) if type(module) == "string" then if module == "DataStore" then module = addon else module = RegisteredModules[module] end end if type(module) == "table" then if module.db.global.Options then module.db.global.Options[option] = value end end end function addon:GetOption(module, option) -- module can be either the module name (string) or the module table -- ex: DS:GetOption("DataStore_Containers", ...) or DS:GetOption(DataStore_Containers, ...) if type(module) == "string" then if module == "DataStore" then module = addon else module = RegisteredModules[module] end end if type(module) == "table" then if module.db.global.Options then return module.db.global.Options[option] end end end -- *** Guild stuff *** function addon:GetGuildMemberInfo(member) -- returns the same info as the genuine GetGuildRosterInfo(), but it can be called by character name instead of by index. local index = guildMembersIndexes[member] if index then -- name, rank, rankIndex, level, class, zone, note, officernote, online, status, englishClass = GetGuildRosterInfo(index) return GetGuildRosterInfo(index) end end function addon:GetGuildMemberAlts(member) local index = onlineMembersAlts[member] if index then return onlineMembersAlts[member] end end function addon:GetOnlineGuildMembers() return onlineMembers end function addon:IsGuildMemberOnline(member) if member == UnitName("player") then -- if self, always return true, may happen if login broadcast hasn't come back yet return true end return onlineMembers[member] end function addon:GetNameOfMain(player) -- returns the name of the guild mate to whom an alt belongs -- ex, player x has alts a, b, c if onlineMembers[player] then -- if x is passed ..it's the main return player -- return it end for member, alts in pairs(onlineMembersAlts) do --if b is passed, browse all online players who sent their alts for _, alt in pairs( { strsplit("|", alts) }) do -- browse the list of alts if alt == player then -- alt found ? return member -- return the name of his main (currently connected) end end end end