chore: flatten Altoholic-Addon/ wrapper + add standard .gitignore/.gitattributes

Each DataStore_* / Altoholic_* addon now lives at the repo root, matching
the Exiles fork-layout convention (one folder per addon, no wrapper dir).
This commit is contained in:
2026-05-25 10:59:24 +02:00
parent 7789489aec
commit bbe2492a5b
387 changed files with 2 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
------------------------------------------------------------------------
r26 | Thaoky | 2010-07-06 17:21:38 +0000 (Tue, 06 Jul 2010) | 1 line
Changed paths:
M /trunk/DataStore.lua
Minor update to prepare the migration of the account sharing process from Altoholic to DataStore.
------------------------------------------------------------------------
r25 | thaoky | 2010-03-17 11:50:20 +0000 (Wed, 17 Mar 2010) | 1 line
Changed paths:
A /trunk/Export
A /trunk/Export/DataStore.xsl
A /trunk/Export/ExportToXML.lua
Added an external script to export databases to XML.
------------------------------------------------------------------------
+655
View File
@@ -0,0 +1,655 @@
--[[ *** 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
+24
View File
@@ -0,0 +1,24 @@
## Interface: 30300
## Title: DataStore
## Notes: Main DataStore Module
## Author: Thaoky (EU-Marécages de Zangar)
## Version: 3.3.001
## OptionalDeps: Ace3, LibAboutPanel
## SavedVariables: DataStoreDB
## X-Embeds: Ace3
## X-Category: Interface Enhancements
## X-Localizations: enUS, frFR, zhCN, zhTW, deDE, koKR, esES, esMX, ruRU
## X-Website: http://wow.curse.com/downloads/wow-addons/details/datastore.aspx
## X-eMail: thaoky.altoholic@yahoo.com
## X-Donate: http://wow.curse.com/downloads/wow-addons/details/altoholic.aspx
## X-Credits: My guild (Odysseüs), all translators, the wowace community, and all users for their invaluable suggestions !
## X-Curse-Packaged-Version: r26
## X-Curse-Project-Name: DataStore
## X-Curse-Project-ID: datastore
## X-Curse-Repository-ID: wow/datastore/mainline
embeds.xml
locale.xml
DataStore.lua
Options.xml
+438
View File
@@ -0,0 +1,438 @@
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns="http://www.w3.org/1999/xhtml">
<xsl:template match="DataStorePage">
<!-- use external layout file -->
<xsl:variable name="Layout" select="@Uses"/>
<html>
<head>
<title>
<xsl:value-of select="@Title" />
</title>
<script src="http://static.wowhead.com/widgets/power.js"></script>
</head>
<body>
<xsl:apply-templates />
</body>
</html>
</xsl:template>
<xsl:template match="*">
<xsl:value-of select="name()" /> : <xsl:value-of select="text()" /><br />
</xsl:template>
<!-- Shared -->
<xsl:template match="Skill">
<xsl:value-of select="@name" /> : <xsl:value-of select="text()" /><br />
</xsl:template>
<xsl:template match="Item">
<xsl:choose>
<xsl:when test="@rarity &lt; 8">
<a class="q{@rarity}" href="http://www.wowhead.com/?item={@id}"><xsl:value-of select="text()"/></a>
</xsl:when>
<xsl:otherwise>
<a href="http://www.wowhead.com/?item={@id}"><xsl:value-of select="text()"/></a>
</xsl:otherwise>
</xsl:choose>
<xsl:if test="@count &gt; 0">
&#x20;x<xsl:value-of select="@count" />
</xsl:if>
<br />
</xsl:template>
<!-- DataStore Achievements -->
<xsl:template match="Achievement">
<a href="http://www.wowhead.com/?achievement={@id}">Achievement <xsl:value-of select="@id"/></a>
Status :
<xsl:choose>
<xsl:when test="text() = 'true'">
Completed on <xsl:value-of select="@completionDate" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="text()"/>
</xsl:otherwise>
</xsl:choose>
<br />
</xsl:template>
<xsl:template match="Achievements">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates select="Achievement" />
</td>
</tr>
</table>
</xsl:template>
<!-- DataStore Auctions -->
<xsl:template match="Auction">
<tr>
<td>
<a href="http://www.wowhead.com/?item={text()}">Item <xsl:value-of select="text()"/></a>
Count : <xsl:value-of select="@count"/>,
<xsl:if test="@highBidder">
Highest Bidder : <xsl:value-of select="@highBidder"/>,
</xsl:if>
<xsl:if test="@ownerName">
Owner : <xsl:value-of select="@ownerName"/>,
</xsl:if>
<xsl:if test="@startPrice">
Starting Price : <xsl:value-of select="@startPrice"/>,
</xsl:if>
<xsl:if test="@bidPrice">
Bid Price : <xsl:value-of select="@bidPrice"/>,
</xsl:if>
Buyout Price : <xsl:value-of select="@buyoutPrice"/>,
Time Left : <xsl:value-of select="@timeLeft"/>
</td>
</tr>
</xsl:template>
<xsl:template match="Auctions|Bids">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<xsl:apply-templates />
</table>
</xsl:template>
<!-- DataStore Containers -->
<xsl:template match="Content">
<xsl:apply-templates select="Item" />
</xsl:template>
<xsl:template match="Bag|Tab">
<tr>
<td>
<b><xsl:value-of select="name()" /> <xsl:value-of select="@id" /></b><br />
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
<xsl:template match="Containers|Tabs">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<xsl:apply-templates />
</table>
</xsl:template>
<!-- DataStore Crafts -->
<xsl:template match="Crafts/Category/Spell">
<a href="http://www.wowhead.com/?spell={text()}">Spell <xsl:value-of select="text()"/></a><br />
</xsl:template>
<xsl:template match="Crafts/Category">
<b><xsl:value-of select="@name" /></b><br />
<xsl:apply-templates select="Spell" />
</xsl:template>
<xsl:template match="Crafts">
<b><xsl:value-of select="name()" /></b><br />
<xsl:apply-templates select="Category" />
</xsl:template>
<xsl:template match="Profession">
<tr>
<td align="center">
<b><xsl:value-of select="@name" /></b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
<xsl:template match="Professions">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<xsl:apply-templates select="Profession" />
</table>
</xsl:template>
<!-- DataStore Currencies -->
<xsl:template match="Currency">
<tr>
<td>
<a href="http://www.wowhead.com/?item={@itemID}"><xsl:value-of select="text()"/></a>
</td>
<td>
<xsl:value-of select="@count"/>
</td>
</tr>
</xsl:template>
<xsl:template match="Currencies/Category">
<tr>
<td>
<b><xsl:value-of select="@name" /></b>
</td>
</tr>
<xsl:apply-templates select="Currency" />
</xsl:template>
<xsl:template match="Currencies">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<xsl:apply-templates select="Category" />
</table>
</xsl:template>
<!-- DataStore Inventory -->
<xsl:template match="Inventory">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates select="Item" />
</td>
</tr>
</table>
</xsl:template>
<!-- DataStore Mails -->
<xsl:template match="Mail">
<tr>
<td>
<xsl:apply-templates />
</td>
</tr>
</xsl:template>
<xsl:template match="Mails">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<xsl:apply-templates select="Mail" />
</table>
</xsl:template>
<!-- DataStore Pets -->
<xsl:template match="Mounts/Spell | Companions/Spell">
<a href="http://www.wowhead.com/?spell={text()}"><xsl:value-of select="@name"/></a><br />
</xsl:template>
<xsl:template match="Mounts|Companions">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates select="Spell" />
</td>
</tr>
</table>
</xsl:template>
<!-- DataStore Quests -->
<!-- Note: for purely practical reasons, the history is not processed at this time (way too long pages, for too little use in this context) -->
<xsl:template match="Quest">
<xsl:choose>
<xsl:when test="@isHeader = 'true'">
<xsl:value-of select="text()" />
</xsl:when>
<xsl:otherwise>
<a href="http://www.wowhead.com/?quest={@id}"><xsl:value-of select="text()"/></a>
</xsl:otherwise>
</xsl:choose>
<br />
</xsl:template>
<xsl:template match="QuestLog">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b>Quest Log</b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates select="Quest" />
</td>
</tr>
</table>
</xsl:template>
<!-- DataStore Reputations -->
<xsl:template match="Faction">
<xsl:value-of select="text()" /> : <xsl:value-of select="@rank" /> (<xsl:value-of select="@numPoints" />/<xsl:value-of select="@maxPoints" />)
<br />
</xsl:template>
<xsl:template match="Factions">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b>Factions</b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates select="Faction" />
</td>
</tr>
</table>
</xsl:template>
<!-- DataStore Skills -->
<xsl:template match="Skills/Category">
<td valign="top">
<b><xsl:value-of select="@name" /></b>
<br />
<xsl:apply-templates select="Skill" />
</td>
</xsl:template>
<xsl:template match="Skills">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center" colspan="6">
<b>Skills</b>
</td>
</tr>
<tr>
<xsl:apply-templates select="Category" />
</tr>
</table>
</xsl:template>
<!-- DataStore Spells -->
<xsl:template match="School/Spell">
<a href="http://www.wowhead.com/?spell={text()}">Spell <xsl:value-of select="text()"/></a>&#x20;<xsl:value-of select="@rank"/>
<br />
</xsl:template>
<xsl:template match="School">
<td valign="top">
<b><xsl:value-of select="@name" /></b>
<br />
<xsl:apply-templates select="Spell" />
</td>
</xsl:template>
<xsl:template match="Spells">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center" colspan="4">
<b>Spellbook</b>
</td>
</tr>
<tr>
<xsl:apply-templates select="School" />
</tr>
</table>
</xsl:template>
<!-- DataStore Stats -->
<xsl:template match="Stats/Spell">
<xsl:value-of select="name()" /> : <xsl:value-of select="text()" />
<br />
</xsl:template>
<xsl:template match="Stats">
<b><xsl:value-of select="name()" /> :</b><br />
<xsl:apply-templates />
</xsl:template>
<!-- DataStore Talents -->
<xsl:template match="Glyph">
<a href="http://www.wowhead.com/?spell={@spellID}">Glyph <xsl:value-of select="text()"/></a>&#x20;
Spec: <xsl:value-of select="@spec"/>, Slot: <xsl:value-of select="@slot"/>, Type: <xsl:value-of select="@glyphType"/>
<br />
</xsl:template>
<xsl:template match="Glyphs">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<b><xsl:value-of select="name()" /></b>
</td>
</tr>
<tr>
<td>
<xsl:apply-templates select="Glyph" />
</td>
</tr>
</table>
</xsl:template>
<xsl:template match="Talent">
[<xsl:value-of select="text()" />] : <xsl:value-of select="@pointsSpent"/>/<xsl:value-of select="@maximumRank"/><br />
</xsl:template>
<xsl:template match="TalentTree">
<td valign="top">
<b><xsl:value-of select="@name" /> (<xsl:value-of select="@spec" />)</b>
<br />
<xsl:apply-templates select="Talent" />
</td>
</xsl:template>
<xsl:template match="TalentTrees">
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td align="center" colspan="6">Talent Trees</td>
</tr>
<tr>
<xsl:apply-templates select="TalentTree" />
</tr>
</table>
</xsl:template>
<!-- Global-->
<xsl:template match="Character | Guild">
<div class="{name()}">
<xsl:value-of select="@account" /> / <xsl:value-of select="@realm" /> / <xsl:value-of select="@name" />
<br />
<xsl:apply-templates />
</div>
<br />
</xsl:template>
<xsl:template match="Characters | Guilds">
<b><xsl:value-of select="name()" /></b>
<br />
<xsl:apply-templates />
</xsl:template>
</xsl:stylesheet>
+670
View File
@@ -0,0 +1,670 @@
--[[ *** DataStore Export Script ***
Written by : Thaoky, EU-Marecages de Zangar
Date: 10-03-2010
READ THIS FIRST !!!
1) Prerequisites
You must have a Lua environment installed on your machine. I suggest getting LuaSocket from this address : http://luaforge.net/projects/luasocket/
Even though this script does not use any network function, LuaSocket is a useful and neat package, and other scripts I may release in the future are likely to use those features :)
Make sure to install it & configure it properly.
2) Setup a small .bat
Create a file called go.bat in this directory
Copy this line into the file :
d:\Lua\lua5.1.exe export.lua
.. where d:\Lua is the directory where your Lua environment is installed
3) Set INPUT_DIR & OUTPUT_DIR to valid directories (don't forget the double backslashes !!)
INPUT_DIR must be set to the directory that contains your DataStore Saved Variables.
OUTPUT_DIR is any directory of your choice.
4) run go.bat
5) After you've ran the .bat, make sure to copy/move the .xsl that comes with the script into the OUTPUT_DIR, for your own convenience.
--]]
print("** DataStore Export **")
-- local INPUT_DIR = ""
local INPUT_DIR = "D:\\World of Warcraft\\WTF\\Account\\YOUR_ACCOUNT\\SavedVariables"
local OUTPUT_DIR = "E:\\Wow\\Export DataStore"
local USE_XSL = true -- adds a line that refers to a basic .xsl file to display exported content, comment this line if you don't want an xsl reference.
local format = string.format
function strsplit(delimiter, text)
-- source : http://lua-users.org/wiki/SplitJoin
local list = {}
local pos = 1
if string.find("", delimiter, 1) then -- this would result in endless loops
error("delimiter matches empty string!")
end
if delimiter == "." then
delimiter = "%."
end
while 1 do
local first, last = string.find(text, delimiter, pos)
if first then -- found?
table.insert(list, string.sub(text, pos, first-1))
pos = last+1
else
table.insert(list, string.sub(text, pos))
break
end
end
return unpack(list)
end
function CreateDir(name)
os.execute("mkdir " .. name)
end
function ChangeDir(name)
os.execute("chdir " .. name)
end
local rarityColors = {
["9d9d9d"] = 0, -- grey
["ffffff"] = 1, -- white
["1eff00"] = 2, -- green
["0070dd"] = 3, -- blue
["a335ee"] = 4, -- purple
["ff8000"] = 5, -- orange
["e5cc80"] = 7, -- heirloom
}
function GetRarityFromLink(link)
local color = link:sub(5, 10)
if color then
return rarityColors[color]
end
end
-- ** xml utility **
function CreateXMLFile(fileName)
local f = assert(io.open(OUTPUT_DIR .. "\\" .. fileName, "w"))
f:write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n")
if USE_XSL then
f:write("<?xml-stylesheet href=\"DataStore.xsl\" type=\"text/xsl\" ?>\n")
end
return f
end
function WriteXMLLine(file, level, text)
file:write(format("%s%s\n", string.rep("\t", level), text:gsub("&", "&amp;")))
end
function OpenXMLTag(file, level, tag, attributes)
if attributes then
WriteXMLLine(file, level, format("<%s %s>", tag, attributes))
else
WriteXMLLine(file, level, format("<%s>", tag))
end
end
function CloseXMLTag(file, level, tag)
WriteXMLLine(file, level, format("</%s>", tag))
end
function SingleLineTag(file, level, tag, value, attributes)
if attributes then
WriteXMLLine(file, level, format("<%s %s>%s</%s>", tag, attributes, value, tag))
else
WriteXMLLine(file, level, format("<%s>%s</%s>", tag, value, tag))
end
end
local timeFields = {
["lastUpdate"] = true,
["ClientTime"] = true,
["lastLogoutTimestamp"] = true,
["lastCheck"] = true,
["HistoryLastUpdate"] = true,
}
local BottomLevels = {
[-42000] = "Hated",
[-6000] = "Hostile",
[-3000] = "Unfriendly",
[0] = "Neutral",
[3000] = "Friendly",
[9000] = "Honored",
[21000] = "Revered",
[42000] = "Exalted",
}
-- ** Module specific export functions **
local specificExport = {
["DataStore_Achievements"] = {
["Achievements"] = function(file, level, source, character)
OpenXMLTag(file, level, "Achievements")
for index, data in pairs(source) do
local attrib = format("id=\"%s\"", index)
if type(data) == "boolean" and data == true then
data = "true" -- achievement has been completed
local month, day, year = character.CompletionDates[index]:match("(%d+):(%d+):(%d+)")
year = tonumber(year) + 2000
attrib = format("%s completionDate=\"%s/%s/%s\"", attrib, month, day, year)
end
SingleLineTag(file, level+1, "Achievement", data, attrib)
end
CloseXMLTag(file, level, "Achievements")
end,
},
["DataStore_Auctions"] = {
["Auctions"] = function(file, level, source)
OpenXMLTag(file, level, "Auctions")
for index, data in pairs(source) do
local isGoblin, itemID, count, highBidder, startPrice, buyoutPrice, timeLeft = strsplit("|", data)
local attrib = format("count=\"%s\" highBidder=\"%s\" startPrice=\"%s\" buyoutPrice=\"%s\" timeLeft=\"%s\"", count, highBidder, startPrice, buyoutPrice, timeLeft)
if isGoblin == "1" then
attrib = format("%s GoblinAH=\"true\"", attrib)
end
SingleLineTag(file, level+1, "Auction", itemID, attrib)
end
CloseXMLTag(file, level, "Auctions")
end,
["Bids"] = function(file, level, source)
OpenXMLTag(file, level, "Bids")
for index, data in pairs(source) do
local isGoblin, itemID, count, ownerName, bidPrice, buyoutPrice, timeLeft = strsplit("|", data)
local attrib = format("count=\"%s\" ownerName=\"%s\" bidPrice=\"%s\" buyoutPrice=\"%s\" timeLeft=\"%s\"", count, ownerName, bidPrice, buyoutPrice, timeLeft)
if isGoblin == "1" then
attrib = format("%s GoblinAH=\"true\"", attrib)
end
SingleLineTag(file, level+1, "Auction", itemID, attrib)
end
CloseXMLTag(file, level, "Bids")
end,
},
["DataStore_Containers"] = {
["Containers"] = function(file, level, source)
OpenXMLTag(file, level, "Containers")
for bagIndex, bag in pairs(source) do
local bagID = tonumber(bagIndex:sub(4))
OpenXMLTag(file, level+1, "Bag", format("id=\"%s\"", bagID))
for key, value in pairs(bag) do
if type(value) == "number" then
SingleLineTag(file, level+2, key, value)
elseif type(value) == "string" then
SingleLineTag(file, level+2, key, value)
elseif type(value) == "boolean" then
SingleLineTag(file, level+2, key, (value) and "true" or "false")
elseif type(value) == "table" then
if key == "ids" then -- ids is the main table, the two others (links & counts) are complement
OpenXMLTag(file, level+2, "Content")
for slotID, itemID in pairs(value) do
local text = format("Item %d", itemID)
local count = 1
if bag.counts and bag.counts[slotID] then
count = bag.counts[slotID]
end
local attrib = format("slot=\"%s\" count=\"%s\" id=\"%s\"", slotID, count, itemID)
if bag.links and bag.links[slotID] then
local link = bag.links[slotID]
text = link:match("%[(.+)%]") -- this gets the itemName
local rarity = GetRarityFromLink(link)
attrib = format("%s rarity=\"%s\" link=\"%s\"", attrib, rarity, link)
end
SingleLineTag(file, level+3, "Item", text, attrib)
end
CloseXMLTag(file, level+2, "Content")
end
end
end
CloseXMLTag(file, level+1, "Bag")
end
CloseXMLTag(file, level, "Containers")
end,
["Tabs"] = function(file, level, source)
OpenXMLTag(file, level, "Tabs")
for tabID, tab in pairs(source) do
OpenXMLTag(file, level+1, "Tab", format("id=\"%s\"", tabID))
for key, value in pairs(tab) do
if type(value) == "number" then
if timeFields[key] then
SingleLineTag(file, level+2, key, os.date("%m/%d/%Y %X", value))
else
SingleLineTag(file, level+2, key, value)
end
elseif type(value) == "string" then
SingleLineTag(file, level+2, key, value)
elseif type(value) == "boolean" then
SingleLineTag(file, level+2, key, (value) and "true" or "false")
elseif type(value) == "table" then
if key == "ids" then -- ids is the main table, the two others (links & counts) are complement
OpenXMLTag(file, level+2, "Content")
for slotID, itemID in pairs(value) do
local text = format("Item %d", itemID)
local count = 1
if tab.counts and tab.counts[slotID] then
count = tab.counts[slotID]
end
local attrib = format("slot=\"%s\" count=\"%s\" id=\"%s\"", slotID, count, itemID)
if tab.links and tab.links[slotID] then
local link = tab.links[slotID]
text = link:match("%[(.+)%]") -- this gets the itemName
local rarity = GetRarityFromLink(link)
attrib = format("%s rarity=\"%s\" link=\"%s\"", attrib, rarity, link)
end
SingleLineTag(file, level+3, "Item", itemID, attrib)
end
CloseXMLTag(file, level+2, "Content")
end
end
end
CloseXMLTag(file, level+1, "Tab")
end
CloseXMLTag(file, level, "Tabs")
end,
},
["DataStore_Crafts"] = {
["Professions"] = function(file, level, source)
OpenXMLTag(file, level, "Professions")
for professionName, profession in pairs(source) do
OpenXMLTag(file, level+1, "Profession", format("name=\"%s\"", professionName))
for key, value in pairs(profession) do
if type(value) == "number" then
SingleLineTag(file, level+2, key, value)
elseif type(value) == "string" then
SingleLineTag(file, level+2, key, value)
elseif type(value) == "boolean" then
SingleLineTag(file, level+2, key, (value) and "true" or "false")
elseif type(value) == "table" then
if key == "Crafts" then -- there shouldn't be any other
OpenXMLTag(file, level+2, "Crafts")
local currentHeader
for index, craft in ipairs(value) do
local color, info = strsplit("|", craft)
if color == "0" then
if currentHeader then
CloseXMLTag(file, level+3, "Category")
end
OpenXMLTag(file, level+3, "Category", format("name=\"%s\"", info))
currentHeader = info
else
SingleLineTag(file, level+4, "Spell", info)
end
end
if currentHeader then
CloseXMLTag(file, level+3, "Category")
end
CloseXMLTag(file, level+2, "Crafts")
end
end
end
CloseXMLTag(file, level+1, "Profession")
end
CloseXMLTag(file, level, "Professions")
end,
},
["DataStore_Currencies"] = {
["Currencies"] = function(file, level, source)
OpenXMLTag(file, level, "Currencies")
local currentCategory
for index, data in ipairs(source) do
local isHeader, name, count, itemID = strsplit("|", data)
isHeader = (isHeader == "0" and true or nil)
if isHeader then
if currentCategory then
CloseXMLTag(file, level+1, "Category")
end
OpenXMLTag(file, level+1, "Category", format("name=\"%s\"", name))
currentCategory = name
else
SingleLineTag(file, level+2, "Currency", name, format("count=\"%s\" itemID=\"%s\"", count, itemID))
end
end
if currentCategory then
CloseXMLTag(file, level+1, "Category")
end
CloseXMLTag(file, level, "Currencies")
end,
},
["DataStore_Inventory"] = {
["Inventory"] = function(file, level, source)
OpenXMLTag(file, level, "Inventory")
for index, item in pairs(source) do
local attrib = format("index=\"%s\"", index)
local text, itemID
if type(item) == "number" then
itemID = item
text = format("Item %d", itemID)
else
itemID = tonumber(item:match("item:(%d+)"))
text = item:match("%[(.+)%]") -- this gets the itemName
local rarity = GetRarityFromLink(item)
attrib = format("%s rarity=\"%s\" link=\"%s\"", attrib, rarity, item)
end
attrib = format("%s id=\"%s\"", attrib, itemID)
SingleLineTag(file, level+1, "Item", text, attrib)
end
CloseXMLTag(file, level, "Inventory")
end,
},
["DataStore_Mails"] = {
["Mails"] = function(file, level, source)
OpenXMLTag(file, level, "Mails")
for index, mail in pairs(source) do
OpenXMLTag(file, level+1, "Mail")
for key, value in pairs(mail) do
if timeFields[key] then
SingleLineTag(file, level+2, key, os.date("%m/%d/%Y %X", value))
else
SingleLineTag(file, level+2, key, value)
end
end
CloseXMLTag(file, level+1, "Mail")
end
CloseXMLTag(file, level, "Mails")
end,
},
["DataStore_Pets"] = {
["CRITTER"] = function(file, level, source)
OpenXMLTag(file, level, "Companions")
for _, data in pairs(source) do
local modelID, name, spellID, icon = strsplit("|", data)
local attrib = format("name=\"%s\" modelID=\"%s\" icon=\"%s\"", name, modelID, icon)
SingleLineTag(file, level+1, "Spell", spellID, attrib)
end
CloseXMLTag(file, level, "Companions")
end,
["MOUNT"] = function(file, level, source)
OpenXMLTag(file, level, "Mounts")
for _, data in pairs(source) do
local modelID, name, spellID, icon = strsplit("|", data)
local attrib = format("name=\"%s\" modelID=\"%s\" icon=\"%s\"", name, modelID, icon)
SingleLineTag(file, level+1, "Spell", spellID, attrib)
end
CloseXMLTag(file, level, "Mounts")
end,
},
["DataStore_Quests"] = {
["History"] = function(file, level, source)
OpenXMLTag(file, level, "History")
for index, data in pairs(source) do
SingleLineTag(file, level+1, "ID", index)
end
CloseXMLTag(file, level, "History")
end,
["Quests"] = function(file, level, source, character)
OpenXMLTag(file, level, "QuestLog")
for index, data in pairs(source) do
local attrib = format("index=\"%s\"", index)
local text
local isHeader, questTag, groupSize, money = strsplit("|", data)
groupSize = tonumber(groupSize)
money = tonumber(money)
if isHeader == "0" then
text = questTag -- catagory name
attrib = format("%s isHeader=\"true\"", attrib)
else
if questTag ~= "" then
attrib = format("%s tag=\"%s\"", attrib, questTag)
end
end
if groupSize and groupSize > 0 then
attrib = format("%s groupSize=\"%s\"", attrib, groupSize)
end
if money and money > 0 then
attrib = format("%s money=\"%s\"", attrib, money)
end
-- Fully functional, uncomment if there's demand.
local link = character.QuestLinks[index]
if link then
local questID, questLevel = link:match("quest:(%d+):(-?%d+)")
text = link:match("%[(.+)%]") -- this gets the questName
attrib = format("%s id=\"%s\" level=\"%s\"", attrib, questID, questLevel)
end
local rewards = character.Rewards[index]
if rewards then
attrib = format("%s rewards=\"%s\"", attrib, rewards)
end
SingleLineTag(file, level+1, "Quest", text, attrib)
end
CloseXMLTag(file, level, "QuestLog")
end,
},
["DataStore_Reputations"] = {
["Factions"] = function(file, level, source)
OpenXMLTag(file, level, "Factions")
for name, data in pairs(source) do
local bottom, top, earned = strsplit("|", data)
bottom = tonumber(bottom)
top = tonumber(top)
earned = tonumber(earned)
SingleLineTag(file, level+1, "Faction", name, format("rank=\"%s\" numPoints=\"%s\" maxPoints=\"%s\"", BottomLevels[bottom], (earned - bottom), (top - bottom)))
end
CloseXMLTag(file, level, "Factions")
end,
},
["DataStore_Spells"] = {
["Spells"] = function(file, level, source)
OpenXMLTag(file, level, "Spells")
for schoolName, school in pairs(source) do
OpenXMLTag(file, level+1, "School", format("name=\"%s\"", schoolName))
local attrib
for index, value in ipairs(school) do
local id, rank = strsplit("|", value)
attrib = format("index=\"%s\"", index)
if rank ~= "" then
attrib = format("%s rank=\"%s\"", attrib, rank)
end
SingleLineTag(file, level+2, "Spell", id, attrib)
end
CloseXMLTag(file, level+1, "School")
end
CloseXMLTag(file, level, "Spells")
end,
},
["DataStore_Skills"] = {
["Skills"] = function(file, level, source)
OpenXMLTag(file, level, "Skills")
for categoryName, category in pairs(source) do
OpenXMLTag(file, level+1, "Category", format("name=\"%s\"", categoryName))
for skillName, skillData in pairs(category) do
SingleLineTag(file, level+2, "Skill", skillData, format("name=\"%s\"", skillName))
end
CloseXMLTag(file, level+1, "Category")
end
CloseXMLTag(file, level, "Skills")
end,
},
["DataStore_Stats"] = {
["Stats"] = function(file, level, source)
-- to do : improve this, needs some specific code per stat type (if there's demand)
OpenXMLTag(file, level, "Stats")
for name, data in pairs(source) do
SingleLineTag(file, level+1, name, data)
end
CloseXMLTag(file, level, "Stats")
end,
},
["DataStore_Talents"] = {
["Glyphs"] = function(file, level, source)
OpenXMLTag(file, level, "Glyphs")
for index, data in pairs(source) do
local enabled, glyphType, spell, icon, glyphID = strsplit("|", data)
if enabled == "1" and spell ~= "" then
glyphType = (glyphType == "1") and "major" or "minor"
local spec = (index <= 6) and "primary" or "secondary"
local slot = (index > 6) and index - 6 or index
SingleLineTag(file, level+1, "Glyph", glyphID, format("spec=\"%s\" slot=\"%s\" glyphType=\"%s\" spellID=\"%s\" icon=\"%s\"", spec, slot, glyphType, spell, icon))
end
end
CloseXMLTag(file, level, "Glyphs")
end,
["TalentTrees"] = function(file, level, source, character)
OpenXMLTag(file, level, "TalentTrees")
for treeIndex, data in pairs(source) do
local treeName, spec = strsplit("|", treeIndex)
spec = (spec == "1") and "primary" or "secondary"
local talentRef = DataStore_TalentsRefDB.global[character.Class].Trees[treeName].talents -- this points to talent info from the ref table
OpenXMLTag(file, level+1, "TalentTree", format("name=\"%s\" spec=\"%s\"", treeName, spec))
for key, value in pairs(data) do
local id, name, _, _, _, maximumRank = strsplit("|", talentRef[key])
SingleLineTag(file, level+2, "Talent", name, format("index=\"%s\" id=\"%s\" pointsSpent=\"%s\" maximumRank=\"%s\"", key, id, value, maximumRank))
end
CloseXMLTag(file, level+1, "TalentTree")
end
CloseXMLTag(file, level, "TalentTrees")
end,
},
}
function ExportCharacters(moduleName, file)
OpenXMLTag(file, 1, "Characters")
local db = _G[moduleName .."DB"]
local level = 2
for characterKey, character in pairs(db.global.Characters) do
local account, realm, characterName = strsplit(".", characterKey)
OpenXMLTag(file, level, format("Character name=\"%s\" realm=\"%s\" account=\"%s\"", characterName, realm, account))
for key, value in pairs(character) do
if type(value) == "number" then
if timeFields[key] then
SingleLineTag(file, level+1, key, os.date("%m/%d/%Y %X", value))
else
SingleLineTag(file, level+1, key, value)
end
elseif type(value) == "string" then
SingleLineTag(file, level+1, key, value)
elseif type(value) == "table" then
if specificExport[moduleName] and specificExport[moduleName][key] then -- ex: if specificExport["DataStore_Reputations"]["Factions"] exists, call it
specificExport[moduleName][key](file, level+1, value, character)
end
end
end
CloseXMLTag(file, level, "Character")
end
CloseXMLTag(file, 1, "Characters")
end
function ExportGuilds(moduleName, file)
OpenXMLTag(file, 1, "Guilds")
local db = _G[moduleName .."DB"]
local level = 2
for guildKey, guild in pairs(db.global.Guilds) do
local account, realm, guildName = strsplit(".", guildKey)
OpenXMLTag(file, level, format("Guild name=\"%s\" realm=\"%s\" account=\"%s\"", guildName, realm, account))
for key, value in pairs(guild) do
if type(value) == "number" then
SingleLineTag(file, level+1, key, value)
elseif type(value) == "string" then
SingleLineTag(file, level+1, key, value)
elseif type(value) == "table" then
if specificExport[moduleName] and specificExport[moduleName][key] then -- ex: if specificExport["DataStore_Reputations"]["Factions"] exists, call it
specificExport[moduleName][key](file, level+1, value)
end
end
end
CloseXMLTag(file, level, "Guild")
end
CloseXMLTag(file, 1, "Guilds")
end
function ExportModule(moduleName)
dofile(INPUT_DIR .. "\\"..moduleName .. ".lua")
print(format("Exporting %s ...", moduleName))
local f = CreateXMLFile(moduleName..".xml")
OpenXMLTag(f, 0, format("DataStorePage Title=\"%s\"", moduleName))
ExportCharacters(moduleName, f)
if moduleName == "DataStore" or moduleName == "DataStore_Containers" then
ExportGuilds(moduleName, f)
end
CloseXMLTag(f, 0, "DataStorePage")
f:close()
end
local modules = {
"DataStore",
"DataStore_Achievements",
"DataStore_Auctions",
"DataStore_Characters",
"DataStore_Containers",
"DataStore_Crafts",
"DataStore_Currencies",
"DataStore_Inventory",
"DataStore_Mails",
"DataStore_Pets",
"DataStore_Quests",
"DataStore_Reputations",
"DataStore_Skills",
"DataStore_Spells",
"DataStore_Stats",
"DataStore_Talents",
}
for _, moduleName in pairs(modules) do
ExportModule(moduleName)
end
print("Export complete !")
+2
View File
@@ -0,0 +1,2 @@
All Rights Reserved unless otherwise explicitly stated.
+12
View File
@@ -0,0 +1,12 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "deDE" )
if not L then return end
L["Disabled"] = "Deaktiviert"
L["Enabled"] = "Aktiviert"
L["Memory used for %d |4character:characters;:"] = "Verwendeter Speicher für %d |4charakter:charaktere;:"
L["HIDE_START_GUILD_TEXT"] = "Verstecke Starter-Gilden (Rising-Gods)"
L["HIDE_START_GUILD_TITLE"] = "Verstecke Starter-Gilden"
L["HIDE_START_GUILD_ENABLED"] = "Addon-Kommunikation aus den Gilden 'Community Allianz' & 'Community Horde' werden ignoriert."
L["HIDE_START_GUILD_DISABLED"] = "Keine Gilde wird ignoriert."
+15
View File
@@ -0,0 +1,15 @@
local debug = false
--[===[@debug@
debug = true
--@end-debug@]===]
local L = LibStub("AceLocale-3.0"):NewLocale("DataStore", "enUS", true, debug)
L["Disabled"] = true
L["Enabled"] = true
L["Memory used for %d |4character:characters;:"] = true
L["HIDE_START_GUILD_TEXT"] = "Hide starter guilds (Rising-Gods)"
L["HIDE_START_GUILD_TITLE"] = "Hide starter guilds"
L["HIDE_START_GUILD_ENABLED"] = "Addon messages from the guilds 'Community Allianz' & 'Community Horde' will be ignored."
L["HIDE_START_GUILD_DISABLED"] = "No guild will be ignored."
+8
View File
@@ -0,0 +1,8 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "esES" )
if not L then return end
L["Disabled"] = "Desactivado"
L["Enabled"] = "Activado"
L["Memory used for %d |4character:characters;:"] = "Memoria utilizada para %d |4personaje:personajes;:"
+5
View File
@@ -0,0 +1,5 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "esMX" )
if not L then return end
+8
View File
@@ -0,0 +1,8 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "frFR" )
if not L then return end
L["Disabled"] = "Désactivée"
L["Enabled"] = "Activée"
L["Memory used for %d |4character:characters;:"] = "Mémoire utilisée pour %d |4personnage:personnages;:"
+5
View File
@@ -0,0 +1,5 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "koKR" )
if not L then return end
+7
View File
@@ -0,0 +1,7 @@
local L = LibStub("AceLocale-3.0"):NewLocale("DataStore", "enUS", true, true)
if not L then return end
L["Enabled"] = true
L["Disabled"] = true
L["Memory used for %d |4character:characters;:"] = true
+5
View File
@@ -0,0 +1,5 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "ruRU" )
if not L then return end
+5
View File
@@ -0,0 +1,5 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "zhCN" )
if not L then return end
+8
View File
@@ -0,0 +1,8 @@
local L = LibStub("AceLocale-3.0"):NewLocale( "DataStore", "zhTW" )
if not L then return end
L["Disabled"] = "禁用"
L["Enabled"] = "啟用"
L["Memory used for %d |4character:characters;:"] = "記憶容量已使用 %d |4角色:角色;:"
+237
View File
@@ -0,0 +1,237 @@
if not DataStore then return end
local addonName = ...
local addon = _G[addonName]
local L = LibStub("AceLocale-3.0"):GetLocale(addonName)
local addonList = {
"DataStore",
"DataStore_Achievements",
"DataStore_Auctions",
"DataStore_Characters",
"DataStore_Containers",
"DataStore_Crafts",
"DataStore_Currencies",
"DataStore_Inventory",
"DataStore_Mails",
"DataStore_Pets",
"DataStore_Quests",
"DataStore_Reputations",
"DataStore_Skills",
"DataStore_Spells",
"DataStore_Stats",
"DataStore_Talents",
}
local WHITE = "|cFFFFFFFF"
local TEAL = "|cFF00FF9A"
local ORANGE = "|cFFFF8400"
local GREEN = "|cFF00FF00"
local RED = "|cFFFF0000"
-- *** DataStore's own help topics ***
local help = {
{ name = "General",
questions = {
"What is DataStore?",
"What are the advantages of this approach?",
"What do all these modules do? Do I need to enable them all?",
"How should I update DataStore and its modules?",
},
answers = {
"DataStore is the main component of a series of addons that serve as data repositories in game. Their respective purpose is to offer scanning and storing services to other addons.",
format("%s\n\n%s\n%s\n%s\n%s",
"There are multiple advantages, for both players and developers:",
"- Data is scanned only once for all client addons (performance gain).",
"- Data is stored only once for all client addons (memory gain).",
"- Add-on authors can spend more time coding higher level features.",
"- Each module is an independant add-on, and therefore has its own SavedVariables file, meaning that you could clean a module's data without disturbing other modules."
),
format("%s\n\n%s",
"'DataStore' is the main module, client add-ons should have a dependency on it, it should therefore remain enabled all the time, as it is the interface used to access data from the various modules.",
"The other modules are technically all optional, and could be enabled/disabled according to your needs. However, for the time being, Altoholic has not yet been modified to fully support this approach. It will happen soon(tm)!"
),
format("%s\n\n%s",
"Altoholic is always packaged with the latest versions, most users should upgrade using this method.",
"If you really can't wait, refer to the add-on's homepage in the 'About' panel. The homepage contains links to all the modules' projects on CurseForge. Alphas are available there for advanced users who are courageous enough to test new bu.. I mean new features!"
)
}
},
{ name = "Clearing data",
questions = {
"How do I clear data from DataStore?",
"What if I want to get rid of Saved Variables?",
},
answers = {
"At this point, characters and guilds can be erased from Altoholic's UI.",
format("%s\n\n%s",
"Databases are located in |cFFFFFFFFWTF \\ Account \\ <your_account> \\ SavedVariables|r.",
format("If you deem it necessary, you can delete %sDataStore.lua|r and all %sDataStore_*.lua|r", GREEN, GREEN)
),
}
},
}
-- *** Utility functions ***
local infoText
-- very basic support for info panes (FAQ, sections, text.. ), improve later if necessary, see if a lib exists to do that, etc..
local function AddHelpLine(str)
infoText = format("%s%s\n", infoText, str)
end
local function AddSection(section)
AddHelpLine(format("%s|r\n", ORANGE..section))
end
local function AddQuestion(question)
AddHelpLine(format("%s) %s", WHITE.."Q", TEAL..question))
end
local function AddAnswer(answer)
AddHelpLine(format("%s) |r%s\n", WHITE.."A", answer))
end
local function AddBulletedText(text)
AddHelpLine(format("%s-|r %s\n", WHITE, text))
end
function addon:SetupInfoPanel(info, helpFrame)
infoText = ""
for _, section in ipairs(info) do
AddSection(section.name)
if section.questions then
for i = 1, #section.questions do
AddQuestion(section.questions[i])
AddAnswer(section.answers[i])
end
elseif section.bulletedList then
for _, text in ipairs(section.bulletedList) do
AddBulletedText(text)
end
elseif section.textLines then
for _, text in ipairs(section.textLines) do
AddHelpLine(text)
end
end
end
helpFrame:SetText(infoText)
infoText = nil
end
function addon:AddOptionCategory(frame, name, parent)
-- tiny wrapper to add categories in Blizzard's options panel
frame.name = name
frame.parent = parent
InterfaceOptions_AddCategory(frame)
end
function addon:SetupOptions()
addon:AddOptionCategory(DataStoreGeneralOptions, addonName)
LibStub("LibAboutPanel").new(addonName, addonName);
addon:AddOptionCategory(DataStoreHelp, HELP_LABEL, addonName) -- more categories will be added as the various modules' OnEnable() get called.
addon:SetupInfoPanel(help, DataStoreHelp_Text)
DataStoreGeneralOptions_Title:SetText(TEAL..format("DataStore %s", DataStore.Version))
DataStoreGeneralOptions_HideStartGuildsText:SetText(L["HIDE_START_GUILD_TEXT"])
DataStore:SetCheckBoxTooltip(DataStoreGeneralOptions_HideStartGuilds, L["HIDE_START_GUILD_TITLE"], L["HIDE_START_GUILD_ENABLED"], L["HIDE_START_GUILD_DISABLED"])
DataStoreGeneralOptions_HideStartGuilds:SetChecked(DataStore:GetOption("DataStore", "HideStartGuilds"))
-- manually adjust the width of a few panes, as resolution/scale may have an impact on the layout
local width = InterfaceOptionsFramePanelContainer:GetWidth() - 45
DataStoreHelp:SetWidth(width)
DataStoreHelp_ScrollFrame:SetWidth(width)
DataStoreHelp_Text:SetWidth(width-35)
end
function addon:ToggleOption(frame, module, option)
if frame:GetChecked() then
addon:SetOption(module, option, 1)
else
addon:SetOption(module, option, 0)
end
end
function addon:UpdateMyMemoryUsage()
collectgarbage()
addon:UpdateMemoryUsage(addonList, DataStoreGeneralOptions, format(L["Memory used for %d |4character:characters;:"], addon:GetNumCharactersInDB()))
end
function addon:UpdateMemoryUsage(addons, parent, totalText)
UpdateAddOnMemoryUsage()
local memInKb
local totalMem = 0
local text
local list = ""
local name = parent:GetName()
-- title
_G[name .. "_AddonsText"]:SetText(ORANGE..ADDONS)
-- headers
for index, dsModule in ipairs(addons) do
list = format("%s%s:\n", list, dsModule)
end
list = format("%s\n%s", list, totalText)
_G[name .. "_AddonsList"]:SetText(list)
-- memory used
list = ""
for index, module in ipairs(addons) do
if IsAddOnLoaded(module) then -- module is enabled
memInKb = GetAddOnMemoryUsage(module)
totalMem = totalMem + memInKb
if memInKb < 1024 then
text = format("%s%.0f %sKB", GREEN, memInKb, WHITE)
else
text = format("%s%.2f %sMB", GREEN, memInKb/1024, WHITE)
end
else -- module is disabled
text = RED..ADDON_DISABLED
end
list = format("%s%s\n", list, text)
end
list = format("%s\n%s", list, format("%s%.2f %sMB", GREEN, totalMem/1024, WHITE))
_G[name .. "_AddonsMem"]:SetText(list)
end
function addon:SetCheckBoxTooltip(frame, title, whenEnabled, whenDisabled)
frame.tooltipText = title
frame.tooltipRequirement = format("%s|r:\n%s\n\n%s|r:\n%s", GREEN..L["Enabled"], whenEnabled, RED..L["Disabled"], whenDisabled)
end
local OptionsPanelWidth, OptionsPanelHeight
local lastOptionsPanelWidth = 0
local lastOptionsPanelHeight = 0
function addon:OnUpdate(self, mandatoryResize)
OptionsPanelWidth = InterfaceOptionsFramePanelContainer:GetWidth()
OptionsPanelHeight = InterfaceOptionsFramePanelContainer:GetHeight()
if not mandatoryResize then -- if resize is not mandatory, allow exit
if OptionsPanelWidth == lastOptionsPanelWidth and OptionsPanelHeight == lastOptionsPanelHeight then return end -- no size change ? exit
end
lastOptionsPanelWidth = OptionsPanelWidth
lastOptionsPanelHeight = OptionsPanelHeight
DataStoreHelp:SetWidth(OptionsPanelWidth-45)
DataStoreHelp_ScrollFrame:SetWidth(OptionsPanelWidth-45)
DataStoreHelp:SetHeight(OptionsPanelHeight-30)
DataStoreHelp_ScrollFrame:SetHeight(OptionsPanelHeight-30)
DataStoreHelp_Text:SetWidth(OptionsPanelWidth-80)
end
+158
View File
@@ -0,0 +1,158 @@
<Ui xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.blizzard.com/wow/ui/">
<Script file="Options.lua"></Script>
<Frame name="DataStoreGeneralOptions" hidden="true">
<Size>
<AbsDimension x="615" y="306"/>
</Size>
<Layers>
<Layer level="OVERLAY">
<FontString name="$parent_Title" inherits="GameFontHighlightLarge" justifyH="CENTER">
<Size>
<AbsDimension x="400" y="30"/>
</Size>
<Anchors>
<Anchor point="TOP" />
</Anchors>
</FontString>
<FontString name="$parent_AddonsText" inherits="GameFontNormal" justifyH="LEFT">
<Size>
<AbsDimension x="60" y="20"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-40"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parent_AddonsList" inherits="GameFontNormal" justifyH="LEFT" justifyV="TOP">
<Size>
<AbsDimension x="220" y="240"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent_AddonsText" relativePoint="BOTTOMLEFT">
<Offset>
<AbsDimension x="20" y="-20"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parent_AddonsMem" inherits="GameFontNormal" justifyH="RIGHT" justifyV="TOP">
<Size>
<AbsDimension x="60" y="240"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent_AddonsList" relativePoint="TOPRIGHT">
<Offset>
<AbsDimension x="20" y="0"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnShow>
DataStore:UpdateMyMemoryUsage()
</OnShow>
</Scripts>
<Frames>
<Button name="$parent_Refresh" inherits="UIPanelButtonTemplate" text="Refresh">
<Size>
<AbsDimension x="100" y="24"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent_AddonsList" relativePoint="BOTTOMLEFT" >
<Offset>
<AbsDimension x="0" y="-10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
DataStore:UpdateMyMemoryUsage()
</OnClick>
</Scripts>
</Button>
<CheckButton name="$parent_HideStartGuilds" inherits="InterfaceOptionsSmallCheckButtonTemplate">
<Size>
<AbsDimension x="20" y="20"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent_Refresh" relativePoint="BOTTOMLEFT" >
<Offset>
<AbsDimension x="0" y="-10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
DataStore:ToggleOption(self, "DataStore", "HideStartGuilds")
</OnClick>
</Scripts>
</CheckButton>
</Frames>
</Frame>
<Frame name="DataStoreHelp" hidden="true">
<Size>
<AbsDimension x="615" y="400"/>
</Size>
<Scripts>
<OnUpdate>
DataStore:OnUpdate(self)
</OnUpdate>
<OnShow>
DataStore:OnUpdate(self, true)
</OnShow>
</Scripts>
<Frames>
<ScrollFrame name="$parent_ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Size>
<AbsDimension x="615" y="400"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="10" y="-20"/>
</Offset>
</Anchor>
</Anchors>
<ScrollChild>
<Frame name="$parentScrollChildFrame">
<Size>
<AbsDimension x="270" y="304"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="0" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString name="DataStoreHelp_Text" inherits="GameFontNormal" justifyH="LEFT" justifyV="TOP">
<Size>
<AbsDimension x="580" />
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="0" y="0"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
</Ui>
+17
View File
@@ -0,0 +1,17 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="libs\libstub\libstub.lua"/>
<Include file="libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="libs\AceAddon-3.0\AceAddon-3.0.xml"/>
<Include file="libs\AceConsole-3.0\AceConsole-3.0.xml"/>
<Include file="libs\AceDB-3.0\AceDB-3.0.xml"/>
<Include file="libs\AceEvent-3.0\AceEvent-3.0.xml"/>
<Include file="libs\AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="libs\AceLocale-3.0\AceLocale-3.0.xml"/>
<Include file="libs\AceComm-3.0\AceComm-3.0.xml" />
<Include file="libs\AceSerializer-3.0\AceSerializer-3.0.xml" />
<Include file="libs\LibAboutPanel\lib.xml"/>
<Script file="libs\LibPeriodicTable-3.1\LibPeriodicTable-3.1.lua"/>
</Ui>
@@ -0,0 +1,649 @@
--- **AceAddon-3.0** provides a template for creating addon objects.
-- It'll provide you with a set of callback functions that allow you to simplify the loading
-- process of your addon.\\
-- Callbacks provided are:\\
-- * **OnInitialize**, which is called directly after the addon is fully loaded.
-- * **OnEnable** which gets called during the PLAYER_LOGIN event, when most of the data provided by the game is already present.
-- * **OnDisable**, which is only called when your addon is manually being disabled.
-- @usage
-- -- A small (but complete) addon, that doesn't do anything,
-- -- but shows usage of the callbacks.
-- local MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon")
--
-- function MyAddon:OnInitialize()
-- -- do init tasks here, like loading the Saved Variables,
-- -- or setting up slash commands.
-- end
--
-- function MyAddon:OnEnable()
-- -- Do more initialization here, that really enables the use of your addon.
-- -- Register Events, Hook functions, Create Frames, Get information from
-- -- the game that wasn't available in OnInitialize
-- end
--
-- function MyAddon:OnDisable()
-- -- Unhook, Unregister Events, Hide frames that you created.
-- -- You would probably only use an OnDisable if you want to
-- -- build a "standby" mode, or be able to toggle modules on/off.
-- end
-- @class file
-- @name AceAddon-3.0.lua
-- @release $Id$
local MAJOR, MINOR = "AceAddon-3.0", 13
local AceAddon, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceAddon then return end -- No Upgrade needed.
AceAddon.frame = AceAddon.frame or CreateFrame("Frame", "AceAddon30Frame") -- Our very own frame
AceAddon.addons = AceAddon.addons or {} -- addons in general
AceAddon.statuses = AceAddon.statuses or {} -- statuses of addon.
AceAddon.initializequeue = AceAddon.initializequeue or {} -- addons that are new and not initialized
AceAddon.enablequeue = AceAddon.enablequeue or {} -- addons that are initialized and waiting to be enabled
AceAddon.embeds = AceAddon.embeds or setmetatable({}, {__index = function(tbl, key) tbl[key] = {} return tbl[key] end }) -- contains a list of libraries embedded in an addon
-- Lua APIs
local tinsert, tconcat, tremove = table.insert, table.concat, table.remove
local fmt, tostring = string.format, tostring
local select, pairs, next, type, unpack = select, pairs, next, type, unpack
local loadstring, assert, error = loadstring, assert, error
local setmetatable, getmetatable, rawset, rawget = setmetatable, getmetatable, rawset, rawget
--[[
xpcall safecall implementation
]]
local xpcall = xpcall
local function errorhandler(err)
return geterrorhandler()(err)
end
local function safecall(func, ...)
-- we check to see if the func is passed is actually a function here and don't error when it isn't
-- this safecall is used for optional functions like OnInitialize OnEnable etc. When they are not
-- present execution should continue without hinderance
if type(func) == "function" then
return xpcall(func, errorhandler, ...)
end
end
-- local functions that will be implemented further down
local Enable, Disable, EnableModule, DisableModule, Embed, NewModule, GetModule, GetName, SetDefaultModuleState, SetDefaultModuleLibraries, SetEnabledState, SetDefaultModulePrototype
-- used in the addon metatable
local function addontostring( self ) return self.name end
-- Check if the addon is queued for initialization
local function queuedForInitialization(addon)
for i = 1, #AceAddon.initializequeue do
if AceAddon.initializequeue[i] == addon then
return true
end
end
return false
end
--- Create a new AceAddon-3.0 addon.
-- Any libraries you specified will be embeded, and the addon will be scheduled for
-- its OnInitialize and OnEnable callbacks.
-- The final addon object, with all libraries embeded, will be returned.
-- @paramsig [object ,]name[, lib, ...]
-- @param object Table to use as a base for the addon (optional)
-- @param name Name of the addon object to create
-- @param lib List of libraries to embed into the addon
-- @usage
-- -- Create a simple addon object
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceEvent-3.0")
--
-- -- Create a Addon object based on the table of a frame
-- local MyFrame = CreateFrame("Frame")
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon(MyFrame, "MyAddon", "AceEvent-3.0")
function AceAddon:NewAddon(objectorname, ...)
local object,name
local i=1
if type(objectorname)=="table" then
object=objectorname
name=...
i=2
else
name=objectorname
end
if type(name)~="string" then
error(("Usage: NewAddon([object,] name, [lib, lib, lib, ...]): 'name' - string expected got '%s'."):format(type(name)), 2)
end
if self.addons[name] then
error(("Usage: NewAddon([object,] name, [lib, lib, lib, ...]): 'name' - Addon '%s' already exists."):format(name), 2)
end
object = object or {}
object.name = name
local addonmeta = {}
local oldmeta = getmetatable(object)
if oldmeta then
for k, v in pairs(oldmeta) do addonmeta[k] = v end
end
addonmeta.__tostring = addontostring
setmetatable( object, addonmeta )
self.addons[name] = object
object.modules = {}
object.orderedModules = {}
object.defaultModuleLibraries = {}
Embed( object ) -- embed NewModule, GetModule methods
self:EmbedLibraries(object, select(i,...))
-- add to queue of addons to be initialized upon ADDON_LOADED
tinsert(self.initializequeue, object)
return object
end
--- Get the addon object by its name from the internal AceAddon registry.
-- Throws an error if the addon object cannot be found (except if silent is set).
-- @param name unique name of the addon object
-- @param silent if true, the addon is optional, silently return nil if its not found
-- @usage
-- -- Get the Addon
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
function AceAddon:GetAddon(name, silent)
if not silent and not self.addons[name] then
error(("Usage: GetAddon(name): 'name' - Cannot find an AceAddon '%s'."):format(tostring(name)), 2)
end
return self.addons[name]
end
-- - Embed a list of libraries into the specified addon.
-- This function will try to embed all of the listed libraries into the addon
-- and error if a single one fails.
--
-- **Note:** This function is for internal use by :NewAddon/:NewModule
-- @paramsig addon, [lib, ...]
-- @param addon addon object to embed the libs in
-- @param lib List of libraries to embed into the addon
function AceAddon:EmbedLibraries(addon, ...)
for i=1,select("#", ... ) do
local libname = select(i, ...)
self:EmbedLibrary(addon, libname, false, 4)
end
end
-- - Embed a library into the addon object.
-- This function will check if the specified library is registered with LibStub
-- and if it has a :Embed function to call. It'll error if any of those conditions
-- fails.
--
-- **Note:** This function is for internal use by :EmbedLibraries
-- @paramsig addon, libname[, silent[, offset]]
-- @param addon addon object to embed the library in
-- @param libname name of the library to embed
-- @param silent marks an embed to fail silently if the library doesn't exist (optional)
-- @param offset will push the error messages back to said offset, defaults to 2 (optional)
function AceAddon:EmbedLibrary(addon, libname, silent, offset)
local lib = LibStub:GetLibrary(libname, true)
if not lib and not silent then
error(("Usage: EmbedLibrary(addon, libname, silent, offset): 'libname' - Cannot find a library instance of %q."):format(tostring(libname)), offset or 2)
elseif lib and type(lib.Embed) == "function" then
lib:Embed(addon)
tinsert(self.embeds[addon], libname)
return true
elseif lib then
error(("Usage: EmbedLibrary(addon, libname, silent, offset): 'libname' - Library '%s' is not Embed capable"):format(libname), offset or 2)
end
end
--- Return the specified module from an addon object.
-- Throws an error if the addon object cannot be found (except if silent is set)
-- @name //addon//:GetModule
-- @paramsig name[, silent]
-- @param name unique name of the module
-- @param silent if true, the module is optional, silently return nil if its not found (optional)
-- @usage
-- -- Get the Addon
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- -- Get the Module
-- MyModule = MyAddon:GetModule("MyModule")
function GetModule(self, name, silent)
if not self.modules[name] and not silent then
error(("Usage: GetModule(name, silent): 'name' - Cannot find module '%s'."):format(tostring(name)), 2)
end
return self.modules[name]
end
local function IsModuleTrue(self) return true end
--- Create a new module for the addon.
-- The new module can have its own embeded libraries and/or use a module prototype to be mixed into the module.\\
-- A module has the same functionality as a real addon, it can have modules of its own, and has the same API as
-- an addon object.
-- @name //addon//:NewModule
-- @paramsig name[, prototype|lib[, lib, ...]]
-- @param name unique name of the module
-- @param prototype object to derive this module from, methods and values from this table will be mixed into the module (optional)
-- @param lib List of libraries to embed into the addon
-- @usage
-- -- Create a module with some embeded libraries
-- MyModule = MyAddon:NewModule("MyModule", "AceEvent-3.0", "AceHook-3.0")
--
-- -- Create a module with a prototype
-- local prototype = { OnEnable = function(self) print("OnEnable called!") end }
-- MyModule = MyAddon:NewModule("MyModule", prototype, "AceEvent-3.0", "AceHook-3.0")
function NewModule(self, name, prototype, ...)
if type(name) ~= "string" then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'name' - string expected got '%s'."):format(type(name)), 2) end
if type(prototype) ~= "string" and type(prototype) ~= "table" and type(prototype) ~= "nil" then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'prototype' - table (prototype), string (lib) or nil expected got '%s'."):format(type(prototype)), 2) end
if self.modules[name] then error(("Usage: NewModule(name, [prototype, [lib, lib, lib, ...]): 'name' - Module '%s' already exists."):format(name), 2) end
-- modules are basically addons. We treat them as such. They will be added to the initializequeue properly as well.
-- NewModule can only be called after the parent addon is present thus the modules will be initialized after their parent is.
local module = AceAddon:NewAddon(fmt("%s_%s", self.name or tostring(self), name))
module.IsModule = IsModuleTrue
module:SetEnabledState(self.defaultModuleState)
module.moduleName = name
if type(prototype) == "string" then
AceAddon:EmbedLibraries(module, prototype, ...)
else
AceAddon:EmbedLibraries(module, ...)
end
AceAddon:EmbedLibraries(module, unpack(self.defaultModuleLibraries))
if not prototype or type(prototype) == "string" then
prototype = self.defaultModulePrototype or nil
end
if type(prototype) == "table" then
local mt = getmetatable(module)
mt.__index = prototype
setmetatable(module, mt) -- More of a Base class type feel.
end
safecall(self.OnModuleCreated, self, module) -- Was in Ace2 and I think it could be a cool thing to have handy.
self.modules[name] = module
tinsert(self.orderedModules, module)
return module
end
--- Returns the real name of the addon or module, without any prefix.
-- @name //addon//:GetName
-- @paramsig
-- @usage
-- print(MyAddon:GetName())
-- -- prints "MyAddon"
function GetName(self)
return self.moduleName or self.name
end
--- Enables the Addon, if possible, return true or false depending on success.
-- This internally calls AceAddon:EnableAddon(), thus dispatching a OnEnable callback
-- and enabling all modules of the addon (unless explicitly disabled).\\
-- :Enable() also sets the internal `enableState` variable to true
-- @name //addon//:Enable
-- @paramsig
-- @usage
-- -- Enable MyModule
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- MyModule = MyAddon:GetModule("MyModule")
-- MyModule:Enable()
function Enable(self)
self:SetEnabledState(true)
-- nevcairiel 2013-04-27: don't enable an addon/module if its queued for init still
-- it'll be enabled after the init process
if not queuedForInitialization(self) then
return AceAddon:EnableAddon(self)
end
end
--- Disables the Addon, if possible, return true or false depending on success.
-- This internally calls AceAddon:DisableAddon(), thus dispatching a OnDisable callback
-- and disabling all modules of the addon.\\
-- :Disable() also sets the internal `enableState` variable to false
-- @name //addon//:Disable
-- @paramsig
-- @usage
-- -- Disable MyAddon
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- MyAddon:Disable()
function Disable(self)
self:SetEnabledState(false)
return AceAddon:DisableAddon(self)
end
--- Enables the Module, if possible, return true or false depending on success.
-- Short-hand function that retrieves the module via `:GetModule` and calls `:Enable` on the module object.
-- @name //addon//:EnableModule
-- @paramsig name
-- @usage
-- -- Enable MyModule using :GetModule
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- MyModule = MyAddon:GetModule("MyModule")
-- MyModule:Enable()
--
-- -- Enable MyModule using the short-hand
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- MyAddon:EnableModule("MyModule")
function EnableModule(self, name)
local module = self:GetModule( name )
return module:Enable()
end
--- Disables the Module, if possible, return true or false depending on success.
-- Short-hand function that retrieves the module via `:GetModule` and calls `:Disable` on the module object.
-- @name //addon//:DisableModule
-- @paramsig name
-- @usage
-- -- Disable MyModule using :GetModule
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- MyModule = MyAddon:GetModule("MyModule")
-- MyModule:Disable()
--
-- -- Disable MyModule using the short-hand
-- MyAddon = LibStub("AceAddon-3.0"):GetAddon("MyAddon")
-- MyAddon:DisableModule("MyModule")
function DisableModule(self, name)
local module = self:GetModule( name )
return module:Disable()
end
--- Set the default libraries to be mixed into all modules created by this object.
-- Note that you can only change the default module libraries before any module is created.
-- @name //addon//:SetDefaultModuleLibraries
-- @paramsig lib[, lib, ...]
-- @param lib List of libraries to embed into the addon
-- @usage
-- -- Create the addon object
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon")
-- -- Configure default libraries for modules (all modules need AceEvent-3.0)
-- MyAddon:SetDefaultModuleLibraries("AceEvent-3.0")
-- -- Create a module
-- MyModule = MyAddon:NewModule("MyModule")
function SetDefaultModuleLibraries(self, ...)
if next(self.modules) then
error("Usage: SetDefaultModuleLibraries(...): cannot change the module defaults after a module has been registered.", 2)
end
self.defaultModuleLibraries = {...}
end
--- Set the default state in which new modules are being created.
-- Note that you can only change the default state before any module is created.
-- @name //addon//:SetDefaultModuleState
-- @paramsig state
-- @param state Default state for new modules, true for enabled, false for disabled
-- @usage
-- -- Create the addon object
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon")
-- -- Set the default state to "disabled"
-- MyAddon:SetDefaultModuleState(false)
-- -- Create a module and explicilty enable it
-- MyModule = MyAddon:NewModule("MyModule")
-- MyModule:Enable()
function SetDefaultModuleState(self, state)
if next(self.modules) then
error("Usage: SetDefaultModuleState(state): cannot change the module defaults after a module has been registered.", 2)
end
self.defaultModuleState = state
end
--- Set the default prototype to use for new modules on creation.
-- Note that you can only change the default prototype before any module is created.
-- @name //addon//:SetDefaultModulePrototype
-- @paramsig prototype
-- @param prototype Default prototype for the new modules (table)
-- @usage
-- -- Define a prototype
-- local prototype = { OnEnable = function(self) print("OnEnable called!") end }
-- -- Set the default prototype
-- MyAddon:SetDefaultModulePrototype(prototype)
-- -- Create a module and explicitly Enable it
-- MyModule = MyAddon:NewModule("MyModule")
-- MyModule:Enable()
-- -- should print "OnEnable called!" now
-- @see NewModule
function SetDefaultModulePrototype(self, prototype)
if next(self.modules) then
error("Usage: SetDefaultModulePrototype(prototype): cannot change the module defaults after a module has been registered.", 2)
end
if type(prototype) ~= "table" then
error(("Usage: SetDefaultModulePrototype(prototype): 'prototype' - table expected got '%s'."):format(type(prototype)), 2)
end
self.defaultModulePrototype = prototype
end
--- Set the state of an addon or module
-- This should only be called before any enabling actually happend, e.g. in/before OnInitialize.
-- @name //addon//:SetEnabledState
-- @paramsig state
-- @param state the state of an addon or module (enabled=true, disabled=false)
function SetEnabledState(self, state)
self.enabledState = state
end
--- Return an iterator of all modules associated to the addon.
-- @name //addon//:IterateModules
-- @paramsig
-- @usage
-- -- Enable all modules
-- for name, module in MyAddon:IterateModules() do
-- module:Enable()
-- end
local function IterateModules(self) return pairs(self.modules) end
-- Returns an iterator of all embeds in the addon
-- @name //addon//:IterateEmbeds
-- @paramsig
local function IterateEmbeds(self) return pairs(AceAddon.embeds[self]) end
--- Query the enabledState of an addon.
-- @name //addon//:IsEnabled
-- @paramsig
-- @usage
-- if MyAddon:IsEnabled() then
-- MyAddon:Disable()
-- end
local function IsEnabled(self) return self.enabledState end
local mixins = {
NewModule = NewModule,
GetModule = GetModule,
Enable = Enable,
Disable = Disable,
EnableModule = EnableModule,
DisableModule = DisableModule,
IsEnabled = IsEnabled,
SetDefaultModuleLibraries = SetDefaultModuleLibraries,
SetDefaultModuleState = SetDefaultModuleState,
SetDefaultModulePrototype = SetDefaultModulePrototype,
SetEnabledState = SetEnabledState,
IterateModules = IterateModules,
IterateEmbeds = IterateEmbeds,
GetName = GetName,
}
local function IsModule(self) return false end
local pmixins = {
defaultModuleState = true,
enabledState = true,
IsModule = IsModule,
}
-- Embed( target )
-- target (object) - target object to embed aceaddon in
--
-- this is a local function specifically since it's meant to be only called internally
function Embed(target, skipPMixins)
for k, v in pairs(mixins) do
target[k] = v
end
if not skipPMixins then
for k, v in pairs(pmixins) do
target[k] = target[k] or v
end
end
end
-- - Initialize the addon after creation.
-- This function is only used internally during the ADDON_LOADED event
-- It will call the **OnInitialize** function on the addon object (if present),
-- and the **OnEmbedInitialize** function on all embeded libraries.
--
-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing.
-- @param addon addon object to intialize
function AceAddon:InitializeAddon(addon)
safecall(addon.OnInitialize, addon)
local embeds = self.embeds[addon]
for i = 1, #embeds do
local lib = LibStub:GetLibrary(embeds[i], true)
if lib then safecall(lib.OnEmbedInitialize, lib, addon) end
end
-- we don't call InitializeAddon on modules specifically, this is handled
-- from the event handler and only done _once_
end
-- - Enable the addon after creation.
-- Note: This function is only used internally during the PLAYER_LOGIN event, or during ADDON_LOADED,
-- if IsLoggedIn() already returns true at that point, e.g. for LoD Addons.
-- It will call the **OnEnable** function on the addon object (if present),
-- and the **OnEmbedEnable** function on all embeded libraries.\\
-- This function does not toggle the enable state of the addon itself, and will return early if the addon is disabled.
--
-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing.
-- Use :Enable on the addon itself instead.
-- @param addon addon object to enable
function AceAddon:EnableAddon(addon)
if type(addon) == "string" then addon = AceAddon:GetAddon(addon) end
if self.statuses[addon.name] or not addon.enabledState then return false end
-- set the statuses first, before calling the OnEnable. this allows for Disabling of the addon in OnEnable.
self.statuses[addon.name] = true
safecall(addon.OnEnable, addon)
-- make sure we're still enabled before continueing
if self.statuses[addon.name] then
local embeds = self.embeds[addon]
for i = 1, #embeds do
local lib = LibStub:GetLibrary(embeds[i], true)
if lib then safecall(lib.OnEmbedEnable, lib, addon) end
end
-- enable possible modules.
local modules = addon.orderedModules
for i = 1, #modules do
self:EnableAddon(modules[i])
end
end
return self.statuses[addon.name] -- return true if we're disabled
end
-- - Disable the addon
-- Note: This function is only used internally.
-- It will call the **OnDisable** function on the addon object (if present),
-- and the **OnEmbedDisable** function on all embeded libraries.\\
-- This function does not toggle the enable state of the addon itself, and will return early if the addon is still enabled.
--
-- **Note:** Do not call this function manually, unless you're absolutely sure that you know what you are doing.
-- Use :Disable on the addon itself instead.
-- @param addon addon object to enable
function AceAddon:DisableAddon(addon)
if type(addon) == "string" then addon = AceAddon:GetAddon(addon) end
if not self.statuses[addon.name] then return false end
-- set statuses first before calling OnDisable, this allows for aborting the disable in OnDisable.
self.statuses[addon.name] = false
safecall( addon.OnDisable, addon )
-- make sure we're still disabling...
if not self.statuses[addon.name] then
local embeds = self.embeds[addon]
for i = 1, #embeds do
local lib = LibStub:GetLibrary(embeds[i], true)
if lib then safecall(lib.OnEmbedDisable, lib, addon) end
end
-- disable possible modules.
local modules = addon.orderedModules
for i = 1, #modules do
self:DisableAddon(modules[i])
end
end
return not self.statuses[addon.name] -- return true if we're disabled
end
--- Get an iterator over all registered addons.
-- @usage
-- -- Print a list of all installed AceAddon's
-- for name, addon in AceAddon:IterateAddons() do
-- print("Addon: " .. name)
-- end
function AceAddon:IterateAddons() return pairs(self.addons) end
--- Get an iterator over the internal status registry.
-- @usage
-- -- Print a list of all enabled addons
-- for name, status in AceAddon:IterateAddonStatus() do
-- if status then
-- print("EnabledAddon: " .. name)
-- end
-- end
function AceAddon:IterateAddonStatus() return pairs(self.statuses) end
-- Following Iterators are deprecated, and their addon specific versions should be used
-- e.g. addon:IterateEmbeds() instead of :IterateEmbedsOnAddon(addon)
function AceAddon:IterateEmbedsOnAddon(addon) return pairs(self.embeds[addon]) end
function AceAddon:IterateModulesOfAddon(addon) return pairs(addon.modules) end
-- Blizzard AddOns which can load very early in the loading process and mess with Ace3 addon loading
local BlizzardEarlyLoadAddons = {
Blizzard_DebugTools = true,
Blizzard_TimeManager = true,
Blizzard_BattlefieldMap = true,
Blizzard_MapCanvas = true,
Blizzard_SharedMapDataProviders = true,
Blizzard_CombatLog = true,
}
-- Event Handling
local function onEvent(this, event, arg1)
-- 2020-08-28 nevcairiel - ignore the load event of Blizzard addons which occur early in the loading process
if (event == "ADDON_LOADED" and (arg1 == nil or not BlizzardEarlyLoadAddons[arg1])) or event == "PLAYER_LOGIN" then
-- if a addon loads another addon, recursion could happen here, so we need to validate the table on every iteration
while(#AceAddon.initializequeue > 0) do
local addon = tremove(AceAddon.initializequeue, 1)
-- this might be an issue with recursion - TODO: validate
if event == "ADDON_LOADED" then addon.baseName = arg1 end
AceAddon:InitializeAddon(addon)
tinsert(AceAddon.enablequeue, addon)
end
if IsLoggedIn() then
while(#AceAddon.enablequeue > 0) do
local addon = tremove(AceAddon.enablequeue, 1)
AceAddon:EnableAddon(addon)
end
end
end
end
AceAddon.frame:RegisterEvent("ADDON_LOADED")
AceAddon.frame:RegisterEvent("PLAYER_LOGIN")
AceAddon.frame:SetScript("OnEvent", onEvent)
-- upgrade embeded
for name, addon in pairs(AceAddon.addons) do
Embed(addon, true)
end
-- 2010-10-27 nevcairiel - add new "orderedModules" table
if oldminor and oldminor < 10 then
for name, addon in pairs(AceAddon.addons) do
addon.orderedModules = {}
for module_name, module in pairs(addon.modules) do
tinsert(addon.orderedModules, module)
end
end
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceAddon-3.0.lua"/>
</Ui>
+301
View File
@@ -0,0 +1,301 @@
--- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels.
-- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\
-- **ChatThrottleLib** is of course being used to avoid being disconnected by the server.
--
-- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceComm itself.\\
-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceComm.
-- @class file
-- @name AceComm-3.0
-- @release $Id$
--[[ AceComm-3.0
TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited.
]]
local CallbackHandler = LibStub("CallbackHandler-1.0")
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
local MAJOR, MINOR = "AceComm-3.0", 14
local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceComm then return end
-- Lua APIs
local type, next, pairs, tostring = type, next, pairs, tostring
local strsub, strfind = string.sub, string.find
local match = string.match
local tinsert, tconcat = table.insert, table.concat
local error, assert = error, assert
-- WoW APIs
local Ambiguate = Ambiguate
AceComm.embeds = AceComm.embeds or {}
-- for my sanity and yours, let's give the message type bytes some names
local MSG_MULTI_FIRST = "\001"
local MSG_MULTI_NEXT = "\002"
local MSG_MULTI_LAST = "\003"
local MSG_ESCAPE = "\004"
-- remove old structures (pre WoW 4.0)
AceComm.multipart_origprefixes = nil
AceComm.multipart_reassemblers = nil
-- the multipart message spool: indexed by a combination of sender+distribution+
AceComm.multipart_spool = AceComm.multipart_spool or {}
--- Register for Addon Traffic on a specified prefix
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters
-- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived"
function AceComm:RegisterComm(prefix, method)
if method == nil then
method = "OnCommReceived"
end
if #prefix > 16 then -- TODO: 15?
error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters")
end
if C_ChatInfo then
C_ChatInfo.RegisterAddonMessagePrefix(prefix)
else
RegisterAddonMessagePrefix(prefix)
end
return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
end
local warnedPrefix=false
--- Send a message over the Addon Channel
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
-- @param text Data to send, nils (\000) not allowed. Any length.
-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
-- @param target Destination for some distributions; see SendAddonMessage API
-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send.
-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery!
if not( type(prefix)=="string" and
type(text)=="string" and
type(distribution)=="string" and
(target==nil or type(target)=="string" or type(target)=="number") and
(prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
) then
error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
end
local textlen = #text
local maxtextlen = 255 -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327
local queueName = prefix
local ctlCallback = nil
if callbackFn then
ctlCallback = function(sent, sendResult)
return callbackFn(callbackArg, sent, textlen, sendResult)
end
end
local forceMultipart
if match(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character
-- we need to escape the first character with a \004
if textlen+1 > maxtextlen then -- would we go over the size limit?
forceMultipart = true -- just make it multipart, no escape problems then
else
text = "\004" .. text
end
end
if not forceMultipart and textlen <= maxtextlen then
-- fits all in one message
CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
else
maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1)
-- first part
local chunk = strsub(text, 1, maxtextlen)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen)
-- continuation
local pos = 1+maxtextlen
while pos+maxtextlen <= textlen do
chunk = strsub(text, pos, pos+maxtextlen-1)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
pos = pos + maxtextlen
end
-- final part
chunk = strsub(text, pos)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen)
end
end
----------------------------------------
-- Message receiving
----------------------------------------
do
local compost = setmetatable({}, {__mode = "k"})
local function new()
local t = next(compost)
if t then
compost[t]=nil
for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten
t[i]=nil
end
return t
end
return {}
end
local function lostdatawarning(prefix,sender,where)
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
end
function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool
--[[
if spool[key] then
lostdatawarning(prefix,sender,"First")
-- continue and overwrite
end
--]]
spool[key] = message -- plain string for now
end
function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool
local olddata = spool[key]
if not olddata then
--lostdatawarning(prefix,sender,"Next")
return
end
if type(olddata)~="table" then
-- ... but what we have is not a table. So make it one. (Pull a composted one if available)
local t = new()
t[1] = olddata -- add old data as first string
t[2] = message -- and new message as second string
spool[key] = t -- and put the table in the spool instead of the old string
else
tinsert(olddata, message)
end
end
function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool
local olddata = spool[key]
if not olddata then
--lostdatawarning(prefix,sender,"End")
return
end
spool[key] = nil
if type(olddata) == "table" then
-- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
tinsert(olddata, message)
AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
compost[olddata] = true
else
-- if we've only received a "first", the spooled data will still only be a string
AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
end
end
end
----------------------------------------
-- Embed CallbackHandler
----------------------------------------
if not AceComm.callbacks then
AceComm.callbacks = CallbackHandler:New(AceComm,
"_RegisterComm",
"UnregisterComm",
"UnregisterAllComm")
end
AceComm.callbacks.OnUsed = nil
AceComm.callbacks.OnUnused = nil
local function OnEvent(self, event, prefix, message, distribution, sender)
if event == "CHAT_MSG_ADDON" then
sender = Ambiguate(sender, "none")
local control, rest = match(message, "^([\001-\009])(.*)")
if control then
if control==MSG_MULTI_FIRST then
AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender)
elseif control==MSG_MULTI_NEXT then
AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender)
elseif control==MSG_MULTI_LAST then
AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender)
elseif control==MSG_ESCAPE then
AceComm.callbacks:Fire(prefix, rest, distribution, sender)
else
-- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!)
end
else
-- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
AceComm.callbacks:Fire(prefix, message, distribution, sender)
end
else
assert(false, "Received "..tostring(event).." event?!")
end
end
AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
AceComm.frame:SetScript("OnEvent", OnEvent)
AceComm.frame:UnregisterAllEvents()
AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")
----------------------------------------
-- Base library stuff
----------------------------------------
local mixins = {
"RegisterComm",
"UnregisterComm",
"UnregisterAllComm",
"SendCommMessage",
}
-- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceComm-3.0 in
function AceComm:Embed(target)
for k, v in pairs(mixins) do
target[v] = self[v]
end
self.embeds[target] = true
return target
end
function AceComm:OnEmbedDisable(target)
target:UnregisterAllComm()
end
-- Update embeds
for target, v in pairs(AceComm.embeds) do
AceComm:Embed(target)
end
@@ -0,0 +1,5 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="ChatThrottleLib.lua"/>
<Script file="AceComm-3.0.lua"/>
</Ui>
@@ -0,0 +1,701 @@
--
-- ChatThrottleLib by Mikk
--
-- Manages AddOn chat output to keep player from getting kicked off.
--
-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
--
-- Priorities get an equal share of available bandwidth when fully loaded.
-- Communication channels are separated on extension+chattype+destination and
-- get round-robinned. (Destination only matters for whispers and channels,
-- obviously)
--
-- Will install hooks for SendChatMessage and SendAddonMessage to measure
-- bandwidth bypassing the library and use less bandwidth itself.
--
--
-- Fully embeddable library. Just copy this file into your addon directory,
-- add it to the .toc, and it's done.
--
-- Can run as a standalone addon also, but, really, just embed it! :-)
--
-- LICENSE: ChatThrottleLib is released into the Public Domain
--
local CTL_VERSION = 31
local _G = _G
if _G.ChatThrottleLib then
if _G.ChatThrottleLib.version >= CTL_VERSION then
-- There's already a newer (or same) version loaded. Buh-bye.
return
elseif not _G.ChatThrottleLib.securelyHooked then
print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, <v16) in an addon somewhere. Get the addon updated or copy in a newer ChatThrottleLib.lua (>=v16) in it!")
-- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
-- ... and if someone has securehooked, they can kiss that goodbye too... >.<
_G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
if _G.ChatThrottleLib.ORIG_SendAddonMessage then
_G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
end
end
_G.ChatThrottleLib.ORIG_SendChatMessage = nil
_G.ChatThrottleLib.ORIG_SendAddonMessage = nil
end
if not _G.ChatThrottleLib then
_G.ChatThrottleLib = {}
end
ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
local ChatThrottleLib = _G.ChatThrottleLib
ChatThrottleLib.version = CTL_VERSION
------------------ TWEAKABLES -----------------
ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
local setmetatable = setmetatable
local table_remove = table.remove
local tostring = tostring
local GetTime = GetTime
local math_min = math.min
local math_max = math.max
local next = next
local strlen = string.len
local GetFramerate = GetFramerate
local unpack,type,pairs,wipe = unpack,type,pairs,table.wipe
-----------------------------------------------------------------------
-- Double-linked ring implementation
local Ring = {}
local RingMeta = { __index = Ring }
function Ring:New()
local ret = {}
setmetatable(ret, RingMeta)
return ret
end
function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
if self.pos then
obj.prev = self.pos.prev
obj.prev.next = obj
obj.next = self.pos
obj.next.prev = obj
else
obj.next = obj
obj.prev = obj
self.pos = obj
end
end
function Ring:Remove(obj)
obj.next.prev = obj.prev
obj.prev.next = obj.next
if self.pos == obj then
self.pos = obj.next
if self.pos == obj then
self.pos = nil
end
end
end
-- Note that this is local because there's no upgrade logic for existing ring
-- metatables, and this isn't present on rings created in versions older than
-- v25.
local function Ring_Link(self, other) -- Move and append all contents of another ring to this ring
if not self.pos then
-- This ring is empty, so just transfer ownership.
self.pos = other.pos
other.pos = nil
elseif other.pos then
-- Our tail should point to their head, and their tail to our head.
self.pos.prev.next, other.pos.prev.next = other.pos, self.pos
-- Our head should point to their tail, and their head to our tail.
self.pos.prev, other.pos.prev = other.pos.prev, self.pos.prev
other.pos = nil
end
end
-----------------------------------------------------------------------
-- Recycling bin for pipes
-- A pipe is a plain integer-indexed queue of messages
-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
local PipeBin = setmetatable({}, {__mode="k"})
local function DelPipe(pipe)
PipeBin[pipe] = true
end
local function NewPipe()
local pipe = next(PipeBin)
if pipe then
wipe(pipe)
PipeBin[pipe] = nil
return pipe
end
return {}
end
-----------------------------------------------------------------------
-- Recycling bin for messages
ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
local MsgBin = setmetatable({}, {__mode="k"})
local function DelMsg(msg)
msg[1] = nil
-- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
MsgBin[msg] = true
end
local function NewMsg()
local msg = next(MsgBin)
if msg then
MsgBin[msg] = nil
return msg
end
return {}
end
-----------------------------------------------------------------------
-- ChatThrottleLib:Init
-- Initialize queues, set up frame for OnUpdate, etc
function ChatThrottleLib:Init()
-- Set up queues
if not self.Prio then
self.Prio = {}
self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
end
if not self.BlockedQueuesDelay then
-- v25: Add blocked queues to rings to handle new client throttles.
for _, Prio in pairs(self.Prio) do
Prio.Blocked = Ring:New()
end
end
-- v4: total send counters per priority
for _, Prio in pairs(self.Prio) do
Prio.nTotalSent = Prio.nTotalSent or 0
end
if not self.avail then
self.avail = 0 -- v5
end
if not self.nTotalSent then
self.nTotalSent = 0 -- v5
end
-- Set up a frame to get OnUpdate events
if not self.Frame then
self.Frame = CreateFrame("Frame")
self.Frame:Hide()
end
self.Frame:SetScript("OnUpdate", self.OnUpdate)
self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
self.OnUpdateDelay = 0
self.BlockedQueuesDelay = 0
self.LastAvailUpdate = GetTime()
self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
-- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
if not self.securelyHooked then
-- Use secure hooks as of v16. Old regular hook support yanked out in v21.
self.securelyHooked = true
--SendChatMessage
if _G.C_ChatInfo and _G.C_ChatInfo.SendChatMessage then
hooksecurefunc(_G.C_ChatInfo, "SendChatMessage", function(...)
return ChatThrottleLib.Hook_SendChatMessage(...)
end)
else
hooksecurefunc("SendChatMessage", function(...)
return ChatThrottleLib.Hook_SendChatMessage(...)
end)
end
--SendAddonMessage
hooksecurefunc(_G.C_ChatInfo, "SendAddonMessage", function(...)
return ChatThrottleLib.Hook_SendAddonMessage(...)
end)
end
-- v26: Hook SendAddonMessageLogged for traffic logging
if not self.securelyHookedLogged then
self.securelyHookedLogged = true
hooksecurefunc(_G.C_ChatInfo, "SendAddonMessageLogged", function(...)
return ChatThrottleLib.Hook_SendAddonMessageLogged(...)
end)
end
-- v29: Hook BNSendGameData for traffic logging
if not self.securelyHookedBNGameData then
self.securelyHookedBNGameData = true
if _G.C_BattleNet and _G.C_BattleNet.SendGameData then
hooksecurefunc(_G.C_BattleNet, "SendGameData", function(...)
return ChatThrottleLib.Hook_BNSendGameData(...)
end)
else
hooksecurefunc("BNSendGameData", function(...)
return ChatThrottleLib.Hook_BNSendGameData(...)
end)
end
end
self.nBypass = 0
end
-----------------------------------------------------------------------
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
local bMyTraffic = false
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
if bMyTraffic then
return
end
local self = ChatThrottleLib
local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
self.avail = self.avail - size
self.nBypass = self.nBypass + size -- just a statistic
end
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
if bMyTraffic then
return
end
local self = ChatThrottleLib
local size = tostring(text or ""):len() + tostring(prefix or ""):len();
size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
self.avail = self.avail - size
self.nBypass = self.nBypass + size -- just a statistic
end
function ChatThrottleLib.Hook_SendAddonMessageLogged(prefix, text, chattype, destination, ...)
ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
end
function ChatThrottleLib.Hook_BNSendGameData(destination, prefix, text)
ChatThrottleLib.Hook_SendAddonMessage(prefix, text, "WHISPER", destination)
end
-----------------------------------------------------------------------
-- ChatThrottleLib:UpdateAvail
-- Update self.avail with how much bandwidth is currently available
function ChatThrottleLib:UpdateAvail()
local now = GetTime()
local MAX_CPS = self.MAX_CPS;
local newavail = MAX_CPS * (now - self.LastAvailUpdate)
local avail = self.avail
if now - self.HardThrottlingBeginTime < 5 then
-- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
self.bChoking = true
elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs
avail = math_min(MAX_CPS, avail + newavail*0.5)
self.bChoking = true -- just a statistic
else
avail = math_min(self.BURST, avail + newavail)
self.bChoking = false
end
avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
self.avail = avail
self.LastAvailUpdate = now
return avail
end
-----------------------------------------------------------------------
-- Despooling logic
-- Reminder:
-- - We have 3 Priorities, each containing a "Ring" construct ...
-- - ... made up of N "Pipe"s (1 for each destination/pipename)
-- - and each pipe contains messages
local SendAddonMessageResult = Enum.SendAddonMessageResult or {
Success = 0,
AddonMessageThrottle = 3,
NotInGroup = 5,
ChannelThrottle = 8,
GeneralError = 9,
}
local function MapToSendResult(ok, ...)
local result
if not ok then
-- The send function itself errored; don't look at anything else.
result = SendAddonMessageResult.GeneralError
else
-- Grab the last return value from the send function and remap
-- it from a boolean to an enum code. If there are no results,
-- assume success (true).
result = select(-1, true, ...)
if result == true then
result = SendAddonMessageResult.Success
elseif result == false then
result = SendAddonMessageResult.GeneralError
end
end
return result
end
local function IsThrottledSendResult(result)
return result == SendAddonMessageResult.AddonMessageThrottle
end
-- A copy of this function exists in FrameXML, but for clarity it's here too.
local function CallErrorHandler(...)
return geterrorhandler()(...)
end
local function PerformSend(sendFunction, ...)
bMyTraffic = true
local sendResult = MapToSendResult(xpcall(sendFunction, CallErrorHandler, ...))
bMyTraffic = false
return sendResult
end
function ChatThrottleLib:Despool(Prio)
local ring = Prio.Ring
while ring.pos and Prio.avail > ring.pos[1].nSize do
local pipe = ring.pos
local msg = pipe[1]
local sendResult = PerformSend(msg.f, unpack(msg, 1, msg.n))
if IsThrottledSendResult(sendResult) then
-- Message was throttled; move the pipe into the blocked ring.
Prio.Ring:Remove(pipe)
Prio.Blocked:Add(pipe)
else
-- Dequeue message after submission.
table_remove(pipe, 1)
DelMsg(msg)
if not pipe[1] then -- did we remove last msg in this pipe?
Prio.Ring:Remove(pipe)
Prio.ByName[pipe.name] = nil
DelPipe(pipe)
else
ring.pos = ring.pos.next
end
-- Update bandwidth counters on successful sends.
local didSend = (sendResult == SendAddonMessageResult.Success)
if didSend then
Prio.avail = Prio.avail - msg.nSize
Prio.nTotalSent = Prio.nTotalSent + msg.nSize
end
-- Notify caller of message submission.
if msg.callbackFn then
securecallfunction(msg.callbackFn, msg.callbackArg, didSend, sendResult)
end
end
end
end
function ChatThrottleLib.OnEvent(this,event)
-- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
local self = ChatThrottleLib
if event == "PLAYER_ENTERING_WORLD" then
self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
self.avail = 0
end
end
function ChatThrottleLib.OnUpdate(this,delay)
local self = ChatThrottleLib
self.OnUpdateDelay = self.OnUpdateDelay + delay
self.BlockedQueuesDelay = self.BlockedQueuesDelay + delay
if self.OnUpdateDelay < 0.08 then
return
end
self.OnUpdateDelay = 0
self:UpdateAvail()
if self.avail < 0 then
return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
end
-- Integrate blocked queues back into their rings periodically.
if self.BlockedQueuesDelay >= 0.35 then
for _, Prio in pairs(self.Prio) do
Ring_Link(Prio.Ring, Prio.Blocked)
end
self.BlockedQueuesDelay = 0
end
-- See how many of our priorities have queued messages. This is split
-- into two counters because priorities that consist only of blocked
-- queues must keep our OnUpdate alive, but shouldn't count toward
-- bandwidth distribution.
local nSendablePrios = 0
local nBlockedPrios = 0
for prioname, Prio in pairs(self.Prio) do
if Prio.Ring.pos then
nSendablePrios = nSendablePrios + 1
elseif Prio.Blocked.pos then
nBlockedPrios = nBlockedPrios + 1
end
-- Collect unused bandwidth from priorities with nothing to send.
if not Prio.Ring.pos then
self.avail = self.avail + Prio.avail
Prio.avail = 0
end
end
-- Bandwidth reclamation may take us back over the burst cap.
self.avail = math_min(self.avail, self.BURST)
-- If we can't currently send on any priorities, stop processing early.
if nSendablePrios == 0 then
-- If we're completely out of data to send, disable queue processing.
if nBlockedPrios == 0 then
self.bQueueing = false
self.Frame:Hide()
end
return
end
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
local avail = self.avail / nSendablePrios
self.avail = 0
for prioname, Prio in pairs(self.Prio) do
if Prio.Ring.pos then
Prio.avail = Prio.avail + avail
self:Despool(Prio)
end
end
end
-----------------------------------------------------------------------
-- Spooling logic
function ChatThrottleLib:Enqueue(prioname, pipename, msg)
local Prio = self.Prio[prioname]
local pipe = Prio.ByName[pipename]
if not pipe then
self.Frame:Show()
pipe = NewPipe()
pipe.name = pipename
Prio.ByName[pipename] = pipe
Prio.Ring:Add(pipe)
end
pipe[#pipe + 1] = msg
self.bQueueing = true
end
function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
if not self or not prio or not prefix or not text or not self.Prio[prio] then
error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
end
if callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
end
local nSize = text:len()
if nSize>255 then
error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
end
nSize = nSize + self.MSG_OVERHEAD
-- Check if there's room in the global available bandwidth gauge to send directly
if not self.bQueueing and nSize < self:UpdateAvail() then
local sendResult = PerformSend(_G.C_ChatInfo.SendChatMessage or _G.SendChatMessage, text, chattype, language, destination)
if not IsThrottledSendResult(sendResult) then
local didSend = (sendResult == SendAddonMessageResult.Success)
if didSend then
self.avail = self.avail - nSize
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
end
if callbackFn then
securecallfunction(callbackFn, callbackArg, didSend, sendResult)
end
return
end
end
-- Message needs to be queued
local msg = NewMsg()
msg.f = _G.C_ChatInfo.SendChatMessage or _G.SendChatMessage
msg[1] = text
msg[2] = chattype or "SAY"
msg[3] = language
msg[4] = destination
msg.n = 4
msg.nSize = nSize
msg.callbackFn = callbackFn
msg.callbackArg = callbackArg
self:Enqueue(prio, queueName or prefix, msg)
end
local function SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
local nSize = #text + self.MSG_OVERHEAD
-- Check if there's room in the global available bandwidth gauge to send directly
if not self.bQueueing and nSize < self:UpdateAvail() then
local sendResult = PerformSend(sendFunction, prefix, text, chattype, target)
if not IsThrottledSendResult(sendResult) then
local didSend = (sendResult == SendAddonMessageResult.Success)
if didSend then
self.avail = self.avail - nSize
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
end
if callbackFn then
securecallfunction(callbackFn, callbackArg, didSend, sendResult)
end
return
end
end
-- Message needs to be queued
local msg = NewMsg()
msg.f = sendFunction
msg[1] = prefix
msg[2] = text
msg[3] = chattype
msg[4] = target
msg.n = (target~=nil) and 4 or 3;
msg.nSize = nSize
msg.callbackFn = callbackFn
msg.callbackArg = callbackArg
self:Enqueue(prio, queueName or prefix, msg)
end
function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
elseif callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
elseif #text>255 then
error("ChatThrottleLib:SendAddonMessage(): message length cannot exceed 255 bytes", 2)
end
local sendFunction = _G.C_ChatInfo.SendAddonMessage
SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
end
function ChatThrottleLib:SendAddonMessageLogged(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
error('Usage: ChatThrottleLib:SendAddonMessageLogged("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
elseif callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:SendAddonMessageLogged(): callbackFn: expected function, got '..type(callbackFn), 2)
elseif #text>255 then
error("ChatThrottleLib:SendAddonMessageLogged(): message length cannot exceed 255 bytes", 2)
end
local sendFunction = _G.C_ChatInfo.SendAddonMessageLogged
SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
end
local function BNSendGameDataReordered(prefix, text, _, gameAccountID)
local bnSendFunc = _G.C_BattleNet and _G.C_BattleNet.SendGameData or _G.BNSendGameData
return bnSendFunc(gameAccountID, prefix, text)
end
function ChatThrottleLib:BNSendGameData(prio, prefix, text, chattype, gameAccountID, queueName, callbackFn, callbackArg)
-- Note that this API is intentionally limited to 255 bytes of data
-- for reasons of traffic fairness, which is less than the 4078 bytes
-- BNSendGameData natively supports. Additionally, a chat type is required
-- but must always be set to 'WHISPER' to match what is exposed by the
-- receipt event.
--
-- If splitting messages, callers must also be aware that message
-- delivery over BNSendGameData is unordered.
if not self or not prio or not prefix or not text or not gameAccountID or not chattype or not self.Prio[prio] then
error('Usage: ChatThrottleLib:BNSendGameData("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype", gameAccountID)', 2)
elseif callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:BNSendGameData(): callbackFn: expected function, got '..type(callbackFn), 2)
elseif #text>255 then
error("ChatThrottleLib:BNSendGameData(): message length cannot exceed 255 bytes", 2)
elseif chattype ~= "WHISPER" then
error("ChatThrottleLib:BNSendGameData(): chat type must be 'WHISPER'", 2)
end
local sendFunction = BNSendGameDataReordered
SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, gameAccountID, queueName, callbackFn, callbackArg)
end
-----------------------------------------------------------------------
-- Get the ball rolling!
ChatThrottleLib:Init()
--[[ WoWBench debugging snippet
if(WOWB_VER) then
local function SayTimer()
print("SAY: "..GetTime().." "..arg1)
end
ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
end
]]
@@ -0,0 +1,246 @@
--- **AceConsole-3.0** provides registration facilities for slash commands.
-- You can register slash commands to your custom functions and use the `GetArgs` function to parse them
-- to your addons individual needs.
--
-- **AceConsole-3.0** can be embeded into your addon, either explicitly by calling AceConsole:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceConsole itself.\\
-- It is recommended to embed AceConsole, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceConsole.
-- @class file
-- @name AceConsole-3.0
-- @release $Id$
local MAJOR,MINOR = "AceConsole-3.0", 7
local AceConsole, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceConsole then return end -- No upgrade needed
AceConsole.embeds = AceConsole.embeds or {} -- table containing objects AceConsole is embedded in.
AceConsole.commands = AceConsole.commands or {} -- table containing commands registered
AceConsole.weakcommands = AceConsole.weakcommands or {} -- table containing self, command => func references for weak commands that don't persist through enable/disable
-- Lua APIs
local tconcat, tostring, select = table.concat, tostring, select
local type, pairs, error = type, pairs, error
local format, strfind, strsub = string.format, string.find, string.sub
local max = math.max
-- WoW APIs
local _G = _G
local tmp={}
local function Print(self,frame,...)
local n=0
if self ~= AceConsole then
n=n+1
tmp[n] = "|cff33ff99"..tostring( self ).."|r:"
end
for i=1, select("#", ...) do
n=n+1
tmp[n] = tostring(select(i, ...))
end
frame:AddMessage( tconcat(tmp," ",1,n) )
end
--- Print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function)
-- @paramsig [chatframe ,] ...
-- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function)
-- @param ... List of any values to be printed
function AceConsole:Print(...)
local frame = ...
if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member?
return Print(self, frame, select(2,...))
else
return Print(self, DEFAULT_CHAT_FRAME, ...)
end
end
--- Formatted (using format()) print to DEFAULT_CHAT_FRAME or given ChatFrame (anything with an .AddMessage function)
-- @paramsig [chatframe ,] "format"[, ...]
-- @param chatframe Custom ChatFrame to print to (or any frame with an .AddMessage function)
-- @param format Format string - same syntax as standard Lua format()
-- @param ... Arguments to the format string
function AceConsole:Printf(...)
local frame = ...
if type(frame) == "table" and frame.AddMessage then -- Is first argument something with an .AddMessage member?
return Print(self, frame, format(select(2,...)))
else
return Print(self, DEFAULT_CHAT_FRAME, format(...))
end
end
--- Register a simple chat command
-- @param command Chat command to be registered WITHOUT leading "/"
-- @param func Function to call when the slash command is being used (funcref or methodname)
-- @param persist if false, the command will be soft disabled/enabled when aceconsole is used as a mixin (default: true)
function AceConsole:RegisterChatCommand( command, func, persist )
if type(command)~="string" then error([[Usage: AceConsole:RegisterChatCommand( "command", func[, persist ]): 'command' - expected a string]], 2) end
if persist==nil then persist=true end -- I'd rather have my addon's "/addon enable" around if the author screws up. Having some extra slash regged when it shouldnt be isn't as destructive. True is a better default. /Mikk
local name = "ACECONSOLE_"..command:upper()
if type( func ) == "string" then
SlashCmdList[name] = function(input, editBox)
self[func](self, input, editBox)
end
else
SlashCmdList[name] = func
end
_G["SLASH_"..name.."1"] = "/"..command:lower()
AceConsole.commands[command] = name
-- non-persisting commands are registered for enabling disabling
if not persist then
if not AceConsole.weakcommands[self] then AceConsole.weakcommands[self] = {} end
AceConsole.weakcommands[self][command] = func
end
return true
end
--- Unregister a chatcommand
-- @param command Chat command to be unregistered WITHOUT leading "/"
function AceConsole:UnregisterChatCommand( command )
local name = AceConsole.commands[command]
if name then
SlashCmdList[name] = nil
_G["SLASH_" .. name .. "1"] = nil
hash_SlashCmdList["/" .. command:upper()] = nil
AceConsole.commands[command] = nil
end
end
--- Get an iterator over all Chat Commands registered with AceConsole
-- @return Iterator (pairs) over all commands
function AceConsole:IterateChatCommands() return pairs(AceConsole.commands) end
local function nils(n, ...)
if n>1 then
return nil, nils(n-1, ...)
elseif n==1 then
return nil, ...
else
return ...
end
end
--- Retreive one or more space-separated arguments from a string.
-- Treats quoted strings and itemlinks as non-spaced.
-- @param str The raw argument string
-- @param numargs How many arguments to get (default 1)
-- @param startpos Where in the string to start scanning (default 1)
-- @return Returns arg1, arg2, ..., nextposition\\
-- Missing arguments will be returned as nils. 'nextposition' is returned as 1e9 at the end of the string.
function AceConsole:GetArgs(str, numargs, startpos)
numargs = numargs or 1
startpos = max(startpos or 1, 1)
local pos=startpos
-- find start of new arg
pos = strfind(str, "[^ ]", pos)
if not pos then -- whoops, end of string
return nils(numargs, 1e9)
end
if numargs<1 then
return pos
end
-- quoted or space separated? find out which pattern to use
local delim_or_pipe
local ch = strsub(str, pos, pos)
if ch=='"' then
pos = pos + 1
delim_or_pipe='([|"])'
elseif ch=="'" then
pos = pos + 1
delim_or_pipe="([|'])"
else
delim_or_pipe="([| ])"
end
startpos = pos
while true do
-- find delimiter or hyperlink
local _
pos,_,ch = strfind(str, delim_or_pipe, pos)
if not pos then break end
if ch=="|" then
-- some kind of escape
if strsub(str,pos,pos+1)=="|H" then
-- It's a |H....|hhyper link!|h
pos=strfind(str, "|h", pos+2) -- first |h
if not pos then break end
pos=strfind(str, "|h", pos+2) -- second |h
if not pos then break end
elseif strsub(str,pos, pos+1) == "|T" then
-- It's a |T....|t texture
pos=strfind(str, "|t", pos+2)
if not pos then break end
end
pos=pos+2 -- skip past this escape (last |h if it was a hyperlink)
else
-- found delimiter, done with this arg
return strsub(str, startpos, pos-1), AceConsole:GetArgs(str, numargs-1, pos+1)
end
end
-- search aborted, we hit end of string. return it all as one argument. (yes, even if it's an unterminated quote or hyperlink)
return strsub(str, startpos), nils(numargs-1, 1e9)
end
--- embedding and embed handling
local mixins = {
"Print",
"Printf",
"RegisterChatCommand",
"UnregisterChatCommand",
"GetArgs",
}
-- Embeds AceConsole into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceBucket in
function AceConsole:Embed( target )
for k, v in pairs( mixins ) do
target[v] = self[v]
end
self.embeds[target] = true
return target
end
function AceConsole:OnEmbedEnable( target )
if AceConsole.weakcommands[target] then
for command, func in pairs( AceConsole.weakcommands[target] ) do
target:RegisterChatCommand( command, func, false, true ) -- nonpersisting and silent registry
end
end
end
function AceConsole:OnEmbedDisable( target )
if AceConsole.weakcommands[target] then
for command, func in pairs( AceConsole.weakcommands[target] ) do
target:UnregisterChatCommand( command ) -- TODO: this could potentially unregister a command from another application in case of command conflicts. Do we care?
end
end
end
for addon in pairs(AceConsole.embeds) do
AceConsole:Embed(addon)
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceConsole-3.0.lua"/>
</Ui>
+805
View File
@@ -0,0 +1,805 @@
--- **AceDB-3.0** manages the SavedVariables of your addon.
-- It offers profile management, smart defaults and namespaces for modules.\\
-- Data can be saved in different data-types, depending on its intended usage.
-- The most common data-type is the `profile` type, which allows the user to choose
-- the active profile, and manage the profiles of all of his characters.\\
-- The following data types are available:
-- * **char** Character-specific data. Every character has its own database.
-- * **realm** Realm-specific data. All of the players characters on the same realm share this database.
-- * **class** Class-specific data. All of the players characters of the same class share this database.
-- * **race** Race-specific data. All of the players characters of the same race share this database.
-- * **faction** Faction-specific data. All of the players characters of the same faction share this database.
-- * **factionrealm** Faction and realm specific data. All of the players characters on the same realm and of the same faction share this database.
-- * **locale** Locale specific data, based on the locale of the players game client.
-- * **global** Global Data. All characters on the same account share this database.
-- * **profile** Profile-specific data. All characters using the same profile share this database. The user can control which profile should be used.
--
-- Creating a new Database using the `:New` function will return a new DBObject. A database will inherit all functions
-- of the DBObjectLib listed here. \\
-- If you create a new namespaced child-database (`:RegisterNamespace`), you'll get a DBObject as well, but note
-- that the child-databases cannot individually change their profile, and are linked to their parents profile - and because of that,
-- the profile related APIs are not available. Only `:RegisterDefaults` and `:ResetProfile` are available on child-databases.
--
-- For more details on how to use AceDB-3.0, see the [[AceDB-3.0 Tutorial]].
--
-- You may also be interested in [[libdualspec-1-0|LibDualSpec-1.0]] to do profile switching automatically when switching specs.
--
-- @usage
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("DBExample")
--
-- -- declare defaults to be used in the DB
-- local defaults = {
-- profile = {
-- setting = true,
-- }
-- }
--
-- function MyAddon:OnInitialize()
-- -- Assuming the .toc says ## SavedVariables: MyAddonDB
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true)
-- end
-- @class file
-- @name AceDB-3.0.lua
-- @release $Id$
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 33
local AceDB = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)
if not AceDB then return end -- No upgrade needed
-- Lua APIs
local type, pairs, next, error = type, pairs, next, error
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
-- WoW APIs
local _G = _G
AceDB.db_registry = AceDB.db_registry or {}
AceDB.frame = AceDB.frame or CreateFrame("Frame")
local CallbackHandler
local CallbackDummy = { Fire = function() end }
local DBObjectLib = {}
--[[-------------------------------------------------------------------------
AceDB Utility Functions
---------------------------------------------------------------------------]]
-- Simple shallow copy for copying defaults
local function copyTable(src, dest)
if type(dest) ~= "table" then dest = {} end
if type(src) == "table" then
for k,v in pairs(src) do
if type(v) == "table" then
-- try to index the key first so that the metatable creates the defaults, if set, and use that table
v = copyTable(v, dest[k])
end
dest[k] = v
end
end
return dest
end
-- Called to add defaults to a section of the database
--
-- When a ["*"] default section is indexed with a new key, a table is returned
-- and set in the host table. These tables must be cleaned up by removeDefaults
-- in order to ensure we don't write empty default tables.
local function copyDefaults(dest, src)
-- this happens if some value in the SV overwrites our default value with a non-table
--if type(dest) ~= "table" then return end
for k, v in pairs(src) do
if k == "*" or k == "**" then
if type(v) == "table" then
-- This is a metatable used for table defaults
local mt = {
-- This handles the lookup and creation of new subtables
__index = function(t,k2)
if k2 == nil then return nil end
local tbl = {}
copyDefaults(tbl, v)
rawset(t, k2, tbl)
return tbl
end,
}
setmetatable(dest, mt)
-- handle already existing tables in the SV
for dk, dv in pairs(dest) do
if not rawget(src, dk) and type(dv) == "table" then
copyDefaults(dv, v)
end
end
else
-- Values are not tables, so this is just a simple return
-- (PR #10 backport: the old `k2~=nil and v or nil` short-circuits to
-- nil whenever the default `v` itself is falsy — so `["*"] = false`
-- defaults silently became nil. Make the read explicit instead.)
local mt = {
__index = function(t,k2)
if k2 == nil then return nil end
return v
end,
}
setmetatable(dest, mt)
end
elseif type(v) == "table" then
if not rawget(dest, k) then rawset(dest, k, {}) end
if type(dest[k]) == "table" then
copyDefaults(dest[k], v)
if src['**'] then
copyDefaults(dest[k], src['**'])
end
end
else
if rawget(dest, k) == nil then
rawset(dest, k, v)
end
end
end
end
-- Called to remove all defaults in the default table from the database
local function removeDefaults(db, defaults, blocker)
-- remove all metatables from the db, so we don't accidentally create new sub-tables through them
setmetatable(db, nil)
-- loop through the defaults and remove their content
for k,v in pairs(defaults) do
if k == "*" or k == "**" then
if type(v) == "table" then
-- Loop through all the actual k,v pairs and remove
for key, value in pairs(db) do
if type(value) == "table" then
-- if the key was not explicitly specified in the defaults table, just strip everything from * and ** tables
if defaults[key] == nil and (not blocker or blocker[key] == nil) then
removeDefaults(value, v)
-- if the table is empty afterwards, remove it
if next(value) == nil then
db[key] = nil
end
-- if it was specified, only strip ** content, but block values which were set in the key table
elseif k == "**" then
removeDefaults(value, v, defaults[key])
end
end
end
elseif k == "*" then
-- check for non-table default
for key, value in pairs(db) do
if defaults[key] == nil and v == value then
db[key] = nil
end
end
end
elseif type(v) == "table" and type(db[k]) == "table" then
-- if a blocker was set, dive into it, to allow multi-level defaults
removeDefaults(db[k], v, blocker and blocker[k])
if next(db[k]) == nil then
db[k] = nil
end
else
-- check if the current value matches the default, and that its not blocked by another defaults table
if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then
db[k] = nil
end
end
end
end
-- This is called when a table section is first accessed, to set up the defaults
local function initSection(db, section, svstore, key, defaults)
local sv = rawget(db, "sv")
local tableCreated
if not sv[svstore] then sv[svstore] = {} end
if not sv[svstore][key] then
sv[svstore][key] = {}
tableCreated = true
end
local tbl = sv[svstore][key]
if defaults then
copyDefaults(tbl, defaults)
end
rawset(db, section, tbl)
return tableCreated, tbl
end
-- Metatable to handle the dynamic creation of sections and copying of sections.
local dbmt = {
__index = function(t, section)
local keys = rawget(t, "keys")
local key = keys[section]
if key then
local defaultTbl = rawget(t, "defaults")
local defaults = defaultTbl and defaultTbl[section]
if section == "profile" then
local new = initSection(t, section, "profiles", key, defaults)
if new then
-- Callback: OnNewProfile, database, newProfileKey
t.callbacks:Fire("OnNewProfile", t, key)
end
elseif section == "profiles" then
local sv = rawget(t, "sv")
if not sv.profiles then sv.profiles = {} end
rawset(t, "profiles", sv.profiles)
elseif section == "global" then
local sv = rawget(t, "sv")
if not sv.global then sv.global = {} end
if defaults then
copyDefaults(sv.global, defaults)
end
rawset(t, section, sv.global)
else
initSection(t, section, section, key, defaults)
end
end
return rawget(t, section)
end
}
local function validateDefaults(defaults, keyTbl, offset)
if not defaults then return end
offset = offset or 0
for k in pairs(defaults) do
if not keyTbl[k] or k == "profiles" then
error(("Usage: AceDBObject:RegisterDefaults(defaults): '%s' is not a valid datatype."):format(k), 3 + offset)
end
end
end
local preserve_keys = {
["callbacks"] = true,
["RegisterCallback"] = true,
["UnregisterCallback"] = true,
["UnregisterAllCallbacks"] = true,
["children"] = true,
}
local realmKey = GetRealmName()
local charKey = UnitName("player") .. " - " .. realmKey
local _, classKey = UnitClass("player")
local _, raceKey = UnitRace("player")
local factionKey = UnitFactionGroup("player")
local factionrealmKey = factionKey .. " - " .. realmKey
local localeKey = GetLocale():lower()
local regionTable = { "US", "KR", "EU", "TW", "CN" }
local regionKey = regionTable[GetCurrentRegion()] or GetCurrentRegionName() or "TR"
local factionrealmregionKey = factionrealmKey .. " - " .. regionKey
-- Actual database initialization function
local function initdb(sv, defaults, defaultProfile, olddb, parent)
-- Generate the database keys for each section
-- map "true" to our "Default" profile
if defaultProfile == true then defaultProfile = "Default" end
local profileKey
if not parent then
-- Make a container for profile keys
if not sv.profileKeys then sv.profileKeys = {} end
-- Try to get the profile selected from the char db
profileKey = sv.profileKeys[charKey] or defaultProfile or charKey
-- save the selected profile for later
sv.profileKeys[charKey] = profileKey
else
-- Use the profile of the parents DB
profileKey = parent.keys.profile or defaultProfile or charKey
-- clear the profileKeys in the DB, namespaces don't need to store them
sv.profileKeys = nil
end
-- This table contains keys that enable the dynamic creation
-- of each section of the table. The 'global' and 'profiles'
-- have a key of true, since they are handled in a special case
local keyTbl= {
["char"] = charKey,
["realm"] = realmKey,
["class"] = classKey,
["race"] = raceKey,
["faction"] = factionKey,
["factionrealm"] = factionrealmKey,
["factionrealmregion"] = factionrealmregionKey,
["profile"] = profileKey,
["locale"] = localeKey,
["global"] = true,
["profiles"] = true,
}
validateDefaults(defaults, keyTbl, 1)
-- This allows us to use this function to reset an entire database
-- Clear out the old database
if olddb then
for k,v in pairs(olddb) do if not preserve_keys[k] then olddb[k] = nil end end
end
-- Give this database the metatable so it initializes dynamically
local db = setmetatable(olddb or {}, dbmt)
if not rawget(db, "callbacks") then
-- try to load CallbackHandler-1.0 if it loaded after our library
if not CallbackHandler then CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0", true) end
db.callbacks = CallbackHandler and CallbackHandler:New(db) or CallbackDummy
end
-- Copy methods locally into the database object, to avoid hitting
-- the metatable when calling methods
if not parent then
for name, func in pairs(DBObjectLib) do
db[name] = func
end
else
-- hack this one in
db.RegisterDefaults = DBObjectLib.RegisterDefaults
db.ResetProfile = DBObjectLib.ResetProfile
end
-- Set some properties in the database object
db.profiles = sv.profiles
db.keys = keyTbl
db.sv = sv
--db.sv_name = name
db.defaults = defaults
db.parent = parent
-- store the DB in the registry
AceDB.db_registry[db] = true
return db
end
-- handle PLAYER_LOGOUT
-- strip all defaults from all databases
-- and cleans up empty sections
local function logoutHandler(frame, event)
if event == "PLAYER_LOGOUT" then
for db in pairs(AceDB.db_registry) do
db.callbacks:Fire("OnDatabaseShutdown", db)
db:RegisterDefaults(nil)
-- cleanup sections that are empty without defaults
local sv = rawget(db, "sv")
for section in pairs(rawget(db, "keys")) do
if rawget(sv, section) then
-- global is special, all other sections have sub-entrys
-- also don't delete empty profiles on main dbs, only on namespaces
if section ~= "global" and (section ~= "profiles" or rawget(db, "parent")) then
for key in pairs(sv[section]) do
if not next(sv[section][key]) then
sv[section][key] = nil
end
end
end
if not next(sv[section]) then
sv[section] = nil
end
end
end
end
-- second pass after everything else is cleaned up to remove empty namespaces
-- can't be run in-loop above since there is no guaranteed order
for db in pairs(AceDB.db_registry) do
local sv = rawget(db, "sv")
local namespaces = rawget(sv, "namespaces")
if namespaces then
for name in pairs(namespaces) do
-- cleanout empty profiles table, if still present
if namespaces[name].profiles and not next(namespaces[name].profiles) then
namespaces[name].profiles = nil
end
-- remove entire namespace, if needed
if not next(namespaces[name]) then
namespaces[name] = nil
end
end
end
end
end
end
AceDB.frame:RegisterEvent("PLAYER_LOGOUT")
AceDB.frame:SetScript("OnEvent", logoutHandler)
--[[-------------------------------------------------------------------------
AceDB Object Method Definitions
---------------------------------------------------------------------------]]
--- Sets the defaults table for the given database object by clearing any
-- that are currently set, and then setting the new defaults.
-- @param defaults A table of defaults for this database
function DBObjectLib:RegisterDefaults(defaults)
if defaults and type(defaults) ~= "table" then
error(("Usage: AceDBObject:RegisterDefaults(defaults): 'defaults' - table or nil expected, got %q."):format(type(defaults)), 2)
end
validateDefaults(defaults, self.keys)
-- Remove any currently set defaults
if self.defaults then
for section,key in pairs(self.keys) do
if self.defaults[section] and rawget(self, section) then
removeDefaults(self[section], self.defaults[section])
end
end
end
-- Set the DBObject.defaults table
self.defaults = defaults
-- Copy in any defaults, only touching those sections already created
if defaults then
for section,key in pairs(self.keys) do
if defaults[section] and rawget(self, section) then
copyDefaults(self[section], defaults[section])
end
end
end
end
--- Changes the profile of the database and all of it's namespaces to the
-- supplied named profile
-- @param name The name of the profile to set as the current profile
function DBObjectLib:SetProfile(name)
if type(name) ~= "string" then
error(("Usage: AceDBObject:SetProfile(name): 'name' - string expected, got %q."):format(type(name)), 2)
end
-- changing to the same profile, dont do anything
if name == self.keys.profile then return end
local oldProfile = self.profile
local defaults = self.defaults and self.defaults.profile
-- Callback: OnProfileShutdown, database
self.callbacks:Fire("OnProfileShutdown", self)
if oldProfile and defaults then
-- Remove the defaults from the old profile
removeDefaults(oldProfile, defaults)
end
self.profile = nil
self.keys["profile"] = name
-- if the storage exists, save the new profile
-- this won't exist on namespaces.
if self.sv.profileKeys then
self.sv.profileKeys[charKey] = name
end
-- populate to child namespaces
if self.children then
for _, db in pairs(self.children) do
DBObjectLib.SetProfile(db, name)
end
end
-- Callback: OnProfileChanged, database, newProfileKey
self.callbacks:Fire("OnProfileChanged", self, name)
end
--- Returns a table with the names of the existing profiles in the database.
-- You can optionally supply a table to re-use for this purpose.
-- @param tbl A table to store the profile names in (optional)
function DBObjectLib:GetProfiles(tbl)
if tbl and type(tbl) ~= "table" then
error(("Usage: AceDBObject:GetProfiles(tbl): 'tbl' - table or nil expected, got %q."):format(type(tbl)), 2)
end
-- Clear the container table
if tbl then
for k,v in pairs(tbl) do tbl[k] = nil end
else
tbl = {}
end
local curProfile = self.keys.profile
local i = 0
for profileKey in pairs(self.profiles) do
i = i + 1
tbl[i] = profileKey
if curProfile and profileKey == curProfile then curProfile = nil end
end
-- Add the current profile, if it hasn't been created yet
if curProfile then
i = i + 1
tbl[i] = curProfile
end
return tbl, i
end
--- Returns the current profile name used by the database
function DBObjectLib:GetCurrentProfile()
return self.keys.profile
end
--- Deletes a named profile. This profile must not be the active profile.
-- @param name The name of the profile to be deleted
-- @param silent If true, do not raise an error when the profile does not exist
function DBObjectLib:DeleteProfile(name, silent)
if type(name) ~= "string" then
error(("Usage: AceDBObject:DeleteProfile(name): 'name' - string expected, got %q."):format(type(name)), 2)
end
if self.keys.profile == name then
error(("Cannot delete the active profile (%q) in an AceDBObject."):format(name), 2)
end
if not rawget(self.profiles, name) and not silent then
error(("Cannot delete profile %q as it does not exist."):format(name), 2)
end
self.profiles[name] = nil
-- populate to child namespaces
if self.children then
for _, db in pairs(self.children) do
DBObjectLib.DeleteProfile(db, name, true)
end
end
-- remove from unloaded namespaces
if self.sv.namespaces then
for nsname, data in pairs(self.sv.namespaces) do
if self.children and self.children[nsname] then
-- already a mapped namespace
elseif data.profiles then
data.profiles[name] = nil
end
end
end
-- switch all characters that use this profile back to the default
if self.sv.profileKeys then
for key, profile in pairs(self.sv.profileKeys) do
if profile == name then
self.sv.profileKeys[key] = nil
end
end
end
-- Callback: OnProfileDeleted, database, profileKey
self.callbacks:Fire("OnProfileDeleted", self, name)
end
--- Copies a named profile into the current profile, overwriting any conflicting
-- settings.
-- @param name The name of the profile to be copied into the current profile
-- @param silent If true, do not raise an error when the profile does not exist
function DBObjectLib:CopyProfile(name, silent)
if type(name) ~= "string" then
error(("Usage: AceDBObject:CopyProfile(name): 'name' - string expected, got %q."):format(type(name)), 2)
end
if name == self.keys.profile then
error(("Cannot have the same source and destination profiles (%q)."):format(name), 2)
end
if not rawget(self.profiles, name) and not silent then
error(("Cannot copy profile %q as it does not exist."):format(name), 2)
end
-- Reset the profile before copying
DBObjectLib.ResetProfile(self, nil, true)
local profile = self.profile
local source = self.profiles[name]
copyTable(source, profile)
-- populate to child namespaces
if self.children then
for _, db in pairs(self.children) do
DBObjectLib.CopyProfile(db, name, true)
end
end
-- copy unloaded namespaces
if self.sv.namespaces then
for nsname, data in pairs(self.sv.namespaces) do
if self.children and self.children[nsname] then
-- already a mapped namespace
elseif data.profiles then
-- reset the current profile
data.profiles[self.keys.profile] = {}
-- copy data
copyTable(data.profiles[name], data.profiles[self.keys.profile])
end
end
end
-- Callback: OnProfileCopied, database, sourceProfileKey
self.callbacks:Fire("OnProfileCopied", self, name)
end
--- Resets the current profile to the default values (if specified).
-- @param noChildren if set to true, the reset will not be populated to the child namespaces of this DB object
-- @param noCallbacks if set to true, won't fire the OnProfileReset callback
function DBObjectLib:ResetProfile(noChildren, noCallbacks)
local profile = self.profile
for k,v in pairs(profile) do
profile[k] = nil
end
local defaults = self.defaults and self.defaults.profile
if defaults then
copyDefaults(profile, defaults)
end
-- populate to child namespaces
if self.children and not noChildren then
for _, db in pairs(self.children) do
DBObjectLib.ResetProfile(db, nil, noCallbacks)
end
end
-- reset unloaded namespaces
if self.sv.namespaces and not noChildren then
for nsname, data in pairs(self.sv.namespaces) do
if self.children and self.children[nsname] then
-- already a mapped namespace
elseif data.profiles then
-- reset the current profile
data.profiles[self.keys.profile] = nil
end
end
end
-- Callback: OnProfileReset, database
if not noCallbacks then
self.callbacks:Fire("OnProfileReset", self)
end
end
--- Resets the entire database, using the string defaultProfile as the new default
-- profile.
-- @param defaultProfile The profile name to use as the default
function DBObjectLib:ResetDB(defaultProfile)
if defaultProfile and type(defaultProfile) ~= "string" and defaultProfile ~= true then
error(("Usage: AceDBObject:ResetDB(defaultProfile): 'defaultProfile' - string or true expected, got %q."):format(type(defaultProfile)), 2)
end
local sv = self.sv
for k,v in pairs(sv) do
sv[k] = nil
end
initdb(sv, self.defaults, defaultProfile, self)
-- fix the child namespaces
if self.children then
if not sv.namespaces then sv.namespaces = {} end
for name, db in pairs(self.children) do
if not sv.namespaces[name] then sv.namespaces[name] = {} end
initdb(sv.namespaces[name], db.defaults, self.keys.profile, db, self)
end
end
-- Callback: OnDatabaseReset, database
self.callbacks:Fire("OnDatabaseReset", self)
-- Callback: OnProfileChanged, database, profileKey
self.callbacks:Fire("OnProfileChanged", self, self.keys["profile"])
return self
end
--- Creates a new database namespace, directly tied to the database. This
-- is a full scale database in it's own rights other than the fact that
-- it cannot control its profile individually
-- @param name The name of the new namespace
-- @param defaults A table of values to use as defaults
function DBObjectLib:RegisterNamespace(name, defaults)
if type(name) ~= "string" then
error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - string expected, got %q."):format(type(name)), 2)
end
if defaults and type(defaults) ~= "table" then
error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'defaults' - table or nil expected, got %q."):format(type(defaults)), 2)
end
if self.children and self.children[name] then
error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - a namespace called %q already exists."):format(name), 2)
end
local sv = self.sv
if not sv.namespaces then sv.namespaces = {} end
if not sv.namespaces[name] then
sv.namespaces[name] = {}
end
local newDB = initdb(sv.namespaces[name], defaults, self.keys.profile, nil, self)
if not self.children then self.children = {} end
self.children[name] = newDB
return newDB
end
--- Returns an already existing namespace from the database object.
-- @param name The name of the new namespace
-- @param silent if true, the addon is optional, silently return nil if its not found
-- @usage
-- local namespace = self.db:GetNamespace('namespace')
-- @return the namespace object if found
function DBObjectLib:GetNamespace(name, silent)
if type(name) ~= "string" then
error(("Usage: AceDBObject:GetNamespace(name): 'name' - string expected, got %q."):format(type(name)), 2)
end
if not silent and not (self.children and self.children[name]) then
error(("Usage: AceDBObject:GetNamespace(name): 'name' - namespace %q does not exist."):format(name), 2)
end
if not self.children then self.children = {} end
return self.children[name]
end
--[[-------------------------------------------------------------------------
AceDB Exposed Methods
---------------------------------------------------------------------------]]
--- Creates a new database object that can be used to handle database settings and profiles.
-- By default, an empty DB is created, using a character specific profile.
--
-- You can override the default profile used by passing any profile name as the third argument,
-- or by passing //true// as the third argument to use a globally shared profile called "Default".
--
-- Note that there is no token replacement in the default profile name, passing a defaultProfile as "char"
-- will use a profile named "char", and not a character-specific profile.
-- @param tbl The name of variable, or table to use for the database
-- @param defaults A table of database defaults
-- @param defaultProfile The name of the default profile. If not set, a character specific profile will be used as the default.
-- You can also pass //true// to use a shared global profile called "Default".
-- @usage
-- -- Create an empty DB using a character-specific default profile.
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB")
-- @usage
-- -- Create a DB using defaults and using a shared default profile
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true)
function AceDB:New(tbl, defaults, defaultProfile)
if type(tbl) == "string" then
local name = tbl
tbl = _G[name]
if not tbl then
tbl = {}
_G[name] = tbl
end
end
if type(tbl) ~= "table" then
error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'tbl' - table expected, got %q."):format(type(tbl)), 2)
end
if defaults and type(defaults) ~= "table" then
error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaults' - table expected, got %q."):format(type(defaults)), 2)
end
if defaultProfile and type(defaultProfile) ~= "string" and defaultProfile ~= true then
error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaultProfile' - string or true expected, got %q."):format(type(defaultProfile)), 2)
end
return initdb(tbl, defaults, defaultProfile)
end
-- upgrade existing databases
for db in pairs(AceDB.db_registry) do
if not db.parent then
for name,func in pairs(DBObjectLib) do
db[name] = func
end
else
db.RegisterDefaults = DBObjectLib.RegisterDefaults
db.ResetProfile = DBObjectLib.ResetProfile
end
end
+4
View File
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceDB-3.0.lua"/>
</Ui>
@@ -0,0 +1,126 @@
--- AceEvent-3.0 provides event registration and secure dispatching.
-- All dispatching is done using **CallbackHandler-1.0**. AceEvent is a simple wrapper around
-- CallbackHandler, and dispatches all game events or addon message to the registrees.
--
-- **AceEvent-3.0** can be embeded into your addon, either explicitly by calling AceEvent:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceEvent itself.\\
-- It is recommended to embed AceEvent, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceEvent.
-- @class file
-- @name AceEvent-3.0
-- @release $Id$
local CallbackHandler = LibStub("CallbackHandler-1.0")
local MAJOR, MINOR = "AceEvent-3.0", 4
local AceEvent = LibStub:NewLibrary(MAJOR, MINOR)
if not AceEvent then return end
-- Lua APIs
local pairs = pairs
AceEvent.frame = AceEvent.frame or CreateFrame("Frame", "AceEvent30Frame") -- our event frame
AceEvent.embeds = AceEvent.embeds or {} -- what objects embed this lib
-- APIs and registry for blizzard events, using CallbackHandler lib
if not AceEvent.events then
AceEvent.events = CallbackHandler:New(AceEvent,
"RegisterEvent", "UnregisterEvent", "UnregisterAllEvents")
end
function AceEvent.events:OnUsed(target, eventname)
AceEvent.frame:RegisterEvent(eventname)
end
function AceEvent.events:OnUnused(target, eventname)
AceEvent.frame:UnregisterEvent(eventname)
end
-- APIs and registry for IPC messages, using CallbackHandler lib
if not AceEvent.messages then
AceEvent.messages = CallbackHandler:New(AceEvent,
"RegisterMessage", "UnregisterMessage", "UnregisterAllMessages"
)
AceEvent.SendMessage = AceEvent.messages.Fire
end
--- embedding and embed handling
local mixins = {
"RegisterEvent", "UnregisterEvent",
"RegisterMessage", "UnregisterMessage",
"SendMessage",
"UnregisterAllEvents", "UnregisterAllMessages",
}
--- Register for a Blizzard Event.
-- The callback will be called with the optional `arg` as the first argument (if supplied), and the event name as the second (or first, if no arg was supplied)
-- Any arguments to the event will be passed on after that.
-- @name AceEvent:RegisterEvent
-- @class function
-- @paramsig event[, callback [, arg]]
-- @param event The event to register for
-- @param callback The callback function to call when the event is triggered (funcref or method, defaults to a method with the event name)
-- @param arg An optional argument to pass to the callback function
--- Unregister an event.
-- @name AceEvent:UnregisterEvent
-- @class function
-- @paramsig event
-- @param event The event to unregister
--- Register for a custom AceEvent-internal message.
-- The callback will be called with the optional `arg` as the first argument (if supplied), and the event name as the second (or first, if no arg was supplied)
-- Any arguments to the event will be passed on after that.
-- @name AceEvent:RegisterMessage
-- @class function
-- @paramsig message[, callback [, arg]]
-- @param message The message to register for
-- @param callback The callback function to call when the message is triggered (funcref or method, defaults to a method with the event name)
-- @param arg An optional argument to pass to the callback function
--- Unregister a message
-- @name AceEvent:UnregisterMessage
-- @class function
-- @paramsig message
-- @param message The message to unregister
--- Send a message over the AceEvent-3.0 internal message system to other addons registered for this message.
-- @name AceEvent:SendMessage
-- @class function
-- @paramsig message, ...
-- @param message The message to send
-- @param ... Any arguments to the message
-- Embeds AceEvent into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceEvent in
function AceEvent:Embed(target)
for k, v in pairs(mixins) do
target[v] = self[v]
end
self.embeds[target] = true
return target
end
-- AceEvent:OnEmbedDisable( target )
-- target (object) - target object that is being disabled
--
-- Unregister all events messages etc when the target disables.
-- this method should be called by the target manually or by an addon framework
function AceEvent:OnEmbedDisable(target)
target:UnregisterAllEvents()
target:UnregisterAllMessages()
end
-- Script to fire blizzard events into the event listeners
local events = AceEvent.events
AceEvent.frame:SetScript("OnEvent", function(this, event, ...)
events:Fire(event, ...)
end)
--- Finally: upgrade our old embeds
for target, v in pairs(AceEvent.embeds) do
AceEvent:Embed(target)
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceEvent-3.0.lua"/>
</Ui>
@@ -0,0 +1,133 @@
--- **AceLocale-3.0** manages localization in addons, allowing for multiple locale to be registered with fallback to the base locale for untranslated strings.
-- @class file
-- @name AceLocale-3.0
-- @release $Id$
local MAJOR,MINOR = "AceLocale-3.0", 6
local AceLocale, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceLocale then return end -- no upgrade needed
-- Lua APIs
local assert, tostring, error = assert, tostring, error
local getmetatable, setmetatable, rawset, rawget = getmetatable, setmetatable, rawset, rawget
local gameLocale = GetLocale()
if gameLocale == "enGB" then
gameLocale = "enUS"
end
AceLocale.apps = AceLocale.apps or {} -- array of ["AppName"]=localetableref
AceLocale.appnames = AceLocale.appnames or {} -- array of [localetableref]="AppName"
-- This metatable is used on all tables returned from GetLocale
local readmeta = {
__index = function(self, key) -- requesting totally unknown entries: fire off a nonbreaking error and return key
rawset(self, key, key) -- only need to see the warning once, really
geterrorhandler()(MAJOR..": "..tostring(AceLocale.appnames[self])..": Missing entry for '"..tostring(key).."'")
return key
end
}
-- This metatable is used on all tables returned from GetLocale if the silent flag is true, it does not issue a warning on unknown keys
local readmetasilent = {
__index = function(self, key) -- requesting totally unknown entries: return key
rawset(self, key, key) -- only need to invoke this function once
return key
end
}
-- Remember the locale table being registered right now (it gets set by :NewLocale())
-- NOTE: Do never try to register 2 locale tables at once and mix their definition.
local registering
-- local assert false function
local assertfalse = function() assert(false) end
-- This metatable proxy is used when registering nondefault locales
local writeproxy = setmetatable({}, {
__newindex = function(self, key, value)
rawset(registering, key, value == true and key or value) -- assigning values: replace 'true' with key string
end,
__index = assertfalse
})
-- This metatable proxy is used when registering the default locale.
-- It refuses to overwrite existing values
-- Reason 1: Allows loading locales in any order
-- Reason 2: If 2 modules have the same string, but only the first one to be
-- loaded has a translation for the current locale, the translation
-- doesn't get overwritten.
--
local writedefaultproxy = setmetatable({}, {
__newindex = function(self, key, value)
if not rawget(registering, key) then
rawset(registering, key, value == true and key or value)
end
end,
__index = assertfalse
})
--- Register a new locale (or extend an existing one) for the specified application.
-- :NewLocale will return a table you can fill your locale into, or nil if the locale isn't needed for the players
-- game locale.
-- @paramsig application, locale[, isDefault[, silent]]
-- @param application Unique name of addon / module
-- @param locale Name of the locale to register, e.g. "enUS", "deDE", etc.
-- @param isDefault If this is the default locale being registered (your addon is written in this language, generally enUS)
-- @param silent If true, the locale will not issue warnings for missing keys. Must be set on the first locale registered. If set to "raw", nils will be returned for unknown keys (no metatable used).
-- @usage
-- -- enUS.lua
-- local L = LibStub("AceLocale-3.0"):NewLocale("TestLocale", "enUS", true)
-- L["string1"] = true
--
-- -- deDE.lua
-- local L = LibStub("AceLocale-3.0"):NewLocale("TestLocale", "deDE")
-- if not L then return end
-- L["string1"] = "Zeichenkette1"
-- @return Locale Table to add localizations to, or nil if the current locale is not required.
function AceLocale:NewLocale(application, locale, isDefault, silent)
-- GAME_LOCALE allows translators to test translations of addons without having that wow client installed
local activeGameLocale = GAME_LOCALE or gameLocale
local app = AceLocale.apps[application]
if silent and app and getmetatable(app) ~= readmetasilent then
geterrorhandler()("Usage: NewLocale(application, locale[, isDefault[, silent]]): 'silent' must be specified for the first locale registered")
end
if not app then
if silent=="raw" then
app = {}
else
app = setmetatable({}, silent and readmetasilent or readmeta)
end
AceLocale.apps[application] = app
AceLocale.appnames[app] = application
end
if locale ~= activeGameLocale and not isDefault then
return -- nop, we don't need these translations
end
registering = app -- remember globally for writeproxy and writedefaultproxy
if isDefault then
return writedefaultproxy
end
return writeproxy
end
--- Returns localizations for the current locale (or default locale if translations are missing).
-- Errors if nothing is registered (spank developer, not just a missing translation)
-- @param application Unique name of addon / module
-- @param silent If true, the locale is optional, silently return nil if it's not found (defaults to false, optional)
-- @return The locale table for the current language.
function AceLocale:GetLocale(application, silent)
if not silent and not AceLocale.apps[application] then
error("Usage: GetLocale(application[, silent]): 'application' - No locales registered for '"..tostring(application).."'", 2)
end
return AceLocale.apps[application]
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceLocale-3.0.lua"/>
</Ui>
@@ -0,0 +1,287 @@
--- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
-- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially
-- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
-- references to the same table will be send individually.
--
-- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceSerializer itself.\\
-- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceSerializer.
-- @class file
-- @name AceSerializer-3.0
-- @release $Id$
local MAJOR,MINOR = "AceSerializer-3.0", 5
local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceSerializer then return end
-- Lua APIs
local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format
local assert, error, pcall = assert, error, pcall
local type, tostring, tonumber = type, tostring, tonumber
local pairs, select, frexp = pairs, select, math.frexp
local tconcat = table.concat
-- quick copies of string representations of wonky numbers
local inf = math.huge
local serNaN -- can't do this in 4.3, see ace3 ticket 268
local serInf, serInfMac = "1.#INF", "inf"
local serNegInf, serNegInfMac = "-1.#INF", "-inf"
-- Serialization functions
local function SerializeStringHelper(ch) -- Used by SerializeValue for strings
-- We use \126 ("~") as an escape character for all nonprints plus a few more
local n = strbyte(ch)
if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
return "\126\122"
elseif n<=32 then -- nonprint + space
return "\126"..strchar(n+64)
elseif n==94 then -- value separator
return "\126\125"
elseif n==126 then -- our own escape character
return "\126\124"
elseif n==127 then -- nonprint (DEL)
return "\126\123"
else
assert(false) -- can't be reached if caller uses a sane regex
end
end
local function SerializeValue(v, res, nres)
-- We use "^" as a value separator, followed by one byte for type indicator
local t=type(v)
if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc)
res[nres+1] = "^S"
res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
nres=nres+2
elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
local str = tostring(v)
if tonumber(str)==v --[[not in 4.3 or str==serNaN]] then
-- translates just fine, transmit as-is
res[nres+1] = "^N"
res[nres+2] = str
nres=nres+2
elseif v == inf or v == -inf then
res[nres+1] = "^N"
res[nres+2] = v == inf and serInf or serNegInf
nres=nres+2
else
local m,e = frexp(v)
res[nres+1] = "^F"
res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999)
res[nres+3] = "^f"
res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation
nres=nres+4
end
elseif t=="table" then -- ^T...^t = table (list of key,value pairs)
nres=nres+1
res[nres] = "^T"
for key,value in pairs(v) do
nres = SerializeValue(key, res, nres)
nres = SerializeValue(value, res, nres)
end
nres=nres+1
res[nres] = "^t"
elseif t=="boolean" then -- ^B = true, ^b = false
nres=nres+1
if v then
res[nres] = "^B" -- true
else
res[nres] = "^b" -- false
end
elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P)
nres=nres+1
res[nres] = "^Z"
else
error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive
end
return nres
end
local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1
--- Serialize the data passed into the function.
-- Takes a list of values (strings, numbers, booleans, nils, tables)
-- and returns it in serialized form (a string).\\
-- May throw errors on invalid data types.
-- @param ... List of values to serialize
-- @return The data in its serialized form (string)
function AceSerializer:Serialize(...)
local nres = 1
for i=1,select("#", ...) do
local v = select(i, ...)
nres = SerializeValue(v, serializeTbl, nres)
end
serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data
return tconcat(serializeTbl, "", 1, nres+1)
end
-- Deserialization functions
local function DeserializeStringHelper(escape)
if escape<"~\122" then
return strchar(strbyte(escape,2,2)-64)
elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
return "\030"
elseif escape=="~\123" then
return "\127"
elseif escape=="~\124" then
return "\126"
elseif escape=="~\125" then
return "\94"
end
error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up
end
local function DeserializeNumberHelper(number)
--[[ not in 4.3 if number == serNaN then
return 0/0
else]]if number == serNegInf or number == serNegInfMac then
return -inf
elseif number == serInf or number == serInfMac then
return inf
else
return tonumber(number)
end
end
-- DeserializeValue: worker function for :Deserialize()
-- It works in two modes:
-- Main (top-level) mode: Deserialize a list of values and return them all
-- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
--
-- The function _always_ works recursively due to having to build a list of values to return
--
-- Callers are expected to pcall(DeserializeValue) to trap errors
local function DeserializeValue(iter,single,ctl,data)
if not single then
ctl,data = iter()
end
if not ctl then
error("Supplied data misses AceSerializer terminator ('^^')")
end
if ctl=="^^" then
-- ignore extraneous data
return
end
local res
if ctl=="^S" then
res = gsub(data, "~.", DeserializeStringHelper)
elseif ctl=="^N" then
res = DeserializeNumberHelper(data)
if not res then
error("Invalid serialized number: '"..tostring(data).."'")
end
elseif ctl=="^F" then -- ^F<mantissa>^f<exponent>
local ctl2,e = iter()
if ctl2~="^f" then
error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
end
local m=tonumber(data)
e=tonumber(e)
if not (m and e) then
error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
end
res = m*(2^e)
elseif ctl=="^B" then -- yeah yeah ignore data portion
res = true
elseif ctl=="^b" then -- yeah yeah ignore data portion
res = false
elseif ctl=="^Z" then -- yeah yeah ignore data portion
res = nil
elseif ctl=="^T" then
-- ignore ^T's data, future extensibility?
res = {}
local k,v
while true do
ctl,data = iter()
if ctl=="^t" then break end -- ignore ^t's data
k = DeserializeValue(iter,true,ctl,data)
if k==nil then
error("Invalid AceSerializer table format (no table end marker)")
end
ctl,data = iter()
v = DeserializeValue(iter,true,ctl,data)
if v==nil then
error("Invalid AceSerializer table format (no table end marker)")
end
res[k]=v
end
else
error("Invalid AceSerializer control code '"..ctl.."'")
end
if not single then
return res,DeserializeValue(iter)
else
return res
end
end
--- Deserializes the data into its original values.
-- Accepts serialized data, ignoring all control characters and whitespace.
-- @param str The serialized data (from :Serialize)
-- @return true followed by a list of values, OR false followed by an error message
function AceSerializer:Deserialize(str)
str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff
local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
local ctl,data = iter()
if not ctl or ctl~="^1" then
-- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
return false, "Supplied data is not AceSerializer data (rev 1)"
end
return pcall(DeserializeValue, iter)
end
----------------------------------------
-- Base library stuff
----------------------------------------
AceSerializer.internals = { -- for test scripts
SerializeValue = SerializeValue,
SerializeStringHelper = SerializeStringHelper,
}
local mixins = {
"Serialize",
"Deserialize",
}
AceSerializer.embeds = AceSerializer.embeds or {}
function AceSerializer:Embed(target)
for k, v in pairs(mixins) do
target[v] = self[v]
end
self.embeds[target] = true
return target
end
-- Update embeds
for target, v in pairs(AceSerializer.embeds) do
AceSerializer:Embed(target)
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceSerializer-3.0.lua"/>
</Ui>
@@ -0,0 +1,278 @@
--- **AceTimer-3.0** provides a central facility for registering timers.
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
-- restricts us to.
--
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
-- need to cancel the timer you just registered.
--
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceTimer.
-- @class file
-- @name AceTimer-3.0
-- @release $Id$
local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceTimer then return end -- No upgrade needed
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
-- Lua APIs
local type, unpack, next, error, select = type, unpack, next, error, select
-- WoW APIs
local GetTime, C_TimerAfter = GetTime, C_Timer.After
local function new(self, loop, func, delay, ...)
if delay < 0.01 then
delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us
end
local timer = {
object = self,
func = func,
looping = loop,
argsCount = select("#", ...),
delay = delay,
ends = GetTime() + delay,
...
}
activeTimers[timer] = timer
-- Create new timer closure to wrap the "timer" object
timer.callback = function()
if not timer.cancelled then
if type(timer.func) == "string" then
-- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil
-- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue.
timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount))
else
timer.func(unpack(timer, 1, timer.argsCount))
end
if timer.looping and not timer.cancelled then
-- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly
-- due to fps differences
local time = GetTime()
local ndelay = timer.delay - (time - timer.ends)
-- Ensure the delay doesn't go below the threshold
if ndelay < 0.01 then ndelay = 0.01 end
C_TimerAfter(ndelay, timer.callback)
timer.ends = time + ndelay
else
activeTimers[timer.handle or timer] = nil
end
end
end
C_TimerAfter(delay, timer.callback)
return timer
end
--- Schedule a new one-shot timer.
-- The timer will fire once in `delay` seconds, unless canceled before.
-- @param func Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self:ScheduleTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- print("5 seconds passed")
-- end
function AceTimer:ScheduleTimer(func, delay, ...)
if not func or not delay then
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
end
if type(func) == "string" then
if type(self) ~= "table" then
error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
elseif not self[func] then
error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
end
end
return new(self, nil, func, delay, ...)
end
--- Schedule a repeating timer.
-- The timer will fire every `delay` seconds, until canceled.
-- @param func Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self.timerCount = 0
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- self.timerCount = self.timerCount + 1
-- print(("%d seconds passed"):format(5 * self.timerCount))
-- -- run 30 seconds in total
-- if self.timerCount == 6 then
-- self:CancelTimer(self.testTimer)
-- end
-- end
function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
if not func or not delay then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
end
if type(func) == "string" then
if type(self) ~= "table" then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
elseif not self[func] then
error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
end
end
return new(self, true, func, delay, ...)
end
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
-- and the timer has not fired yet or was canceled before.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
function AceTimer:CancelTimer(id)
local timer = activeTimers[id]
if not timer then
return false
else
timer.cancelled = true
activeTimers[id] = nil
return true
end
end
--- Cancels all timers registered to the current addon object ('self')
function AceTimer:CancelAllTimers()
for k,v in next, activeTimers do
if v.object == self then
AceTimer.CancelTimer(self, k)
end
end
end
--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
-- This function will return 0 when the id is invalid.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
-- @return The time left on the timer.
function AceTimer:TimeLeft(id)
local timer = activeTimers[id]
if not timer then
return 0
else
return timer.ends - GetTime()
end
end
-- ---------------------------------------------------------------------
-- Upgrading
-- Upgrade from old hash-bucket based timers to C_Timer.After timers.
if oldminor and oldminor < 10 then
-- disable old timer logic
AceTimer.frame:SetScript("OnUpdate", nil)
AceTimer.frame:SetScript("OnEvent", nil)
AceTimer.frame:UnregisterAllEvents()
-- convert timers
for object,timers in next, AceTimer.selfs do
for handle,timer in next, timers do
if type(timer) == "table" and timer.callback then
local newTimer
if timer.delay then
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
else
newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
end
-- Use the old handle for old timers
activeTimers[newTimer] = nil
activeTimers[handle] = newTimer
newTimer.handle = handle
end
end
end
AceTimer.selfs = nil
AceTimer.hash = nil
AceTimer.debug = nil
elseif oldminor and oldminor < 17 then
-- Upgrade from old animation based timers to C_Timer.After timers.
AceTimer.inactiveTimers = nil
AceTimer.frame = nil
local oldTimers = AceTimer.activeTimers
-- Clear old timer table and update upvalue
AceTimer.activeTimers = {}
activeTimers = AceTimer.activeTimers
for handle, timer in next, oldTimers do
local newTimer
-- Stop the old timer animation
local duration, elapsed = timer:GetDuration(), timer:GetElapsed()
timer:GetParent():Stop()
if timer.looping then
newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount))
else
newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount))
end
-- Use the old handle for old timers
activeTimers[newTimer] = nil
activeTimers[handle] = newTimer
newTimer.handle = handle
end
-- Migrate transitional handles
if oldminor < 13 and AceTimer.hashCompatTable then
for handle, id in next, AceTimer.hashCompatTable do
local t = activeTimers[id]
if t then
activeTimers[id] = nil
activeTimers[handle] = t
t.handle = handle
end
end
AceTimer.hashCompatTable = nil
end
end
-- ---------------------------------------------------------------------
-- Embed handling
AceTimer.embeds = AceTimer.embeds or {}
local mixins = {
"ScheduleTimer", "ScheduleRepeatingTimer",
"CancelTimer", "CancelAllTimers",
"TimeLeft"
}
function AceTimer:Embed(target)
AceTimer.embeds[target] = true
for _,v in next, mixins do
target[v] = AceTimer[v]
end
return target
end
-- AceTimer:OnEmbedDisable(target)
-- target (object) - target object that AceTimer is embedded in.
--
-- cancel all timers registered for the object
function AceTimer:OnEmbedDisable(target)
target:CancelAllTimers()
end
for addon in next, AceTimer.embeds do
AceTimer:Embed(addon)
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceTimer-3.0.lua"/>
</Ui>
@@ -0,0 +1,202 @@
--[[ $Id: CallbackHandler-1.0.lua 25 2022-12-12 15:02:36Z nevcairiel $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 8
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
if not CallbackHandler then return end -- No upgrade needed
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
-- Lua APIs
local securecallfunction, error = securecallfunction, error
local setmetatable, rawget = setmetatable, rawget
local next, select, pairs, type, tostring = next, select, pairs, type, tostring
local function Dispatch(handlers, ...)
local index, method = next(handlers)
if not method then return end
repeat
securecallfunction(method, ...)
index, method = next(handlers, index)
until not method
end
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
function CallbackHandler.New(_self, target, RegisterName, UnregisterName, UnregisterAllName)
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, ...)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
Dispatch(events[eventname], eventname, ...)
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for event,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, event) or not next(events[event]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for object,func in pairs(callbacks) do
events[event][object] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, event)
first = nil
end
end
end
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
method = method or eventname
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
end
local regfunc
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) self[method](self,arg,...) end
else
regfunc = function(...) self[method](self,...) end
end
else
-- function ref with self=object or self="addonId" or self=thread
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) method(arg,...) end
else
regfunc = method
end
end
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
registry.insertQueue[eventname][self] = regfunc
end
end
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(...)
if select("#",...)<1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
end
if select("#",...)==1 and ...==target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
end
for i=1,select("#",...) do
local self = select(i,...)
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="CallbackHandler-1.0.lua"/>
</Ui>
@@ -0,0 +1,201 @@
--[[
****************************************************************************************
LibAboutPanel
File date: 2009-06-23T02:04:30Z
Project version: v1.43
Author: Tekkub, Ackis
****************************************************************************************
]]--
local lib, oldminor = LibStub:NewLibrary("LibAboutPanel", 2)
if not lib then return end
function lib.new(parent, addonname)
local frame = CreateFrame("Frame", nil, UIParent)
frame.name, frame.parent, frame.addonname = not parent and gsub(addonname," ","") or "About", parent, gsub(addonname," ","") -- Remove spaces from addonname because GetMetadata doesn't like that
frame:Hide()
frame:SetScript("OnShow", lib.OnShow)
InterfaceOptions_AddCategory(frame)
return frame
end
local GAME_LOCALE = GetLocale()
local L = {}
-- frFR
if GAME_LOCALE == "frFR" then
L["About"] = "à propos de"
L["Click and press Ctrl-C to copy"] = "Click and press Ctrl-C to copy"
-- deDE
elseif GAME_LOCALE == "deDE" then
L["About"] = "Über"
L["Click and press Ctrl-C to copy"] = "Klicken und Strg-C drücken zum kopieren"
-- esES
elseif GAME_LOCALE == "esES" then
L["About"] = "Acerca de"
L["Click and press Ctrl-C to copy"] = "Click and press Ctrl-C to copy"
-- esMX
elseif GAME_LOCALE == "esMX" then
L["About"] = "Sobre"
L["Click and press Ctrl-C to copy"] = "Click and press Ctrl-C to copy"
-- koKR
elseif GAME_LOCALE == "koKR" then
L["About"] = "대하여"
L["Click and press Ctrl-C to copy"] = "Click and press Ctrl-C to copy"
-- ruRU
elseif GAME_LOCALE == "ruRU" then
L["About"] = "Об аддоне"
L["Click and press Ctrl-C to copy"] = "Click and press Ctrl-C to copy"
-- zhCN
elseif GAME_LOCALE == "zhCN" then
L["About"] = "关于"
L["Click and press Ctrl-C to copy"] = "点击并 Ctrl-C 复制"
-- zhTW
elseif GAME_LOCALE == "zhTW" then
L["About"] = "關於"
L["Click and press Ctrl-C to copy"] = "點擊並 Ctrl-C 復制"
-- enUS and non-localized
else
L["About"] ="About"
L["Click and press Ctrl-C to copy"] = "Click and press Ctrl-C to copy"
end
local editbox = CreateFrame('EditBox', nil, UIParent)
editbox:Hide()
editbox:SetAutoFocus(true)
editbox:SetHeight(32)
editbox:SetFontObject('GameFontHighlightSmall')
lib.editbox = editbox
local left = editbox:CreateTexture(nil, "BACKGROUND")
left:SetWidth(8) left:SetHeight(20)
left:SetPoint("LEFT", -5, 0)
left:SetTexture("Interface\\Common\\Common-Input-Border")
left:SetTexCoord(0, 0.0625, 0, 0.625)
local right = editbox:CreateTexture(nil, "BACKGROUND")
right:SetWidth(8) right:SetHeight(20)
right:SetPoint("RIGHT", 0, 0)
right:SetTexture("Interface\\Common\\Common-Input-Border")
right:SetTexCoord(0.9375, 1, 0, 0.625)
local center = editbox:CreateTexture(nil, "BACKGROUND")
center:SetHeight(20)
center:SetPoint("RIGHT", right, "LEFT", 0, 0)
center:SetPoint("LEFT", left, "RIGHT", 0, 0)
center:SetTexture("Interface\\Common\\Common-Input-Border")
center:SetTexCoord(0.0625, 0.9375, 0, 0.625)
editbox:SetScript("OnEscapePressed", editbox.ClearFocus)
editbox:SetScript("OnEnterPressed", editbox.ClearFocus)
editbox:SetScript("OnEditFocusLost", editbox.Hide)
editbox:SetScript("OnEditFocusGained", editbox.HighlightText)
editbox:SetScript("OnTextChanged", function(self)
self:SetText(self:GetParent().val)
self:HighlightText()
end)
function lib.OpenEditbox(self)
editbox:SetText(self.val)
editbox:SetParent(self)
editbox:SetPoint("LEFT", self)
editbox:SetPoint("RIGHT", self)
editbox:Show()
end
local fields = {"Version", "Author", "X-Category", "X-License", "X-Email", "Email", "eMail", "X-Website", "X-Credits", "X-Localizations", "X-Donate"}
local haseditbox = {["X-Website"] = true, ["X-Email"] = true, ["X-Donate"] = true, ["Email"] = true, ["eMail"] = true}
local function HideTooltip() GameTooltip:Hide() end
local function ShowTooltip(self)
GameTooltip:SetOwner(self, "ANCHOR_TOPRIGHT")
GameTooltip:SetText(L["Click and press Ctrl-C to copy"])
--GameTooltip:SetText("Click and press Ctrl-C to copy")
end
function lib.OnShow(frame)
local notefield = "Notes"
if (GAME_LOCALE ~= "enUS") then
notefield = notefield .. "-" .. GAME_LOCALE
end
-- Get the localized version of notes if it exists or fall back to the english one.
local notes = GetAddOnMetadata(frame.addonname, notefield) or GetAddOnMetadata(frame.addonname, "Notes")
local title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
title:SetPoint("TOPLEFT", 16, -16)
title:SetText(frame.parent and (frame.parent.." - " .. L["About"]) or frame.name)
local subtitle = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
subtitle:SetHeight(32)
subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
subtitle:SetPoint("RIGHT", frame, -32, 0)
subtitle:SetNonSpaceWrap(true)
subtitle:SetJustifyH("LEFT")
subtitle:SetJustifyV("TOP")
subtitle:SetText(notes)
local anchor
for _,field in pairs(fields) do
local val = GetAddOnMetadata(frame.addonname, field)
if val then
local title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
title:SetWidth(75)
if not anchor then title:SetPoint("TOPLEFT", subtitle, "BOTTOMLEFT", -2, -12)
else title:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -10) end
title:SetJustifyH("RIGHT")
title:SetText(field:gsub("X%-", ""))
local detail = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
detail:SetHeight(32)
detail:SetPoint("LEFT", title, "RIGHT", 4, 0)
detail:SetPoint("RIGHT", frame, -16, 0)
detail:SetJustifyH("LEFT")
if (field == "Author") then
local authorservername = GetAddOnMetadata(frame.addonname, "X-Author-Server")
local authorfaction = GetAddOnMetadata(frame.addonname, "X-Author-Faction")
if authorservername and authorfaction then
detail:SetText((haseditbox[field] and "|cff9999ff" or "").. val .. " on " .. authorservername .. " (" .. authorfaction .. ")")
elseif authorservername and not authorfaction then
detail:SetText((haseditbox[field] and "|cff9999ff" or "").. val .. " on " .. authorservername)
elseif not authorservername and authorfaction then
detail:SetText((haseditbox[field] and "|cff9999ff" or "").. val .. " (" .. authorfaction .. ")")
else
detail:SetText((haseditbox[field] and "|cff9999ff" or "").. val)
end
elseif (field == "Version") then
local addonversion = GetAddOnMetadata(frame.addonname, field)
-- Remove @project-revision@ and replace it with Repository
addonversion = string.gsub(addonversion,"@project.revision@","Repository")
detail:SetText((haseditbox[field] and "|cff9999ff" or "").. addonversion)
else
detail:SetText((haseditbox[field] and "|cff9999ff" or "").. val)
end
if haseditbox[field] then
local button = CreateFrame("Button", nil, frame)
button:SetAllPoints(detail)
button.val = val
button:SetScript("OnClick", lib.OpenEditbox)
button:SetScript("OnEnter", ShowTooltip)
button:SetScript("OnLeave", HideTooltip)
end
anchor = title
end
end
end
@@ -0,0 +1,36 @@
## Title: Lib: AboutPanel
## X-Curse-Packaged-Version: v1.43
## X-Curse-Project-Name: LibAboutPanel
## X-Curse-Project-ID: libaboutpanel
## X-Curse-Repository-ID: wow/libaboutpanel/mainline
## Notes: Adds an about panel to interface options.
## Author: Ackis
## eMail: ackis AT shaw DOT ca
##X-Author-Faction = Alliance
##X-Author-Server = Thunderlord US
## X-Donate: http://www.curseforge.com/projects/libaboutpanel/#w_donations
## Interface: 30300
## Version: 1.43
## X-Revision: @project-revision@
## X-Date: 2009-12-18T22:27:31Z
## X-Category: Libraries
## X-Localizations: enUS
## X-Website: http://www.wowwiki.com/LibAboutPanel
## X-Feedback: http://wow.curse.com/downloads/wow-addons/details/libaboutpanel.aspx
## Dependencies:
## X-Embeds: LibStub, CallbackHandler-1.0
## OptionalDeps: LibStub, CallbackHandler-1.0
## DefaultState: Enabled
## LoadOnDemand: 0
#@no-lib-strip@
libs\LibStub\LibStub.lua
libs\CallbackHandler-1.0\CallbackHandler-1.0.xml
#@end-no-lib-strip@
LibAboutPanel.lua
+4
View File
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="LibAboutPanel.lua" />
</Ui>
@@ -0,0 +1,371 @@
--[[
Name: PeriodicTable-3.1
Revision: $Rev: 6 $
Author: Nymbia (nymbia@gmail.com)
Many thanks to Tekkub for writing PeriodicTable 1 and 2, and for permission to use the name PeriodicTable!
Website: http://www.wowace.com/wiki/PeriodicTable-3.1
Documentation: http://www.wowace.com/wiki/PeriodicTable-3.1/API
SVN: http://svn.wowace.com/wowace/trunk/PeriodicTable-3.1/PeriodicTable-3.1/
Description: Library of compressed itemid sets.
Dependencies: AceLibrary
License: LGPL v2.1
Copyright (C) 2007 Nymbia
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
]]
local PT3, oldminor = LibStub:NewLibrary("LibPeriodicTable-3.1", tonumber(("$Revision: 6 $"):match("(%d+)")) + 90000)
if not PT3 then
return
end
-- local references to oft-used global functions.
local type = type
local rawget = rawget
local tonumber = tonumber
local pairs = pairs
local ipairs = ipairs
local next = next
local assert = assert
local table_concat = table.concat
local iternum, iterpos, cache, sets, embedversions
---------------------------------------------
-- Internal / Local Functions --
---------------------------------------------
local getItemID, makeNonPresentMultiSet, shredCache, setiter, multisetiter
function getItemID(item)
-- accepts either an item string ie "item:12345:0:0:0:2342:123324:12:1", hyperlink, or an itemid.
-- returns a number'ified itemid.
return tonumber(item) or tonumber(item:match("item:(%d+)")) or (-1 * ((item:match("enchant:(%d+)") or item:match("spell:(%d+)")) or 0))
end
do
local tables = setmetatable({},{__mode = 'k'})
function makeNonPresentMultiSet(parentname)
-- makes an implied multiset, ie if you define only the set "a.b.c",
-- a request to "a.b" will come through here for a.b to be built.
-- an expensive function because it needs to iterate all active sets,
-- moreso for invalid sets.
-- store some temp tables with weak keys to reduce garbage churn
local temp = next(tables)
if temp then
tables[temp] = nil
else
temp = {}
end
-- Escape characters that will screw up the name matching.
local escapedparentname = parentname:gsub("([%.%(%)%%%+%-%*%?%[%]%^%$])", "%%%1")
-- Check all the sets to see if they start with this name.
for k in pairs(sets) do
if k:match("^"..escapedparentname.."%.") then
temp[#temp+1] = k
end
end
if #temp == 0 then
sets[parentname] = false
else
sets[parentname] = "m,"..table_concat(temp, ',')
end
-- clear the temp table then feed it back into the recycler
for k in pairs(temp) do
temp[k] = nil
end
tables[temp] = true
end
end
function shredCache(setname)
-- If there's a cache for this set, delete it, since we just added a new copy.
if rawget(cache, setname) then
cache[setname] = nil
end
local parentname = setname:match("^(.+)%.[^%.]+$")
if parentname then
-- Recurse and do the same for the parent set if we find one.
shredCache(parentname)
end
end
function setiter(t)
local k,v
if iterpos then
-- We already have a position that we're at in the iteration, grab the next value up.
k,v = next(t,iterpos)
else
-- We havent yet touched this set, grab the first value.
k,v = next(t)
end
if k == "set" then
k,v = next(t, k)
end
if k then
iterpos = k
return k,v,t.set
end
end
function multisetiter(t)
local k,v
if iterpos then
-- We already have a position that we're at in the iteration, grab the next value up.
k,v = next(t[iternum],iterpos)
else
-- We havent yet touched this set, grab the first value.
k,v = next(t[iternum])
end
if k == "set" then
k,v = next(t[iternum], k)
end
if k then
-- There's an entry here, no need to move on to the next table yet.
iterpos = k
return k,v,t[iternum].set
else
-- No entry, time to check for a new table.
iternum = iternum + 1
if not t[iternum] then
return
end
k,v = next(t[iternum])
if k == "set" then
k,v = next(t[iternum],k)
end
iterpos = k
return k,v,t[iternum].set
end
end
do
-- Handle the initial scan of LoD data modules, storing in this local table so the sets metatable can find em
local lodmodules = {}
for i = 1, GetNumAddOns() do
local metadata = GetAddOnMetadata(i, "X-PeriodicTable-3.1-Module")
if metadata then
local name, _, _, enabled = GetAddOnInfo(i)
if enabled then
lodmodules[metadata] = name
end
end
end
PT3.sets = setmetatable(PT3.sets or {}, {
__index = function(self, key)
local base = key:match("^([^%.]+)%.") or key
if lodmodules[base] then
LoadAddOn(lodmodules[base])
lodmodules[base] = nil -- don't try to load again
-- still may need to generate multiset or something like that, so re-call the metamethod if need be
return self[key]
end
makeNonPresentMultiSet(key) -- this will store it as empty if this is an invalid set.
return self[key]
end
})
end
PT3.embedversions = PT3.embedversions or {}
sets = PT3.sets
embedversions = PT3.embedversions
cache = setmetatable({}, {
__mode = 'v', -- weaken this table's values.
__index = function(self, key)
-- Get the setstring in question. This call does most of the hairy stuff
-- like putting together implied but absent multisets and finding child sets
local setstring = sets[key]
if not setstring then
return
end
if setstring:sub(1,2) == "m," then
-- This table is a list of references to the members of this set.
self[key] = {}
local working = self[key]
for childset in setstring:sub(3):gmatch("([^,]+)") do
if childset ~= key then -- infinite loops is bad
local pointer = cache[childset]
if pointer then
local _, firstv = next(pointer)
if type(firstv) == "table" then
-- This is a multiset, copy its references
for _,v in ipairs(pointer) do
working[#working+1] = v
end
elseif firstv then
-- This is not a multiset, just stick a reference in.
working[#working+1] = pointer
end
end
end
end
return working
else
-- normal ol' set. Well, maybe not, but close enough.
self[key] = {}
local working = self[key]
for itemstring in setstring:gmatch("([^,]+)") do
-- for each item (comma seperated)..
-- ...check to see if we have a value set (ie "14543:1121")
local id, value = itemstring:match("^([^:]+):(.+)$")
-- if we don't, (ie "14421,12312"), then set the value to true.
id, value = tonumber(id) or tonumber(itemstring), value or true
assert(id, 'malformed set? '..key)
working[id] = value
end
-- stick the set name in there so that we can find out which set an item originally came from.
working.set = key
return working
end
end
})
---------------------------------------------
-- API --
---------------------------------------------
-- These three are pretty simple. Note that non-present chunks will be generated by the metamethods.
function PT3:GetSetTable(set)
assert(type(set) == "string", "Invalid arg1: set must be a string")
return cache[set]
end
function PT3:GetSetString(set)
assert(type(set) == "string", "Invalid arg1: set must be a string")
return sets[set]
end
function PT3:IsSetMulti(set)
assert(type(set) == "string", "Invalid arg1: set must be a string")
-- Check if this set's a multiset by checking if its table contains tables instead of strings/booleans
local pointer = cache[set]
if not pointer then
return
end
local _, firstv = next(pointer)
if type(firstv) == "table" then
return true
else
return false
end
end
function PT3:IterateSet(set)
-- most of the work here is handled by the local functions above.
--!! this could maybe use some improvement...
local t = cache[set]
assert(t, "Invalid set: "..set)
if self:IsSetMulti(set) then
iternum, iterpos = 1, nil
return multisetiter, t
else
iterpos = nil
return setiter, t
end
end
-- Check if the item's contained in this set or any of it's child sets. If it is, return the value
-- (which is true for items with no value set) and the set where the item is contained in data.
function PT3:ItemInSet(item, set)
assert(type(item) == "number" or type(item) == "string", "Invalid arg1: item must be a number or item link")
assert(type(set) == "string", "Invalid arg2: set must be a string")
-- Type the passed item out to an itemid.
item = getItemID(item)
assert(item ~= 0,"Invalid arg1: invalid item.")
local pointer = cache[set]
if not pointer then
return
end
local _, firstv = next(pointer)
if type(firstv) == "table" then
-- The requested set is a multiset, iterate its children. Return the first matching item.
for _,v in ipairs(pointer) do
if v[item] then
return v[item], v.set
end
end
elseif pointer[item] then
-- Not a multiset, just return the value and set name.
return pointer[item], pointer.set
end
end
function PT3:AddData(arg1, arg2, arg3)
assert(type(arg1) == "string", "Invalid arg1: name must be a string")
assert(type(arg2) == "string" or type(arg2) == "table", "Invalid arg2: must be set contents string or table, or revision string")
assert((arg3 and type(arg3) == "table") or not arg3, "Invalid arg3: must be a table")
if not arg3 and type(arg2) == "string" then
-- Just a string.
local replacing
if rawget(sets, arg1) then
replacing = true
end
sets[arg1] = arg2
-- Clear the cache of this set's data if it exists, avoiding invoking the metamethod.
-- No sense generating data if we're just gonna nuke it anyway ;)
if replacing then
shredCache(arg1)
end
else
-- Table of sets passed.
if arg3 then
-- Woot, version numbers and everything.
assert(type(arg2) == "string", "Invalid arg2: must be revision string")
local version = tonumber(arg2:match("(%d+)"))
if embedversions[arg1] and embedversions[arg1] >= version then
-- The loaded version is newer than this one.
return
end
embedversions[arg1] = version
for k,v in pairs(arg3) do
-- Looks good, throw 'em in there one by one
self:AddData(k,v)
end
else
-- Boo, no version numbers. Just overwrite all these sets.
for k,v in pairs(arg2) do
self:AddData(k,v)
end
end
end
end
function PT3:ItemSearch(item)
assert(type(item) == "number" or type(item) == "string", "Invalid arg1: item must be a number or item link")
item = tonumber(item) or tonumber(item:match("item:(%d+)"))
if item == 0 then
self:error("Invalid arg1: invalid item.")
end
local matches = {}
for k,v in pairs(self.sets) do
local _, set = self:ItemInSet(item, k)
if set then
local have
for _,v in ipairs(matches) do
if v == set then
have = true
end
end
if not have then
table.insert(matches, set)
end
end
end
if #matches > 0 then
return matches
end
end
-- ie, LibStub('PeriodicTable-3.1')('InstanceLoot') == LibStub('LibPeriodicTable-3.1'):GetSetTable('InstanceLoot')
setmetatable(PT3, { __call = PT3.GetSetTable })
+30
View File
@@ -0,0 +1,30 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end
+13
View File
@@ -0,0 +1,13 @@
## Interface: 20400
## Title: Lib: LibStub
## Notes: Universal Library Stub
## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel
## X-Website: http://jira.wowace.com/browse/LS
## X-Category: Library
## X-License: Public Domain
## X-Curse-Packaged-Version: 1.0
## X-Curse-Project-Name: LibStub
## X-Curse-Project-ID: libstub
## X-Curse-Repository-ID: wow/libstub/mainline
LibStub.lua
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Ui xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.blizzard.com/wow/ui/" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Include file="Locales\deDE.lua"/>
<Include file="Locales\enUS.lua"/>
<Include file="Locales\esES.lua"/>
<Include file="Locales\esMX.lua"/>
<Include file="Locales\frFR.lua"/>
<Include file="Locales\koKR.lua"/>
<Include file="Locales\ruRU.lua"/>
<Include file="Locales\zhCN.lua"/>
<Include file="Locales\zhTW.lua"/>
</Ui>