- hard copied libraries to try avoid an error with the packpager.
This commit is contained in:
@@ -1,35 +1,18 @@
|
||||
package-as: Details
|
||||
|
||||
externals:
|
||||
Libs/LibStub: https://repos.wowace.com/wow/ace3/trunk/LibStub
|
||||
Libs/AceAddon-3.0: https://repos.wowace.com/wow/ace3/trunk/AceAddon-3.0
|
||||
Libs/AceComm-3.0: https://repos.wowace.com/wow/ace3/trunk/AceComm-3.0
|
||||
Libs/AceLocale-3.0: https://repos.wowace.com/wow/ace3/trunk/AceLocale-3.0
|
||||
Libs/AceSerializer-3.0: https://repos.wowace.com/wow/ace3/trunk/AceSerializer-3.0
|
||||
Libs/AceTimer-3.0: https://repos.wowace.com/wow/ace3/trunk/AceTimer-3.0
|
||||
Libs/CallbackHandler-1.0: https://repos.wowace.com/wow/ace3/trunk/CallbackHandler-1.0
|
||||
Libs/LibSharedMedia-3.0: https://repos.wowace.com/wow/libsharedmedia-3-0/trunk/LibSharedMedia-3.0
|
||||
Libs/LibGraph-2.0: https://repos.wowace.com/wow/libgraph-2-0/trunk/LibGraph-2.0
|
||||
Libs/LibDBIcon-1.0: https://repos.wowace.com/wow/libdbicon-1-0/trunk/LibDBIcon-1.0
|
||||
Libs/LibWindow-1.1: https://repos.wowace.com/wow/libwindow-1-1/trunk/LibWindow-1.1
|
||||
Libs/LibCompress: https://repos.wowace.com/wow/libcompress/trunk
|
||||
Libs/LibGroupInSpecT-1.1: https://repos.wowace.com/wow/libgroupinspect/trunk
|
||||
Libs/LibItemUpgradeInfo-1.0: https://repos.wowace.com/wow/libitemupgradeinfo-1-0
|
||||
Libs/NickTag-1.0: https://repos.curseforge.com/wow/nicktag
|
||||
Libs/LibDataBroker-1.1: https://repos.wowace.com/wow/libdatabroker-1-1
|
||||
|
||||
move-folders:
|
||||
Details/plugins/Details_3DModelsPaths: Details_3DModelsPaths
|
||||
Details/plugins/Details_DataStorage: Details_DataStorage
|
||||
Details/plugins/Details_DmgRank: Details_DmgRank
|
||||
Details/plugins/Details_DpsTuning: Details_DpsTuning
|
||||
Details/plugins/Details_EncounterDetails: Details_EncounterDetails
|
||||
Details/plugins/Details_SpellDetails: Details_SpellDetails
|
||||
Details/plugins/Details_TimeAttack: Details_TimeAttack
|
||||
Details/plugins/Details_TinyThreat: Details_TinyThreat
|
||||
Details/plugins/Details_Vanguard: Details_Vanguard
|
||||
Details/plugins/Details_DataStorage: Details_DataStorage
|
||||
Details/plugins/Details_3DModelsPaths: Details_3DModelsPaths
|
||||
Details/plugins/Details_RaidCheck: Details_RaidCheck
|
||||
Details/plugins/Details_DpsTuning: Details_DpsTuning
|
||||
Details/plugins/Details_Streamer: Details_Streamer
|
||||
Details/plugins/Details_RaidInfo-EmeraldNightmare: Details_RaidInfo-EmeraldNightmare
|
||||
Details/plugins/Details_RaidInfo-Nighthold: Details_RaidInfo-Nighthold
|
||||
Details/plugins/Details_RaidInfo-TrialOfValor: Details_RaidInfo-TrialOfValor
|
||||
Details/plugins/Details_RaidInfo-TombOfSargeras: Details_RaidInfo-TombOfSargeras
|
||||
Details/plugins/Details_Streamer: Details_Streamer
|
||||
Details/plugins/Details_TimeAttack: Details_TimeAttack
|
||||
Details/plugins/Details_TinyThreat: Details_TinyThreat
|
||||
Details/plugins/Details_Vanguard: Details_Vanguard
|
||||
Details/plugins/Details_RaidInfo-TombOfSargeras: Details_RaidInfo-TombOfSargeras
|
||||
@@ -0,0 +1,674 @@
|
||||
--- **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: AceAddon-3.0.lua 1084 2013-04-27 20:14:11Z nevcairiel $
|
||||
|
||||
local MAJOR, MINOR = "AceAddon-3.0", 12
|
||||
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
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, IsLoggedIn, geterrorhandler
|
||||
|
||||
--[[
|
||||
xpcall safecall implementation
|
||||
]]
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local xpcall, eh = ...
|
||||
local method, ARGS
|
||||
local function call() return method(ARGS) end
|
||||
|
||||
local function dispatch(func, ...)
|
||||
method = func
|
||||
if not method then return end
|
||||
ARGS = ...
|
||||
return xpcall(call, eh)
|
||||
end
|
||||
|
||||
return dispatch
|
||||
]]
|
||||
|
||||
local ARGS = {}
|
||||
for i = 1, argCount do ARGS[i] = "arg"..i end
|
||||
code = code:gsub("ARGS", tconcat(ARGS, ", "))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
Dispatchers[0] = function(func)
|
||||
return xpcall(func, errorhandler)
|
||||
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 Dispatchers[select('#', ...)](func, ...)
|
||||
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
|
||||
|
||||
-- Event Handling
|
||||
local function onEvent(this, event, arg1)
|
||||
-- 2011-08-17 nevcairiel - ignore the load event of Blizzard_DebugTools, so a potential startup error isn't swallowed up
|
||||
if (event == "ADDON_LOADED" and arg1 ~= "Blizzard_DebugTools") 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>
|
||||
@@ -0,0 +1,302 @@
|
||||
--- **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.lua 1107 2014-02-19 16:40:32Z nevcairiel $
|
||||
|
||||
--[[ 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 MAJOR, MINOR = "AceComm-3.0", 9
|
||||
|
||||
local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceComm then return end
|
||||
|
||||
local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
|
||||
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
|
||||
|
||||
-- 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
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix
|
||||
|
||||
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
|
||||
RegisterAddonMessagePrefix(prefix)
|
||||
|
||||
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") 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..distribution..(target or "")
|
||||
|
||||
local ctlCallback = nil
|
||||
if callbackFn then
|
||||
ctlCallback = function(sent)
|
||||
return callbackFn(callbackArg, sent, textlen)
|
||||
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,524 @@
|
||||
--
|
||||
-- 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 = 23
|
||||
|
||||
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 strlower = string.lower
|
||||
local unpack,type,pairs,wipe = unpack,type,pairs,wipe
|
||||
local UnitInRaid,UnitInParty = UnitInRaid,UnitInParty
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- 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
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- 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
|
||||
|
||||
-- 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.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
|
||||
hooksecurefunc("SendChatMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendChatMessage(...)
|
||||
end)
|
||||
--SendAddonMessage
|
||||
hooksecurefunc("SendAddonMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendAddonMessage(...)
|
||||
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
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- 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
|
||||
|
||||
function ChatThrottleLib:Despool(Prio)
|
||||
local ring = Prio.Ring
|
||||
while ring.pos and Prio.avail > ring.pos[1].nSize do
|
||||
local msg = table_remove(ring.pos, 1)
|
||||
if not ring.pos[1] then -- did we remove last msg in this pipe?
|
||||
local pipe = Prio.Ring.pos
|
||||
Prio.Ring:Remove(pipe)
|
||||
Prio.ByName[pipe.name] = nil
|
||||
DelPipe(pipe)
|
||||
else
|
||||
Prio.Ring.pos = Prio.Ring.pos.next
|
||||
end
|
||||
local didSend=false
|
||||
local lowerDest = strlower(msg[3] or "")
|
||||
if lowerDest == "raid" and not UnitInRaid("player") then
|
||||
-- do nothing
|
||||
elseif lowerDest == "party" and not UnitInParty("player") then
|
||||
-- do nothing
|
||||
else
|
||||
Prio.avail = Prio.avail - msg.nSize
|
||||
bMyTraffic = true
|
||||
msg.f(unpack(msg, 1, msg.n))
|
||||
bMyTraffic = false
|
||||
Prio.nTotalSent = Prio.nTotalSent + msg.nSize
|
||||
DelMsg(msg)
|
||||
didSend = true
|
||||
end
|
||||
-- notify caller of delivery (even if we didn't send it)
|
||||
if msg.callbackFn then
|
||||
msg.callbackFn (msg.callbackArg, didSend)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
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
|
||||
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
|
||||
|
||||
-- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
|
||||
local n = 0
|
||||
for prioname,Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Anything queued still?
|
||||
if n<1 then
|
||||
-- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
self.avail = self.avail + Prio.avail
|
||||
Prio.avail = 0
|
||||
end
|
||||
self.bQueueing = false
|
||||
self.Frame:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
|
||||
local avail = self.avail/n
|
||||
self.avail = 0
|
||||
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
Prio.avail = Prio.avail + avail
|
||||
if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
|
||||
self:Despool(Prio)
|
||||
-- Note: We might not get here if the user-supplied callback function errors out! Take care!
|
||||
end
|
||||
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
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendChatMessage(text, chattype, language, destination)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _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..(chattype or "SAY")..(destination or "")), 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)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = text:len();
|
||||
|
||||
if RegisterAddonMessagePrefix then
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendAddonMessage(): message length cannot exceed 255 bytes", 2)
|
||||
end
|
||||
else
|
||||
nSize = nSize + prefix:len() + 1
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
|
||||
end
|
||||
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
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendAddonMessage(prefix, text, chattype, target)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendAddonMessage
|
||||
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..chattype..(target or "")), msg)
|
||||
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,137 @@
|
||||
--- **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: AceLocale-3.0.lua 1035 2011-07-09 03:20:13Z kaelten $
|
||||
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
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: GAME_LOCALE, geterrorhandler
|
||||
|
||||
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 gameLocale = 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 ~= gameLocale 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: AceSerializer-3.0.lua 1135 2015-09-19 20:39:16Z nevcairiel $
|
||||
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 k,v in pairs(v) do
|
||||
nres = SerializeValue(k, res, nres)
|
||||
nres = SerializeValue(v, 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,276 @@
|
||||
--- **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: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
|
||||
|
||||
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 = {...}
|
||||
timer.object = self
|
||||
timer.func = func
|
||||
timer.looping = loop
|
||||
timer.argsCount = select("#", ...)
|
||||
timer.delay = delay
|
||||
timer.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 delay = timer.delay - (time - timer.ends)
|
||||
-- Ensure the delay doesn't go below the threshold
|
||||
if delay < 0.01 then delay = 0.01 end
|
||||
C_TimerAfter(delay, timer.callback)
|
||||
timer.ends = time + delay
|
||||
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 callback 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 callback 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 pairs(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 pairs(AceTimer.selfs) do
|
||||
for handle,timer in pairs(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 pairs(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 pairs(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 pairs(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 pairs(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,238 @@
|
||||
--[[ $Id: CallbackHandler-1.0.lua 1131 2015-06-04 07:29:24Z nevcairiel $ ]]
|
||||
local MAJOR, MINOR = "CallbackHandler-1.0", 6
|
||||
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 tconcat = table.concat
|
||||
local assert, error, loadstring = assert, error, loadstring
|
||||
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
|
||||
local next, select, pairs, type, tostring = next, select, pairs, type, tostring
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: geterrorhandler
|
||||
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local next, xpcall, eh = ...
|
||||
|
||||
local method, ARGS
|
||||
local function call() method(ARGS) end
|
||||
|
||||
local function dispatch(handlers, ...)
|
||||
local index
|
||||
index, method = next(handlers)
|
||||
if not method then return end
|
||||
local OLD_ARGS = ARGS
|
||||
ARGS = ...
|
||||
repeat
|
||||
xpcall(call, eh)
|
||||
index, method = next(handlers, index)
|
||||
until not method
|
||||
ARGS = OLD_ARGS
|
||||
end
|
||||
|
||||
return dispatch
|
||||
]]
|
||||
|
||||
local ARGS, OLD_ARGS = {}, {}
|
||||
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
|
||||
code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", "))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
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(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
|
||||
|
||||
Dispatchers[select('#', ...) + 1](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 eventname,callbacks in pairs(registry.insertQueue) do
|
||||
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.
|
||||
for self,func in pairs(callbacks) do
|
||||
events[eventname][self] = func
|
||||
-- fire OnUsed callback?
|
||||
if first and registry.OnUsed then
|
||||
registry.OnUsed(registry, target, eventname)
|
||||
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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
## Interface: 70200
|
||||
|
||||
## Title: Lib: Compress
|
||||
## Notes: Compression and Decompression library
|
||||
## Author: Galmok at Stormrage-EU (Horde) and JJSheets
|
||||
## Version: @project-version@
|
||||
## X-Website: http://www.wowace.com/addons/libcompress/
|
||||
## X-Category: Library
|
||||
## X-eMail: galmok AT gmail DOT com, sheets DOT jeff AT gmail DOT com
|
||||
## X-License: LGPL v2.1
|
||||
## LoadOnDemand: 1
|
||||
|
||||
LibStub\LibStub.lua
|
||||
lib.xml
|
||||
@@ -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="LibCompress.lua" />
|
||||
</Ui>
|
||||
@@ -0,0 +1,333 @@
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- LibDBIcon-1.0
|
||||
--
|
||||
-- Allows addons to easily create a lightweight minimap icon as an alternative to heavier LDB displays.
|
||||
--
|
||||
|
||||
local DBICON10 = "LibDBIcon-1.0"
|
||||
local DBICON10_MINOR = 34 -- Bump on changes
|
||||
if not LibStub then error(DBICON10 .. " requires LibStub.") end
|
||||
local ldb = LibStub("LibDataBroker-1.1", true)
|
||||
if not ldb then error(DBICON10 .. " requires LibDataBroker-1.1.") end
|
||||
local lib = LibStub:NewLibrary(DBICON10, DBICON10_MINOR)
|
||||
if not lib then return end
|
||||
|
||||
lib.disabled = lib.disabled or nil
|
||||
lib.objects = lib.objects or {}
|
||||
lib.callbackRegistered = lib.callbackRegistered or nil
|
||||
lib.callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib)
|
||||
lib.notCreated = lib.notCreated or {}
|
||||
|
||||
function lib:IconCallback(event, name, key, value)
|
||||
if lib.objects[name] then
|
||||
if key == "icon" then
|
||||
lib.objects[name].icon:SetTexture(value)
|
||||
elseif key == "iconCoords" then
|
||||
lib.objects[name].icon:UpdateCoord()
|
||||
elseif key == "iconR" then
|
||||
local _, g, b = lib.objects[name].icon:GetVertexColor()
|
||||
lib.objects[name].icon:SetVertexColor(value, g, b)
|
||||
elseif key == "iconG" then
|
||||
local r, _, b = lib.objects[name].icon:GetVertexColor()
|
||||
lib.objects[name].icon:SetVertexColor(r, value, b)
|
||||
elseif key == "iconB" then
|
||||
local r, g = lib.objects[name].icon:GetVertexColor()
|
||||
lib.objects[name].icon:SetVertexColor(r, g, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not lib.callbackRegistered then
|
||||
ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__icon", "IconCallback")
|
||||
ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconCoords", "IconCallback")
|
||||
ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconR", "IconCallback")
|
||||
ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconG", "IconCallback")
|
||||
ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__iconB", "IconCallback")
|
||||
lib.callbackRegistered = true
|
||||
end
|
||||
|
||||
local function getAnchors(frame)
|
||||
local x, y = frame:GetCenter()
|
||||
if not x or not y then return "CENTER" end
|
||||
local hhalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or ""
|
||||
local vhalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM"
|
||||
return vhalf..hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP")..hhalf
|
||||
end
|
||||
|
||||
local function onEnter(self)
|
||||
if self.isMoving then return end
|
||||
local obj = self.dataObject
|
||||
if obj.OnTooltipShow then
|
||||
GameTooltip:SetOwner(self, "ANCHOR_NONE")
|
||||
GameTooltip:SetPoint(getAnchors(self))
|
||||
obj.OnTooltipShow(GameTooltip)
|
||||
GameTooltip:Show()
|
||||
elseif obj.OnEnter then
|
||||
obj.OnEnter(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function onLeave(self)
|
||||
local obj = self.dataObject
|
||||
GameTooltip:Hide()
|
||||
if obj.OnLeave then obj.OnLeave(self) end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local onClick, onMouseUp, onMouseDown, onDragStart, onDragStop, updatePosition
|
||||
|
||||
do
|
||||
local minimapShapes = {
|
||||
["ROUND"] = {true, true, true, true},
|
||||
["SQUARE"] = {false, false, false, false},
|
||||
["CORNER-TOPLEFT"] = {false, false, false, true},
|
||||
["CORNER-TOPRIGHT"] = {false, false, true, false},
|
||||
["CORNER-BOTTOMLEFT"] = {false, true, false, false},
|
||||
["CORNER-BOTTOMRIGHT"] = {true, false, false, false},
|
||||
["SIDE-LEFT"] = {false, true, false, true},
|
||||
["SIDE-RIGHT"] = {true, false, true, false},
|
||||
["SIDE-TOP"] = {false, false, true, true},
|
||||
["SIDE-BOTTOM"] = {true, true, false, false},
|
||||
["TRICORNER-TOPLEFT"] = {false, true, true, true},
|
||||
["TRICORNER-TOPRIGHT"] = {true, false, true, true},
|
||||
["TRICORNER-BOTTOMLEFT"] = {true, true, false, true},
|
||||
["TRICORNER-BOTTOMRIGHT"] = {true, true, true, false},
|
||||
}
|
||||
|
||||
function updatePosition(button)
|
||||
local angle = math.rad(button.db and button.db.minimapPos or button.minimapPos or 225)
|
||||
local x, y, q = math.cos(angle), math.sin(angle), 1
|
||||
if x < 0 then q = q + 1 end
|
||||
if y > 0 then q = q + 2 end
|
||||
local minimapShape = GetMinimapShape and GetMinimapShape() or "ROUND"
|
||||
local quadTable = minimapShapes[minimapShape]
|
||||
if quadTable[q] then
|
||||
x, y = x*80, y*80
|
||||
else
|
||||
local diagRadius = 103.13708498985 --math.sqrt(2*(80)^2)-10
|
||||
x = math.max(-80, math.min(x*diagRadius, 80))
|
||||
y = math.max(-80, math.min(y*diagRadius, 80))
|
||||
end
|
||||
button:SetPoint("CENTER", Minimap, "CENTER", x, y)
|
||||
end
|
||||
end
|
||||
|
||||
function onClick(self, b) if self.dataObject.OnClick then self.dataObject.OnClick(self, b) end end
|
||||
function onMouseDown(self) self.isMouseDown = true; self.icon:UpdateCoord() end
|
||||
function onMouseUp(self) self.isMouseDown = false; self.icon:UpdateCoord() end
|
||||
|
||||
do
|
||||
local function onUpdate(self)
|
||||
local mx, my = Minimap:GetCenter()
|
||||
local px, py = GetCursorPosition()
|
||||
local scale = Minimap:GetEffectiveScale()
|
||||
px, py = px / scale, py / scale
|
||||
if self.db then
|
||||
self.db.minimapPos = math.deg(math.atan2(py - my, px - mx)) % 360
|
||||
else
|
||||
self.minimapPos = math.deg(math.atan2(py - my, px - mx)) % 360
|
||||
end
|
||||
updatePosition(self)
|
||||
end
|
||||
|
||||
function onDragStart(self)
|
||||
self:LockHighlight()
|
||||
self.isMouseDown = true
|
||||
self.icon:UpdateCoord()
|
||||
self:SetScript("OnUpdate", onUpdate)
|
||||
self.isMoving = true
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function onDragStop(self)
|
||||
self:SetScript("OnUpdate", nil)
|
||||
self.isMouseDown = false
|
||||
self.icon:UpdateCoord()
|
||||
self:UnlockHighlight()
|
||||
self.isMoving = nil
|
||||
end
|
||||
|
||||
local defaultCoords = {0, 1, 0, 1}
|
||||
local function updateCoord(self)
|
||||
local coords = self:GetParent().dataObject.iconCoords or defaultCoords
|
||||
local deltaX, deltaY = 0, 0
|
||||
if not self:GetParent().isMouseDown then
|
||||
deltaX = (coords[2] - coords[1]) * 0.05
|
||||
deltaY = (coords[4] - coords[3]) * 0.05
|
||||
end
|
||||
self:SetTexCoord(coords[1] + deltaX, coords[2] - deltaX, coords[3] + deltaY, coords[4] - deltaY)
|
||||
end
|
||||
|
||||
local function createButton(name, object, db)
|
||||
local button = CreateFrame("Button", "LibDBIcon10_"..name, Minimap)
|
||||
button.dataObject = object
|
||||
button.db = db
|
||||
button:SetFrameStrata("MEDIUM")
|
||||
button:SetSize(31, 31)
|
||||
button:SetFrameLevel(8)
|
||||
button:RegisterForClicks("anyUp")
|
||||
button:RegisterForDrag("LeftButton")
|
||||
button:SetHighlightTexture(136477) --"Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight"
|
||||
local overlay = button:CreateTexture(nil, "OVERLAY")
|
||||
overlay:SetSize(53, 53)
|
||||
overlay:SetTexture(136430) --"Interface\\Minimap\\MiniMap-TrackingBorder"
|
||||
overlay:SetPoint("TOPLEFT")
|
||||
local background = button:CreateTexture(nil, "BACKGROUND")
|
||||
background:SetSize(20, 20)
|
||||
background:SetTexture(136467) --"Interface\\Minimap\\UI-Minimap-Background"
|
||||
background:SetPoint("TOPLEFT", 7, -5)
|
||||
local icon = button:CreateTexture(nil, "ARTWORK")
|
||||
icon:SetSize(17, 17)
|
||||
icon:SetTexture(object.icon)
|
||||
icon:SetPoint("TOPLEFT", 7, -6)
|
||||
button.icon = icon
|
||||
button.isMouseDown = false
|
||||
|
||||
local r, g, b = icon:GetVertexColor()
|
||||
icon:SetVertexColor(object.iconR or r, object.iconG or g, object.iconB or b)
|
||||
|
||||
icon.UpdateCoord = updateCoord
|
||||
icon:UpdateCoord()
|
||||
|
||||
button:SetScript("OnEnter", onEnter)
|
||||
button:SetScript("OnLeave", onLeave)
|
||||
button:SetScript("OnClick", onClick)
|
||||
if not db or not db.lock then
|
||||
button:SetScript("OnDragStart", onDragStart)
|
||||
button:SetScript("OnDragStop", onDragStop)
|
||||
end
|
||||
button:SetScript("OnMouseDown", onMouseDown)
|
||||
button:SetScript("OnMouseUp", onMouseUp)
|
||||
|
||||
lib.objects[name] = button
|
||||
|
||||
if lib.loggedIn then
|
||||
updatePosition(button)
|
||||
if not db or not db.hide then button:Show()
|
||||
else button:Hide() end
|
||||
end
|
||||
lib.callbacks:Fire("LibDBIcon_IconCreated", button, name) -- Fire 'Icon Created' callback
|
||||
end
|
||||
|
||||
-- We could use a metatable.__index on lib.objects, but then we'd create
|
||||
-- the icons when checking things like :IsRegistered, which is not necessary.
|
||||
local function check(name)
|
||||
if lib.notCreated[name] then
|
||||
createButton(name, lib.notCreated[name][1], lib.notCreated[name][2])
|
||||
lib.notCreated[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
lib.loggedIn = lib.loggedIn or false
|
||||
-- Wait a bit with the initial positioning to let any GetMinimapShape addons
|
||||
-- load up.
|
||||
if not lib.loggedIn then
|
||||
local f = CreateFrame("Frame")
|
||||
f:SetScript("OnEvent", function()
|
||||
for _, object in pairs(lib.objects) do
|
||||
updatePosition(object)
|
||||
if not lib.disabled and (not object.db or not object.db.hide) then object:Show()
|
||||
else object:Hide() end
|
||||
end
|
||||
lib.loggedIn = true
|
||||
f:SetScript("OnEvent", nil)
|
||||
f = nil
|
||||
end)
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
end
|
||||
|
||||
local function getDatabase(name)
|
||||
return lib.notCreated[name] and lib.notCreated[name][2] or lib.objects[name].db
|
||||
end
|
||||
|
||||
function lib:Register(name, object, db)
|
||||
if not object.icon then error("Can't register LDB objects without icons set!") end
|
||||
if lib.objects[name] or lib.notCreated[name] then error("Already registered, nubcake.") end
|
||||
if not lib.disabled and (not db or not db.hide) then
|
||||
createButton(name, object, db)
|
||||
else
|
||||
lib.notCreated[name] = {object, db}
|
||||
end
|
||||
end
|
||||
|
||||
function lib:Lock(name)
|
||||
if not lib:IsRegistered(name) then return end
|
||||
if lib.objects[name] then
|
||||
lib.objects[name]:SetScript("OnDragStart", nil)
|
||||
lib.objects[name]:SetScript("OnDragStop", nil)
|
||||
end
|
||||
local db = getDatabase(name)
|
||||
if db then db.lock = true end
|
||||
end
|
||||
|
||||
function lib:Unlock(name)
|
||||
if not lib:IsRegistered(name) then return end
|
||||
if lib.objects[name] then
|
||||
lib.objects[name]:SetScript("OnDragStart", onDragStart)
|
||||
lib.objects[name]:SetScript("OnDragStop", onDragStop)
|
||||
end
|
||||
local db = getDatabase(name)
|
||||
if db then db.lock = nil end
|
||||
end
|
||||
|
||||
function lib:Hide(name)
|
||||
if not lib.objects[name] then return end
|
||||
lib.objects[name]:Hide()
|
||||
end
|
||||
function lib:Show(name)
|
||||
if lib.disabled then return end
|
||||
check(name)
|
||||
lib.objects[name]:Show()
|
||||
updatePosition(lib.objects[name])
|
||||
end
|
||||
function lib:IsRegistered(name)
|
||||
return (lib.objects[name] or lib.notCreated[name]) and true or false
|
||||
end
|
||||
function lib:Refresh(name, db)
|
||||
if lib.disabled then return end
|
||||
check(name)
|
||||
local button = lib.objects[name]
|
||||
if db then button.db = db end
|
||||
updatePosition(button)
|
||||
if not button.db or not button.db.hide then
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
if not button.db or not button.db.lock then
|
||||
button:SetScript("OnDragStart", onDragStart)
|
||||
button:SetScript("OnDragStop", onDragStop)
|
||||
else
|
||||
button:SetScript("OnDragStart", nil)
|
||||
button:SetScript("OnDragStop", nil)
|
||||
end
|
||||
end
|
||||
function lib:GetMinimapButton(name)
|
||||
return lib.objects[name]
|
||||
end
|
||||
|
||||
function lib:EnableLibrary()
|
||||
lib.disabled = nil
|
||||
for name, object in pairs(lib.objects) do
|
||||
if not object.db or not object.db.hide then
|
||||
object:Show()
|
||||
updatePosition(object)
|
||||
end
|
||||
end
|
||||
for name, data in pairs(lib.notCreated) do
|
||||
if not data.db or not data.db.hide then
|
||||
createButton(name, data[1], data[2])
|
||||
lib.notCreated[name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function lib:DisableLibrary()
|
||||
lib.disabled = true
|
||||
for name, object in pairs(lib.objects) do
|
||||
object:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<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="LibDBIcon-1.0.lua"/>
|
||||
|
||||
</Ui>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,910 @@
|
||||
-- vim: ts=2 sw=2 ai et fenc=utf8
|
||||
|
||||
--[[
|
||||
-- These events can be registered for using the regular CallbackHandler ways.
|
||||
--
|
||||
-- "GroupInSpecT_Update", guid, unit, info
|
||||
-- "GroupInSpecT_Remove, guid
|
||||
-- "GroupInSpecT_InspectReady", guid, unit
|
||||
--
|
||||
-- Where <info> is a table containing some or all of the following:
|
||||
-- .guid
|
||||
-- .name
|
||||
-- .realm
|
||||
-- .race
|
||||
-- .race_localized
|
||||
-- .class
|
||||
-- .class_localized
|
||||
-- .class_id
|
||||
-- .gender -- 2 = male, 3 = female
|
||||
-- .global_spec_id
|
||||
-- .spec_index
|
||||
-- .spec_name_localized
|
||||
-- .spec_description
|
||||
-- .spec_icon
|
||||
-- .spec_background
|
||||
-- .spec_role
|
||||
-- .spec_role_detailed
|
||||
-- .spec_group -- active spec group (1/2/nil)
|
||||
-- .talents = {
|
||||
-- [<talent_id>] = {
|
||||
-- .tier
|
||||
-- .column
|
||||
-- .name_localized
|
||||
-- .icon
|
||||
-- .talent_id
|
||||
-- .spell_id
|
||||
-- }
|
||||
-- ...
|
||||
-- }
|
||||
-- .lku -- last known unit id
|
||||
-- .not_visible
|
||||
--
|
||||
-- Functions for external use:
|
||||
--
|
||||
-- lib:Rescan (guid or nil)
|
||||
-- Force a rescan of the given group member GUID, or of all current group members if nil.
|
||||
--
|
||||
-- lib:QueuedInspections ()
|
||||
-- Returns an array of GUIDs of outstanding inspects.
|
||||
--
|
||||
-- lib:StaleInspections ()
|
||||
-- Returns an array of GUIDs for which the data has become stale and is
|
||||
-- awaiting an update (no action required, the refresh happens internally).
|
||||
-- Due to Blizzard exposing no events on (re/un)talent, there will be
|
||||
-- frequent marking of inspect data as being stale.
|
||||
--
|
||||
-- lib:GetCachedInfo (guid)
|
||||
-- Returns the cached info for the given GUID, if available, nil otherwise.
|
||||
-- Information is cached for current group members only.
|
||||
--
|
||||
-- lib:GroupUnits ()
|
||||
-- Returns an array with the set of unit ids for the current group.
|
||||
--]]
|
||||
|
||||
local MAJOR, MINOR = "LibGroupInSpecT-1.1", tonumber (("$Revision: 89 $"):match ("(%d+)") or 0)
|
||||
|
||||
if not LibStub then error(MAJOR.." requires LibStub") end
|
||||
local lib = LibStub:NewLibrary (MAJOR, MINOR)
|
||||
if not lib then return end
|
||||
|
||||
lib.events = lib.events or LibStub ("CallbackHandler-1.0"):New (lib)
|
||||
if not lib.events then error(MAJOR.." requires CallbackHandler") end
|
||||
|
||||
|
||||
local UPDATE_EVENT = "GroupInSpecT_Update"
|
||||
local REMOVE_EVENT = "GroupInSpecT_Remove"
|
||||
local INSPECT_READY_EVENT = "GroupInSpecT_InspectReady"
|
||||
local QUEUE_EVENT = "GroupInSpecT_QueueChanged"
|
||||
|
||||
local COMMS_PREFIX = "LGIST11"
|
||||
local COMMS_FMT = "1"
|
||||
local COMMS_DELIM = "\a"
|
||||
|
||||
local INSPECT_DELAY = 1.5
|
||||
local INSPECT_TIMEOUT = 10 -- If we get no notification within 10s, give up on unit
|
||||
|
||||
local MAX_ATTEMPTS = 2
|
||||
|
||||
--@debug@
|
||||
lib.debug = false
|
||||
local function debug (...)
|
||||
if lib.debug then -- allow programmatic override of debug output by client addons
|
||||
print (...)
|
||||
end
|
||||
end
|
||||
--@end-debug@
|
||||
|
||||
function lib.events:OnUsed(target, eventname)
|
||||
if eventname == INSPECT_READY_EVENT then
|
||||
target.inspect_ready_used = true
|
||||
end
|
||||
end
|
||||
|
||||
function lib.events:OnUnused(target, eventname)
|
||||
if eventname == INSPECT_READY_EVENT then
|
||||
target.inspect_ready_used = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Frame for events
|
||||
local frame = _G[MAJOR .. "_Frame"] or CreateFrame ("Frame", MAJOR .. "_Frame")
|
||||
lib.frame = frame
|
||||
frame:Hide()
|
||||
frame:UnregisterAllEvents ()
|
||||
frame:RegisterEvent ("PLAYER_LOGIN")
|
||||
frame:RegisterEvent ("PLAYER_LOGOUT")
|
||||
if not frame.OnEvent then
|
||||
frame.OnEvent = function(this, event, ...)
|
||||
local eventhandler = lib[event]
|
||||
return eventhandler and eventhandler (lib, ...)
|
||||
end
|
||||
frame:SetScript ("OnEvent", frame.OnEvent)
|
||||
end
|
||||
|
||||
|
||||
-- Hide our run-state in an easy-to-dump object
|
||||
lib.state = {
|
||||
mainq = {}, staleq = {}, -- inspect queues
|
||||
t = 0,
|
||||
last_inspect = 0,
|
||||
current_guid = nil,
|
||||
throttle = 0,
|
||||
tt = 0,
|
||||
debounce_send_update = 0,
|
||||
}
|
||||
lib.cache = {}
|
||||
lib.static_cache = {}
|
||||
|
||||
|
||||
-- Note: if we cache NotifyInspect, we have to hook before we cache it!
|
||||
if not lib.hooked then
|
||||
hooksecurefunc("NotifyInspect", function (...) return lib:NotifyInspect (...) end)
|
||||
lib.hooked = true
|
||||
end
|
||||
function lib:NotifyInspect(unit)
|
||||
self.state.last_inspect = GetTime()
|
||||
end
|
||||
|
||||
|
||||
-- Get local handles on the key API functions
|
||||
local CanInspect = _G.CanInspect
|
||||
local ClearInspectPlayer = _G.ClearInspectPlayer
|
||||
local GetClassInfo = _G.GetClassInfo
|
||||
local GetNumSubgroupMembers = _G.GetNumSubgroupMembers
|
||||
local GetNumSpecializationsForClassID = _G.GetNumSpecializationsForClassID
|
||||
local GetPlayerInfoByGUID = _G.GetPlayerInfoByGUID
|
||||
local GetInspectSpecialization = _G.GetInspectSpecialization
|
||||
local GetSpecialization = _G.GetSpecialization
|
||||
local GetSpecializationInfo = _G.GetSpecializationInfo
|
||||
local GetSpecializationInfoForClassID = _G.GetSpecializationInfoForClassID
|
||||
local GetSpecializationRoleByID = _G.GetSpecializationRoleByID
|
||||
local GetSpellInfo = _G.GetSpellInfo
|
||||
local GetTalentInfo = _G.GetTalentInfo
|
||||
local IsInRaid = _G.IsInRaid
|
||||
--local NotifyInspect = _G.NotifyInspect -- Don't cache, as to avoid missing future hooks
|
||||
local GetNumClasses = _G.GetNumClasses
|
||||
local UnitExists = _G.UnitExists
|
||||
local UnitGUID = _G.UnitGUID
|
||||
local UnitInParty = _G.UnitInParty
|
||||
local UnitInRaid = _G.UnitInRaid
|
||||
local UnitIsConnected = _G.UnitIsConnected
|
||||
local UnitIsPlayer = _G.UnitIsPlayer
|
||||
local UnitIsUnit = _G.UnitIsUnit
|
||||
local UnitName = _G.UnitName
|
||||
|
||||
|
||||
local global_spec_id_roles_detailed = {
|
||||
-- Death Knight
|
||||
[250] = "tank", -- Blood
|
||||
[251] = "melee", -- Frost
|
||||
[252] = "melee", -- Unholy
|
||||
-- Demon Hunter
|
||||
[577] = "melee", -- Havoc
|
||||
[581] = "tank", -- Vengeance
|
||||
-- Druid
|
||||
[102] = "ranged", -- Balance
|
||||
[103] = "melee", -- Feral
|
||||
[104] = "tank", -- Guardian
|
||||
[105] = "healer", -- Restoration
|
||||
-- Hunter
|
||||
[253] = "ranged", -- Beast Mastery
|
||||
[254] = "ranged", -- Marksmanship
|
||||
[255] = "melee", -- Survival
|
||||
-- Mage
|
||||
[62] = "ranged", -- Arcane
|
||||
[63] = "ranged", -- Fire
|
||||
[64] = "ranged", -- Frost
|
||||
-- Monk
|
||||
[268] = "tank", -- Brewmaster
|
||||
[269] = "melee", -- Windwalker
|
||||
[270] = "healer", -- Mistweaver
|
||||
-- Paladin
|
||||
[65] = "healer", -- Holy
|
||||
[66] = "tank", -- Protection
|
||||
[70] = "melee", -- Retribution
|
||||
-- Priest
|
||||
[256] = "healer", -- Discipline
|
||||
[257] = "healer", -- Holy
|
||||
[258] = "ranged", -- Shadow
|
||||
-- Rogue
|
||||
[259] = "melee", -- Assassination
|
||||
[260] = "melee", -- Combat
|
||||
[261] = "melee", -- Subtlety
|
||||
-- Shaman
|
||||
[262] = "ranged", -- Elemental
|
||||
[263] = "melee", -- Enhancement
|
||||
[264] = "healer", -- Restoration
|
||||
-- Warlock
|
||||
[265] = "ranged", -- Affliction
|
||||
[266] = "ranged", -- Demonology
|
||||
[267] = "ranged", -- Destruction
|
||||
-- Warrior
|
||||
[71] = "melee", -- Arms
|
||||
[72] = "melee", -- Fury
|
||||
[73] = "tank", -- Protection
|
||||
}
|
||||
|
||||
local class_fixed_roles = {
|
||||
HUNTER = "DAMAGER",
|
||||
MAGE = "DAMAGER",
|
||||
ROGUE = "DAMAGER",
|
||||
WARLOCK = "DAMAGER",
|
||||
}
|
||||
|
||||
local class_fixed_roles_detailed = {
|
||||
MAGE = "ranged",
|
||||
ROGUE = "melee",
|
||||
WARLOCK = "ranged",
|
||||
}
|
||||
|
||||
-- Inspects only work after being fully logged in, so track that
|
||||
function lib:PLAYER_LOGIN ()
|
||||
self.state.logged_in = true
|
||||
|
||||
self:CacheGameData ()
|
||||
|
||||
frame:RegisterEvent ("INSPECT_READY")
|
||||
frame:RegisterEvent ("GROUP_ROSTER_UPDATE")
|
||||
frame:RegisterEvent ("PLAYER_ENTERING_WORLD")
|
||||
frame:RegisterEvent ("UNIT_LEVEL")
|
||||
frame:RegisterEvent ("PLAYER_TALENT_UPDATE")
|
||||
frame:RegisterEvent ("PLAYER_SPECIALIZATION_CHANGED")
|
||||
frame:RegisterEvent ("UNIT_SPELLCAST_SUCCEEDED")
|
||||
frame:RegisterEvent ("UNIT_NAME_UPDATE")
|
||||
frame:RegisterEvent ("UNIT_AURA")
|
||||
frame:RegisterEvent ("CHAT_MSG_ADDON")
|
||||
RegisterAddonMessagePrefix (COMMS_PREFIX)
|
||||
|
||||
local guid = UnitGUID ("player")
|
||||
local info = self:BuildInfo ("player")
|
||||
self.events:Fire (UPDATE_EVENT, guid, "player", info)
|
||||
end
|
||||
|
||||
function lib:PLAYER_LOGOUT ()
|
||||
self.state.logged_in = false
|
||||
end
|
||||
|
||||
|
||||
-- Simple timer
|
||||
do
|
||||
lib.state.t = 0
|
||||
if not frame.OnUpdate then -- ticket #4 if the OnUpdate code every changes we should stop borrowing the existing handler
|
||||
frame.OnUpdate = function(this, elapsed)
|
||||
lib.state.t = lib.state.t + elapsed
|
||||
lib.state.tt = lib.state.tt + elapsed
|
||||
if lib.state.t > INSPECT_DELAY then
|
||||
lib:ProcessQueues ()
|
||||
lib.state.t = 0
|
||||
end
|
||||
-- Unthrottle, essentially allowing 1 msg every 3 seconds, but with substantial burst capacity
|
||||
if lib.state.tt > 3 and lib.state.throttle > 0 then
|
||||
lib.state.throttle = lib.state.throttle - 1
|
||||
lib.state.tt = 0
|
||||
end
|
||||
if lib.state.debounce_send_update > 0 then
|
||||
local debounce = lib.state.debounce_send_update - elapsed
|
||||
lib.state.debounce_send_update = debounce
|
||||
if debounce <= 0 then lib:SendLatestSpecData () end
|
||||
end
|
||||
end
|
||||
frame:SetScript("OnUpdate", frame.OnUpdate) -- this is good regardless of the handler check above because otherwise a new anonymous function is created every time the OnUpdate code runs
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Internal library functions
|
||||
|
||||
-- Caches to deal with API shortcomings as well as performance
|
||||
lib.static_cache.global_specs = {} -- [gspec] -> { .idx, .name_localized, .description, .icon, .background, .role }
|
||||
lib.static_cache.class_to_class_id = {} -- [CLASS] -> class_id
|
||||
|
||||
-- The talents cache can no longer be pre-fetched on login, but is now constructed class-by-class as we inspect people.
|
||||
-- This probably means we want to only ever access it through the GetCachedTalentInfo() helper function below.
|
||||
lib.static_cache.talents = {} -- [talent_id] -> { .spell_id, .talent_id, .name_localized, .icon, .tier, .column }
|
||||
|
||||
-- Dridzt: I'd love another way but none of the GetTalent* functions return spellID, GetTalentLink() and parsing the link gives talentID that's not related to spellID as well
|
||||
-- A quick tooltip scan is cheap though so elegance aside this is a good workaround considering this only runs once
|
||||
local tip = CreateFrame ("GameTooltip", MAJOR.."ScanTip", nil, "GameTooltipTemplate")
|
||||
tip:SetOwner (UIParent, "ANCHOR_NONE")
|
||||
|
||||
function lib:GetCachedTalentInfo (class_id, tier, col, group, is_inspect, unit)
|
||||
local talents = self.static_cache.talents
|
||||
local talent_id, name, icon, sel, avail = GetTalentInfo (tier, col, group, is_inspect, unit)
|
||||
if not talent_id or not class_id then
|
||||
--@debug@
|
||||
debug ("GetCachedTalentInfo("..tostring(class_id)..","..tier..","..col..","..group..","..tostring(is_inspect)..","..tostring(unit)..") returned nil") --@end-debug@
|
||||
return {}
|
||||
end
|
||||
talents[class_id] = talents[class_id] or {}
|
||||
local class_talents = talents[class_id]
|
||||
if not class_talents[talent_id] then
|
||||
tip:ClearLines ()
|
||||
tip:SetTalent (talent_id, is_inspect, group)
|
||||
local _, _,spell_id = tip:GetSpell ()
|
||||
class_talents[talent_id] = {
|
||||
spell_id = spell_id,
|
||||
talent_id = talent_id,
|
||||
name_localized = name,
|
||||
icon = icon,
|
||||
tier = tier,
|
||||
column = col,
|
||||
}
|
||||
end
|
||||
return class_talents[talent_id], sel
|
||||
end
|
||||
|
||||
|
||||
function lib:CacheGameData ()
|
||||
local gspecs = self.static_cache.global_specs
|
||||
gspecs[0] = {} -- Handle no-specialization case
|
||||
for class_id = 1, GetNumClasses () do
|
||||
for idx = 1, GetNumSpecializationsForClassID (class_id) do
|
||||
local gspec_id, name, description, icon, background = GetSpecializationInfoForClassID (class_id, idx)
|
||||
gspecs[gspec_id] = {}
|
||||
local gspec = gspecs[gspec_id]
|
||||
gspec.idx = idx
|
||||
gspec.name_localized = name
|
||||
gspec.description = description
|
||||
gspec.icon = icon
|
||||
gspec.background = background
|
||||
gspec.role = GetSpecializationRoleByID (gspec_id)
|
||||
end
|
||||
|
||||
local _, class = GetClassInfo (class_id)
|
||||
self.static_cache.class_to_class_id[class] = class_id
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:GuidToUnit (guid)
|
||||
local info = self.cache[guid]
|
||||
if info and info.lku and UnitGUID (info.lku) == guid then return info.lku end
|
||||
|
||||
for i,unit in ipairs (self:GroupUnits ()) do
|
||||
if UnitExists (unit) and UnitGUID (unit) == guid then
|
||||
if info then info.lku = unit end
|
||||
return unit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:Query (unit)
|
||||
if not UnitIsPlayer (unit) then return end -- NPC
|
||||
|
||||
if UnitIsUnit (unit, "player") then
|
||||
self.events:Fire (UPDATE_EVENT, UnitGUID("player"), "player", self:BuildInfo ("player"))
|
||||
return
|
||||
end
|
||||
|
||||
local mainq, staleq = self.state.mainq, self.state.staleq
|
||||
|
||||
local guid = UnitGUID (unit)
|
||||
if not mainq[guid] then
|
||||
mainq[guid] = 1
|
||||
staleq[guid] = nil
|
||||
self.frame:Show () -- Start timer if not already running
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:Refresh (unit)
|
||||
local guid = UnitGUID (unit)
|
||||
if not guid then return end
|
||||
--@debug@
|
||||
debug ("Refreshing "..unit) --@end-debug@
|
||||
if not self.state.mainq[guid] then
|
||||
self.state.staleq[guid] = 1
|
||||
self.frame:Show ()
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:ProcessQueues ()
|
||||
if not self.state.logged_in then return end
|
||||
if InCombatLockdown () then return end -- Never inspect while in combat
|
||||
if UnitIsDead ("player") then return end -- You can't inspect while dead, so don't even try
|
||||
if InspectFrame and InspectFrame:IsShown () then return end -- Don't mess with the UI's inspections
|
||||
|
||||
local mainq = self.state.mainq
|
||||
local staleq = self.state.staleq
|
||||
|
||||
if not next (mainq) and next(staleq) then
|
||||
--@debug@
|
||||
debug ("Main queue empty, swapping main and stale queues") --@end-debug@
|
||||
self.state.mainq, self.state.staleq = self.state.staleq, self.state.mainq
|
||||
mainq, staleq = staleq, mainq
|
||||
end
|
||||
|
||||
if (self.state.last_inspect + INSPECT_TIMEOUT) < GetTime () then
|
||||
-- If there was an inspect going, it's timed out, so either retry or move it to stale queue
|
||||
local guid = self.state.current_guid
|
||||
if guid then
|
||||
--@debug@
|
||||
debug ("Inspect timed out for "..guid) --@end-debug@
|
||||
|
||||
local count = mainq and mainq[guid] or (MAX_ATTEMPTS + 1)
|
||||
if not self:GuidToUnit (guid) then
|
||||
--@debug@
|
||||
debug ("No longer applicable, removing from queues") --@end-debug@
|
||||
mainq[guid], staleq[guid] = nil, nil
|
||||
elseif count > MAX_ATTEMPTS then
|
||||
--@debug@
|
||||
debug ("Excessive retries, moving to stale queue") --@end-debug@
|
||||
mainq[guid], staleq[guid] = nil, 1
|
||||
else
|
||||
mainq[guid] = count + 1
|
||||
end
|
||||
self.state.current_guid = nil
|
||||
end
|
||||
end
|
||||
|
||||
if self.state.current_guid then return end -- Still waiting on our inspect data
|
||||
|
||||
for guid,count in pairs (mainq) do
|
||||
local unit = self:GuidToUnit (guid)
|
||||
if not unit then
|
||||
--@debug@
|
||||
debug ("No longer applicable, removing from queues") --@end-debug@
|
||||
mainq[guid], staleq[guid] = nil, nil
|
||||
elseif not CanInspect (unit) or not UnitIsConnected (unit) then
|
||||
--@debug@
|
||||
debug ("Cannot inspect "..unit..", aka "..(UnitName(unit) or "nil")..", moving to stale queue") --@end-debug@
|
||||
mainq[guid], staleq[guid] = nil, 1
|
||||
else
|
||||
--@debug@
|
||||
debug ("Inspecting "..unit..", aka "..(UnitName(unit) or "nil")) --@end-debug@
|
||||
mainq[guid] = count + 1
|
||||
self.state.current_guid = guid
|
||||
NotifyInspect (unit)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not next (mainq) and not next (staleq) and self.state.throttle == 0 and self.state.debounce_send_update <= 0 then
|
||||
frame:Hide() -- Cancel timer, nothing queued and no unthrottling to be done
|
||||
end
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
|
||||
|
||||
function lib:UpdatePlayerInfo (guid, unit, info)
|
||||
info.class_localized, info.class, info.race_localized, info.race, info.gender, info.name, info.realm = GetPlayerInfoByGUID (guid)
|
||||
local class = info.class
|
||||
if info.realm and info.realm == "" then info.realm = nil end
|
||||
info.class_id = class and self.static_cache.class_to_class_id[class]
|
||||
if not info.spec_role then info.spec_role = class and class_fixed_roles[class] end
|
||||
if not info.spec_role_detailed then info.spec_role_detailed = class and class_fixed_roles_detailed[class] end
|
||||
info.lku = unit
|
||||
end
|
||||
|
||||
|
||||
function lib:BuildInfo (unit)
|
||||
local guid = UnitGUID (unit)
|
||||
if not guid then return end
|
||||
|
||||
local cache = self.cache
|
||||
local info = cache[guid] or {}
|
||||
cache[guid] = info
|
||||
info.guid = guid
|
||||
|
||||
self:UpdatePlayerInfo (guid, unit, info)
|
||||
-- On a cold login, GetPlayerInfoByGUID() doesn't seem to be usable, so mark as stale
|
||||
local class = info.class
|
||||
if not class and not self.state.mainq[guid] then
|
||||
self.state.staleq[guid] = 1
|
||||
self.frame:Show ()
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
|
||||
local is_inspect = not UnitIsUnit (unit, "player")
|
||||
local spec = GetSpecialization ()
|
||||
info.global_spec_id = is_inspect and GetInspectSpecialization (unit) or spec and GetSpecializationInfo (spec)
|
||||
|
||||
local gspecs = self.static_cache.global_specs
|
||||
if not info.global_spec_id or not gspecs[info.global_spec_id] then -- not a valid spec_id
|
||||
info.global_spec_id = nil
|
||||
else
|
||||
local gspec_id = info.global_spec_id
|
||||
local spec_info = gspecs[gspec_id]
|
||||
info.spec_index = spec_info.idx
|
||||
info.spec_name_localized = spec_info.name_localized
|
||||
info.spec_description = spec_info.description
|
||||
info.spec_icon = spec_info.icon
|
||||
info.spec_background = spec_info.background
|
||||
info.spec_role = spec_info.role
|
||||
info.spec_role_detailed = global_spec_id_roles_detailed[gspec_id]
|
||||
end
|
||||
|
||||
if not info.spec_role then info.spec_role = class and class_fixed_roles[class] end
|
||||
if not info.spec_role_detailed then info.spec_role_detailed = class and class_fixed_roles_detailed[class] end
|
||||
|
||||
info.talents = info.talents or {}
|
||||
|
||||
-- If GetPlayerInfoByGUID didn't return the class, we can't do talents yet
|
||||
if info.class_id then
|
||||
info.spec_group = GetActiveSpecGroup (is_inspect)
|
||||
wipe (info.talents) -- Due to spec-specific talents we might leave things in on a spec-change otherwise
|
||||
for tier = 1, MAX_TALENT_TIERS do
|
||||
for col = 1, NUM_TALENT_COLUMNS do
|
||||
local talent, sel = self:GetCachedTalentInfo (info.class_id, tier, col, info.spec_group, is_inspect, unit)
|
||||
if talent and talent.talent_id and sel then
|
||||
info.talents[talent.talent_id] = talent
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
info.glyphs = wipe (info.glyphs or {}) -- kept for addons that still refer to this
|
||||
|
||||
if is_inspect and not UnitIsVisible (unit) and UnitIsConnected (unit) then info.not_visible = true end
|
||||
|
||||
return info
|
||||
end
|
||||
|
||||
|
||||
function lib:INSPECT_READY (guid)
|
||||
local unit = self:GuidToUnit (guid)
|
||||
local finalize = false
|
||||
if unit then
|
||||
if guid == self.state.current_guid then
|
||||
self.state.current_guid = nil -- Got what we asked for
|
||||
finalize = true
|
||||
--@debug@
|
||||
debug ("Got inspection data for requested guid "..guid) --@end-debug@
|
||||
end
|
||||
|
||||
local mainq, staleq = self.state.mainq, self.state.staleq
|
||||
mainq[guid], staleq[guid] = nil, nil
|
||||
|
||||
local gspec_id = GetInspectSpecialization (unit)
|
||||
if not self.static_cache.global_specs[gspec_id] then -- Bah, got garbage, flag as stale and try again
|
||||
staleq[guid] = 1
|
||||
return
|
||||
end
|
||||
|
||||
self.events:Fire (UPDATE_EVENT, guid, unit, self:BuildInfo (unit))
|
||||
self.events:Fire (INSPECT_READY_EVENT, guid, unit)
|
||||
end
|
||||
if finalize then
|
||||
ClearInspectPlayer ()
|
||||
end
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
|
||||
|
||||
function lib:PLAYER_ENTERING_WORLD ()
|
||||
if self.commScope == "INSTANCE_CHAT" then
|
||||
-- Handle moving directly from one LFG to another
|
||||
self.commScope = nil
|
||||
self:UpdateCommScope ()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Group handling parts
|
||||
|
||||
local members = {}
|
||||
function lib:GROUP_ROSTER_UPDATE ()
|
||||
local group = self.cache
|
||||
local units = self:GroupUnits ()
|
||||
-- Find new members
|
||||
for i,unit in ipairs (self:GroupUnits ()) do
|
||||
local guid = UnitGUID (unit)
|
||||
if guid then
|
||||
members[guid] = true
|
||||
if not group[guid] then
|
||||
self:Query (unit)
|
||||
-- Update with what we have so far (guid, unit, name/class/race?)
|
||||
self.events:Fire (UPDATE_EVENT, guid, unit, self:BuildInfo (unit))
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Find removed members
|
||||
for guid in pairs (group) do
|
||||
if not members[guid] then
|
||||
group[guid] = nil
|
||||
self.events:Fire (REMOVE_EVENT, guid, nil)
|
||||
end
|
||||
end
|
||||
wipe (members)
|
||||
self:UpdateCommScope ()
|
||||
end
|
||||
|
||||
|
||||
function lib:DoPlayerUpdate ()
|
||||
self:Query ("player")
|
||||
self.state.debounce_send_update = 2.5 -- Hold off 2.5sec before sending update
|
||||
self.frame:Show ()
|
||||
end
|
||||
|
||||
|
||||
function lib:SendLatestSpecData ()
|
||||
local scope = self.commScope
|
||||
if not scope then return end
|
||||
|
||||
local guid = UnitGUID ("player")
|
||||
local info = self.cache[guid]
|
||||
if not info then return end
|
||||
|
||||
-- fmt, guid, global_spec_id, talent1 -> MAX_TALENT_TIERS
|
||||
-- sequentially, allow no gaps for missing talents we decode by index on the receiving end.
|
||||
local datastr = COMMS_FMT..COMMS_DELIM..guid..COMMS_DELIM..(info.global_spec_id or 0)
|
||||
local talentCount = 1
|
||||
for k in pairs(info.talents) do
|
||||
datastr = datastr..COMMS_DELIM..k
|
||||
talentCount = talentCount + 1
|
||||
end
|
||||
for i=talentCount,MAX_TALENT_TIERS do
|
||||
datastr = datastr..COMMS_DELIM..0
|
||||
end
|
||||
|
||||
--@debug@
|
||||
debug ("Sending LGIST update to "..scope) --@end-debug@
|
||||
SendAddonMessage(COMMS_PREFIX, datastr, scope)
|
||||
end
|
||||
|
||||
|
||||
function lib:UpdateCommScope ()
|
||||
local scope = (IsInGroup (LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT") or (IsInRaid () and "RAID") or (IsInGroup (LE_PARTY_CATEGORY_HOME) and "PARTY")
|
||||
if self.commScope ~= scope then
|
||||
self.commScope = scope
|
||||
self:DoPlayerUpdate ()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Indicies for various parts of the split data msg
|
||||
local msg_idx = {}
|
||||
msg_idx.fmt = 1
|
||||
msg_idx.guid = msg_idx.fmt + 1
|
||||
msg_idx.global_spec_id = msg_idx.guid + 1
|
||||
msg_idx.talents = msg_idx.global_spec_id + 1
|
||||
msg_idx.end_talents = msg_idx.talents + MAX_TALENT_TIERS - 1
|
||||
|
||||
function lib:CHAT_MSG_ADDON (prefix, datastr, scope, sender)
|
||||
if prefix ~= COMMS_PREFIX or scope ~= self.commScope then return end
|
||||
--@debug@
|
||||
debug ("Incoming LGIST update from "..(scope or "nil").."/"..(sender or "nil")..": "..(datastr:gsub(COMMS_DELIM,";") or "nil")) --@end-debug@
|
||||
|
||||
local data = { strsplit (COMMS_DELIM,datastr) }
|
||||
local fmt = data[msg_idx.fmt]
|
||||
if fmt ~= COMMS_FMT then return end -- Unknown format, ignore
|
||||
|
||||
local guid = data[msg_idx.guid]
|
||||
|
||||
local senderguid = UnitGUID(sender)
|
||||
if senderguid and senderguid ~= guid then return end
|
||||
|
||||
local info = guid and self.cache[guid]
|
||||
if not info then return end -- Never allow random message to create new group member entries!
|
||||
|
||||
local unit = self:GuidToUnit (guid)
|
||||
if not unit then return end
|
||||
if UnitIsUnit (unit, "player") then return end -- we're already up-to-date, comment out for solo debugging
|
||||
|
||||
self.state.throttle = self.state.throttle + 1
|
||||
self.frame:Show () -- Ensure we're unthrottling
|
||||
if self.state.throttle > 40 then return end -- If we ever hit this, someone's being "funny"
|
||||
|
||||
info.class_localized, info.class, info.race_localized, info.race, info.gender, info.name, info.realm = GetPlayerInfoByGUID (guid)
|
||||
if info.realm and info.realm == "" then info.realm = nil end
|
||||
info.class_id = self.static_cache.class_to_class_id[info.class]
|
||||
|
||||
local gspecs = self.static_cache.global_specs
|
||||
|
||||
local gspec_id = data[msg_idx.global_spec_id] and tonumber (data[msg_idx.global_spec_id])
|
||||
if not gspec_id or not gspecs[gspec_id] then return end -- Malformed message, avoid throwing errors by using this nil
|
||||
|
||||
info.global_spec_id = gspec_id
|
||||
info.spec_index = gspecs[gspec_id].idx
|
||||
info.spec_name_localized = gspecs[gspec_id].name_localized
|
||||
info.spec_description = gspecs[gspec_id].description
|
||||
info.spec_icon = gspecs[gspec_id].icon
|
||||
info.spec_background = gspecs[gspec_id].background
|
||||
info.spec_role = gspecs[gspec_id].role
|
||||
info.spec_role_detailed = global_spec_id_roles_detailed[gspec_id]
|
||||
|
||||
local need_inspect = nil
|
||||
info.talents = wipe (info.talents or {})
|
||||
local talents = self.static_cache.talents[info.class_id]
|
||||
if talents then -- The group entry is created before we have inspect-data, so may not have cached talents yet
|
||||
for i = msg_idx.talents, msg_idx.end_talents do
|
||||
local talent_id = tonumber (data[i])
|
||||
if talent_id and talent_id > 0 then
|
||||
if talents[talent_id] then
|
||||
info.talents[talent_id] = talents[talent_id]
|
||||
else
|
||||
-- While we had some talents for this class, we apparently didn't have all for this particular spec, so mark for inspect
|
||||
need_inspect = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Talents weren't pre-cached, so mark for inspect
|
||||
need_inspect = 1
|
||||
end
|
||||
|
||||
info.glyphs = wipe (info.glyphs or {}) -- kept for addons that still refer to this
|
||||
|
||||
local mainq, staleq = self.state.mainq, self.state.staleq
|
||||
local want_inspect = not need_inspect and self.inspect_ready_used and (mainq[guid] or staleq[guid]) and 1 or nil
|
||||
mainq[guid], staleq[guid] = need_inspect, want_inspect
|
||||
if need_inspect or want_inspect then self.frame:Show () end
|
||||
|
||||
--@debug@
|
||||
debug ("Firing LGIST update event for unit "..unit..", GUID "..guid) --@end-debug@
|
||||
self.events:Fire (UPDATE_EVENT, guid, unit, info)
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
|
||||
|
||||
function lib:UNIT_LEVEL (unit)
|
||||
if UnitInRaid (unit) or UnitInParty (unit) then
|
||||
self:Refresh (unit)
|
||||
end
|
||||
if UnitIsUnit (unit, "player") then
|
||||
self:DoPlayerUpdate ()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:PLAYER_TALENT_UPDATE ()
|
||||
self:DoPlayerUpdate ()
|
||||
end
|
||||
|
||||
|
||||
function lib:PLAYER_SPECIALIZATION_CHANGED (unit)
|
||||
-- This event seems to fire a lot, and for no particular reason *sigh*
|
||||
-- if UnitInRaid (unit) or UnitInParty (unit) then
|
||||
-- self:Refresh (unit)
|
||||
-- end
|
||||
if unit and UnitIsUnit (unit, "player") then
|
||||
self:DoPlayerUpdate ()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:UNIT_NAME_UPDATE (unit)
|
||||
local group = self.cache
|
||||
local guid = UnitGUID (unit)
|
||||
local info = guid and group[guid]
|
||||
if info then
|
||||
self:UpdatePlayerInfo (guid, unit, info)
|
||||
if info.name ~= UNKNOWN then
|
||||
self.events:Fire (UPDATE_EVENT, guid, unit, info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Always get a UNIT_AURA when a unit's UnitIsVisible() changes
|
||||
function lib:UNIT_AURA (unit)
|
||||
local group = self.cache
|
||||
local guid = UnitGUID (unit)
|
||||
local info = guid and group[guid]
|
||||
if info then
|
||||
if not UnitIsUnit (unit, "player") then
|
||||
if UnitIsVisible (unit) then
|
||||
if info.not_visible then
|
||||
info.not_visible = nil
|
||||
--@debug@
|
||||
debug (unit..", aka "..(UnitName(unit) or "nil")..", is now visible") --@end-debug@
|
||||
if not self.state.mainq[guid] then
|
||||
self.state.staleq[guid] = 1
|
||||
self.frame:Show ()
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
end
|
||||
elseif UnitIsConnected (unit) then
|
||||
--@debug@
|
||||
if not info.not_visible then
|
||||
debug (unit..", aka "..(UnitName(unit) or "nil")..", is no longer visible")
|
||||
end
|
||||
--@end-debug@
|
||||
info.not_visible = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lib:UNIT_SPELLCAST_SUCCEEDED (unit, spellname, rank, lineid, spellid)
|
||||
if spellid == 200749 then
|
||||
self:Query (unit) -- Definitely changed, so high prio refresh
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- External library functions
|
||||
|
||||
function lib:QueuedInspections ()
|
||||
local q = {}
|
||||
for guid in pairs (self.state.mainq) do
|
||||
table.insert (q, guid)
|
||||
end
|
||||
return q
|
||||
end
|
||||
|
||||
|
||||
function lib:StaleInspections ()
|
||||
local q = {}
|
||||
for guid in pairs (self.state.staleq) do
|
||||
table.insert (q, guid)
|
||||
end
|
||||
return q
|
||||
end
|
||||
|
||||
|
||||
function lib:IsInspectQueued (guid)
|
||||
return guid and ((self.state.mainq[guid] or self.state.staleq[guid]) and true)
|
||||
end
|
||||
|
||||
|
||||
function lib:GetCachedInfo (guid)
|
||||
local group = self.cache
|
||||
return guid and group[guid]
|
||||
end
|
||||
|
||||
|
||||
function lib:Rescan (guid)
|
||||
local mainq, staleq = self.state.mainq, self.state.staleq
|
||||
if guid then
|
||||
local unit = self:GuidToUnit (guid)
|
||||
if unit then
|
||||
if UnitIsUnit (unit, "player") then
|
||||
self.events:Fire (UPDATE_EVENT, guid, "player", self:BuildInfo ("player"))
|
||||
elseif not mainq[guid] then
|
||||
staleq[guid] = 1
|
||||
end
|
||||
end
|
||||
else
|
||||
for i,unit in ipairs (self:GroupUnits ()) do
|
||||
if UnitExists (unit) then
|
||||
if UnitIsUnit (unit, "player") then
|
||||
self.events:Fire (UPDATE_EVENT, UnitGUID("player"), "player", self:BuildInfo ("player"))
|
||||
else
|
||||
local guid = UnitGUID (unit)
|
||||
if guid and not mainq[guid] then
|
||||
staleq[guid] = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self.frame:Show () -- Start timer if not already running
|
||||
|
||||
-- Evict any stale entries
|
||||
self:GROUP_ROSTER_UPDATE ()
|
||||
self.events:Fire (QUEUE_EVENT)
|
||||
end
|
||||
|
||||
|
||||
local unitstrings = {
|
||||
raid = { "player" }, -- This seems to be needed under certain circumstances. Odd.
|
||||
party = { "player" }, -- Player not part of partyN
|
||||
player = { "player" }
|
||||
}
|
||||
for i = 1,40 do table.insert (unitstrings.raid, "raid"..i) end
|
||||
for i = 1,4 do table.insert (unitstrings.party, "party"..i) end
|
||||
|
||||
|
||||
-- Returns an array with the set of unit ids for the current group
|
||||
function lib:GroupUnits ()
|
||||
local units
|
||||
if IsInRaid () then
|
||||
units = unitstrings.raid
|
||||
elseif GetNumSubgroupMembers () > 0 then
|
||||
units = unitstrings.party
|
||||
else
|
||||
units = unitstrings.player
|
||||
end
|
||||
return units
|
||||
end
|
||||
|
||||
|
||||
-- If demand-loaded, we need to synthesize a login event
|
||||
if IsLoggedIn () then lib:PLAYER_LOGIN () end
|
||||
@@ -0,0 +1,11 @@
|
||||
## Interface: 70200
|
||||
## Title: Lib: GroupInSpecT-1.1
|
||||
## Notes: Keeps track of group members and keeps an up-to-date cache of their specialization and talents.
|
||||
## Version: @project-version@
|
||||
## Author: Anyia of HordeYakka (Jubei'Thos)
|
||||
## X-Category: Library
|
||||
|
||||
Libs\LibStub\LibStub.lua
|
||||
Libs\CallbackHandler-1.0\CallbackHandler-1.0.lua
|
||||
|
||||
lib.xml
|
||||
@@ -0,0 +1,3 @@
|
||||
<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="LibGroupInSpecT-1.1.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,18 @@
|
||||
tag 5876ed6c59ef3d2568d88b9bba9ec77682e6074e Release-70200-28
|
||||
Author: Alar of Runetotem <alar@aspide.it>
|
||||
Date: Tue Mar 28 12:21:53 2017 +0200
|
||||
|
||||
Toc updated to 70200
|
||||
|
||||
commit 7c5485ab35700b2e38a008feb84e4af34b7396f0
|
||||
Author: Alar of Runetotem <alar@aspide.it>
|
||||
Date: Tue Mar 28 12:07:53 2017 +0200
|
||||
|
||||
Updated TOC to 70200
|
||||
|
||||
commit c4ed50190aad123c1297705da7f38c86f7ab1d10
|
||||
Author: Alar of Runetotem <alar@aspide.it>
|
||||
Date: Tue Mar 28 11:30:56 2017 +0200
|
||||
|
||||
toc update
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
local MAJOR, MINOR = "LibItemUpgradeInfo-1.0", 28
|
||||
local type,tonumber,select,strsplit,GetItemInfoFromHyperlink=type,tonumber,select,strsplit,GetItemInfoFromHyperlink
|
||||
local library,previous = _G.LibStub:NewLibrary(MAJOR, MINOR)
|
||||
local lib=library --#lib Needed to keep Eclipse LDT happy
|
||||
if not lib then return end
|
||||
local pp=print
|
||||
--[===[@debug@
|
||||
LoadAddOn("Blizzard_DebugTools")
|
||||
LoadAddOn("LibDebug")
|
||||
if LibDebug then LibDebug() end
|
||||
--@end-debug@]===]
|
||||
--@non-debug@
|
||||
local print=function() end
|
||||
--@end-non-debug@
|
||||
--[[
|
||||
Caching system
|
||||
1 itemName String The name of the item.
|
||||
2 itemLink String The item link of the item.
|
||||
3 itemRarity Number The quality of the item. The value is 0 to 7, which represents Poor to Heirloom. This appears to include gains from upgrades/bonuses.
|
||||
4 itemLevel Number The item level of this item, not including item levels gained from upgrades. There is currently no API to get the item level including upgrades/bonuses.
|
||||
5 itemMinLevel Number The minimum level required to use the item, 0 meaning no level requirement.
|
||||
6 itemType String The type of the item: Armor, Weapon, Quest, Key, etc. (localized)
|
||||
7 itemSubType String The sub-type of the item: Enchanting, Cloth, Sword, etc. See itemType. (localized)
|
||||
8 itemStackCount Number How many of the item per stack: 20 for Runecloth, 1 for weapon, 100 for Alterac Ram Hide, etc.
|
||||
9 itemEquipLoc String The type of inventory equipment location in which the item may be equipped, or "" if it can't be equippable. The string returned is also the name of a global string variable e.g. if "INVTYPE_WEAPONMAINHAND" is returned, _G["INVTYPE_WEAPONMAINHAND"] will be the localized, displayable name of the location.
|
||||
10 iconFileDataID Number The FileDataID for the icon texture for the item.
|
||||
11 itemSellPrice Number The price, in copper, a vendor is willing to pay for this item, 0 for items that cannot be sold.
|
||||
12 itemClassID Number This is the numerical value that determines the string to display for 'itemType'.
|
||||
13 itemSubClassID Number This is the numerical value that determines the string to display for 'itemSubType'
|
||||
14 ? number
|
||||
15 expansionId
|
||||
16 ? ?
|
||||
17 ? boolean
|
||||
--]]
|
||||
-- ItemLink Constants
|
||||
local i_Name=1
|
||||
local i_Link=2
|
||||
local i_Rarity=3
|
||||
local i_Quality=3
|
||||
local i_Level=4
|
||||
local i_MinLevel =5
|
||||
local i_ClassName=6
|
||||
local i_SubClassName=7
|
||||
local i_StackCount=8
|
||||
local i_EquipLoc=9
|
||||
local i_TextureId=10
|
||||
local i_SellPrice=11
|
||||
local i_ClassID=12
|
||||
local i_SubClass_ID=13
|
||||
local i_unk1=14
|
||||
local i_unk2=15
|
||||
local i_unk3=16
|
||||
local i_unk4=17
|
||||
|
||||
|
||||
do
|
||||
local oGetItemInfo=GetItemInfo
|
||||
lib.itemcache=lib.itemcache or
|
||||
setmetatable({miss=0,tot=0},{
|
||||
__index=function(table,key)
|
||||
if (not key) then return "" end
|
||||
if (key=="miss") then return 0 end
|
||||
if (key=="tot") then return 0 end
|
||||
local cached={oGetItemInfo(key)}
|
||||
if #cached==0 then return nil end
|
||||
local itemLink=cached[2]
|
||||
if not itemLink then return nil end
|
||||
local itemID=lib:GetItemID(itemLink)
|
||||
local quality=cached[3]
|
||||
local cacheIt=true
|
||||
if quality==LE_ITEM_QUALITY_ARTIFACT then
|
||||
local relic1, relic2, relic3 = select(4,strsplit(':', itemLink))
|
||||
if relic1 and relic1 ~= '' and not oGetItemInfo(relic1) then cacheIt = false end
|
||||
if relic2 and relic2 ~= '' and not oGetItemInfo(relic2) then cacheIt = false end
|
||||
if relic3 and relic3 ~= '' and not oGetItemInfo(relic3) then cacheIt = false end
|
||||
end
|
||||
cached.englishClass=GetItemClassInfo(cached[12])
|
||||
cached.englishSubClass=GetItemSubClassInfo(cached[12],cached[13])
|
||||
if cacheIt then
|
||||
rawset(table,key,cached)
|
||||
end
|
||||
table.miss=table.miss+1
|
||||
return cached
|
||||
end
|
||||
|
||||
})
|
||||
end
|
||||
local cache,select,unpack=lib.itemcache,select,unpack
|
||||
local function CachedGetItemInfo(key,index)
|
||||
if not key then return nil end
|
||||
index=index or 1
|
||||
cache.tot=cache.tot+1
|
||||
local cached=cache[key]
|
||||
if cached and type(cached)=='table' then
|
||||
return select(index,unpack(cached))
|
||||
else
|
||||
rawset(cache,key,nil) -- voiding broken cache entry
|
||||
end
|
||||
end
|
||||
|
||||
local upgradeTable = {
|
||||
[ 1] = { upgrade = 1, max = 1, ilevel = 8 },
|
||||
[373] = { upgrade = 1, max = 3, ilevel = 4 },
|
||||
[374] = { upgrade = 2, max = 3, ilevel = 8 },
|
||||
[375] = { upgrade = 1, max = 3, ilevel = 4 },
|
||||
[376] = { upgrade = 2, max = 3, ilevel = 4 },
|
||||
[377] = { upgrade = 3, max = 3, ilevel = 4 },
|
||||
[378] = { ilevel = 7 },
|
||||
[379] = { upgrade = 1, max = 2, ilevel = 4 },
|
||||
[380] = { upgrade = 2, max = 2, ilevel = 4 },
|
||||
[445] = { upgrade = 0, max = 2, ilevel = 0 },
|
||||
[446] = { upgrade = 1, max = 2, ilevel = 4 },
|
||||
[447] = { upgrade = 2, max = 2, ilevel = 8 },
|
||||
[451] = { upgrade = 0, max = 1, ilevel = 0 },
|
||||
[452] = { upgrade = 1, max = 1, ilevel = 8 },
|
||||
[453] = { upgrade = 0, max = 2, ilevel = 0 },
|
||||
[454] = { upgrade = 1, max = 2, ilevel = 4 },
|
||||
[455] = { upgrade = 2, max = 2, ilevel = 8 },
|
||||
[456] = { upgrade = 0, max = 1, ilevel = 0 },
|
||||
[457] = { upgrade = 1, max = 1, ilevel = 8 },
|
||||
[458] = { upgrade = 0, max = 4, ilevel = 0 },
|
||||
[459] = { upgrade = 1, max = 4, ilevel = 4 },
|
||||
[460] = { upgrade = 2, max = 4, ilevel = 8 },
|
||||
[461] = { upgrade = 3, max = 4, ilevel = 12 },
|
||||
[462] = { upgrade = 4, max = 4, ilevel = 16 },
|
||||
[465] = { upgrade = 0, max = 2, ilevel = 0 },
|
||||
[466] = { upgrade = 1, max = 2, ilevel = 4 },
|
||||
[467] = { upgrade = 2, max = 2, ilevel = 8 },
|
||||
[468] = { upgrade = 0, max = 4, ilevel = 0 },
|
||||
[469] = { upgrade = 1, max = 4, ilevel = 4 },
|
||||
[470] = { upgrade = 2, max = 4, ilevel = 8 },
|
||||
[471] = { upgrade = 3, max = 4, ilevel = 12 },
|
||||
[472] = { upgrade = 4, max = 4, ilevel = 16 },
|
||||
[491] = { upgrade = 0, max = 4, ilevel = 0 },
|
||||
[492] = { upgrade = 1, max = 4, ilevel = 4 },
|
||||
[493] = { upgrade = 2, max = 4, ilevel = 8 },
|
||||
[494] = { upgrade = 0, max = 6, ilevel = 0 },
|
||||
[495] = { upgrade = 1, max = 6, ilevel = 4 },
|
||||
[496] = { upgrade = 2, max = 6, ilevel = 8 },
|
||||
[497] = { upgrade = 3, max = 6, ilevel = 12 },
|
||||
[498] = { upgrade = 4, max = 6, ilevel = 16 },
|
||||
[503] = { upgrade = 3, max = 3, ilevel = 1 },
|
||||
[504] = { upgrade = 3, max = 4, ilevel = 12 },
|
||||
[505] = { upgrade = 4, max = 4, ilevel = 16 },
|
||||
[506] = { upgrade = 5, max = 6, ilevel = 20 },
|
||||
[507] = { upgrade = 6, max = 6, ilevel = 24 },
|
||||
[529] = { upgrade = 0, max = 2, ilevel = 0 },
|
||||
[530] = { upgrade = 1, max = 2, ilevel = 5 },
|
||||
[531] = { upgrade = 2, max = 2, ilevel = 10 },
|
||||
[535] = { upgrade = 1, max = 3, ilevel = 15 },
|
||||
[536] = { upgrade = 2, max = 3, ilevel = 30 },
|
||||
[537] = { upgrade = 3, max = 3, ilevel = 45 },
|
||||
[538] = { upgrade = 0, max = 3, ilevel = 0 },
|
||||
|
||||
}
|
||||
do
|
||||
local stub = { ilevel = 0 }
|
||||
setmetatable(upgradeTable, { __index = function(t, key)
|
||||
return stub
|
||||
end})
|
||||
end
|
||||
-- Tooltip Scanning stuff
|
||||
local itemLevelPattern = _G.ITEM_LEVEL:gsub("%%d", "(%%d+)")
|
||||
local soulboundPattern = _G.ITEM_SOULBOUND
|
||||
local boePattern=_G.ITEM_BIND_ON_EQUIP
|
||||
local bopPattern=_G.ITEM_BIND_ON_PICKUP
|
||||
local boaPattern1=_G.ITEM_BIND_TO_BNETACCOUNT
|
||||
local boaPattern2=_G.ITEM_BNETACCOUNTBOUND
|
||||
|
||||
local scanningTooltip
|
||||
local anchor
|
||||
local tipCache = lib.tipCache or setmetatable({},{__index=function(table,key) return {} end})
|
||||
local emptytable={}
|
||||
|
||||
local function ScanTip(itemLink,itemLevel,show)
|
||||
if type(itemLink)=="number" then
|
||||
itemLink=CachedGetItemInfo(itemLink,2)
|
||||
if not itemLink then return emptytable end
|
||||
end
|
||||
if type(tipCache[itemLink].ilevel)=="nil"then -- or not tipCache[itemLink].cached then
|
||||
local cacheIt=true
|
||||
if not scanningTooltip then
|
||||
anchor=CreateFrame("Frame")
|
||||
anchor:Hide()
|
||||
scanningTooltip = _G.CreateFrame("GameTooltip", "LibItemUpgradeInfoTooltip", nil, "GameTooltipTemplate")
|
||||
end
|
||||
--scanningTooltip:ClearLines()
|
||||
GameTooltip_SetDefaultAnchor(scanningTooltip,anchor)
|
||||
local itemString=itemLink:match("|H(.-)|h")
|
||||
local rc,message=pcall(scanningTooltip.SetHyperlink,scanningTooltip,itemString)
|
||||
if (not rc) then
|
||||
return emptytable
|
||||
end
|
||||
scanningTooltip:Show()
|
||||
local quality,_,_,class,subclass,_,_,_,_,classIndex,subclassIndex=CachedGetItemInfo(itemLink,3)
|
||||
|
||||
-- line 1 is the item name
|
||||
-- line 2 may be the item level, or it may be a modifier like "Heroic"
|
||||
-- check up to line 6 just in case
|
||||
local ilevel,soulbound,bop,boe,boa,heirloom
|
||||
if quality==LE_ITEM_QUALITY_ARTIFACT and itemLevel then
|
||||
local relic1, relic2, relic3 = select(4,strsplit(':', itemLink))
|
||||
if relic1 and relic1 ~= '' and not CachedGetItemInfo(relic1) then cacheIt = false end
|
||||
if relic2 and relic2 ~= '' and not CachedGetItemInfo(relic2) then cacheIt = false end
|
||||
if relic3 and relic3 ~= '' and not CachedGetItemInfo(relic3) then cacheIt = false end
|
||||
ilevel=itemLevel
|
||||
end
|
||||
if show then
|
||||
for i=1,12 do
|
||||
local l, ltext = _G["LibItemUpgradeInfoTooltipTextLeft"..i], nil
|
||||
local r, rtext = _G["LibItemUpgradeInfoTooltipTextRight"..i], nil
|
||||
ltext=l:GetText()
|
||||
rtext=r:GetText()
|
||||
pp(i,ltext,' - ',rtext)
|
||||
end
|
||||
end
|
||||
for i = 2, 6 do
|
||||
local label, text = _G["LibItemUpgradeInfoTooltipTextLeft"..i], nil
|
||||
if label then text=label:GetText() end
|
||||
if text then
|
||||
if ilevel==nil then ilevel = tonumber(text:match(itemLevelPattern)) end
|
||||
if soulbound==nil then soulbound = text:find(soulboundPattern) end
|
||||
if bop==nil then bop = text:find(bopPattern) end
|
||||
if boe==nil then boe = text:find(boePattern) end
|
||||
if boa==nil then boa = text:find(boaPattern1) end
|
||||
if boa==nil then boa = text:find(boaPattern2) end
|
||||
end
|
||||
end
|
||||
tipCache[itemLink]={
|
||||
ilevel=ilevel or itemLevel,
|
||||
soulbound=soulbound,
|
||||
bop=bop,
|
||||
boe=boe,
|
||||
cached=cacheIt
|
||||
}
|
||||
scanningTooltip:Hide()
|
||||
end
|
||||
return tipCache[itemLink]
|
||||
end
|
||||
|
||||
|
||||
-- GetUpgradeID(itemString)
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Number - The upgrade ID (possibly 0), or nil if the input is invalid or
|
||||
-- does not contain upgrade info
|
||||
function lib:GetUpgradeID(itemString)
|
||||
if type(itemString)~="string" then return end
|
||||
local itemString = itemString:match("item[%-?%d:]+") or ""-- Standardize itemlink to itemstring
|
||||
local instaid, _, numBonuses, affixes = select(12, strsplit(":", itemString, 15))
|
||||
instaid=tonumber(instaid) or 7
|
||||
numBonuses=tonumber(numBonuses) or 0
|
||||
if instaid >0 and (instaid-4)%8==0 then
|
||||
return tonumber((select(numBonuses + 1, strsplit(":", affixes))))
|
||||
end
|
||||
end
|
||||
|
||||
-- GetCurrentUpgrade(id)
|
||||
--
|
||||
-- Returns the current upgrade level of the item, e.g. 1 for a 1/2 item.
|
||||
--
|
||||
-- Arguments:
|
||||
-- id - Number - The upgrade ID of the item (obtained via GetUpgradeID())
|
||||
--
|
||||
-- Returns:
|
||||
-- Number - The current upgrade level of the item. Returns nil if the item
|
||||
-- cannot be upgraded
|
||||
function lib:GetCurrentUpgrade(id)
|
||||
return upgradeTable[id].upgrade
|
||||
end
|
||||
|
||||
-- GetMaximumUpgrade(id)
|
||||
--
|
||||
-- Returns the maximum upgrade level of the item, e.g. 2 for a 1/2 item.
|
||||
--
|
||||
-- Arguments:
|
||||
-- id - Number - The upgrade ID of the item (obtained via GetUpgradeID())
|
||||
--
|
||||
-- Returns:
|
||||
-- Number - The maximum upgrade level of the item. Returns nil if the item
|
||||
-- cannot be upgraded
|
||||
function lib:GetMaximumUpgrade(id)
|
||||
return upgradeTable[id].max
|
||||
end
|
||||
|
||||
-- GetItemLevelUpgrade(id)
|
||||
--
|
||||
-- Returns the item level increase that this upgrade is worth, e.g. 4 for a
|
||||
-- 1/2 item or 8 for a 2/2 item.
|
||||
--
|
||||
-- Arguments:
|
||||
-- id - Number - The upgrade ID of the item (obtained via GetUpgradeID())
|
||||
--
|
||||
-- Returns:
|
||||
-- Number - The item level increase of the item. Returns 0 if the item
|
||||
-- cannot be or has not been upgraded
|
||||
function lib:GetItemLevelUpgrade(id)
|
||||
return upgradeTable[id].ilevel
|
||||
end
|
||||
|
||||
-- GetItemUpgradeInfo(itemString)
|
||||
--
|
||||
-- Returns the current upgrade level, maximum upgrade level, and item level
|
||||
-- increase for an item.
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns if the item can be upgraded:
|
||||
-- Number - The current upgrade level of the item
|
||||
-- Number - The maximum upgrade level of the item
|
||||
-- Number - The item level increase of the item
|
||||
-- or if the item cannot be upgraded:
|
||||
-- nil
|
||||
-- nil
|
||||
-- 0
|
||||
-- or if the item is invalid or does not contain upgrade info:
|
||||
-- nil
|
||||
function lib:GetItemUpgradeInfo(itemString)
|
||||
local id = self:GetUpgradeID(itemString)
|
||||
if id then
|
||||
local cur = self:GetCurrentUpgrade(id)
|
||||
local max = self:GetMaximumUpgrade(id)
|
||||
local delta = self:GetItemLevelUpgrade(id)
|
||||
return cur, max, delta
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- GetHeirloomTrueLevel(itemString)
|
||||
--
|
||||
-- Returns the true item level for an heirloom (actually, returns the true level for any adapting item)
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Number, Boolean - The true item level of the item. If the item is not
|
||||
-- an heirloom, or an error occurs when trying to scan the
|
||||
-- item tooltip, the second return value is false. Otherwise
|
||||
-- the second return value is true. If the input is invalid,
|
||||
-- (nil, false) is returned.
|
||||
-- Convert the ITEM_LEVEL constant into a pattern for our use
|
||||
function lib:GetHeirloomTrueLevel(itemString)
|
||||
if type(itemString) ~= "string" then return nil,false end
|
||||
local _, itemLink, rarity, itemLevel = CachedGetItemInfo(itemString)
|
||||
if (not itemLink) then
|
||||
return nil,false
|
||||
end
|
||||
local rc=ScanTip(itemLink,itemLevel)
|
||||
if rc.ilevel then
|
||||
return rc.ilevel,true
|
||||
end
|
||||
return itemLevel, false
|
||||
end
|
||||
|
||||
-- GetUpgradedItemLevel(itemString)
|
||||
--
|
||||
-- Returns the true item level of the item, including upgrades and heirlooms.
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Number - The true item level of the item, or nil if the input is invalid
|
||||
function lib:GetUpgradedItemLevel(itemString)
|
||||
-- check for heirlooms first
|
||||
local ilvl, isTrue = self:GetHeirloomTrueLevel(itemString)
|
||||
if isTrue then
|
||||
return ilvl
|
||||
end
|
||||
-- not an heirloom? fall back to the regular item logic
|
||||
local id = self:GetUpgradeID(itemString)
|
||||
if ilvl and id then
|
||||
ilvl = ilvl + self:GetItemLevelUpgrade(id)
|
||||
end
|
||||
return ilvl
|
||||
end
|
||||
|
||||
-- IsBop(itemString)
|
||||
--
|
||||
-- Check an item for Bind On Pickup.
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Boolean - True if Bind On Pickup
|
||||
|
||||
function lib:IsBop(itemString)
|
||||
local rc=ScanTip(itemString)
|
||||
return rc.bop
|
||||
end
|
||||
-- IsBoe(itemString)
|
||||
--
|
||||
-- Check an item for Bind On Equip.
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Boolean - True if Bind On Equip
|
||||
|
||||
function lib:IsBoe(itemString)
|
||||
local rc=ScanTip(itemString)
|
||||
return rc.boe
|
||||
end
|
||||
-- IsBoa(itemString)
|
||||
--
|
||||
-- Check an item for Bind On Aaccount
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Boolean - True if Bind On Equip
|
||||
|
||||
function lib:IsBoa(itemString)
|
||||
local rc=ScanTip(itemString)
|
||||
return rc.boa
|
||||
end
|
||||
|
||||
-- IsArtifact(itemString)
|
||||
--
|
||||
-- Check an item for Heirloom
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Boolean - True if Artifact
|
||||
|
||||
function lib:IsArtifact(itemString)
|
||||
return CachedGetItemInfo(itemString,i_Quality)==LE_ITEM_QUALITY_ARTIFACT
|
||||
end
|
||||
|
||||
-- GetClassInfoIsHeirloom(itemString)
|
||||
--
|
||||
-- Retrieve class and subclass
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- class,subclass
|
||||
|
||||
|
||||
function lib:GetClassInfo(itemString)
|
||||
local rc=ScantTip(itemString)
|
||||
return rc.class,rc.subclass
|
||||
end
|
||||
|
||||
|
||||
-- IsHeirloom(itemString)
|
||||
--
|
||||
-- Check an item for Heirloom
|
||||
--
|
||||
-- Arguments:
|
||||
-- itemString - String - An itemLink or itemString denoting the item
|
||||
--
|
||||
-- Returns:
|
||||
-- Boolean - True if Heirloom
|
||||
|
||||
function lib:IsHeirloom(itemString)
|
||||
return CachedGetItemInfo(itemString,i_Quality) ==LE_ITEM_QUALITY_HEIRLOOM
|
||||
end
|
||||
---
|
||||
-- Parses an itemlink and returns itemId without calling API again
|
||||
-- @param #lib self
|
||||
-- @param #string itemlink
|
||||
-- @return #number itemId or 0
|
||||
function lib:GetItemID(itemlink)
|
||||
if (type(itemlink)=="string") then
|
||||
local itemid,context=GetItemInfoFromHyperlink(itemlink)
|
||||
return tonumber(itemid) or 0
|
||||
--return tonumber(itemlink:match("Hitem:(%d+):")) or 0
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
--
|
||||
-- Returns a caching version of GetItemInfo. Can be used to override the original one.
|
||||
-- Adds a second parameter to directly retrieving a specific value
|
||||
-- (Note: internally uses select so it's actually like calling select(n,GetItemInfo(itemID))
|
||||
--
|
||||
-- Arguments:
|
||||
-- self #lib self
|
||||
--
|
||||
-- Returns:
|
||||
-- #function The new function
|
||||
|
||||
--@do-not-package--
|
||||
function lib:ScanTip(itemLink)
|
||||
local GameTooltip=LibItemUpgradeInfoTooltip
|
||||
if GameTooltip then
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, UIParent)
|
||||
GameTooltip:SetHyperlink(itemLink)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
return ScanTip(itemLink,100,true)
|
||||
end
|
||||
function lib:GetCachingGetItemInfo()
|
||||
return CachedGetItemInfo
|
||||
end
|
||||
function lib:GetCacheStats()
|
||||
local c=lib.itemcache
|
||||
local h=c.tot-c.miss
|
||||
local perc=( h>0) and h/c.tot*100 or 0
|
||||
return c.miss,h,perc
|
||||
end
|
||||
function lib:GetCache()
|
||||
return lib.itemcache
|
||||
end
|
||||
function lib:CleanCache()
|
||||
return wipe(lib.itemcache)
|
||||
end
|
||||
|
||||
--[===========[ ]===========]
|
||||
--[===[ Debug utilities ]===]
|
||||
--[===========[ ]===========]
|
||||
|
||||
local function compareTables(t1, t2)
|
||||
local seen = {}
|
||||
for k, v1 in pairs(t1) do
|
||||
seen[k] = true
|
||||
local v2 = rawget(t2, k)
|
||||
if not v2 then return false end
|
||||
if type(v1) ~= type(v2) then return false end
|
||||
if type(v1) == "table" then
|
||||
if not compareTables(v1, v2) then return false end
|
||||
elseif v1 ~= v2 then return false end
|
||||
end
|
||||
for k in pairs(t2) do
|
||||
if not seen[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- prints the table rows in red and green
|
||||
-- omits the lead { and the trailing }
|
||||
local function printDiffTable(t1, t2)
|
||||
local keys, seen = {}, {}
|
||||
for k in pairs(t1) do
|
||||
keys[#keys+1] = k
|
||||
seen[k] = true
|
||||
end
|
||||
for k in pairs(t2) do
|
||||
if not seen[k] then
|
||||
keys[#keys+1] = k
|
||||
end
|
||||
end
|
||||
table.sort(keys)
|
||||
local function formatTable(t)
|
||||
local comps = {}
|
||||
for k, v in pairs(t) do
|
||||
comps[#comps+1] = ("%s = %d"):format(k, v)
|
||||
end
|
||||
return "{ " .. table.concat(comps, ", ") .. " }"
|
||||
end
|
||||
for _, k in ipairs(keys) do
|
||||
local v1, v2 = rawget(t1, k), rawget(t2, k)
|
||||
local equal
|
||||
if type(v1) == "table" and type(v2) == "table" then equal = compareTables(v1, v2)
|
||||
else equal = v1 == v2 end
|
||||
if not equal then
|
||||
if v1 then
|
||||
pp(("|cffff0000 [%d] = %s,|r"):format(k, formatTable(v1)))
|
||||
end
|
||||
if v2 then
|
||||
pp(("|cff00ff00 [%d] = %s,|r"):format(k, formatTable(v2)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Scans the first 10000 upgrade IDs
|
||||
-- Run this with /run LibStub:GetLibrary("LibItemUpgradeInfo-1.0"):_CheckUpgradeTable()
|
||||
-- If you don't have Aspirant's Staff of Harmony cached it may error out, just try again.
|
||||
do
|
||||
local debugFrame
|
||||
local worker
|
||||
local newTable
|
||||
local debugTooltip
|
||||
function lib:_CheckUpgradeTable(itemLink)
|
||||
if worker then
|
||||
pp("|cffff0000LibItemUpgradeInfo-1.0: upgrade check already in progress")
|
||||
return
|
||||
end
|
||||
if not debugFrame then
|
||||
debugFrame = _G.CreateFrame("frame")
|
||||
debugFrame:Hide()
|
||||
debugFrame:SetScript("OnUpdate", function()
|
||||
local ok, result, count, max = pcall(worker)
|
||||
if not ok or result then
|
||||
debugFrame:Hide()
|
||||
worker = nil
|
||||
end
|
||||
if not ok then
|
||||
pp("|cffff0000LibItemUpgradeInfo-1.0 error: " .. result .. "|r")
|
||||
elseif result then
|
||||
pp("LibItemUpgradeInfo-1.0: scan complete")
|
||||
if compareTables(upgradeTable, newTable) then
|
||||
pp("LibItemUpgradeInfo-1.0: |cff00ff00No changes|r")
|
||||
else
|
||||
pp("LibItemUpgradeInfo-1.0: |cffff0000New table:|r {")
|
||||
printDiffTable(upgradeTable, newTable)
|
||||
pp("}")
|
||||
end
|
||||
else
|
||||
pp("LibItemUpgradeInfo-1.0: scanning " .. count .. "/" .. max)
|
||||
end
|
||||
end)
|
||||
end
|
||||
if not debugTooltip then
|
||||
debugTooltip = _G.CreateFrame("GameTooltip", "LibItemUpgradeInfoDebugTooltip", nil, "GameTooltipTemplate")
|
||||
debugTooltip:SetOwner(_G.WorldFrame, "ANCHOR_NONE")
|
||||
end
|
||||
newTable = {}
|
||||
--local itemLink = "|cff0070dd|Hitem:89551:0:0:0:0:0:0:0:90:253:0:0:1:0|h[Aspirant's Staff of Harmony]|h|r"
|
||||
local itemLink = itemLink or "|cff0070dd|Hitem:89551:0:0:0:0:0:0:0:100:253:4:0:0:0|h[Aspirant's Staff of Harmony]|h|r"
|
||||
-- Livello è il 9,upgradeid il 14. Al decimo posto, un valore che deve essere 4 o 4+n *8) per far scattare l'uso dell'upgradeid
|
||||
local itemLevel = select(4, _G.GetItemInfo(itemLink))
|
||||
assert(itemLevel, "Can't find item level for itemLink")
|
||||
local count, max, batchsize = 0, 10000, 200
|
||||
worker = function()
|
||||
for i = count, math.min(max, count+batchsize) do
|
||||
local link = itemLink:gsub("%d+|h", i.."|h")
|
||||
debugTooltip:ClearLines()
|
||||
debugTooltip:SetHyperlink(link)
|
||||
local upgrade, max
|
||||
local curLevel, maxLevel = _G.LibItemUpgradeInfoDebugTooltipTextLeft3:GetText():match("^Upgrade Level: (%d+)/(%d+)")
|
||||
local ilvl = tonumber(_G.LibItemUpgradeInfoDebugTooltipTextLeft2:GetText():match("Item Level (%d+)"))
|
||||
if not ilvl then
|
||||
ilvl = tonumber(_G.LibItemUpgradeInfoDebugTooltipTextLeft3:GetText():match("Item Level (%d+)"))
|
||||
end
|
||||
assert(ilvl ~= nil, "Can't find ItemLevel in tooltip: " .. _G.LibItemUpgradeInfoDebugTooltipTextLeft2:GetText())
|
||||
if curLevel or maxLevel or ilvl ~= itemLevel then
|
||||
newTable[i] = { upgrade = tonumber(curLevel), max = tonumber(maxLevel), ilevel = ilvl - itemLevel }
|
||||
end
|
||||
end
|
||||
count = count + batchsize
|
||||
return (count > max), count, max
|
||||
end
|
||||
debugFrame:Show()
|
||||
end
|
||||
end
|
||||
--@end-do-not-package--
|
||||
|
||||
-- vim: set noet sw=4 ts=4:
|
||||
@@ -0,0 +1,9 @@
|
||||
## Interface: 70200
|
||||
## Title: Lib: ItemUpgradeInfo-1.0
|
||||
## Notes: Database of item upgrade IDs
|
||||
## Author: eridius
|
||||
## Version: Release-70200-28 70200
|
||||
## X-Revision: 7c5485a
|
||||
## X-Category: Library
|
||||
|
||||
LibItemUpgradeInfo-1.0.xml
|
||||
@@ -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="LibStub\LibStub.lua"/>
|
||||
<Script file="Core.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,51 @@
|
||||
-- $Id: LibStub.lua 76 2007-09-03 01:50:17Z mikk $
|
||||
-- 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]
|
||||
|
||||
-- Check to see is this version of the stub is obsolete
|
||||
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
|
||||
LibStub = LibStub or {libs = {}, minors = {} }
|
||||
_G[LIBSTUB_MAJOR] = LibStub
|
||||
LibStub.minor = LIBSTUB_MINOR
|
||||
|
||||
-- LibStub:NewLibrary(major, minor)
|
||||
-- major (string) - the major version of the library
|
||||
-- minor (string or number ) - the minor version of the library
|
||||
--
|
||||
-- returns nil if a newer or same version of the lib is already present
|
||||
-- returns empty library object or old library object if upgrade is needed
|
||||
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
|
||||
|
||||
-- LibStub:GetLibrary(major, [silent])
|
||||
-- major (string) - the major version of the library
|
||||
-- silent (boolean) - if true, library is optional, silently return nil if its not found
|
||||
--
|
||||
-- throws an error if the library can not be found (except silent is set)
|
||||
-- returns the library object if found
|
||||
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
|
||||
|
||||
-- LibStub:IterateLibraries()
|
||||
--
|
||||
-- Returns an iterator for the currently registered libraries
|
||||
function LibStub:IterateLibraries()
|
||||
return pairs(self.libs)
|
||||
end
|
||||
|
||||
setmetatable(LibStub, { __call = LibStub.GetLibrary })
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
## Interface: 70200
|
||||
## Title: Lib: LibStub
|
||||
## Notes: Universal Library Stub
|
||||
## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel
|
||||
## X-Website: http://www.wowace.com/addons/libstub/
|
||||
## X-Category: Library
|
||||
## X-License: Public Domain
|
||||
|
||||
LibStub.lua
|
||||
@@ -0,0 +1,41 @@
|
||||
debugstack = debug.traceback
|
||||
strmatch = string.match
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
local lib, oldMinor = LibStub:NewLibrary("Pants", 1) -- make a new thingy
|
||||
assert(lib) -- should return the library table
|
||||
assert(not oldMinor) -- should not return the old minor, since it didn't exist
|
||||
|
||||
-- the following is to create data and then be able to check if the same data exists after the fact
|
||||
function lib:MyMethod()
|
||||
end
|
||||
local MyMethod = lib.MyMethod
|
||||
lib.MyTable = {}
|
||||
local MyTable = lib.MyTable
|
||||
|
||||
local newLib, newOldMinor = LibStub:NewLibrary("Pants", 1) -- try to register a library with the same version, should silently fail
|
||||
assert(not newLib) -- should not return since out of date
|
||||
|
||||
local newLib, newOldMinor = LibStub:NewLibrary("Pants", 0) -- try to register a library with a previous, should silently fail
|
||||
assert(not newLib) -- should not return since out of date
|
||||
|
||||
local newLib, newOldMinor = LibStub:NewLibrary("Pants", 2) -- register a new version
|
||||
assert(newLib) -- library table
|
||||
assert(rawequal(newLib, lib)) -- should be the same reference as the previous
|
||||
assert(newOldMinor == 1) -- should return the minor version of the previous version
|
||||
|
||||
assert(rawequal(lib.MyMethod, MyMethod)) -- verify that values were saved
|
||||
assert(rawequal(lib.MyTable, MyTable)) -- verify that values were saved
|
||||
|
||||
local newLib, newOldMinor = LibStub:NewLibrary("Pants", "Blah 3 Blah") -- register a new version with a string minor version (instead of a number)
|
||||
assert(newLib) -- library table
|
||||
assert(newOldMinor == 2) -- previous version was 2
|
||||
|
||||
local newLib, newOldMinor = LibStub:NewLibrary("Pants", "Blah 4 and please ignore 15 Blah") -- register a new version with a string minor version (instead of a number)
|
||||
assert(newLib)
|
||||
assert(newOldMinor == 3) -- previous version was 3 (even though it gave a string)
|
||||
|
||||
local newLib, newOldMinor = LibStub:NewLibrary("Pants", 5) -- register a new library, using a normal number instead of a string
|
||||
assert(newLib)
|
||||
assert(newOldMinor == 4) -- previous version was 4 (even though it gave a string)
|
||||
@@ -0,0 +1,27 @@
|
||||
debugstack = debug.traceback
|
||||
strmatch = string.match
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
for major, library in LibStub:IterateLibraries() do
|
||||
-- check that MyLib doesn't exist yet, by iterating through all the libraries
|
||||
assert(major ~= "MyLib")
|
||||
end
|
||||
|
||||
assert(not LibStub:GetLibrary("MyLib", true)) -- check that MyLib doesn't exist yet by direct checking
|
||||
assert(not pcall(LibStub.GetLibrary, LibStub, "MyLib")) -- don't silently fail, thus it should raise an error.
|
||||
local lib = LibStub:NewLibrary("MyLib", 1) -- create the lib
|
||||
assert(lib) -- check it exists
|
||||
assert(rawequal(LibStub:GetLibrary("MyLib"), lib)) -- verify that :GetLibrary("MyLib") properly equals the lib reference
|
||||
|
||||
assert(LibStub:NewLibrary("MyLib", 2)) -- create a new version
|
||||
|
||||
local count=0
|
||||
for major, library in LibStub:IterateLibraries() do
|
||||
-- check that MyLib exists somewhere in the libraries, by iterating through all the libraries
|
||||
if major == "MyLib" then -- we found it!
|
||||
count = count +1
|
||||
assert(rawequal(library, lib)) -- verify that the references are equal
|
||||
end
|
||||
end
|
||||
assert(count == 1) -- verify that we actually found it, and only once
|
||||
@@ -0,0 +1,14 @@
|
||||
debugstack = debug.traceback
|
||||
strmatch = string.match
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
local proxy = newproxy() -- non-string
|
||||
|
||||
assert(not pcall(LibStub.NewLibrary, LibStub, proxy, 1)) -- should error, proxy is not a string, it's userdata
|
||||
local success, ret = pcall(LibStub.GetLibrary, proxy, true)
|
||||
assert(not success or not ret) -- either error because proxy is not a string or because it's not actually registered.
|
||||
|
||||
assert(not pcall(LibStub.NewLibrary, LibStub, "Something", "No number in here")) -- should error, minor has no string in it.
|
||||
|
||||
assert(not LibStub:GetLibrary("Something", true)) -- shouldn't've created it from the above statement
|
||||
@@ -0,0 +1,41 @@
|
||||
debugstack = debug.traceback
|
||||
strmatch = string.match
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
|
||||
-- Pretend like loaded libstub is old and doesn't have :IterateLibraries
|
||||
assert(LibStub.minor)
|
||||
LibStub.minor = LibStub.minor - 0.0001
|
||||
LibStub.IterateLibraries = nil
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
assert(type(LibStub.IterateLibraries)=="function")
|
||||
|
||||
|
||||
-- Now pretend that we're the same version -- :IterateLibraries should NOT be re-created
|
||||
LibStub.IterateLibraries = 123
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
assert(LibStub.IterateLibraries == 123)
|
||||
|
||||
|
||||
-- Now pretend that a newer version is loaded -- :IterateLibraries should NOT be re-created
|
||||
LibStub.minor = LibStub.minor + 0.0001
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
assert(LibStub.IterateLibraries == 123)
|
||||
|
||||
|
||||
-- Again with a huge number
|
||||
LibStub.minor = LibStub.minor + 1234567890
|
||||
|
||||
loadfile("../LibStub.lua")()
|
||||
|
||||
assert(LibStub.IterateLibraries == 123)
|
||||
|
||||
|
||||
print("OK")
|
||||
@@ -0,0 +1,292 @@
|
||||
--[[
|
||||
Name: LibSharedMedia-3.0
|
||||
Revision: $Revision: 91 $
|
||||
Author: Elkano (elkano@gmx.de)
|
||||
Inspired By: SurfaceLib by Haste/Otravi (troeks@gmail.com)
|
||||
Website: http://www.wowace.com/projects/libsharedmedia-3-0/
|
||||
Description: Shared handling of media data (fonts, sounds, textures, ...) between addons.
|
||||
Dependencies: LibStub, CallbackHandler-1.0
|
||||
License: LGPL v2.1
|
||||
]]
|
||||
|
||||
local MAJOR, MINOR = "LibSharedMedia-3.0", 6010002 -- 6.1.0 v2 / increase manually on changes
|
||||
local lib = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not lib then return end
|
||||
|
||||
local _G = getfenv(0)
|
||||
|
||||
local pairs = _G.pairs
|
||||
local type = _G.type
|
||||
|
||||
local band = _G.bit.band
|
||||
|
||||
local table_insert = _G.table.insert
|
||||
local table_sort = _G.table.sort
|
||||
|
||||
local locale = GetLocale()
|
||||
local locale_is_western
|
||||
local LOCALE_MASK = 0
|
||||
lib.LOCALE_BIT_koKR = 1
|
||||
lib.LOCALE_BIT_ruRU = 2
|
||||
lib.LOCALE_BIT_zhCN = 4
|
||||
lib.LOCALE_BIT_zhTW = 8
|
||||
lib.LOCALE_BIT_western = 128
|
||||
|
||||
local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
|
||||
|
||||
lib.callbacks = lib.callbacks or CallbackHandler:New(lib)
|
||||
|
||||
lib.DefaultMedia = lib.DefaultMedia or {}
|
||||
lib.MediaList = lib.MediaList or {}
|
||||
lib.MediaTable = lib.MediaTable or {}
|
||||
lib.MediaType = lib.MediaType or {}
|
||||
lib.OverrideMedia = lib.OverrideMedia or {}
|
||||
|
||||
local defaultMedia = lib.DefaultMedia
|
||||
local mediaList = lib.MediaList
|
||||
local mediaTable = lib.MediaTable
|
||||
local overrideMedia = lib.OverrideMedia
|
||||
|
||||
|
||||
-- create mediatype constants
|
||||
lib.MediaType.BACKGROUND = "background" -- background textures
|
||||
lib.MediaType.BORDER = "border" -- border textures
|
||||
lib.MediaType.FONT = "font" -- fonts
|
||||
lib.MediaType.STATUSBAR = "statusbar" -- statusbar textures
|
||||
lib.MediaType.SOUND = "sound" -- sound files
|
||||
|
||||
-- populate lib with default Blizzard data
|
||||
-- BACKGROUND
|
||||
if not lib.MediaTable.background then lib.MediaTable.background = {} end
|
||||
lib.MediaTable.background["None"] = [[]]
|
||||
lib.MediaTable.background["Blizzard Collections Background"] = [[Interface\Collections\CollectionsBackgroundTile]]
|
||||
lib.MediaTable.background["Blizzard Dialog Background"] = [[Interface\DialogFrame\UI-DialogBox-Background]]
|
||||
lib.MediaTable.background["Blizzard Dialog Background Dark"] = [[Interface\DialogFrame\UI-DialogBox-Background-Dark]]
|
||||
lib.MediaTable.background["Blizzard Dialog Background Gold"] = [[Interface\DialogFrame\UI-DialogBox-Gold-Background]]
|
||||
lib.MediaTable.background["Blizzard Garrison Background"] = [[Interface\Garrison\GarrisonUIBackground]]
|
||||
lib.MediaTable.background["Blizzard Garrison Background 2"] = [[Interface\Garrison\GarrisonUIBackground2]]
|
||||
lib.MediaTable.background["Blizzard Garrison Background 3"] = [[Interface\Garrison\GarrisonMissionUIInfoBoxBackgroundTile]]
|
||||
lib.MediaTable.background["Blizzard Low Health"] = [[Interface\FullScreenTextures\LowHealth]]
|
||||
lib.MediaTable.background["Blizzard Marble"] = [[Interface\FrameGeneral\UI-Background-Marble]]
|
||||
lib.MediaTable.background["Blizzard Out of Control"] = [[Interface\FullScreenTextures\OutOfControl]]
|
||||
lib.MediaTable.background["Blizzard Parchment"] = [[Interface\AchievementFrame\UI-Achievement-Parchment-Horizontal]]
|
||||
lib.MediaTable.background["Blizzard Parchment 2"] = [[Interface\AchievementFrame\UI-GuildAchievement-Parchment-Horizontal]]
|
||||
lib.MediaTable.background["Blizzard Rock"] = [[Interface\FrameGeneral\UI-Background-Rock]]
|
||||
lib.MediaTable.background["Blizzard Tabard Background"] = [[Interface\TabardFrame\TabardFrameBackground]]
|
||||
lib.MediaTable.background["Blizzard Tooltip"] = [[Interface\Tooltips\UI-Tooltip-Background]]
|
||||
lib.MediaTable.background["Solid"] = [[Interface\Buttons\WHITE8X8]]
|
||||
lib.DefaultMedia.background = "None"
|
||||
|
||||
-- BORDER
|
||||
if not lib.MediaTable.border then lib.MediaTable.border = {} end
|
||||
lib.MediaTable.border["None"] = [[]]
|
||||
lib.MediaTable.border["Blizzard Achievement Wood"] = [[Interface\AchievementFrame\UI-Achievement-WoodBorder]]
|
||||
lib.MediaTable.border["Blizzard Chat Bubble"] = [[Interface\Tooltips\ChatBubble-Backdrop]]
|
||||
lib.MediaTable.border["Blizzard Dialog"] = [[Interface\DialogFrame\UI-DialogBox-Border]]
|
||||
lib.MediaTable.border["Blizzard Dialog Gold"] = [[Interface\DialogFrame\UI-DialogBox-Gold-Border]]
|
||||
lib.MediaTable.border["Blizzard Party"] = [[Interface\CHARACTERFRAME\UI-Party-Border]]
|
||||
lib.MediaTable.border["Blizzard Tooltip"] = [[Interface\Tooltips\UI-Tooltip-Border]]
|
||||
lib.DefaultMedia.border = "None"
|
||||
|
||||
-- FONT
|
||||
if not lib.MediaTable.font then lib.MediaTable.font = {} end
|
||||
local SML_MT_font = lib.MediaTable.font
|
||||
--[[
|
||||
All font files are currently in all clients, the following table depicts which font supports which charset as of 5.0.4
|
||||
Fonts were checked using langcover.pl from DejaVu fonts (http://sourceforge.net/projects/dejavu/) and FontForge (http://fontforge.org/)
|
||||
latin means check for: de, en, es, fr, it, pt
|
||||
|
||||
file name latin koKR ruRU zhCN zhTW
|
||||
2002.ttf 2002 X X X - -
|
||||
2002B.ttf 2002 Bold X X X - -
|
||||
ARHei.ttf AR CrystalzcuheiGBK Demibold X - X X X
|
||||
ARIALN.TTF Arial Narrow X - X - -
|
||||
ARKai_C.ttf AR ZhongkaiGBK Medium (Combat) X - X X X
|
||||
ARKai_T.ttf AR ZhongkaiGBK Medium X - X X X
|
||||
bHEI00M.ttf AR Heiti2 Medium B5 - - - - X
|
||||
bHEI01B.ttf AR Heiti2 Bold B5 - - - - X
|
||||
bKAI00M.ttf AR Kaiti Medium B5 - - - - X
|
||||
bLEI00D.ttf AR Leisu Demi B5 - - - - X
|
||||
FRIZQT__.TTF Friz Quadrata TT X - - - -
|
||||
FRIZQT___CYR.TTF FrizQuadrataCTT x - X - -
|
||||
K_Damage.TTF YDIWingsM - X X - -
|
||||
K_Pagetext.TTF MoK X X X - -
|
||||
MORPHEUS.TTF Morpheus X - - - -
|
||||
MORPHEUS_CYR.TTF Morpheus X - X - -
|
||||
NIM_____.ttf Nimrod MT X - X - -
|
||||
SKURRI.TTF Skurri X - - - -
|
||||
SKURRI_CYR.TTF Skurri X - X - -
|
||||
|
||||
WARNING: Although FRIZQT___CYR is available on western clients, it doesn't support special European characters e.g. é, ï, ö
|
||||
Due to this, we cannot use it as a replacement for FRIZQT__.TTF
|
||||
]]
|
||||
|
||||
if locale == "koKR" then
|
||||
LOCALE_MASK = lib.LOCALE_BIT_koKR
|
||||
--
|
||||
SML_MT_font["굵은 글꼴"] = [[Fonts\2002B.TTF]]
|
||||
SML_MT_font["기본 글꼴"] = [[Fonts\2002.TTF]]
|
||||
SML_MT_font["데미지 글꼴"] = [[Fonts\K_Damage.TTF]]
|
||||
SML_MT_font["퀘스트 글꼴"] = [[Fonts\K_Pagetext.TTF]]
|
||||
--
|
||||
lib.DefaultMedia["font"] = "기본 글꼴" -- someone from koKR please adjust if needed
|
||||
--
|
||||
elseif locale == "zhCN" then
|
||||
LOCALE_MASK = lib.LOCALE_BIT_zhCN
|
||||
--
|
||||
SML_MT_font["伤害数字"] = [[Fonts\ARKai_C.ttf]]
|
||||
SML_MT_font["默认"] = [[Fonts\ARKai_T.ttf]]
|
||||
SML_MT_font["聊天"] = [[Fonts\ARHei.ttf]]
|
||||
--
|
||||
lib.DefaultMedia["font"] = "默认" -- someone from zhCN please adjust if needed
|
||||
--
|
||||
elseif locale == "zhTW" then
|
||||
LOCALE_MASK = lib.LOCALE_BIT_zhTW
|
||||
--
|
||||
SML_MT_font["提示訊息"] = [[Fonts\bHEI00M.ttf]]
|
||||
SML_MT_font["聊天"] = [[Fonts\bHEI01B.ttf]]
|
||||
SML_MT_font["傷害數字"] = [[Fonts\bKAI00M.ttf]]
|
||||
SML_MT_font["預設"] = [[Fonts\bLEI00D.ttf]]
|
||||
--
|
||||
lib.DefaultMedia["font"] = "預設" -- someone from zhTW please adjust if needed
|
||||
|
||||
elseif locale == "ruRU" then
|
||||
LOCALE_MASK = lib.LOCALE_BIT_ruRU
|
||||
--
|
||||
SML_MT_font["2002"] = [[Fonts\2002.TTF]]
|
||||
SML_MT_font["2002 Bold"] = [[Fonts\2002B.TTF]]
|
||||
SML_MT_font["AR CrystalzcuheiGBK Demibold"] = [[Fonts\ARHei.TTF]]
|
||||
SML_MT_font["AR ZhongkaiGBK Medium (Combat)"] = [[Fonts\ARKai_C.TTF]]
|
||||
SML_MT_font["AR ZhongkaiGBK Medium"] = [[Fonts\ARKai_T.TTF]]
|
||||
SML_MT_font["Arial Narrow"] = [[Fonts\ARIALN.TTF]]
|
||||
SML_MT_font["Friz Quadrata TT"] = [[Fonts\FRIZQT___CYR.TTF]]
|
||||
SML_MT_font["MoK"] = [[Fonts\K_Pagetext.TTF]]
|
||||
SML_MT_font["Morpheus"] = [[Fonts\MORPHEUS_CYR.TTF]]
|
||||
SML_MT_font["Nimrod MT"] = [[Fonts\NIM_____.ttf]]
|
||||
SML_MT_font["Skurri"] = [[Fonts\SKURRI_CYR.TTF]]
|
||||
--
|
||||
lib.DefaultMedia.font = "Friz Quadrata TT"
|
||||
--
|
||||
else
|
||||
LOCALE_MASK = lib.LOCALE_BIT_western
|
||||
locale_is_western = true
|
||||
--
|
||||
SML_MT_font["2002"] = [[Fonts\2002.TTF]]
|
||||
SML_MT_font["2002 Bold"] = [[Fonts\2002B.TTF]]
|
||||
SML_MT_font["AR CrystalzcuheiGBK Demibold"] = [[Fonts\ARHei.TTF]]
|
||||
SML_MT_font["AR ZhongkaiGBK Medium (Combat)"] = [[Fonts\ARKai_C.TTF]]
|
||||
SML_MT_font["AR ZhongkaiGBK Medium"] = [[Fonts\ARKai_T.TTF]]
|
||||
SML_MT_font["Arial Narrow"] = [[Fonts\ARIALN.TTF]]
|
||||
SML_MT_font["Friz Quadrata TT"] = [[Fonts\FRIZQT__.TTF]]
|
||||
SML_MT_font["MoK"] = [[Fonts\K_Pagetext.TTF]]
|
||||
SML_MT_font["Morpheus"] = [[Fonts\MORPHEUS_CYR.TTF]]
|
||||
SML_MT_font["Nimrod MT"] = [[Fonts\NIM_____.ttf]]
|
||||
SML_MT_font["Skurri"] = [[Fonts\SKURRI_CYR.TTF]]
|
||||
--
|
||||
lib.DefaultMedia.font = "Friz Quadrata TT"
|
||||
--
|
||||
end
|
||||
|
||||
-- STATUSBAR
|
||||
if not lib.MediaTable.statusbar then lib.MediaTable.statusbar = {} end
|
||||
lib.MediaTable.statusbar["Blizzard"] = [[Interface\TargetingFrame\UI-StatusBar]]
|
||||
lib.MediaTable.statusbar["Blizzard Character Skills Bar"] = [[Interface\PaperDollInfoFrame\UI-Character-Skills-Bar]]
|
||||
lib.MediaTable.statusbar["Blizzard Raid Bar"] = [[Interface\RaidFrame\Raid-Bar-Hp-Fill]]
|
||||
lib.DefaultMedia.statusbar = "Blizzard"
|
||||
|
||||
-- SOUND
|
||||
if not lib.MediaTable.sound then lib.MediaTable.sound = {} end
|
||||
lib.MediaTable.sound["None"] = [[Interface\Quiet.ogg]] -- Relies on the fact that PlaySound[File] doesn't error on non-existing input.
|
||||
lib.DefaultMedia.sound = "None"
|
||||
|
||||
local function rebuildMediaList(mediatype)
|
||||
local mtable = mediaTable[mediatype]
|
||||
if not mtable then return end
|
||||
if not mediaList[mediatype] then mediaList[mediatype] = {} end
|
||||
local mlist = mediaList[mediatype]
|
||||
-- list can only get larger, so simply overwrite it
|
||||
local i = 0
|
||||
for k in pairs(mtable) do
|
||||
i = i + 1
|
||||
mlist[i] = k
|
||||
end
|
||||
table_sort(mlist)
|
||||
end
|
||||
|
||||
function lib:Register(mediatype, key, data, langmask)
|
||||
if type(mediatype) ~= "string" then
|
||||
error(MAJOR..":Register(mediatype, key, data, langmask) - mediatype must be string, got "..type(mediatype))
|
||||
end
|
||||
if type(key) ~= "string" then
|
||||
error(MAJOR..":Register(mediatype, key, data, langmask) - key must be string, got "..type(key))
|
||||
end
|
||||
mediatype = mediatype:lower()
|
||||
if mediatype == lib.MediaType.FONT and ((langmask and band(langmask, LOCALE_MASK) == 0) or not (langmask or locale_is_western)) then return false end
|
||||
if mediatype == lib.MediaType.SOUND and type(data) == "string" then
|
||||
local path = data:lower()
|
||||
-- Only ogg and mp3 are valid sounds.
|
||||
if not path:find(".ogg", nil, true) and not path:find(".mp3", nil, true) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
if not mediaTable[mediatype] then mediaTable[mediatype] = {} end
|
||||
local mtable = mediaTable[mediatype]
|
||||
if mtable[key] then return false end
|
||||
|
||||
mtable[key] = data
|
||||
rebuildMediaList(mediatype)
|
||||
self.callbacks:Fire("LibSharedMedia_Registered", mediatype, key)
|
||||
return true
|
||||
end
|
||||
|
||||
function lib:Fetch(mediatype, key, noDefault)
|
||||
local mtt = mediaTable[mediatype]
|
||||
local overridekey = overrideMedia[mediatype]
|
||||
local result = mtt and ((overridekey and mtt[overridekey] or mtt[key]) or (not noDefault and defaultMedia[mediatype] and mtt[defaultMedia[mediatype]])) or nil
|
||||
return result ~= "" and result or nil
|
||||
end
|
||||
|
||||
function lib:IsValid(mediatype, key)
|
||||
return mediaTable[mediatype] and (not key or mediaTable[mediatype][key]) and true or false
|
||||
end
|
||||
|
||||
function lib:HashTable(mediatype)
|
||||
return mediaTable[mediatype]
|
||||
end
|
||||
|
||||
function lib:List(mediatype)
|
||||
if not mediaTable[mediatype] then
|
||||
return nil
|
||||
end
|
||||
if not mediaList[mediatype] then
|
||||
rebuildMediaList(mediatype)
|
||||
end
|
||||
return mediaList[mediatype]
|
||||
end
|
||||
|
||||
function lib:GetGlobal(mediatype)
|
||||
return overrideMedia[mediatype]
|
||||
end
|
||||
|
||||
function lib:SetGlobal(mediatype, key)
|
||||
if not mediaTable[mediatype] then
|
||||
return false
|
||||
end
|
||||
overrideMedia[mediatype] = (key and mediaTable[mediatype][key]) and key or nil
|
||||
self.callbacks:Fire("LibSharedMedia_SetGlobal", mediatype, overrideMedia[mediatype])
|
||||
return true
|
||||
end
|
||||
|
||||
function lib:GetDefault(mediatype)
|
||||
return defaultMedia[mediatype]
|
||||
end
|
||||
|
||||
function lib:SetDefault(mediatype, key)
|
||||
if mediaTable[mediatype] and mediaTable[mediatype][key] and not defaultMedia[mediatype] then
|
||||
defaultMedia[mediatype] = key
|
||||
return true
|
||||
else
|
||||
return false
|
||||
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="LibSharedMedia-3.0.lua" />
|
||||
</Ui>
|
||||
@@ -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
|
||||
@@ -0,0 +1,317 @@
|
||||
--[[
|
||||
Name: LibWindow-1.1
|
||||
Revision: $Rev: 8 $
|
||||
Author(s): Mikk (dpsgnome@mail.com)
|
||||
Website: http://old.wowace.com/wiki/LibWindow-1.1
|
||||
Documentation: http://old.wowace.com/wiki/LibWindow-1.1
|
||||
SVN: http://svn.wowace.com/root/trunk/WindowLib/Window-1.0
|
||||
Description: A library that handles the basics of "window" style frames: scaling, smart position saving, dragging..
|
||||
Dependencies: none
|
||||
License: Public Domain
|
||||
]]
|
||||
|
||||
local MAJOR = "LibWindow-1.1"
|
||||
local MINOR = tonumber(("$Revision: 8 $"):match("(%d+)"))
|
||||
|
||||
local lib = LibStub:NewLibrary(MAJOR,MINOR)
|
||||
if not lib then return end
|
||||
|
||||
local min,max,abs = min,max,abs
|
||||
local pairs = pairs
|
||||
local tostring = tostring
|
||||
local UIParent,GetScreenWidth,GetScreenHeight,IsAltKeyDown = UIParent,GetScreenWidth,GetScreenHeight,IsAltKeyDown
|
||||
-- GLOBALS: error, ChatFrame1, assert
|
||||
|
||||
local function print(msg) ChatFrame1:AddMessage(MAJOR..": "..tostring(msg)) end
|
||||
|
||||
lib.utilFrame = lib.utilFrame or CreateFrame("Frame")
|
||||
lib.delayedSavePosition = lib.delayedSavePosition or {}
|
||||
lib.windowData = lib.windowData or {}
|
||||
--[frameref]={
|
||||
-- names={optional names data from .RegisterConfig()}
|
||||
-- storage= -- tableref where config data is read/written
|
||||
-- altEnable=true/false
|
||||
--}
|
||||
|
||||
|
||||
lib.embeds = lib.embeds or {}
|
||||
|
||||
local mixins = {} -- "FuncName"=true
|
||||
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- UTILITIES
|
||||
---------------------------------------------------------
|
||||
|
||||
|
||||
local function getStorageName(frame, name)
|
||||
local names = lib.windowData[frame].names
|
||||
if names then
|
||||
if names[name] then
|
||||
return names[name]
|
||||
end
|
||||
if names.prefix then
|
||||
return names.prefix .. name;
|
||||
end
|
||||
end
|
||||
return name;
|
||||
end
|
||||
|
||||
local function setStorage(frame, name, value)
|
||||
lib.windowData[frame].storage[getStorageName(frame, name)] = value
|
||||
end
|
||||
|
||||
local function getStorage(frame, name)
|
||||
return lib.windowData[frame].storage[getStorageName(frame, name)]
|
||||
end
|
||||
|
||||
|
||||
lib.utilFrame:SetScript("OnUpdate", function(this)
|
||||
this:Hide()
|
||||
for frame,_ in pairs(lib.delayedSavePosition) do
|
||||
lib.delayedSavePosition[frame] = nil
|
||||
lib.SavePosition(frame)
|
||||
end
|
||||
end)
|
||||
|
||||
local function queueSavePosition(frame)
|
||||
lib.delayedSavePosition[frame] = true
|
||||
lib.utilFrame:Show()
|
||||
end
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- IMPORTANT APIS
|
||||
---------------------------------------------------------
|
||||
|
||||
mixins["RegisterConfig"]=true
|
||||
function lib.RegisterConfig(frame, storage, names)
|
||||
if not lib.windowData[frame] then
|
||||
lib.windowData[frame] = {}
|
||||
end
|
||||
lib.windowData[frame].names = names
|
||||
lib.windowData[frame].storage = storage
|
||||
|
||||
--[[ debug
|
||||
frame.tx = frame:CreateTexture()
|
||||
frame.tx:SetTexture(0,0,0, 0.4)
|
||||
frame.tx:SetAllPoints(frame)
|
||||
frame.tx:Show()
|
||||
]]
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- POSITIONING AND SCALING
|
||||
---------------------------------------------------------
|
||||
|
||||
local nilParent = {
|
||||
GetWidth = function()
|
||||
return GetScreenWidth() * UIParent:GetScale()
|
||||
end,
|
||||
GetHeight = function()
|
||||
return GetScreenHeight() * UIParent:GetScale()
|
||||
end,
|
||||
GetScale = function()
|
||||
return 1
|
||||
end,
|
||||
}
|
||||
|
||||
mixins["SavePosition"]=true
|
||||
function lib.SavePosition(frame)
|
||||
local parent = frame:GetParent() or nilParent
|
||||
-- No, this won't work very well with frames that aren't parented to nil or UIParent
|
||||
local s = frame:GetScale()
|
||||
local left,top = frame:GetLeft()*s, frame:GetTop()*s
|
||||
local right,bottom = frame:GetRight()*s, frame:GetBottom()*s
|
||||
local pwidth, pheight = parent:GetWidth(), parent:GetHeight()
|
||||
|
||||
local x,y,point;
|
||||
if left < (pwidth-right) and left < abs((left+right)/2 - pwidth/2) then
|
||||
x = left;
|
||||
point="LEFT";
|
||||
elseif (pwidth-right) < abs((left+right)/2 - pwidth/2) then
|
||||
x = right-pwidth;
|
||||
point="RIGHT";
|
||||
else
|
||||
x = (left+right)/2 - pwidth/2;
|
||||
point="";
|
||||
end
|
||||
|
||||
if bottom < (pheight-top) and bottom < abs((bottom+top)/2 - pheight/2) then
|
||||
y = bottom;
|
||||
point="BOTTOM"..point;
|
||||
elseif (pheight-top) < abs((bottom+top)/2 - pheight/2) then
|
||||
y = top-pheight;
|
||||
point="TOP"..point;
|
||||
else
|
||||
y = (bottom+top)/2 - pheight/2;
|
||||
-- point=""..point;
|
||||
end
|
||||
|
||||
if point=="" then
|
||||
point = "CENTER"
|
||||
end
|
||||
|
||||
setStorage(frame, "x", x)
|
||||
setStorage(frame, "y", y)
|
||||
setStorage(frame, "point", point)
|
||||
setStorage(frame, "scale", s)
|
||||
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint(point, frame:GetParent(), point, x/s, y/s);
|
||||
end
|
||||
|
||||
|
||||
mixins["RestorePosition"]=true
|
||||
function lib.RestorePosition(frame)
|
||||
local x = getStorage(frame, "x")
|
||||
local y = getStorage(frame, "y")
|
||||
local point = getStorage(frame, "point")
|
||||
|
||||
local s = getStorage(frame, "scale")
|
||||
if s then
|
||||
(frame.lw11origSetScale or frame.SetScale)(frame,s)
|
||||
else
|
||||
s = frame:GetScale()
|
||||
end
|
||||
|
||||
if not x or not y then -- nothing stored in config yet, smack it in the center
|
||||
x=0; y=0; point="CENTER"
|
||||
end
|
||||
|
||||
x = x/s
|
||||
y = y/s
|
||||
|
||||
frame:ClearAllPoints()
|
||||
if not point and y==0 then -- errr why did i do this check again? must have been a reason, but i can't remember it =/
|
||||
point="CENTER"
|
||||
end
|
||||
|
||||
if not point then -- we have position, but no point, which probably means we're going from data stored by the addon itself before LibWindow was added to it. It was PROBABLY topleft->bottomleft anchored. Most do it that way.
|
||||
frame:SetPoint("TOPLEFT", frame:GetParent(), "BOTTOMLEFT", x, y)
|
||||
-- make it compute a better attachpoint (on next update)
|
||||
queueSavePosition(frame)
|
||||
return
|
||||
end
|
||||
|
||||
frame:SetPoint(point, frame:GetParent(), point, x, y)
|
||||
end
|
||||
|
||||
|
||||
mixins["SetScale"]=true
|
||||
function lib.SetScale(frame, scale)
|
||||
setStorage(frame, "scale", scale);
|
||||
(frame.lw11origSetScale or frame.SetScale)(frame,scale)
|
||||
lib.RestorePosition(frame)
|
||||
end
|
||||
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- DRAG SUPPORT
|
||||
---------------------------------------------------------
|
||||
|
||||
|
||||
function lib.OnDragStart(frame)
|
||||
lib.windowData[frame].isDragging = true
|
||||
frame:StartMoving()
|
||||
end
|
||||
|
||||
|
||||
function lib.OnDragStop(frame)
|
||||
frame:StopMovingOrSizing()
|
||||
lib.SavePosition(frame)
|
||||
lib.windowData[frame].isDragging = false
|
||||
if lib.windowData[frame].altEnable and not IsAltKeyDown() then
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
end
|
||||
|
||||
local function onDragStart(...) return lib.OnDragStart(...) end -- upgradable
|
||||
local function onDragStop(...) return lib.OnDragStop(...) end -- upgradable
|
||||
|
||||
mixins["MakeDraggable"]=true
|
||||
function lib.MakeDraggable(frame)
|
||||
assert(lib.windowData[frame])
|
||||
frame:SetMovable(true)
|
||||
frame:SetScript("OnDragStart", onDragStart)
|
||||
frame:SetScript("OnDragStop", onDragStop)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
end
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- MOUSEWHEEL
|
||||
---------------------------------------------------------
|
||||
|
||||
function lib.OnMouseWheel(frame, dir)
|
||||
local scale = getStorage(frame, "scale")
|
||||
if dir<0 then
|
||||
scale=max(scale*0.9, 0.1)
|
||||
else
|
||||
scale=min(scale/0.9, 3)
|
||||
end
|
||||
lib.SetScale(frame, scale)
|
||||
end
|
||||
|
||||
local function onMouseWheel(...) return lib.OnMouseWheel(...) end -- upgradable
|
||||
|
||||
mixins["EnableMouseWheelScaling"]=true
|
||||
function lib.EnableMouseWheelScaling(frame)
|
||||
frame:SetScript("OnMouseWheel", onMouseWheel)
|
||||
end
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- ENABLEMOUSE-ON-ALT
|
||||
---------------------------------------------------------
|
||||
|
||||
lib.utilFrame:SetScript("OnEvent", function(this, event, key, state)
|
||||
if event=="MODIFIER_STATE_CHANGED" then
|
||||
if key == "LALT" or key == "RALT" then
|
||||
for frame,_ in pairs(lib.altEnabledFrames) do
|
||||
if not lib.windowData[frame].isDragging then -- if it's already dragging, it'll disable mouse on DragStop instead
|
||||
frame:EnableMouse(state == 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
mixins["EnableMouseOnAlt"]=true
|
||||
function lib.EnableMouseOnAlt(frame)
|
||||
assert(lib.windowData[frame])
|
||||
lib.windowData[frame].altEnable = true
|
||||
frame:EnableMouse(not not IsAltKeyDown())
|
||||
if not lib.altEnabledFrames then
|
||||
lib.altEnabledFrames = {}
|
||||
lib.utilFrame:RegisterEvent("MODIFIER_STATE_CHANGED")
|
||||
end
|
||||
lib.altEnabledFrames[frame] = true
|
||||
end
|
||||
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
-- Embed support (into FRAMES, not addons!)
|
||||
---------------------------------------------------------
|
||||
|
||||
function lib:Embed(target)
|
||||
if not target or not target[0] or not target.GetObjectType then
|
||||
error("Usage: LibWindow:Embed(frame)", 1)
|
||||
end
|
||||
target.lw11origSetScale = target.SetScale
|
||||
for name, _ in pairs(mixins) do
|
||||
target[name] = self[name]
|
||||
end
|
||||
lib.embeds[target] = true
|
||||
return target
|
||||
end
|
||||
|
||||
for target, _ in pairs(lib.embeds) do
|
||||
lib:Embed(target)
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
tag v1.1.4
|
||||
ddb0519a000c69ddf3a28c3f9fe2e62bb3fd00c5
|
||||
Tekkub <tekkub@gmail.com>
|
||||
2008-11-06 22:03:04 -0700
|
||||
|
||||
Build 1.1.4
|
||||
|
||||
|
||||
--------------------
|
||||
|
||||
Tekkub:
|
||||
Add pairs and ipairs iters, since we can't use the normal iters on our dataobjs
|
||||
Simplify readme, all docs have been moved into GitHub wiki pages
|
||||
Documentation on how to use LDB data (for display addons)
|
||||
Add StatBlockCore forum link
|
||||
Add link to Fortress thread
|
||||
And rearrange the addon list a bit too
|
||||
Make field lists into nice pretty tables
|
||||
Add list of who is using LDB
|
||||
Always with the typos, I hate my fingers
|
||||
Add tooltiptext and OnTooltipShow to data addon spec
|
||||
Readme rejiggering
|
||||
Add in some documentation on how to push data into LDB
|
||||
Meh, fuck you textile
|
||||
Adding readme
|
||||
Pass current dataobj with attr change callbacks to avoid excessive calls to :GetDataObjectByName
|
||||
Tekkub Stoutwrithe:
|
||||
Make passed dataobj actually work
|
||||
I always forget the 'then'
|
||||
Minor memory optimization
|
||||
- Only hold upvalues to locals in the functions called frequently
|
||||
- Retain the metatable across future lib upgrades (the one in v1 will be lost)
|
||||
Allow caller to pass a pre-populated table to NewDataObject
|
||||
@@ -0,0 +1,90 @@
|
||||
|
||||
assert(LibStub, "LibDataBroker-1.1 requires LibStub")
|
||||
assert(LibStub:GetLibrary("CallbackHandler-1.0", true), "LibDataBroker-1.1 requires CallbackHandler-1.0")
|
||||
|
||||
local lib, oldminor = LibStub:NewLibrary("LibDataBroker-1.1", 4)
|
||||
if not lib then return end
|
||||
oldminor = oldminor or 0
|
||||
|
||||
|
||||
lib.callbacks = lib.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(lib)
|
||||
lib.attributestorage, lib.namestorage, lib.proxystorage = lib.attributestorage or {}, lib.namestorage or {}, lib.proxystorage or {}
|
||||
local attributestorage, namestorage, callbacks = lib.attributestorage, lib.namestorage, lib.callbacks
|
||||
|
||||
if oldminor < 2 then
|
||||
lib.domt = {
|
||||
__metatable = "access denied",
|
||||
__index = function(self, key) return attributestorage[self] and attributestorage[self][key] end,
|
||||
}
|
||||
end
|
||||
|
||||
if oldminor < 3 then
|
||||
lib.domt.__newindex = function(self, key, value)
|
||||
if not attributestorage[self] then attributestorage[self] = {} end
|
||||
if attributestorage[self][key] == value then return end
|
||||
attributestorage[self][key] = value
|
||||
local name = namestorage[self]
|
||||
if not name then return end
|
||||
callbacks:Fire("LibDataBroker_AttributeChanged", name, key, value, self)
|
||||
callbacks:Fire("LibDataBroker_AttributeChanged_"..name, name, key, value, self)
|
||||
callbacks:Fire("LibDataBroker_AttributeChanged_"..name.."_"..key, name, key, value, self)
|
||||
callbacks:Fire("LibDataBroker_AttributeChanged__"..key, name, key, value, self)
|
||||
end
|
||||
end
|
||||
|
||||
if oldminor < 2 then
|
||||
function lib:NewDataObject(name, dataobj)
|
||||
if self.proxystorage[name] then return end
|
||||
|
||||
if dataobj then
|
||||
assert(type(dataobj) == "table", "Invalid dataobj, must be nil or a table")
|
||||
self.attributestorage[dataobj] = {}
|
||||
for i,v in pairs(dataobj) do
|
||||
self.attributestorage[dataobj][i] = v
|
||||
dataobj[i] = nil
|
||||
end
|
||||
end
|
||||
dataobj = setmetatable(dataobj or {}, self.domt)
|
||||
self.proxystorage[name], self.namestorage[dataobj] = dataobj, name
|
||||
self.callbacks:Fire("LibDataBroker_DataObjectCreated", name, dataobj)
|
||||
return dataobj
|
||||
end
|
||||
end
|
||||
|
||||
if oldminor < 1 then
|
||||
function lib:DataObjectIterator()
|
||||
return pairs(self.proxystorage)
|
||||
end
|
||||
|
||||
function lib:GetDataObjectByName(dataobjectname)
|
||||
return self.proxystorage[dataobjectname]
|
||||
end
|
||||
|
||||
function lib:GetNameByDataObject(dataobject)
|
||||
return self.namestorage[dataobject]
|
||||
end
|
||||
end
|
||||
|
||||
if oldminor < 4 then
|
||||
local next = pairs(attributestorage)
|
||||
function lib:pairs(dataobject_or_name)
|
||||
local t = type(dataobject_or_name)
|
||||
assert(t == "string" or t == "table", "Usage: ldb:pairs('dataobjectname') or ldb:pairs(dataobject)")
|
||||
|
||||
local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name
|
||||
assert(attributestorage[dataobj], "Data object not found")
|
||||
|
||||
return next, attributestorage[dataobj], nil
|
||||
end
|
||||
|
||||
local ipairs_iter = ipairs(attributestorage)
|
||||
function lib:ipairs(dataobject_or_name)
|
||||
local t = type(dataobject_or_name)
|
||||
assert(t == "string" or t == "table", "Usage: ldb:ipairs('dataobjectname') or ldb:ipairs(dataobject)")
|
||||
|
||||
local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name
|
||||
assert(attributestorage[dataobj], "Data object not found")
|
||||
|
||||
return ipairs_iter, attributestorage[dataobj], 0
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
LibDataBroker is a small WoW addon library designed to provide a "MVC":http://en.wikipedia.org/wiki/Model-view-controller interface for use in various addons.
|
||||
LDB's primary goal is to "detach" plugins for TitanPanel and FuBar from the display addon.
|
||||
Plugins can provide data into a simple table, and display addons can receive callbacks to refresh their display of this data.
|
||||
LDB also provides a place for addons to register "quicklaunch" functions, removing the need for authors to embed many large libraries to create minimap buttons.
|
||||
Users who do not wish to be "plagued" by these buttons simply do not install an addon to render them.
|
||||
|
||||
Due to it's simple generic design, LDB can be used for any design where you wish to have an addon notified of changes to a table.
|
||||
|
||||
h2. Links
|
||||
|
||||
* "API documentation":http://github.com/tekkub/libdatabroker-1-1/wikis/api
|
||||
* "Data specifications":http://github.com/tekkub/libdatabroker-1-1/wikis/data-specifications
|
||||
* "Addons using LDB":http://github.com/tekkub/libdatabroker-1-1/wikis/addons-using-ldb
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
## Interface: 70200
|
||||
## Title: NickTag-1.0
|
||||
## Notes: Standalone version of the NickTag library.
|
||||
|
||||
NickTag-1.0.xml
|
||||
@@ -0,0 +1,3 @@
|
||||
<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="NickTag-1.0.lua"/>
|
||||
</Ui>
|
||||
Reference in New Issue
Block a user