initial commit
This commit is contained in:
8
External/AccurateTime/!AccurateTime.toc
vendored
Normal file
8
External/AccurateTime/!AccurateTime.toc
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
## Interface: 50400
|
||||
## Title: AccurateTime
|
||||
## Notes: Provides accurate, millisecond-level timing.
|
||||
## Author: Sapu94
|
||||
## Version: @project-version@
|
||||
|
||||
|
||||
AccurateTime.lua
|
||||
142
External/AccurateTime/AccurateTime.lua
vendored
Normal file
142
External/AccurateTime/AccurateTime.lua
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
--[[
|
||||
|
||||
This library is intended to fix the shortfalls of using debugprofilestop() for
|
||||
getting accurate sub-second timing in addons. Specifically, this library aims
|
||||
to prevent any conflicts that may arrise with multiple addons using
|
||||
debugprofilestart and debugprofilestop. While perfect accuracy is not
|
||||
guarenteed due to the potential for an addon to load before this library and
|
||||
use the original debugprofilestart/debugprofilestop functions, this library
|
||||
provides a best-effort means of correcting any issues if this is the case.
|
||||
The best solution is for addons to NOT use debugprofilestart() and to NOT store
|
||||
a local reference to debugprofilestop(), even if they aren't using this library
|
||||
directly.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
AccurateTime is hereby placed in the Public Domain
|
||||
See the wowace page for usage and documentation.
|
||||
Author: Sapu94 (sapu94@gmail.com)
|
||||
Website: http://www.wowace.com/addons/accuratetime/
|
||||
--]]
|
||||
|
||||
local _G = _G
|
||||
local AT_VERSION = 7
|
||||
|
||||
|
||||
-- Check if we're already loaded
|
||||
-- If this is a newer version, remove the old hooks and we'll re-hook
|
||||
if _G.AccurateTime then
|
||||
if _G.AccurateTime.version > AT_VERSION then
|
||||
-- newer (or same) version already loaded - abort
|
||||
return
|
||||
end
|
||||
|
||||
-- undo hook so we can re-hook
|
||||
debugprofilestart = _G.AccurateTime._debugprofilestart
|
||||
debugprofilestop = _G.AccurateTime._debugprofilestop
|
||||
end
|
||||
|
||||
|
||||
-- setup global library reference
|
||||
_G.AccurateTime = {}
|
||||
AccurateTime = _G.AccurateTime
|
||||
AccurateTime.version = AT_VERSION
|
||||
|
||||
-- Store original functions.
|
||||
-- debugprofilestart should never be called, but we'll store it just in case.
|
||||
AccurateTime._debugprofilestop = debugprofilestop
|
||||
AccurateTime._debugprofilestart = debugprofilestart
|
||||
AccurateTime._currentDebugprofilestop = debugprofilestop
|
||||
|
||||
-- other internal variables
|
||||
AccurateTime._errorTime = AccurateTime._errorTime or 0
|
||||
AccurateTime._timers = AccurateTime._timers or {}
|
||||
|
||||
|
||||
-- Gets the current time in milliseconds. Will be directly from the original
|
||||
-- debugprofilestop() with any error we've detected added in. This error would
|
||||
-- come solely from an addon calling the unhooked debugprofilestart().
|
||||
function AccurateTime:GetAbsTime()
|
||||
return AccurateTime._debugprofilestop() + AccurateTime._errorTime
|
||||
end
|
||||
|
||||
-- It is up to the caller to ensure the key they are using is unique.
|
||||
-- Using table reference or description strings is preferable.
|
||||
-- If no key is specified, a unique key will be created and returned.
|
||||
-- If the timer is already running, restart it.
|
||||
-- Usage: local key = AccurateTime:GetTimer([key])
|
||||
function AccurateTime:StartTimer(key)
|
||||
key = key or {}
|
||||
AccurateTime._timers[key] = AccurateTime._timers[key] or AccurateTime:GetAbsTime()
|
||||
return key
|
||||
end
|
||||
|
||||
-- gets the current value of a timer
|
||||
-- Usage: local value = AccurateTime:GetTimer(key[, silent])
|
||||
function AccurateTime:GetTimer(key, silent)
|
||||
assert(key, "No key specified.")
|
||||
if silent and not AccurateTime._timers[key] then return end
|
||||
assert(AccurateTime._timers[key], "No timer currently running for the given key.")
|
||||
return AccurateTime:GetAbsTime() - AccurateTime._timers[key]
|
||||
end
|
||||
|
||||
-- Removes a timer and returns its current value.
|
||||
-- Usage: local value = AccurateTime:StopTimer(key)
|
||||
function AccurateTime:StopTimer(key)
|
||||
local value = AccurateTime:GetTimer(key)
|
||||
AccurateTime._timers[key] = nil
|
||||
return value
|
||||
end
|
||||
|
||||
|
||||
-- apply hooks
|
||||
debugprofilestart = function() error("You should never use debugprofilestart()!", 2) end
|
||||
debugprofilestop = function() return AccurateTime._currentDebugprofilestop() end
|
||||
|
||||
|
||||
-- Create an OnUpdate script to detect and attempt to correct other addons
|
||||
-- which use the original (non-hooked) debugprofilestart(). This should in
|
||||
-- theory never happen, but we'll do a best-effort correction if it does.
|
||||
local function OnUpdate(self)
|
||||
local absTime = AccurateTime:GetAbsTime()
|
||||
if absTime < self.lastUpdateAbsTime then
|
||||
-- debugprofilestart() was called and the back-end timer was reset
|
||||
-- Estimate what the absolute time should be using GetTime() (converted
|
||||
-- to ms) and add it to AccurateTime._errorTime.
|
||||
local realAbsTime = self.lastUpdateAbsTime + (GetTime() - self.lastUpdateTime) * 1000
|
||||
AccurateTime._errorTime = AccurateTime._errorTime + (realAbsTime - absTime)
|
||||
if AccurateTime._errorTime > 0 then
|
||||
-- update AccurateTime._currentDebugprofilestop() to use our version of the function now that there is some error time
|
||||
AccurateTime._currentDebugprofilestop = function() return AccurateTime:GetAbsTime() end
|
||||
end
|
||||
end
|
||||
self.lastUpdateAbsTime = absTime
|
||||
self.lastUpdateTime = GetTime()
|
||||
end
|
||||
if not AccurateTime._frame then
|
||||
-- create frame just once
|
||||
AccurateTime._frame = CreateFrame("Frame")
|
||||
AccurateTime._frame.lastUpdateTime = GetTime()
|
||||
AccurateTime._frame.lastUpdateAbsTime = 0
|
||||
end
|
||||
-- upgrade the frame
|
||||
AccurateTime._frame:SetScript("OnUpdate", OnUpdate)
|
||||
|
||||
--[[
|
||||
function AccurateTimeTest()
|
||||
local start = debugprofilestop()
|
||||
for i=1, 10000000 do
|
||||
end
|
||||
print("loop", debugprofilestop()-start)
|
||||
start = debugprofilestop()
|
||||
for i=1, 10000000 do
|
||||
debugprofilestop()
|
||||
end
|
||||
print("overriden", debugprofilestop()-start)
|
||||
start = debugprofilestop()
|
||||
for i=1, 10000000 do
|
||||
AccurateTime._debugprofilestop()
|
||||
end
|
||||
print("raw", debugprofilestop()-start)
|
||||
end
|
||||
--]]
|
||||
305
External/EmbeddedLibs/AceComm-3.0/AceComm-3.0.lua
vendored
Normal file
305
External/EmbeddedLibs/AceComm-3.0/AceComm-3.0.lua
vendored
Normal file
@@ -0,0 +1,305 @@
|
||||
--- **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 1202 2019-05-15 23:11:22Z 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 CallbackHandler = LibStub("CallbackHandler-1.0")
|
||||
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
|
||||
|
||||
local MAJOR, MINOR = "AceComm-3.0", 12
|
||||
local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceComm then return end
|
||||
|
||||
-- Lua APIs
|
||||
local type, next, pairs, tostring = type, next, pairs, tostring
|
||||
local strsub, strfind = string.sub, string.find
|
||||
local match = string.match
|
||||
local tinsert, tconcat = table.insert, table.concat
|
||||
local error, assert = error, assert
|
||||
|
||||
-- WoW APIs
|
||||
local Ambiguate = Ambiguate
|
||||
|
||||
-- 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
|
||||
if C_ChatInfo then
|
||||
C_ChatInfo.RegisterAddonMessagePrefix(prefix)
|
||||
else
|
||||
RegisterAddonMessagePrefix(prefix)
|
||||
end
|
||||
|
||||
return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
|
||||
end
|
||||
|
||||
local warnedPrefix=false
|
||||
|
||||
--- Send a message over the Addon Channel
|
||||
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
|
||||
-- @param text Data to send, nils (\000) not allowed. Any length.
|
||||
-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
|
||||
-- @param target Destination for some distributions; see SendAddonMessage API
|
||||
-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
|
||||
-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send.
|
||||
-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
|
||||
function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
|
||||
prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery!
|
||||
if not( type(prefix)=="string" and
|
||||
type(text)=="string" and
|
||||
type(distribution)=="string" and
|
||||
(target==nil or type(target)=="string" or type(target)=="number") and
|
||||
(prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
|
||||
) then
|
||||
error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
|
||||
end
|
||||
|
||||
local textlen = #text
|
||||
local maxtextlen = 255 -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327
|
||||
local queueName = prefix..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
|
||||
5
External/EmbeddedLibs/AceComm-3.0/AceComm-3.0.xml
vendored
Normal file
5
External/EmbeddedLibs/AceComm-3.0/AceComm-3.0.xml
vendored
Normal file
@@ -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>
|
||||
534
External/EmbeddedLibs/AceComm-3.0/ChatThrottleLib.lua
vendored
Normal file
534
External/EmbeddedLibs/AceComm-3.0/ChatThrottleLib.lua
vendored
Normal file
@@ -0,0 +1,534 @@
|
||||
--
|
||||
-- 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 = 24
|
||||
|
||||
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
|
||||
if _G.C_ChatInfo then
|
||||
hooksecurefunc(_G.C_ChatInfo, "SendAddonMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendAddonMessage(...)
|
||||
end)
|
||||
else
|
||||
hooksecurefunc("SendAddonMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendAddonMessage(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
self.nBypass = 0
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
|
||||
|
||||
local bMyTraffic = false
|
||||
|
||||
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = tostring(text or ""):len() + tostring(prefix or ""):len();
|
||||
size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- 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 C_ChatInfo or 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
|
||||
if _G.C_ChatInfo then
|
||||
_G.C_ChatInfo.SendAddonMessage(prefix, text, chattype, target)
|
||||
else
|
||||
_G.SendAddonMessage(prefix, text, chattype, target)
|
||||
end
|
||||
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.C_ChatInfo and _G.C_ChatInfo.SendAddonMessage or _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
|
||||
]]
|
||||
|
||||
|
||||
287
External/EmbeddedLibs/AceSerializer-3.0/AceSerializer-3.0.lua
vendored
Normal file
287
External/EmbeddedLibs/AceSerializer-3.0/AceSerializer-3.0.lua
vendored
Normal file
@@ -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 1202 2019-05-15 23:11:22Z 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
|
||||
4
External/EmbeddedLibs/AceSerializer-3.0/AceSerializer-3.0.xml
vendored
Normal file
4
External/EmbeddedLibs/AceSerializer-3.0/AceSerializer-3.0.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceSerializer-3.0.lua"/>
|
||||
</Ui>
|
||||
212
External/EmbeddedLibs/CallbackHandler-1.0/CallbackHandler-1.0.lua
vendored
Normal file
212
External/EmbeddedLibs/CallbackHandler-1.0/CallbackHandler-1.0.lua
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
--[[ $Id: CallbackHandler-1.0.lua 1186 2018-07-21 14:19:18Z nevcairiel $ ]]
|
||||
local MAJOR, MINOR = "CallbackHandler-1.0", 7
|
||||
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 Dispatch(handlers, ...)
|
||||
local index, method = next(handlers)
|
||||
if not method then return end
|
||||
repeat
|
||||
xpcall(method, errorhandler, ...)
|
||||
index, method = next(handlers, index)
|
||||
until not method
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- CallbackHandler:New
|
||||
--
|
||||
-- target - target object to embed public APIs in
|
||||
-- RegisterName - name of the callback registration API, default "RegisterCallback"
|
||||
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
|
||||
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
|
||||
|
||||
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
|
||||
|
||||
RegisterName = RegisterName or "RegisterCallback"
|
||||
UnregisterName = UnregisterName or "UnregisterCallback"
|
||||
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
|
||||
UnregisterAllName = "UnregisterAllCallbacks"
|
||||
end
|
||||
|
||||
-- we declare all objects and exported APIs inside this closure to quickly gain access
|
||||
-- to e.g. function names, the "target" parameter, etc
|
||||
|
||||
|
||||
-- Create the registry object
|
||||
local events = setmetatable({}, meta)
|
||||
local registry = { recurse=0, events=events }
|
||||
|
||||
-- registry:Fire() - fires the given event/message into the registry
|
||||
function registry:Fire(eventname, ...)
|
||||
if not rawget(events, eventname) or not next(events[eventname]) then return end
|
||||
local oldrecurse = registry.recurse
|
||||
registry.recurse = oldrecurse + 1
|
||||
|
||||
Dispatch(events[eventname], eventname, ...)
|
||||
|
||||
registry.recurse = oldrecurse
|
||||
|
||||
if registry.insertQueue and oldrecurse==0 then
|
||||
-- Something in one of our callbacks wanted to register more callbacks; they got queued
|
||||
for 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.
|
||||
|
||||
4
External/EmbeddedLibs/CallbackHandler-1.0/CallbackHandler-1.0.xml
vendored
Normal file
4
External/EmbeddedLibs/CallbackHandler-1.0/CallbackHandler-1.0.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="CallbackHandler-1.0.lua"/>
|
||||
</Ui>
|
||||
470
External/EmbeddedLibs/LibDBIcon-1.0/LibDBIcon-1.0.lua
vendored
Normal file
470
External/EmbeddedLibs/LibDBIcon-1.0/LibDBIcon-1.0.lua
vendored
Normal file
@@ -0,0 +1,470 @@
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- 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 = 43 -- 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.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 {}
|
||||
lib.radius = lib.radius or 5
|
||||
lib.tooltip = lib.tooltip or CreateFrame("GameTooltip", "LibDBIconTooltip", UIParent, "GameTooltipTemplate")
|
||||
local next, Minimap = next, Minimap
|
||||
local isDraggingButton = false
|
||||
|
||||
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 isDraggingButton then return end
|
||||
|
||||
for _, button in next, lib.objects do
|
||||
if button.showOnMouseover then
|
||||
button.fadeOut:Stop()
|
||||
button:SetAlpha(1)
|
||||
end
|
||||
end
|
||||
|
||||
local obj = self.dataObject
|
||||
if obj.OnTooltipShow then
|
||||
lib.tooltip:SetOwner(self, "ANCHOR_NONE")
|
||||
lib.tooltip:SetPoint(getAnchors(self))
|
||||
obj.OnTooltipShow(lib.tooltip)
|
||||
lib.tooltip:Show()
|
||||
elseif obj.OnEnter then
|
||||
obj.OnEnter(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function onLeave(self)
|
||||
lib.tooltip:Hide()
|
||||
|
||||
if not isDraggingButton then
|
||||
for _, button in next, lib.objects do
|
||||
if button.showOnMouseover then
|
||||
button.fadeOut:Play()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local obj = self.dataObject
|
||||
if obj.OnLeave then
|
||||
obj.OnLeave(self)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local onDragStart, 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},
|
||||
}
|
||||
|
||||
local rad, cos, sin, sqrt, max, min = math.rad, math.cos, math.sin, math.sqrt, math.max, math.min
|
||||
function updatePosition(button, position)
|
||||
local angle = rad(position or 225)
|
||||
local x, y, q = cos(angle), 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]
|
||||
local w = (Minimap:GetWidth() / 2) + lib.radius
|
||||
local h = (Minimap:GetHeight() / 2) + lib.radius
|
||||
if quadTable[q] then
|
||||
x, y = x*w, y*h
|
||||
else
|
||||
local diagRadiusW = sqrt(2*(w)^2)-10
|
||||
local diagRadiusH = sqrt(2*(h)^2)-10
|
||||
x = max(-w, min(x*diagRadiusW, w))
|
||||
y = max(-h, min(y*diagRadiusH, h))
|
||||
end
|
||||
button:SetPoint("CENTER", Minimap, "CENTER", x, y)
|
||||
end
|
||||
end
|
||||
|
||||
local function onClick(self, b)
|
||||
if self.dataObject.OnClick then
|
||||
self.dataObject.OnClick(self, b)
|
||||
end
|
||||
end
|
||||
|
||||
local function onMouseDown(self)
|
||||
self.isMouseDown = true
|
||||
self.icon:UpdateCoord()
|
||||
end
|
||||
|
||||
local function onMouseUp(self)
|
||||
self.isMouseDown = false
|
||||
self.icon:UpdateCoord()
|
||||
end
|
||||
|
||||
do
|
||||
local deg, atan2 = math.deg, math.atan2
|
||||
local function onUpdate(self)
|
||||
local mx, my = Minimap:GetCenter()
|
||||
local px, py = GetCursorPosition()
|
||||
local scale = Minimap:GetEffectiveScale()
|
||||
px, py = px / scale, py / scale
|
||||
local pos = 225
|
||||
if self.db then
|
||||
pos = deg(atan2(py - my, px - mx)) % 360
|
||||
self.db.minimapPos = pos
|
||||
else
|
||||
pos = deg(atan2(py - my, px - mx)) % 360
|
||||
self.minimapPos = pos
|
||||
end
|
||||
updatePosition(self, pos)
|
||||
end
|
||||
|
||||
function onDragStart(self)
|
||||
self:LockHighlight()
|
||||
self.isMouseDown = true
|
||||
self.icon:UpdateCoord()
|
||||
self:SetScript("OnUpdate", onUpdate)
|
||||
isDraggingButton = true
|
||||
lib.tooltip:Hide()
|
||||
for _, button in next, lib.objects do
|
||||
if button.showOnMouseover then
|
||||
button.fadeOut:Stop()
|
||||
button:SetAlpha(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function onDragStop(self)
|
||||
self:SetScript("OnUpdate", nil)
|
||||
self.isMouseDown = false
|
||||
self.icon:UpdateCoord()
|
||||
self:UnlockHighlight()
|
||||
isDraggingButton = false
|
||||
for _, button in next, lib.objects do
|
||||
if button.showOnMouseover then
|
||||
button.fadeOut:Play()
|
||||
end
|
||||
end
|
||||
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)
|
||||
|
||||
button.fadeOut = button:CreateAnimationGroup()
|
||||
local animOut = button.fadeOut:CreateAnimation("Alpha")
|
||||
animOut:SetOrder(1)
|
||||
animOut:SetDuration(0.2)
|
||||
animOut:SetFromAlpha(1)
|
||||
animOut:SetToAlpha(0)
|
||||
animOut:SetStartDelay(1)
|
||||
button.fadeOut:SetToFinalAlpha(true)
|
||||
|
||||
lib.objects[name] = button
|
||||
|
||||
if lib.loggedIn then
|
||||
updatePosition(button, db and db.minimapPos)
|
||||
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
|
||||
|
||||
-- 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(f)
|
||||
for _, button in next, lib.objects do
|
||||
updatePosition(button, button.db and button.db.minimapPos)
|
||||
if not button.db or not button.db.hide then
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
end
|
||||
lib.loggedIn = true
|
||||
f:SetScript("OnEvent", 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(DBICON10.. ": Object '".. name .."' is already registered.") end
|
||||
if 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)
|
||||
check(name)
|
||||
local button = lib.objects[name]
|
||||
if button then
|
||||
button:Show()
|
||||
updatePosition(button, button.db and button.db.minimapPos or button.minimapPos)
|
||||
end
|
||||
end
|
||||
|
||||
function lib:IsRegistered(name)
|
||||
return (lib.objects[name] or lib.notCreated[name]) and true or false
|
||||
end
|
||||
|
||||
function lib:Refresh(name, db)
|
||||
check(name)
|
||||
local button = lib.objects[name]
|
||||
if db then
|
||||
button.db = db
|
||||
end
|
||||
updatePosition(button, button.db and button.db.minimapPos or button.minimapPos)
|
||||
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
|
||||
|
||||
do
|
||||
local function OnMinimapEnter()
|
||||
if isDraggingButton then return end
|
||||
for _, button in next, lib.objects do
|
||||
if button.showOnMouseover then
|
||||
button.fadeOut:Stop()
|
||||
button:SetAlpha(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
local function OnMinimapLeave()
|
||||
if isDraggingButton then return end
|
||||
for _, button in next, lib.objects do
|
||||
if button.showOnMouseover then
|
||||
button.fadeOut:Play()
|
||||
end
|
||||
end
|
||||
end
|
||||
Minimap:HookScript("OnEnter", OnMinimapEnter)
|
||||
Minimap:HookScript("OnLeave", OnMinimapLeave)
|
||||
|
||||
function lib:ShowOnEnter(name, value)
|
||||
local button = lib.objects[name]
|
||||
if button then
|
||||
if value then
|
||||
button.showOnMouseover = true
|
||||
button.fadeOut:Stop()
|
||||
button:SetAlpha(0)
|
||||
else
|
||||
button.showOnMouseover = false
|
||||
button.fadeOut:Stop()
|
||||
button:SetAlpha(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function lib:GetButtonList()
|
||||
local t = {}
|
||||
for name in next, lib.objects do
|
||||
t[#t+1] = name
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function lib:SetButtonRadius(radius)
|
||||
if type(radius) == "number" then
|
||||
lib.radius = radius
|
||||
for _, button in next, lib.objects do
|
||||
updatePosition(button, button.db and button.db.minimapPos or button.minimapPos)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function lib:SetButtonToPosition(button, position)
|
||||
updatePosition(lib.objects[button] or button, position)
|
||||
end
|
||||
|
||||
-- Upgrade!
|
||||
for name, button in next, lib.objects do
|
||||
local db = getDatabase(name)
|
||||
if not db or not db.lock then
|
||||
button:SetScript("OnDragStart", onDragStart)
|
||||
button:SetScript("OnDragStop", onDragStop)
|
||||
end
|
||||
button:SetScript("OnEnter", onEnter)
|
||||
button:SetScript("OnLeave", onLeave)
|
||||
button:SetScript("OnClick", onClick)
|
||||
button:SetScript("OnMouseDown", onMouseDown)
|
||||
button:SetScript("OnMouseUp", onMouseUp)
|
||||
|
||||
if not button.fadeOut then -- Upgrade to 39
|
||||
button.fadeOut = button:CreateAnimationGroup()
|
||||
local animOut = button.fadeOut:CreateAnimation("Alpha")
|
||||
animOut:SetOrder(1)
|
||||
animOut:SetDuration(0.2)
|
||||
animOut:SetFromAlpha(1)
|
||||
animOut:SetToAlpha(0)
|
||||
animOut:SetStartDelay(1)
|
||||
button.fadeOut:SetToFinalAlpha(true)
|
||||
end
|
||||
end
|
||||
lib:SetButtonRadius(lib.radius) -- Upgrade to 40
|
||||
7
External/EmbeddedLibs/LibDBIcon-1.0/lib.xml
vendored
Normal file
7
External/EmbeddedLibs/LibDBIcon-1.0/lib.xml
vendored
Normal file
@@ -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>
|
||||
|
||||
90
External/EmbeddedLibs/LibDataBroker-1.1/LibDataBroker-1.1.lua
vendored
Normal file
90
External/EmbeddedLibs/LibDataBroker-1.1/LibDataBroker-1.1.lua
vendored
Normal file
@@ -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
|
||||
13
External/EmbeddedLibs/LibDataBroker-1.1/README.textile
vendored
Normal file
13
External/EmbeddedLibs/LibDataBroker-1.1/README.textile
vendored
Normal file
@@ -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
|
||||
19
External/EmbeddedLibs/LibDeflate/LICENSE.txt
vendored
Normal file
19
External/EmbeddedLibs/LibDeflate/LICENSE.txt
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
zlib License
|
||||
|
||||
(C) 2018-2020 Haoqian He
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
3525
External/EmbeddedLibs/LibDeflate/LibDeflate.lua
vendored
Normal file
3525
External/EmbeddedLibs/LibDeflate/LibDeflate.lua
vendored
Normal file
File diff suppressed because it is too large
Load Diff
11
External/EmbeddedLibs/LibDeflate/LibDeflate.toc
vendored
Normal file
11
External/EmbeddedLibs/LibDeflate/LibDeflate.toc
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
## Interface: 80300
|
||||
## Title: Lib: LibDeflate
|
||||
## Notes: Compressor and decompressor with high compression ratio using DEFLATE/zlib format.
|
||||
## Author: Haoqian He (WoW: Safetyy at Illidan-US (Horde))
|
||||
## Version: @project-version@
|
||||
## X-Website: https://wow.curseforge.com/projects/libdeflate
|
||||
## X-Category: Library
|
||||
## X-License: zlib
|
||||
|
||||
LibStub\LibStub.lua
|
||||
lib.xml
|
||||
4
External/EmbeddedLibs/LibDeflate/lib.xml
vendored
Normal file
4
External/EmbeddedLibs/LibDeflate/lib.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="LibDeflate.lua" />
|
||||
</Ui>
|
||||
17
External/EmbeddedLibs/LibRealmInfo/LICENSE.txt
vendored
Normal file
17
External/EmbeddedLibs/LibRealmInfo/LICENSE.txt
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
Copyright (C) 2014-2018 Phanx <addons@phanx.net>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
1405
External/EmbeddedLibs/LibRealmInfo/LibRealmInfo.lua
vendored
Normal file
1405
External/EmbeddedLibs/LibRealmInfo/LibRealmInfo.lua
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
External/EmbeddedLibs/LibRealmInfo/LibRealmInfo.toc
vendored
Normal file
21
External/EmbeddedLibs/LibRealmInfo/LibRealmInfo.toc
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
## Interface: 70300
|
||||
## Version: @project-version@
|
||||
|
||||
## Title: Lib: Realm Info
|
||||
## Notes: Library to provide information about realms.
|
||||
## Notes-deDE: Bibliothek, um Informationen über Realms zu liefern.
|
||||
## Notes-esES: Biblioteca para proporcionar información sobre reinos.
|
||||
## Notes-esMX: Biblioteca para proporcionar información sobre reinos.
|
||||
|
||||
## Author: Phanx
|
||||
## X-Email: addons@phanx.net
|
||||
## X-License: Zlib
|
||||
## X-Website: https://github.com/phanx-wow/LibRealmInfo
|
||||
## X-Curse-Project-ID: 83224
|
||||
## X-WoWI-ID: 22987
|
||||
|
||||
## LoadOnDemand: 1
|
||||
## OptionalDeps: LibStub
|
||||
LibStub\LibStub.lua
|
||||
|
||||
LibRealmInfo.lua
|
||||
42
External/EmbeddedLibs/LibRealmInfo/README.md
vendored
Normal file
42
External/EmbeddedLibs/LibRealmInfo/README.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
LibRealmInfo
|
||||
===============
|
||||
|
||||
Library to provide information about realms.
|
||||
|
||||
If you only need to know the names of realms connected to the player's current realm, you should just use [GetAutoCompleteRealms](http://wowpedia.org/API_GetAutoCompleteRealms) instead of this library.
|
||||
|
||||
If you only need to know which region the player is currently on, you can use [GetCurrentRegion](http://wowpedia.org/API_GetCurrentRegion), but you should be aware that this function is not always reliable.
|
||||
|
||||
* [Download on Curse](https://wow.curseforge.com/projects/librealminfo)
|
||||
* [Download on WoWInterface](https://www.wowinterface.com/downloads/info22987-LibRealmInfo.html)
|
||||
* [Source Code](https://github.com/phanx-wow/LibRealmInfo)
|
||||
* [Issue Tracker](https://github.com/phanx-wow/LibRealmInfo/issues)
|
||||
* [Documentation](https://github.com/phanx-wow/LibRealmInfo/wiki)
|
||||
|
||||
|
||||
Usage
|
||||
--------
|
||||
|
||||
Available API methods:
|
||||
|
||||
* `:GetCurrentRegion()` - Get the two-letter abbrevation for the region the player is currently connected to; one of US, EU, KR, TW, or CN. Returns nil on PTR and Beta realms.
|
||||
* `:GetRealmInfo(name[, region])` - Get information about a realm by name; if no region is provided, the player's current region will be assumed.
|
||||
* `:GetRealmInfoByID(id)` - Get information about a realm by ID.
|
||||
* `:GetRealmInfoByGUID(guid)` - Get information about the realm the given player GUID belongs to.
|
||||
* `:GetRealmInfoByUnit(unit)` - Get information about the realm the given player unit belongs to.
|
||||
|
||||
All of the above methods return the following values:
|
||||
|
||||
1. `id` - the numeric unique ID of the realm
|
||||
2. `name` - the name of the realm
|
||||
3. `apiName` - the name of the realm without spaces, as seen in chat etc.
|
||||
4. `rules` - one of "PVP", "PVE", "RP" or "RPPVP"
|
||||
5. `locale` - the official language of the realm
|
||||
6. `battlegroup` - the name of the realm's battlegroup
|
||||
7. `region` - one of "US", "EU", "KR", "CN" or "TW"
|
||||
8. `timezone` - for realms in the US region, a string describing the realm's time zone, eg. "PST" or "AEST"
|
||||
9. `connections` - for connected realms, a table listing the IDs of connected realms
|
||||
10. `latinName` - for Russian-language realms, the English name of the realm
|
||||
11. `latinApiName` - for Russian-language realms, the English name of the realm without spaces
|
||||
|
||||
Note that the realm IDs contained in the GUIDs of player characters on connected realms indicate the realm hosting the connected realm group, which may not be the realm that character actually belongs to. Use [GetPlayerInfoByGUID](http://wowpedia.org/API_GetPlayerInfoByGUID) to get the real realm name, or use the :GetRealmInfoByGUID or :GetRealmInfoByUnit methods provided by LibRealmInfo.
|
||||
21
External/EmbeddedLibs/LibSerialize/LICENSE
vendored
Normal file
21
External/EmbeddedLibs/LibSerialize/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Ross Nichols
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1304
External/EmbeddedLibs/LibSerialize/LibSerialize.lua
vendored
Normal file
1304
External/EmbeddedLibs/LibSerialize/LibSerialize.lua
vendored
Normal file
File diff suppressed because it is too large
Load Diff
272
External/EmbeddedLibs/LibSerialize/README.md
vendored
Normal file
272
External/EmbeddedLibs/LibSerialize/README.md
vendored
Normal file
@@ -0,0 +1,272 @@
|
||||
# LibSerialize
|
||||
|
||||
LibSerialize is a Lua library for efficiently serializing/deserializing arbitrary values.
|
||||
It supports serializing nils, numbers, booleans, strings, and tables containing these types.
|
||||
|
||||
It is best paired with [LibDeflate](https://github.com/safeteeWow/LibDeflate), to compress
|
||||
the serialized output and optionally encode it for World of Warcraft addon or chat channels.
|
||||
IMPORTANT: if you decide not to compress the output and plan on transmitting over an addon
|
||||
channel, it still needs to be encoded, but encoding via `LibDeflate:EncodeForWoWAddonChannel()`
|
||||
or `LibCompress:GetAddonEncodeTable()` will likely inflate the size of the serialization
|
||||
by a considerable amount. See the usage below for an alternative.
|
||||
|
||||
Note that serialization and compression are sensitive to the specifics of your data set.
|
||||
You should experiment with the available libraries (LibSerialize, AceSerializer, LibDeflate,
|
||||
LibCompress, etc.) to determine which combination works best for you.
|
||||
|
||||
|
||||
## Usage:
|
||||
|
||||
```lua
|
||||
-- Dependencies: AceAddon-3.0, AceComm-3.0, LibSerialize, LibDeflate
|
||||
MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceComm-3.0")
|
||||
local LibSerialize = LibStub("LibSerialize")
|
||||
local LibDeflate = LibStub("LibDeflate")
|
||||
|
||||
function MyAddon:OnEnable()
|
||||
self:RegisterComm("MyPrefix")
|
||||
end
|
||||
|
||||
-- With compression (recommended):
|
||||
function MyAddon:Transmit(data)
|
||||
local serialized = LibSerialize:Serialize(data)
|
||||
local compressed = LibDeflate:CompressDeflate(serialized)
|
||||
local encoded = LibDeflate:EncodeForWoWAddonChannel(compressed)
|
||||
self:SendCommMessage("MyPrefix", encoded, "WHISPER", UnitName("player"))
|
||||
end
|
||||
|
||||
function MyAddon:OnCommReceived(prefix, payload, distribution, sender)
|
||||
local decoded = LibDeflate:DecodeForWoWAddonChannel(payload)
|
||||
if not decoded then return end
|
||||
local decompressed = LibDeflate:DecompressDeflate(decoded)
|
||||
if not decompressed then return end
|
||||
local success, data = LibSerialize:Deserialize(decompressed)
|
||||
if not success then return end
|
||||
|
||||
-- Handle `data`
|
||||
end
|
||||
|
||||
-- Without compression (custom codec):
|
||||
MyAddon._codec = LibDeflate:CreateCodec("\000", "\255", "")
|
||||
function MyAddon:Transmit(data)
|
||||
local serialized = LibSerialize:Serialize(data)
|
||||
local encoded = self._codec:Encode(serialized)
|
||||
self:SendCommMessage("MyPrefix", encoded, "WHISPER", UnitName("player"))
|
||||
end
|
||||
function MyAddon:OnCommReceived(prefix, payload, distribution, sender)
|
||||
local decoded = self._codec:Decode(payload)
|
||||
if not decoded then return end
|
||||
local success, data = LibSerialize:Deserialize(decoded)
|
||||
if not success then return end
|
||||
|
||||
-- Handle `data`
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
## API:
|
||||
* **`LibSerialize:SerializeEx(opts, ...)`**
|
||||
|
||||
Arguments:
|
||||
* `opts`: options (see below)
|
||||
* `...`: a variable number of serializable values
|
||||
|
||||
Returns:
|
||||
* result: `...` serialized as a string
|
||||
|
||||
* **`LibSerialize:Serialize(...)`**
|
||||
|
||||
Arguments:
|
||||
* `...`: a variable number of serializable values
|
||||
|
||||
Returns:
|
||||
* `result`: `...` serialized as a string
|
||||
|
||||
Calls `SerializeEx(opts, ...)` with the default options (see below)
|
||||
|
||||
* **`LibSerialize:Deserialize(input)`**
|
||||
|
||||
Arguments:
|
||||
* `input`: a string previously returned from `LibSerialize:Serialize()`
|
||||
|
||||
Returns:
|
||||
* `success`: a boolean indicating if deserialization was successful
|
||||
* `...`: the deserialized value(s), or a string containing the encountered Lua error
|
||||
|
||||
* **`LibSerialize:DeserializeValue(input)`**
|
||||
|
||||
Arguments:
|
||||
* `input`: a string previously returned from `LibSerialize:Serialize()`
|
||||
|
||||
Returns:
|
||||
* `...`: the deserialized value(s)
|
||||
|
||||
* **`LibSerialize:IsSerializableType(...)`**
|
||||
|
||||
Arguments:
|
||||
* `...`: a variable number of values
|
||||
|
||||
Returns:
|
||||
* `result`: true if all of the values' types are serializable.
|
||||
|
||||
Note that if you pass a table, it will be considered serializable
|
||||
even if it contains unserializable keys or values. Only the types
|
||||
of the arguments are checked.
|
||||
|
||||
`Serialize()` will raise a Lua error if the input cannot be serialized.
|
||||
This will occur if any of the following exceed 16777215: any string length,
|
||||
any table key count, number of unique strings, number of unique tables.
|
||||
It will also occur by default if any unserializable types are encountered,
|
||||
though that behavior may be disabled (see options).
|
||||
|
||||
`Deserialize()` and `DeserializeValue()` are equivalent, except the latter
|
||||
returns the deserialization result directly and will not catch any Lua
|
||||
errors that may occur when deserializing invalid input.
|
||||
|
||||
Note that none of the serialization/deseriazation methods support reentrancy,
|
||||
and modifying tables during the serialization process is unspecified and
|
||||
should be avoided. Table serialization is multi-phased and assumes a consistent
|
||||
state for the key/value pairs across the phases.
|
||||
|
||||
|
||||
## Options:
|
||||
The following serialization options are supported:
|
||||
* `errorOnUnserializableType`: `boolean` (default true)
|
||||
* `true`: unserializable types will raise a Lua error
|
||||
* `false`: unserializable types will be ignored. If it's a table key or value,
|
||||
the key/value pair will be skipped. If it's one of the arguments to the
|
||||
call to SerializeEx(), it will be replaced with `nil`.
|
||||
* `stable`: `boolean` (default false)
|
||||
* `true`: the resulting string will be stable, even if the input includes
|
||||
maps. This option comes with an extra memory usage and CPU time cost.
|
||||
* `false`: the resulting string will be unstable and will potentially differ
|
||||
between invocations if the input includes maps
|
||||
* `filter`: `function(t, k, v) => boolean` (default nil)
|
||||
* If specified, the function will be called on every key/value pair in every
|
||||
table encountered during serialization. The function must return true for
|
||||
the pair to be serialized. It may be called multiple times on a table for
|
||||
the same key/value pair. See notes on reeentrancy and table modification.
|
||||
|
||||
If an option is unspecified in the table, then its default will be used.
|
||||
This means that if an option `foo` defaults to true, then:
|
||||
* `myOpts.foo = false`: option `foo` is false
|
||||
* `myOpts.foo = nil`: option `foo` is true
|
||||
|
||||
|
||||
## Customizing table serialization:
|
||||
For any serialized table, LibSerialize will check for the presence of a
|
||||
metatable key `__LibSerialize`. It will be interpreted as a table with
|
||||
the following possible keys:
|
||||
* `filter`: `function(t, k, v) => boolean`
|
||||
* If specified, the function will be called on every key/value pair in that
|
||||
table. The function must return true for the pair to be serialized. It may
|
||||
be called multiple times on a table for the same key/value pair. See notes
|
||||
on reeentrancy and table modification. If combined with the `filter` option,
|
||||
both functions must return true.
|
||||
|
||||
|
||||
## Examples:
|
||||
1. `LibSerialize:Serialize()` supports variadic arguments and arbitrary key types,
|
||||
maintaining a consistent internal table identity.
|
||||
```lua
|
||||
local t = { "test", [false] = {} }
|
||||
t[ t[false] ] = "hello"
|
||||
local serialized = LibSerialize:Serialize(t, "extra")
|
||||
local success, tab, str = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab[1] == "test")
|
||||
assert(tab[ tab[false] ] == "hello")
|
||||
assert(str == "extra")
|
||||
```
|
||||
|
||||
2. Normally, unserializable types raise an error when encountered during serialization,
|
||||
but that behavior can be disabled in order to silently ignore them instead.
|
||||
```lua
|
||||
local serialized = LibSerialize:SerializeEx(
|
||||
{ errorOnUnserializableType = false },
|
||||
print, { a = 1, b = print })
|
||||
local success, fn, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(fn == nil)
|
||||
assert(tab.a == 1)
|
||||
assert(tab.b == nil)
|
||||
```
|
||||
|
||||
3. Tables may reference themselves recursively and will still be serialized properly.
|
||||
```lua
|
||||
local t = { a = 1 }
|
||||
t.t = t
|
||||
t[t] = "test"
|
||||
local serialized = LibSerialize:Serialize(t)
|
||||
local success, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab.t.t.t.t.t.t.a == 1)
|
||||
assert(tab[tab.t] == "test")
|
||||
```
|
||||
|
||||
4. You may specify a global filter that applies to all tables encountered during
|
||||
serialization, and to individual tables via their metatable.
|
||||
```lua
|
||||
local t = { a = 1, b = print, c = 3 }
|
||||
local nested = { a = 1, b = print, c = 3 }
|
||||
t.nested = nested
|
||||
setmetatable(nested, { __LibSerialize = {
|
||||
filter = function(t, k, v) return k ~= "c" end
|
||||
}})
|
||||
local opts = {
|
||||
filter = function(t, k, v) return LibSerialize:IsSerializableType(k, v) end
|
||||
}
|
||||
local serialized = LibSerialize:SerializeEx(opts, t)
|
||||
local success, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab.a == 1)
|
||||
assert(tab.b == nil)
|
||||
assert(tab.c == 3)
|
||||
assert(tab.nested.a == 1)
|
||||
assert(tab.nested.b == nil)
|
||||
assert(tab.nested.c == nil)
|
||||
```
|
||||
|
||||
|
||||
## Encoding format:
|
||||
Every object is encoded as a type byte followed by type-dependent payload.
|
||||
|
||||
For numbers, the payload is the number itself, using a number of bytes
|
||||
appropriate for the number. Small numbers can be embedded directly into
|
||||
the type byte, optionally with an additional byte following for more
|
||||
possible values. Negative numbers are encoded as their absolute value,
|
||||
with the type byte indicating that it is negative. Floats are decomposed
|
||||
into their eight bytes, unless serializing as a string is shorter.
|
||||
|
||||
For strings and tables, the length/count is also encoded so that the
|
||||
payload doesn't need a special terminator. Small counts can be embedded
|
||||
directly into the type byte, whereas larger counts are encoded directly
|
||||
following the type byte, before the payload.
|
||||
|
||||
Strings are stored directly, with no transformations. Tables are stored
|
||||
in one of three ways, depending on their layout:
|
||||
* Array-like: all keys are numbers starting from 1 and increasing by 1.
|
||||
Only the table's values are encoded.
|
||||
* Map-like: the table has no array-like keys.
|
||||
The table is encoded as key-value pairs.
|
||||
* Mixed: the table has both map-like and array-like keys.
|
||||
The table is encoded first with the values of the array-like keys,
|
||||
followed by key-value pairs for the map-like keys. For this version,
|
||||
two counts are encoded, one each for the two different portions.
|
||||
|
||||
Strings and tables are also tracked as they are encountered, to detect reuse.
|
||||
If a string or table is reused, it is encoded instead as an index into the
|
||||
tracking table for that type. Strings must be >2 bytes in length to be tracked.
|
||||
Tables may reference themselves recursively.
|
||||
|
||||
|
||||
#### Type byte:
|
||||
The type byte uses the following formats to implement the above:
|
||||
|
||||
* `NNNN NNN1`: a 7 bit non-negative int
|
||||
* `CCCC TT10`: a 2 bit type index and 4 bit count (strlen, #tab, etc.)
|
||||
* Followed by the type-dependent payload
|
||||
* `NNNN S100`: the lower four bits of a 12 bit int and 1 bit for its sign
|
||||
* Followed by a byte for the upper bits
|
||||
* `TTTT T000`: a 5 bit type index
|
||||
* Followed by the type-dependent payload, including count(s) if needed
|
||||
4
External/EmbeddedLibs/LibSerialize/lib.xml
vendored
Normal file
4
External/EmbeddedLibs/LibSerialize/lib.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="LibSerialize.lua" />
|
||||
</Ui>
|
||||
301
External/EmbeddedLibs/LibSerialize/tests.lua
vendored
Normal file
301
External/EmbeddedLibs/LibSerialize/tests.lua
vendored
Normal file
@@ -0,0 +1,301 @@
|
||||
local LibSerialize = require("LibSerialize")
|
||||
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local tostring = tostring
|
||||
local assert = assert
|
||||
local unpack = unpack
|
||||
local pcall = pcall
|
||||
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Examples from the top of LibSerialize.lua
|
||||
--]]---------------------------------------------------------------------------
|
||||
|
||||
do
|
||||
local t = { "test", [false] = {} }
|
||||
t[ t[false] ] = "hello"
|
||||
local serialized = LibSerialize:Serialize(t, "extra")
|
||||
local success, tab, str = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab[1] == "test")
|
||||
assert(tab[ tab[false] ] == "hello")
|
||||
assert(str == "extra")
|
||||
end
|
||||
|
||||
do
|
||||
local serialized = LibSerialize:SerializeEx(
|
||||
{ errorOnUnserializableType = false },
|
||||
print, { a = 1, b = print })
|
||||
local success, fn, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(fn == nil)
|
||||
assert(tab.a == 1)
|
||||
assert(tab.b == nil)
|
||||
end
|
||||
|
||||
do
|
||||
local t = { a = 1 }
|
||||
t.t = t
|
||||
t[t] = "test"
|
||||
local serialized = LibSerialize:Serialize(t)
|
||||
local success, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab.t.t.t.t.t.t.a == 1)
|
||||
assert(tab[tab.t] == "test")
|
||||
end
|
||||
|
||||
do
|
||||
local t = { a = 1, b = print, c = 3 }
|
||||
local nested = { a = 1, b = print, c = 3 }
|
||||
t.nested = nested
|
||||
setmetatable(nested, { __LibSerialize = {
|
||||
filter = function(t, k, v) return k ~= "c" end
|
||||
}})
|
||||
local opts = {
|
||||
filter = function(t, k, v) return LibSerialize:IsSerializableType(k, v) end
|
||||
}
|
||||
local serialized = LibSerialize:SerializeEx(opts, t)
|
||||
local success, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab.a == 1)
|
||||
assert(tab.b == nil)
|
||||
assert(tab.c == 3)
|
||||
assert(tab.nested.a == 1)
|
||||
assert(tab.nested.b == nil)
|
||||
assert(tab.nested.c == nil)
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Test of stable serialization
|
||||
--]]---------------------------------------------------------------------------
|
||||
|
||||
do
|
||||
local t = { a = 1, b = print, c = 3 }
|
||||
local nested = { a = 1, b = print, c = 3 }
|
||||
t.nested = nested
|
||||
setmetatable(nested, { __LibSerialize = {
|
||||
filter = function(t, k, v) return k ~= "c" end
|
||||
}})
|
||||
local opts = {
|
||||
filter = function(t, k, v) return LibSerialize:IsSerializableType(k, v) end,
|
||||
stable = true
|
||||
}
|
||||
local serialized = LibSerialize:SerializeEx(opts, t)
|
||||
local success, tab = LibSerialize:Deserialize(serialized)
|
||||
assert(success)
|
||||
assert(tab.a == 1)
|
||||
assert(tab.b == nil)
|
||||
assert(tab.c == 3)
|
||||
assert(tab.nested.a == 1)
|
||||
assert(tab.nested.b == nil)
|
||||
assert(tab.nested.c == nil)
|
||||
end
|
||||
|
||||
do
|
||||
local t1 = { x = "y", "test", [false] = { 1, 2, 3, a = "b" } }
|
||||
local opts = {
|
||||
stable = true,
|
||||
filter = function(t, k, v) return not tonumber(k) or tonumber(k) < 100 end
|
||||
}
|
||||
local serialized1 = LibSerialize:SerializeEx(opts, t1)
|
||||
local success1, tab1 = LibSerialize:Deserialize(serialized1)
|
||||
assert(success1)
|
||||
assert(tab1[1] == "test")
|
||||
assert(tab1.x == "y")
|
||||
assert(tab1[false][1] == 1)
|
||||
assert(tab1[false][2] == 2)
|
||||
assert(tab1[false][3] == 3)
|
||||
assert(tab1[false].a == "b")
|
||||
|
||||
-- make a copy of the original table, but first insert a bunch of extra keys (which we'll
|
||||
-- filter out) to force the order of the hashes to be different (tested with lua 5.1 and 5.2)
|
||||
local t2 = {}
|
||||
for i = 100, 10000 do
|
||||
t2[tostring(i)] = i
|
||||
end
|
||||
t2.x = "y"
|
||||
t2[1] = "test"
|
||||
t2[false] = { 1, 2, 3, a = "b" }
|
||||
|
||||
-- ensure the iteration order is different
|
||||
local isDifferent = false
|
||||
local k1, k2 = nil, nil
|
||||
while true do
|
||||
k1 = next(t1, k1)
|
||||
-- get the next key from t2 that's not going to be filtered
|
||||
while true do
|
||||
k2 = next(t2, k2)
|
||||
if k2 == nil or not tonumber(k2) or tonumber(k2) < 100 then
|
||||
break
|
||||
end
|
||||
end
|
||||
if k1 == nil and k2 == nil then
|
||||
break
|
||||
end
|
||||
assert(k1 ~= nil and k2 ~= nil)
|
||||
isDifferent = isDifferent or k1 ~= k2
|
||||
end
|
||||
assert(isDifferent)
|
||||
|
||||
-- serialize the copy and ensure the result is the same
|
||||
local serialized2 = LibSerialize:SerializeEx(opts, t2)
|
||||
assert(serialized2 == serialized1)
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Utilities
|
||||
--]]---------------------------------------------------------------------------
|
||||
|
||||
local function tCompare(lhsTable, rhsTable, depth)
|
||||
depth = depth or 1
|
||||
for key, value in pairs(lhsTable) do
|
||||
if type(value) == "table" then
|
||||
local rhsValue = rhsTable[key]
|
||||
if type(rhsValue) ~= "table" then
|
||||
return false
|
||||
end
|
||||
if depth > 1 then
|
||||
if not tCompare(value, rhsValue, depth - 1) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
elseif value ~= rhsTable[key] then
|
||||
-- print("mismatched value: " .. key .. ": " .. tostring(value) .. ", " .. tostring(rhsTable[key]))
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- Check for any keys that are in rhsTable and not lhsTable.
|
||||
for key, value in pairs(rhsTable) do
|
||||
if lhsTable[key] == nil then
|
||||
-- print("mismatched key: " .. key)
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------------------------
|
||||
Test cases for serialization
|
||||
--]]---------------------------------------------------------------------------
|
||||
|
||||
local function fail(value, desc)
|
||||
assert(false, ("Test failed (%s): %s"):format(tostring(value), desc))
|
||||
end
|
||||
|
||||
local function testfilter(t, k, v)
|
||||
return k ~= "banned" and v ~= "banned"
|
||||
end
|
||||
|
||||
local function check(value, bytelen, cmp)
|
||||
local serialized = LibSerialize:SerializeEx({ errorOnUnserializableType = false, filter = testfilter }, value)
|
||||
if #serialized ~= bytelen then
|
||||
fail(value, ("Unexpected serialized length (%d, expected %d)"):format(#serialized, bytelen))
|
||||
end
|
||||
|
||||
local success, deserialized = LibSerialize:Deserialize(serialized)
|
||||
if not success then
|
||||
fail(value, ("Deserialization failed: %s"):format(deserialized))
|
||||
end
|
||||
|
||||
local typ = type(value)
|
||||
if typ == "table" and not tCompare(cmp or value, deserialized) then
|
||||
fail(value, "Non-matching deserialization result")
|
||||
elseif typ ~= "table" and value ~= deserialized then
|
||||
fail(value, ("Non-matching deserialization result: %s"):format(tostring(deserialized)))
|
||||
end
|
||||
end
|
||||
|
||||
-- Format: each test case is { value, bytelen, cmp }. The value will be serialized
|
||||
-- and then deserialized, checking for success and equality, and the length of
|
||||
-- the serialized string will be compared against bytelen. If `cmp` is provided,
|
||||
-- it will be used for comparison against the deserialized result instead of `value`.
|
||||
-- Note that the length always contains one extra byte for the version number.
|
||||
local testCases = {
|
||||
{ nil, 2 },
|
||||
{ true, 2 },
|
||||
{ false, 2 },
|
||||
{ 0, 2 },
|
||||
{ 1, 2 },
|
||||
{ 127, 2 },
|
||||
{ 128, 3 },
|
||||
{ 4095, 3 },
|
||||
{ 4096, 4 },
|
||||
{ 65535, 4 },
|
||||
{ 65536, 5 },
|
||||
{ 16777215, 5 },
|
||||
{ 16777216, 6 },
|
||||
{ 4294967295, 6 },
|
||||
{ 4294967296, 9 },
|
||||
{ 9007199254740992, 9 },
|
||||
{ 1.5, 6 },
|
||||
{ 27.32, 8 },
|
||||
{ 123.45678901235, 10 },
|
||||
{ 148921291233.23, 10 },
|
||||
{ -1, 3 },
|
||||
{ -4095, 3 },
|
||||
{ -4096, 4 },
|
||||
{ -65535, 4 },
|
||||
{ -65536, 5 },
|
||||
{ -16777215, 5 },
|
||||
{ -16777216, 6 },
|
||||
{ -4294967295, 6 },
|
||||
{ -4294967296, 9 },
|
||||
{ -9007199254740992, 9 },
|
||||
{ -1.5, 6 },
|
||||
{ -123.45678901235, 10 },
|
||||
{ -148921291233.23, 10 },
|
||||
{ "", 2 },
|
||||
{ "a", 3 },
|
||||
{ "abcdefghijklmno", 17 },
|
||||
{ "abcdefghijklmnop", 19 },
|
||||
{ ("1234567890"):rep(30), 304 },
|
||||
{ {}, 2 },
|
||||
{ { 1 }, 3 },
|
||||
{ { 1, 2, 3, 4, 5 }, 7 },
|
||||
{ { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, 17 },
|
||||
{ { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, 19 },
|
||||
{ { 1, 2, 3, 4, a = 1, b = 2, [true] = 3, d = 4 }, 17 },
|
||||
{ { 1, 2, 3, 4, 5, a = 1, b = 2, c = true, d = 4 }, 21 },
|
||||
{ { 1, 2, 3, 4, 5, a = 1, b = 2, c = 3, d = 4, e = false }, 24 },
|
||||
{ { a = 1, b = 2, c = 3 }, 11 },
|
||||
{ { "aa", "bb", "aa", "bb" }, 14 },
|
||||
{ { "aa1", "bb2", "aa3", "bb4" }, 18 },
|
||||
{ { "aa1", "bb2", "aa1", "bb2" }, 14 },
|
||||
{ { "aa1", "bb2", "bb2", "aa1" }, 14 },
|
||||
{ { "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmno" }, 24 },
|
||||
{ { "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmno", "abcdefghijklmnop" }, 40 },
|
||||
{ { 1, 2, 3, print, print, 6 }, 7, { 1, 2, 3, nil, nil, 6 } },
|
||||
{ { 1, 2, 3, print, 5, 6 }, 8, { 1, 2, 3, nil, 5, 6 } },
|
||||
{ { a = print, b = 1, c = print }, 5, { b = 1 } },
|
||||
{ { a = print, [print] = "a" }, 2, {} },
|
||||
{ { "banned", 1, 2, 3, banned = 4, test = "banned", a = 1 }, 9, { nil, 1, 2, 3, a = 1 } },
|
||||
}
|
||||
|
||||
do
|
||||
local t = { a = 1, b = 2 }
|
||||
table.insert(testCases, { { t, t, t }, 13 })
|
||||
table.insert(testCases, { { { a = 1, b = 2 }, { a = 1, b = 2 }, { a = 1, b = 2 } }, 23 })
|
||||
end
|
||||
|
||||
for _, testCase in ipairs(testCases) do
|
||||
check(unpack(testCase))
|
||||
end
|
||||
|
||||
-- Since all the above tests assume serialization success, try some failures now.
|
||||
local failCases = {
|
||||
{ print },
|
||||
{ [print] = true },
|
||||
{ [true] = print },
|
||||
print,
|
||||
}
|
||||
|
||||
for _, testCase in ipairs(failCases) do
|
||||
local success = pcall(LibSerialize.Serialize, LibSerialize, testCase)
|
||||
assert(success == false)
|
||||
end
|
||||
|
||||
print("All tests passed!")
|
||||
51
External/EmbeddedLibs/LibStub/LibStub.lua
vendored
Normal file
51
External/EmbeddedLibs/LibStub/LibStub.lua
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
-- $Id: LibStub.lua 103 2014-10-16 03:02:50Z mikk $
|
||||
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/addons/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
|
||||
9
External/EmbeddedLibs/LibStub/LibStub.toc
vendored
Normal file
9
External/EmbeddedLibs/LibStub/LibStub.toc
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
## Interface: 80000
|
||||
## 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
|
||||
41
External/EmbeddedLibs/LibStub/tests/test.lua
vendored
Normal file
41
External/EmbeddedLibs/LibStub/tests/test.lua
vendored
Normal file
@@ -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)
|
||||
27
External/EmbeddedLibs/LibStub/tests/test2.lua
vendored
Normal file
27
External/EmbeddedLibs/LibStub/tests/test2.lua
vendored
Normal file
@@ -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
|
||||
14
External/EmbeddedLibs/LibStub/tests/test3.lua
vendored
Normal file
14
External/EmbeddedLibs/LibStub/tests/test3.lua
vendored
Normal file
@@ -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
|
||||
41
External/EmbeddedLibs/LibStub/tests/test4.lua
vendored
Normal file
41
External/EmbeddedLibs/LibStub/tests/test4.lua
vendored
Normal file
@@ -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")
|
||||
21
External/LibTSMClass/LICENSE.txt
vendored
Normal file
21
External/LibTSMClass/LICENSE.txt
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2019-present TradeSkillMaster LLC (https://tradeskillmaster.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
375
External/LibTSMClass/LibTSMClass.lua
vendored
Normal file
375
External/LibTSMClass/LibTSMClass.lua
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
--- LibTSMClass Library
|
||||
-- Allows for OOP in lua through the implementation of classes. Many features of proper classes are supported including
|
||||
-- inhertiance, polymorphism, and virtual methods.
|
||||
-- @author TradeSkillMaster Team (admin@tradeskillmaster.com)
|
||||
-- @license MIT
|
||||
-- @module LibTSMClass
|
||||
|
||||
local Lib = {}
|
||||
local private = { classInfo = {}, instInfo = {}, constructTbl = nil }
|
||||
-- Set the keys as weak so that instances of classes can be GC'd (classes are never GC'd)
|
||||
setmetatable(private.instInfo, { __mode = "k" })
|
||||
local SPECIAL_PROPERTIES = {
|
||||
__init = true,
|
||||
__tostring = true,
|
||||
__dump = true,
|
||||
__class = true,
|
||||
__isa = true,
|
||||
__super = true,
|
||||
__name = true,
|
||||
__as = true,
|
||||
}
|
||||
local RESERVED_KEYS = {
|
||||
__super = true,
|
||||
__isa = true,
|
||||
__class = true,
|
||||
__name = true,
|
||||
__as = true,
|
||||
}
|
||||
local DEFAULT_INST_FIELDS = {
|
||||
__init = function(self)
|
||||
-- do nothing
|
||||
end,
|
||||
__tostring = function(self)
|
||||
return private.instInfo[self].str
|
||||
end,
|
||||
__dump = function(self)
|
||||
return private.InstDump(self)
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Library Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Lib.DefineClass(name, superclass, ...)
|
||||
if type(name) ~= "string" then
|
||||
error("Invalid class name: "..tostring(name), 2)
|
||||
end
|
||||
local abstract = false
|
||||
for i = 1, select('#', ...) do
|
||||
local modifier = select(i, ...)
|
||||
if modifier == "ABSTRACT" then
|
||||
abstract = true
|
||||
else
|
||||
error("Invalid modifier: "..tostring(modifier), 2)
|
||||
end
|
||||
end
|
||||
|
||||
local class = setmetatable({}, private.CLASS_MT)
|
||||
private.classInfo[class] = {
|
||||
name = name,
|
||||
static = {},
|
||||
superStatic = {},
|
||||
superclass = superclass,
|
||||
abstract = abstract,
|
||||
isStaticReference = false,
|
||||
}
|
||||
while superclass do
|
||||
for key, value in pairs(private.classInfo[superclass].static) do
|
||||
if not private.classInfo[class].superStatic[key] then
|
||||
private.classInfo[class].superStatic[key] = { class = superclass, value = value }
|
||||
end
|
||||
end
|
||||
private.classInfo[superclass].subclassed = true
|
||||
superclass = superclass.__super
|
||||
end
|
||||
return class
|
||||
end
|
||||
|
||||
function Lib.ConstructWithTable(tbl, class, ...)
|
||||
private.constructTbl = tbl
|
||||
local inst = class(...)
|
||||
assert(not private.constructTbl and inst == tbl, "Internal error!")
|
||||
return inst
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Instance Metatable
|
||||
-- ============================================================================
|
||||
|
||||
private.INST_MT = {
|
||||
__newindex = function(self, key, value)
|
||||
if RESERVED_KEYS[key] then
|
||||
error("Can't set reserved key: "..tostring(key), 2)
|
||||
end
|
||||
if private.classInfo[self.__class].static[key] ~= nil then
|
||||
private.classInfo[self.__class].static[key] = value
|
||||
elseif not private.instInfo[self].hasSuperclass then
|
||||
-- we just set this directly on the instance table for better performance
|
||||
rawset(self, key, value)
|
||||
else
|
||||
private.instInfo[self].fields[key] = value
|
||||
end
|
||||
end,
|
||||
__index = function(self, key)
|
||||
-- This method is super optimized since it's used for every class instance access, meaning function calls and
|
||||
-- table lookup is kept to an absolute minimum, at the expense of readability and code reuse.
|
||||
local instInfo = private.instInfo[self]
|
||||
|
||||
-- check if this key is an instance field first, since this is the most common case
|
||||
local res = instInfo.fields[key]
|
||||
if res ~= nil then
|
||||
instInfo.currentClass = nil
|
||||
return res
|
||||
end
|
||||
|
||||
-- check if it's the special __super field or __as method
|
||||
if key == "__super" then
|
||||
if not instInfo.hasSuperclass then
|
||||
error("The class of this instance has no superclass.", 2)
|
||||
end
|
||||
-- The class of the current class method we are in, or nil if we're not in a class method.
|
||||
local methodClass = instInfo.methodClass
|
||||
-- We can only access the superclass within a class method and will use the class which defined that method
|
||||
-- as the base class to jump to the superclass of, regardless of what class the instance actually is.
|
||||
if not methodClass then
|
||||
error("The superclass can only be referenced within a class method.", 2)
|
||||
end
|
||||
return private.InstAs(self, private.classInfo[instInfo.currentClass or methodClass].superclass)
|
||||
elseif key == "__as" then
|
||||
return private.InstAs
|
||||
end
|
||||
|
||||
-- reset the current class since we're not continuing the __super chain
|
||||
local class = instInfo.currentClass or instInfo.class
|
||||
instInfo.currentClass = nil
|
||||
|
||||
-- check if this is a static key
|
||||
local classInfo = private.classInfo[class]
|
||||
res = classInfo.static[key]
|
||||
if res ~= nil then
|
||||
return res
|
||||
end
|
||||
|
||||
-- check if it's a static field in the superclass
|
||||
local superStaticRes = classInfo.superStatic[key]
|
||||
if superStaticRes then
|
||||
res = superStaticRes.value
|
||||
return res
|
||||
end
|
||||
|
||||
-- check if this field has a default value
|
||||
res = DEFAULT_INST_FIELDS[key]
|
||||
if res ~= nil then
|
||||
return res
|
||||
end
|
||||
|
||||
return nil
|
||||
end,
|
||||
__tostring = function(self)
|
||||
return self:__tostring()
|
||||
end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Metatable
|
||||
-- ============================================================================
|
||||
|
||||
private.CLASS_MT = {
|
||||
__newindex = function(self, key, value)
|
||||
local classInfo = private.classInfo[self]
|
||||
if classInfo.subclassed then
|
||||
error("Can't modify classes after they are subclassed", 2)
|
||||
end
|
||||
if classInfo.static[key] then
|
||||
error("Can't modify or override static members", 2)
|
||||
end
|
||||
if RESERVED_KEYS[key] then
|
||||
error("Reserved word: "..tostring(key), 2)
|
||||
end
|
||||
local isMethod = type(value) == "function"
|
||||
if classInfo.isStaticReference then
|
||||
-- we are defining a static class function, not a class method
|
||||
assert(isMethod)
|
||||
classInfo.isStaticReference = false
|
||||
isMethod = false
|
||||
end
|
||||
if isMethod then
|
||||
-- We wrap class methods so that within them, the instance appears to be of the defining class
|
||||
classInfo.static[key] = function(inst, ...)
|
||||
local instInfo = private.instInfo[inst]
|
||||
if not instInfo.isClassLookup[self] then
|
||||
error(format("Attempt to call class method on non-object (%s)!", tostring(inst)), 2)
|
||||
end
|
||||
if not instInfo.hasSuperclass then
|
||||
-- don't need to worry about methodClass so just call the function directly
|
||||
return value(inst, ...)
|
||||
else
|
||||
local prevMethodClass = instInfo.methodClass
|
||||
instInfo.methodClass = self
|
||||
return private.InstMethodReturnHelper(prevMethodClass, instInfo, value(inst, ...))
|
||||
end
|
||||
end
|
||||
else
|
||||
classInfo.static[key] = value
|
||||
end
|
||||
end,
|
||||
__index = function(self, key)
|
||||
local classInfo = private.classInfo[self]
|
||||
assert(not classInfo.isStaticReference)
|
||||
-- check if it's the special __isa method which all classes implicitly have
|
||||
if key == "__isa" then
|
||||
return private.ClassIsA
|
||||
elseif key == "__name" then
|
||||
return classInfo.name
|
||||
elseif key == "__super" then
|
||||
return classInfo.superclass
|
||||
elseif key == "__static" then
|
||||
classInfo.isStaticReference = true
|
||||
return self
|
||||
elseif classInfo.static[key] ~= nil then
|
||||
return classInfo.static[key]
|
||||
end
|
||||
error(format("Invalid static class key (%s)", tostring(key)), 2)
|
||||
end,
|
||||
__tostring = function(self)
|
||||
return "class:"..private.classInfo[self].name
|
||||
end,
|
||||
__call = function(self, ...)
|
||||
if private.classInfo[self].abstract then
|
||||
error("Attempting to instantiate an abstract class!", 2)
|
||||
end
|
||||
-- Create a new instance of this class
|
||||
local inst = private.constructTbl or {}
|
||||
local instStr = strmatch(tostring(inst), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")
|
||||
setmetatable(inst, private.INST_MT)
|
||||
local hasSuperclass = private.classInfo[self].superclass and true or false
|
||||
private.instInfo[inst] = {
|
||||
class = self,
|
||||
fields = {
|
||||
__class = self,
|
||||
__isa = private.InstIsA,
|
||||
},
|
||||
str = private.classInfo[self].name..":"..instStr,
|
||||
isClassLookup = {},
|
||||
hasSuperclass = hasSuperclass,
|
||||
currentClass = nil,
|
||||
}
|
||||
if not hasSuperclass then
|
||||
-- set the static members directly on this object for better performance
|
||||
for key, value in pairs(private.classInfo[self].static) do
|
||||
if not SPECIAL_PROPERTIES[key] then
|
||||
rawset(inst, key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
local c = self
|
||||
while c do
|
||||
private.instInfo[inst].isClassLookup[c] = true
|
||||
c = private.classInfo[c].superclass
|
||||
end
|
||||
if private.constructTbl then
|
||||
-- re-set all the object attributes through the proper metamethod
|
||||
for k, v in pairs(inst) do
|
||||
rawset(inst, k, nil)
|
||||
inst[k] = v
|
||||
end
|
||||
private.constructTbl = nil
|
||||
end
|
||||
if select("#", inst:__init(...)) > 0 then
|
||||
error("__init must not return any values", 2)
|
||||
end
|
||||
return inst
|
||||
end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.InstMethodReturnHelper(class, instInfo, ...)
|
||||
-- reset methodClass now that the function returned
|
||||
instInfo.methodClass = class
|
||||
return ...
|
||||
end
|
||||
|
||||
function private.InstIsA(inst, targetClass)
|
||||
return private.instInfo[inst].isClassLookup[targetClass]
|
||||
end
|
||||
|
||||
function private.InstAs(inst, targetClass)
|
||||
local instInfo = private.instInfo[inst]
|
||||
instInfo.currentClass = targetClass
|
||||
if not targetClass then
|
||||
error(format("Requested class does not exist!"), 2)
|
||||
elseif not instInfo.isClassLookup[targetClass] then
|
||||
error(format("Object is not an instance of the requested class (%s)!", tostring(targetClass)), 2)
|
||||
end
|
||||
-- For classes with no superclass, we don't go through the __index metamethod, so can't use __as
|
||||
if not instInfo.hasSuperclass then
|
||||
error("The class of this instance has no superclass.", 2)
|
||||
end
|
||||
-- We can only access the superclass within a class method.
|
||||
if not instInfo.methodClass then
|
||||
error("The superclass can only be referenced within a class method.", 2)
|
||||
end
|
||||
return inst
|
||||
end
|
||||
|
||||
function private.ClassIsA(class, targetClass)
|
||||
while class do
|
||||
if class == targetClass then return true end
|
||||
class = class.__super
|
||||
end
|
||||
end
|
||||
|
||||
function private.InstDump(inst)
|
||||
local instInfo = private.instInfo[inst]
|
||||
local tbl = instInfo.hasSuperclass and instInfo.fields or inst
|
||||
print(instInfo.str.." {")
|
||||
for key, value in pairs(tbl) do
|
||||
local valueStr = nil
|
||||
if type(value) == "table" then
|
||||
if private.classInfo[value] or private.instInfo[value] then
|
||||
-- this is a class or instance of a class
|
||||
valueStr = tostring(value)
|
||||
elseif next(value) then
|
||||
valueStr = "{ ... }"
|
||||
else
|
||||
valueStr = "{}"
|
||||
end
|
||||
elseif type(value) == "string" or type(value) == "number" or type(value) == "boolean" then
|
||||
valueStr = tostring(value)
|
||||
end
|
||||
if valueStr then
|
||||
print(format(" |cff88ccff%s|r=%s", tostring(key), valueStr))
|
||||
end
|
||||
end
|
||||
print("}")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Initialization Code
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
-- register with LibStub
|
||||
local libStubTbl = LibStub:NewLibrary("LibTSMClass", 1)
|
||||
if libStubTbl then
|
||||
for k, v in pairs(Lib) do
|
||||
libStubTbl[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- register with TSM
|
||||
local addonName, addonTable = ...
|
||||
if addonName == "TradeSkillMaster" then
|
||||
local tsmModuleTbl = addonTable.Init("LibTSMClass")
|
||||
for k, v in pairs(Lib) do
|
||||
tsmModuleTbl[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
243
External/LibTSMClass/README.md
vendored
Normal file
243
External/LibTSMClass/README.md
vendored
Normal file
@@ -0,0 +1,243 @@
|
||||
# LibTSMClass
|
||||
|
||||
The LibTSMClass library allows for writing objected-oriented code in lua! There are many OOP / class libraries out there for lua, but none of them had all the features which we needed for TradeSkillMaster, were easily imported into WoW, and were sufficiently performant.
|
||||
|
||||
## Features
|
||||
|
||||
### Class Definition
|
||||
|
||||
To define a new class, simply use the `LibTSMClass.DefineClass()` method of the library:
|
||||
```lua
|
||||
local MyClass = LibTSMClass.DefineClass("MyClass")
|
||||
```
|
||||
|
||||
This function takes at least one argument, which is the name of the class. This class name is primarily used to make debugging easier, by leveraging it in the `__tostring()` metamethod for both the class and instances of the class.
|
||||
|
||||
### Instantiation
|
||||
|
||||
The class can be called as a function to create an instance of the class.
|
||||
|
||||
```lua
|
||||
local classInst = MyClass()
|
||||
```
|
||||
|
||||
If a table containing existing attributes already exists, it can be converted into an instance of the class via the `LibTSMClass.ConstructWithTable()` method.
|
||||
|
||||
```lua
|
||||
local tbl = { existingValue = 2 }
|
||||
local classInst = LibTSMClass.ConstructWithTable(tbl, MyClass)
|
||||
print(classInst.existingValue) -- prints 2
|
||||
```
|
||||
|
||||
### Static Attributes
|
||||
|
||||
Static fields are allowed on all classes and can be accessed by instances of the class.
|
||||
|
||||
```lua
|
||||
MyClass.staticValue = 31
|
||||
print(MyClass.staticValue) -- prints 31
|
||||
local classInst = MyClass()
|
||||
print(classInst.staticValue) -- prints 31
|
||||
```
|
||||
|
||||
### Method Definition
|
||||
|
||||
Classes define their methods by simply defining the functions on the class object which was previously created.
|
||||
|
||||
```lua
|
||||
function MyClass.SayHi(self)
|
||||
print("Hello from MyClass!")
|
||||
end
|
||||
function MyClass.GetValue(self)
|
||||
return self._value
|
||||
end
|
||||
```
|
||||
|
||||
### Constructor
|
||||
|
||||
The constructor is a special class method with a name of `__init()` and is called whenever a class is instantiated. Any arguments passed when instantiating the class will be passed along to the constructor. Note that the constructor should never return any values.
|
||||
|
||||
```lua
|
||||
function MyClass.__init(self, value)
|
||||
self._value = value
|
||||
end
|
||||
function MyClass.GetValue(self)
|
||||
return self._value
|
||||
end
|
||||
local classInst = MyClass(42)
|
||||
print(classInst:GetValue()) -- prints 42
|
||||
```
|
||||
|
||||
### Inheritance
|
||||
|
||||
Classes can be sub-classed by specifying their base class when defining them. Any methods which are defined on the base class can then be overridden. The subclass is also allowed to access any methods or properties of its base class.
|
||||
|
||||
```lua
|
||||
local MySubClass = LibTSMClass.DefineClass("MySubClass", MyClass)
|
||||
function MySubClass.SayHi(self)
|
||||
print("Hello from MySubClass")
|
||||
end
|
||||
```
|
||||
|
||||
### Accessing the Base Class
|
||||
|
||||
In order to explicitly access a method or attribute of the parent class, the `__super` attribute can be used. This is generally used to call the parent class's implementation of a given method. Note that the `__super` attribute can only be accessed from within a class method. This attribute can be used multiple times to continue to walk up the chain of parent classes for cases where there is more than one level of sub-classing.
|
||||
|
||||
```lua
|
||||
function MySubClass.SayHiAll(self)
|
||||
print("Hello from MySubClass")
|
||||
end
|
||||
function MySubClass.GetValue(self)
|
||||
return self.__super:GetValue() + 2
|
||||
end
|
||||
```
|
||||
|
||||
Note that `__super` may also be used on class objects themselves, including outside of any class methods.
|
||||
|
||||
```lua
|
||||
MyClass.staticValue = 2
|
||||
MySubClass.staticValue = 5
|
||||
print(MySubClass.__super.staticValue) -- prints 2
|
||||
```
|
||||
|
||||
Another mechanism for accessing an explicit parent class from a subclass is by using the special `__as` instance method. This can be especially useful when there is a long chain of inheritance.
|
||||
|
||||
```lua
|
||||
function MySubClass.GetValue(self)
|
||||
return self:__as(MyClass):GetValue() + 2
|
||||
end
|
||||
```
|
||||
|
||||
### Other Useful Attributes
|
||||
|
||||
#### `__tostring()`
|
||||
|
||||
Every class and instance has a special `__tostring()` method which can be used to convert it to a string. This is generally useful for debugging. Classes can override this method in order to provide a custom implementation.
|
||||
|
||||
```lua
|
||||
function MySubClass.__tostring(self)
|
||||
return "MySubClass with a value of "..self._value
|
||||
end
|
||||
local classInst = MyClass(0)
|
||||
print(classInst) -- prints "MyClass:00B8C688"
|
||||
print(MySubClass) -- prints "class:MySubClass"
|
||||
local subClassInst = MySubClass(3)
|
||||
print(subClassInst) -- prints "MySubClass with a value of 3"
|
||||
```
|
||||
|
||||
#### `__name`
|
||||
|
||||
The `__name` attribute is provided on all classes to look up the name of the class.
|
||||
|
||||
```lua
|
||||
print(MyClass.__name) -- prints "MyClass"
|
||||
```
|
||||
|
||||
### `__dump()`
|
||||
|
||||
All instances have a special `__dump()` method which can be used to pretty-print the fields of class for debugging. Similarly to `__tostring()`, the default implementation may be overridden in order to provide a custom implementation.
|
||||
|
||||
```lua
|
||||
local classInst = MyClass(0)
|
||||
classInst:__dump()
|
||||
-- prints [[
|
||||
MyClass:00B8C688 {
|
||||
_value = 0
|
||||
}
|
||||
]]
|
||||
```
|
||||
|
||||
#### `__class`
|
||||
|
||||
The special `__class` field is provided on every instance in order to introspect the class to which the instance belongs.
|
||||
|
||||
```lua
|
||||
local classInst = MyClass(0)
|
||||
print(classInst.__class) -- prints "class:MyClass"
|
||||
```
|
||||
|
||||
#### `__isa()`
|
||||
|
||||
In order to test whether or not an instance belongs to a given class, the `__isa` method is provided on all instances.
|
||||
|
||||
```lua
|
||||
local classInst = MyClass(3)
|
||||
print(classInst:__isa(MyClass)) -- prints true
|
||||
print(classInst:__isa(MySubClass)) -- prints false
|
||||
```
|
||||
|
||||
### Virtual Methods
|
||||
|
||||
One of the most powerful features of LibTSMClass is support for virtual class methods. What this means is that within a base class method, an instance of a class is still treated as its an instance of its actual class, not the base class. This is best demonstrated with an example:
|
||||
|
||||
```lua
|
||||
function MyClass.GetMagicNumber(self)
|
||||
return 99
|
||||
end
|
||||
function MyClass.PrintMagicNumber(self)
|
||||
print(self:GetMagicNumber())
|
||||
end
|
||||
function MySubClass.GetMagicNumber(self)
|
||||
return 88
|
||||
end
|
||||
local subClassInst = MySubClass(0)
|
||||
subClassInst:PrintMagicNumber() -- prints 88
|
||||
```
|
||||
|
||||
### Abstract Classes
|
||||
|
||||
An abstract class is one which can't be directly instantiated. Other than this restriction, abstract classes behave exactly the same as normal classes, including the ability to be sub-classed. This is useful in order to define a common interface which multiple child classes are expected to adhere to. An abstract class is defined by passing an extra argument when defining the class as shown below:
|
||||
```lua
|
||||
local AbstractClass = LibTSMClass.DefineClass("AbstractClass", nil, "ABSTRACT")
|
||||
```
|
||||
|
||||
## Limitations, Gotchas, and Notes
|
||||
|
||||
### Access Restrictions (Private / Protected)
|
||||
|
||||
All instance variables and class methods are publicly accessible. While it's possible for LibTSMClass to be extended to allow for enforcing private / protected access restrictions, it's not currently implemented in order to keep the library as simple and performant as possible. With that being said, a general convention of adding an leading underscore to things which shouldn't be used externally (i.e. private members / methods) is encouraged. If true private members are needed, another alternative is to create scope-limited lookup tables or functions within the file where the class is defined.
|
||||
|
||||
### Classes are Immutable and Should be Atomically Defined
|
||||
|
||||
One gotcha of LibTSMClass is that all methods and static fields of a class must be fully defined before that class is subclassed or instantiated. This means that changing the definition of a class at runtime is not supported, and may lead to undefined behavior. Along the same lines, once a class's methods are defined, they may not be changed later on.
|
||||
|
||||
### Highly-Performant Base Classes
|
||||
|
||||
Inheritance is one of the most powerful uses of OOP, and LibTSMClass fully supports it. However, for cases where performance is of the utmost importance, LibTSMClass is heavily optimized to reduce the overhead of a class which does not subclass anything to be as close to direct table access as possible (without metamethod calls).
|
||||
|
||||
## Example
|
||||
|
||||
A basic example of the library is below:
|
||||
```lua
|
||||
local LibTSMClass = LibStub("LibTSMClass")
|
||||
local MyClass = LibTSMClass.DefineClass("MyClass")
|
||||
|
||||
function MyClass.__init(self, value)
|
||||
self._value = value
|
||||
end
|
||||
|
||||
function MyClass.GetValue(self)
|
||||
return self._value
|
||||
end
|
||||
|
||||
function MyClass.SetValue(self, value)
|
||||
self._value = value
|
||||
end
|
||||
|
||||
local MySubClass = LibTSMClass.DefineClass("MySubClass", MyClass)
|
||||
|
||||
function MySubClass.AddValue(self, value)
|
||||
self:SetValue(self:GetValue() + value)
|
||||
end
|
||||
|
||||
local obj = MySubClass(4)
|
||||
print(obj:GetValue()) -- 4
|
||||
obj:SetValue(10)
|
||||
print(obj:GetValue()) -- 10
|
||||
obj:AddValue(5)
|
||||
print(obj:GetValue()) -- 15
|
||||
```
|
||||
|
||||
## License and Contributes
|
||||
|
||||
LibTSMClass is licensed under the MIT license. See LICENSE.txt for more information. If you would like to contribute to LibTSMClass, opening an issue or submitting a pull request against the [LibTSMClass Bitbucket project](https://bitbucket.org/tradeskillmasteraddon/libtsmclass) is highly encouraged.
|
||||
Reference in New Issue
Block a user