initial commit
This commit is contained in:
90
LibTSM/Util/Analytics.lua
Normal file
90
LibTSM/Util/Analytics.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Analytics = TSM.Init("Util.Analytics")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
events = {},
|
||||
lastEventTime = nil,
|
||||
argsTemp = {},
|
||||
session = time(),
|
||||
sequenceNumber = 1,
|
||||
}
|
||||
local MAX_ANALYTICS_AGE = 14 * 24 * 60 * 60 -- 2 weeks
|
||||
local HIT_TYPE_IS_VALID = {
|
||||
AC = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Analytics.Action(name, ...)
|
||||
private.InsertHit("AC", name, ...)
|
||||
end
|
||||
|
||||
function Analytics.Save(appDB)
|
||||
appDB.analytics = appDB.analytics or {updateTime=0, data={}}
|
||||
if private.lastEventTime then
|
||||
appDB.analytics.updateTime = private.lastEventTime
|
||||
end
|
||||
-- remove any events which are too old
|
||||
for i = #appDB.analytics.data, 1, -1 do
|
||||
local _, _, timeStr = strsplit(",", appDB.analytics.data[i])
|
||||
local eventTime = (tonumber(timeStr) or 0) / 1000
|
||||
if eventTime < time() - MAX_ANALYTICS_AGE then
|
||||
tremove(appDB.analytics.data, i)
|
||||
end
|
||||
end
|
||||
for _, event in ipairs(private.events) do
|
||||
tinsert(appDB.analytics.data, event)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.InsertHit(hitType, ...)
|
||||
assert(HIT_TYPE_IS_VALID[hitType])
|
||||
wipe(private.argsTemp)
|
||||
for i = 1, select("#", ...) do
|
||||
local arg = select(i, ...)
|
||||
local argType = type(arg)
|
||||
if argType == "string" then
|
||||
-- remove non-printable and non-ascii characters
|
||||
arg = gsub(arg, "[^ -~]", "")
|
||||
-- remove characters we don't want in the JSON
|
||||
arg = gsub(arg, "[\\\"]", "")
|
||||
arg = private.AddQuotes(arg)
|
||||
elseif argType == "number" then
|
||||
-- pass
|
||||
elseif argType == "boolean" then
|
||||
arg = tostring(arg)
|
||||
else
|
||||
error("Invalid arg type: "..argType)
|
||||
end
|
||||
tinsert(private.argsTemp, arg)
|
||||
end
|
||||
Log.Info("%s %s", hitType, strjoin(" ", tostringall(...)))
|
||||
hitType = private.AddQuotes(hitType)
|
||||
local version = private.AddQuotes(TSM.GetVersion() or "???")
|
||||
local timeMs = Debug.GetTimeMilliseconds()
|
||||
local jsonStr = strjoin(",", hitType, version, timeMs, private.session, private.sequenceNumber, unpack(private.argsTemp))
|
||||
tinsert(private.events, "["..jsonStr.."]")
|
||||
private.sequenceNumber = private.sequenceNumber + 1
|
||||
private.lastEventTime = time()
|
||||
end
|
||||
|
||||
function private.AddQuotes(str)
|
||||
return "\""..str.."\""
|
||||
end
|
||||
99
LibTSM/Util/CSV.lua
Normal file
99
LibTSM/Util/CSV.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- CSV Functions
|
||||
-- @module CSV
|
||||
|
||||
local _, TSM = ...
|
||||
local CSV = TSM.Init("Util.CSV")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function CSV.EncodeStart(keys)
|
||||
local context = TempTable.Acquire()
|
||||
context.keys = keys
|
||||
context.lines = TempTable.Acquire()
|
||||
context.lineParts = TempTable.Acquire()
|
||||
tinsert(context.lines, table.concat(keys, ","))
|
||||
return context
|
||||
end
|
||||
|
||||
function CSV.EncodeAddRowData(context, data)
|
||||
wipe(context.lineParts)
|
||||
for _, key in ipairs(context.keys) do
|
||||
tinsert(context.lineParts, data[key] or "")
|
||||
end
|
||||
tinsert(context.lines, table.concat(context.lineParts, ","))
|
||||
end
|
||||
|
||||
function CSV.EncodeAddRowDataRaw(context, ...)
|
||||
tinsert(context.lines, strjoin(",", ...))
|
||||
end
|
||||
|
||||
function CSV.EncodeEnd(context)
|
||||
local result = table.concat(context.lines, "\n")
|
||||
TempTable.Release(context.lineParts)
|
||||
TempTable.Release(context.lines)
|
||||
TempTable.Release(context)
|
||||
return result
|
||||
end
|
||||
|
||||
function CSV.Encode(keys, data)
|
||||
local context = CSV.EncodeStart(keys)
|
||||
for _, row in ipairs(data) do
|
||||
CSV.EncodeAddRowData(context, row)
|
||||
end
|
||||
return CSV.EncodeEnd(context)
|
||||
end
|
||||
|
||||
function CSV.DecodeStart(str, fields)
|
||||
local func = gmatch(str, strrep("([^\n,]+),", #fields - 1).."([^\n,]+)(,?[^\n,]*)")
|
||||
if strjoin(",", func()) ~= table.concat(fields, ",").."," then
|
||||
return
|
||||
end
|
||||
local context = TempTable.Acquire()
|
||||
context.func = func
|
||||
context.extraArgPos = #fields + 1
|
||||
context.result = true
|
||||
return context
|
||||
end
|
||||
|
||||
function CSV.DecodeIterator(context)
|
||||
return private.DecodeIteratorHelper, context
|
||||
end
|
||||
|
||||
function CSV.DecodeEnd(context)
|
||||
local result = context.result
|
||||
TempTable.Release(context)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.DecodeIteratorHelper(context)
|
||||
return private.DecodeIteratorHelper2(context, context.func())
|
||||
end
|
||||
|
||||
function private.DecodeIteratorHelper2(context, v1, ...)
|
||||
if not v1 then
|
||||
return
|
||||
end
|
||||
if select(context.extraArgPos, v1, ...) ~= "" then
|
||||
context.result = false
|
||||
return
|
||||
end
|
||||
return v1, ...
|
||||
end
|
||||
209
LibTSM/Util/Color.lua
Normal file
209
LibTSM/Util/Color.lua
Normal file
@@ -0,0 +1,209 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Color Functions.
|
||||
-- @module Color
|
||||
|
||||
local _, TSM = ...
|
||||
local Color = TSM.Init("Util.Color")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local HSLuv = TSM.Include("Util.HSLuv")
|
||||
local private = {
|
||||
context = {},
|
||||
transparent = nil,
|
||||
fullWhite = nil,
|
||||
fullBlack = nil,
|
||||
}
|
||||
local TINT_VALUES = {
|
||||
SELECTED = 15,
|
||||
HOVER = 12,
|
||||
SELECTED_HOVER = 20,
|
||||
DISABLED = -40,
|
||||
}
|
||||
local OPACITY_VALUES = {
|
||||
HIGHLIGHT = 50,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local COLOR_MT = {
|
||||
__index = {
|
||||
GetTint = function(self, tintPct)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
if type(tintPct) == "string" then
|
||||
local sign, tintKey = strmatch(tintPct, "^([%+%-])([A-Z_]+)$")
|
||||
assert(TINT_VALUES[tintKey])
|
||||
tintPct = tonumber(TINT_VALUES[tintKey]) * (sign == "+" and 1 or -1)
|
||||
end
|
||||
assert(type(tintPct) == "number")
|
||||
if tintPct == 0 then
|
||||
return self
|
||||
end
|
||||
if not context.tints[tintPct] then
|
||||
local l = context.l + tintPct
|
||||
l = min(l, 100)
|
||||
assert(private.IsValidValue(l, 100))
|
||||
local r, g, b = HSLuv.ToRGB(context.h, context.s, l)
|
||||
context.tints[tintPct] = private.NewColorHelper(r, g, b, context.a)
|
||||
end
|
||||
return context.tints[tintPct]
|
||||
end,
|
||||
GetOpacity = function(self, opacityPct)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
if type(opacityPct) == "string" then
|
||||
assert(OPACITY_VALUES[opacityPct])
|
||||
opacityPct = tonumber(OPACITY_VALUES[opacityPct])
|
||||
end
|
||||
assert(private.IsValidValue(opacityPct, 100))
|
||||
if opacityPct == 100 then
|
||||
return self
|
||||
end
|
||||
if not context.opacities[opacityPct] then
|
||||
assert(context.a == 255)
|
||||
local a = Math.Round(255 * opacityPct / 100)
|
||||
assert(private.IsValidValue(a, 255))
|
||||
context.opacities[opacityPct] = private.NewColorHelper(context.r, context.g, context.b, a)
|
||||
end
|
||||
return context.opacities[opacityPct]
|
||||
end,
|
||||
GetRGBA = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.r, context.g, context.b, context.a
|
||||
end,
|
||||
GetFractionalRGBA = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.r / 255, context.g / 255, context.b / 255, context.a / 255
|
||||
end,
|
||||
IsLight = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.l >= 50
|
||||
end,
|
||||
GetHex = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.hex
|
||||
end,
|
||||
ColorText = function(self, text)
|
||||
return self:GetTextColorPrefix()..text.."|r"
|
||||
end,
|
||||
GetTextColorPrefix = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return format("|c%02x%02x%02x%02x", context.a, context.r, context.g, context.b)
|
||||
end,
|
||||
Equals = function(self, other)
|
||||
return self:GetHex() == other:GetHex()
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("Color cannot be modified") end,
|
||||
__metatable = false,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
return "Color:"..context.hex
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Color:OnModuleLoad(function()
|
||||
private.transparent = private.NewColorHelper(0, 0, 0, 0)
|
||||
private.fullWhite = private.NewColorHelper(255, 255, 255, 255)
|
||||
private.fullBlack = private.NewColorHelper(0, 0, 0, 255)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new color object from a hex string.
|
||||
-- @tparam string hex The hex string which represents the color (in either "#AARRGGBB" or "#RRGGBB" format)
|
||||
-- @treturn Color The color object
|
||||
function Color.NewFromHex(hex)
|
||||
return private.NewColorHelper(private.HexToRGBA(hex))
|
||||
end
|
||||
|
||||
--- Returns whether or not the argument is a color object.
|
||||
-- @param arg The argument to check
|
||||
-- @treturn boolean Whether or not the argument is a color object.
|
||||
function Color.IsInstance(arg)
|
||||
return type(arg) == "table" and private.context[arg] and true or false
|
||||
end
|
||||
|
||||
--- Gets a predefined fully-transparent color.
|
||||
-- @treturn Color The color object
|
||||
function Color.GetTransparent()
|
||||
return private.transparent
|
||||
end
|
||||
|
||||
--- Gets a predefined fully-opaque white color.
|
||||
-- @treturn Color The color object
|
||||
function Color.GetFullWhite()
|
||||
return private.fullWhite
|
||||
end
|
||||
|
||||
--- Gets a predefined fully-opaque black color.
|
||||
-- @treturn Color The color object
|
||||
function Color.GetFullBlack()
|
||||
return private.fullBlack
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NewColorHelper(r, g, b, a)
|
||||
assert(private.IsValidValue(r, 255) and private.IsValidValue(g, 255) and private.IsValidValue(b, 255) and private.IsValidValue(a, 255))
|
||||
if a == 0 then
|
||||
assert(r == 0 and g == 0 and b == 0, "Invalid color with alpha of 0")
|
||||
end
|
||||
local context = {
|
||||
tints = {},
|
||||
opacities = {},
|
||||
r = r,
|
||||
g = g,
|
||||
b = b,
|
||||
a = a,
|
||||
h = nil,
|
||||
s = nil,
|
||||
l = nil,
|
||||
hex = private.RGBAToHex(r, g, b, a),
|
||||
}
|
||||
context.h, context.s, context.l = HSLuv.FromRGB(r, g, b)
|
||||
context.hex = private.RGBAToHex(r, g, b, a)
|
||||
local color = setmetatable({}, COLOR_MT)
|
||||
private.context[color] = context
|
||||
return color
|
||||
end
|
||||
|
||||
function private.IsValidValue(value, maxValue)
|
||||
return type(value) == "number" and value >= 0 and value <= maxValue and value == floor(value)
|
||||
end
|
||||
|
||||
function private.HexToRGBA(hex)
|
||||
local a, r, g, b = strmatch(strlower(hex), "^#([0-9a-f]?[0-9a-f]?)([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$")
|
||||
return tonumber(r, 16), tonumber(g, 16), tonumber(b, 16), tonumber(a ~= "" and a or "ff", 16)
|
||||
end
|
||||
|
||||
function private.RGBAToHex(r, g, b, a)
|
||||
return format("#%02x%02x%02x%02x", Math.Round(a), Math.Round(r), Math.Round(g), Math.Round(b))
|
||||
end
|
||||
127
LibTSM/Util/Database.lua
Normal file
127
LibTSM/Util/Database.lua
Normal file
@@ -0,0 +1,127 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Database Functions.
|
||||
-- @module Database
|
||||
|
||||
local _, TSM = ...
|
||||
local Database = TSM.Init("Util.Database")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local Schema = TSM.Include("Util.DatabaseClasses.Schema")
|
||||
local Table = TSM.Include("Util.DatabaseClasses.DBTable")
|
||||
local private = {
|
||||
dbByNameLookup = {},
|
||||
infoNameDB = nil,
|
||||
infoFieldDB = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Database:OnModuleLoad(function()
|
||||
-- create our info database tables - don't use :Commit() to create these since that'll insert into these tables
|
||||
private.infoNameDB = Database.NewSchema("DEBUG_INFO_NAME")
|
||||
:AddUniqueStringField("name")
|
||||
:AddIndex("name")
|
||||
:Commit()
|
||||
private.infoFieldDB = Database.NewSchema("DEBUG_INFO_FIELD")
|
||||
:AddStringField("dbName")
|
||||
:AddStringField("field")
|
||||
:AddStringField("type")
|
||||
:AddStringField("attributes")
|
||||
:AddNumberField("order")
|
||||
:AddIndex("dbName")
|
||||
:Commit()
|
||||
|
||||
Table.SetCreateCallback(private.OnTableCreate)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Database.NewSchema(name)
|
||||
return Schema.Get(name)
|
||||
end
|
||||
|
||||
function Database.OtherFieldQueryParam(otherFieldName)
|
||||
return Constants.OTHER_FIELD_QUERY_PARAM, otherFieldName
|
||||
end
|
||||
|
||||
function Database.BoundQueryParam()
|
||||
return Constants.BOUND_QUERY_PARAM
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Debug Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Database.InfoNameIterator()
|
||||
return private.infoNameDB:NewQuery()
|
||||
:Select("name")
|
||||
:OrderBy("name", true)
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function Database.CreateInfoFieldQuery(dbName)
|
||||
return private.infoFieldDB:NewQuery()
|
||||
:Equal("dbName", dbName)
|
||||
end
|
||||
|
||||
function Database.GetNumRows(dbName)
|
||||
return private.dbByNameLookup[dbName]:GetNumRows()
|
||||
end
|
||||
|
||||
function Database.GetNumActiveQueries(dbName)
|
||||
return #private.dbByNameLookup[dbName]._queries
|
||||
end
|
||||
|
||||
function Database.CreateDBQuery(dbName)
|
||||
return private.dbByNameLookup[dbName]:NewQuery()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnTableCreate(tbl, schema)
|
||||
local name = schema:_GetName()
|
||||
assert(not private.dbByNameLookup[name], "A database with this name already exists")
|
||||
private.dbByNameLookup[name] = tbl
|
||||
|
||||
private.infoNameDB:NewRow()
|
||||
:SetField("name", name)
|
||||
:Create()
|
||||
|
||||
for index, fieldName, fieldType, isIndex, isUnique in schema:_FieldIterator() do
|
||||
local fieldAttributes = (isIndex and isUnique and "index,unique") or (isIndex and "index") or (isUnique and "unique") or ""
|
||||
private.infoFieldDB:NewRow()
|
||||
:SetField("dbName", name)
|
||||
:SetField("field", fieldName)
|
||||
:SetField("type", fieldType)
|
||||
:SetField("attributes", fieldAttributes)
|
||||
:SetField("order", index)
|
||||
:Create()
|
||||
end
|
||||
for fieldName in schema:_MultiFieldIndexIterator() do
|
||||
private.infoFieldDB:NewRow()
|
||||
:SetField("dbName", name)
|
||||
:SetField("field", fieldName)
|
||||
:SetField("type", "-")
|
||||
:SetField("attributes", "multi-field index")
|
||||
:SetField("order", -1)
|
||||
:Create()
|
||||
end
|
||||
end
|
||||
12
LibTSM/Util/DatabaseClasses/Constants.lua
Normal file
12
LibTSM/Util/DatabaseClasses/Constants.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Constants = TSM.Init("Util.DatabaseClasses.Constants")
|
||||
Constants.DB_INDEX_FIELD_SEP = "~"
|
||||
Constants.DB_INDEX_VALUE_SEP = "\001"
|
||||
Constants.OTHER_FIELD_QUERY_PARAM = newproxy()
|
||||
Constants.BOUND_QUERY_PARAM = newproxy()
|
||||
1247
LibTSM/Util/DatabaseClasses/DBTable.lua
Normal file
1247
LibTSM/Util/DatabaseClasses/DBTable.lua
Normal file
File diff suppressed because it is too large
Load Diff
1619
LibTSM/Util/DatabaseClasses/Query.lua
Normal file
1619
LibTSM/Util/DatabaseClasses/Query.lua
Normal file
File diff suppressed because it is too large
Load Diff
447
LibTSM/Util/DatabaseClasses/QueryClause.lua
Normal file
447
LibTSM/Util/DatabaseClasses/QueryClause.lua
Normal file
@@ -0,0 +1,447 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryClause = TSM.Init("Util.DatabaseClasses.QueryClause")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local Util = TSM.Include("Util.DatabaseClasses.Util")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local DatabaseQueryClause = LibTSMClass.DefineClass("DatabaseQueryClause")
|
||||
local private = {
|
||||
objectPool = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
QueryClause:OnModuleLoad(function()
|
||||
private.objectPool = ObjectPool.New("DATABASE_QUERY_CLAUSES", DatabaseQueryClause, 1)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryClause.Get(query, parent)
|
||||
local clause = private.objectPool:Get()
|
||||
clause:_Acquire(query, parent)
|
||||
return clause
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Method Methods
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause.__init(self)
|
||||
self._query = nil
|
||||
self._operation = nil
|
||||
self._parent = nil
|
||||
-- comparison
|
||||
self._field = nil
|
||||
self._value = nil
|
||||
self._boundValue = nil
|
||||
self._otherField = nil
|
||||
-- or / and
|
||||
self._subClauses = {}
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._Acquire(self, query, parent)
|
||||
self._query = query
|
||||
self._parent = parent
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._Release(self)
|
||||
self._query = nil
|
||||
self._operation = nil
|
||||
self._parent = nil
|
||||
self._field = nil
|
||||
self._value = nil
|
||||
self._boundValue = nil
|
||||
self._otherField = nil
|
||||
for _, clause in ipairs(self._subClauses) do
|
||||
clause:_Release()
|
||||
end
|
||||
wipe(self._subClauses)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause.Equal(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.NotEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("NOT_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.LessThan(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("LESS", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.LessThanOrEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("LESS_OR_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.GreaterThan(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("GREATER", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.GreaterThanOrEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("GREATER_OR_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Matches(self, field, value)
|
||||
return self:_SetComparisonOperation("MATCHES", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Contains(self, field, value)
|
||||
return self:_SetComparisonOperation("CONTAINS", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.StartsWith(self, field, value)
|
||||
return self:_SetComparisonOperation("STARTS_WITH", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.IsNil(self, field)
|
||||
return self:_SetComparisonOperation("IS_NIL", field)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.IsNotNil(self, field)
|
||||
return self:_SetComparisonOperation("IS_NOT_NIL", field)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Custom(self, func, arg)
|
||||
return self:_SetComparisonOperation("CUSTOM", func, arg)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.HashEqual(self, fields, value)
|
||||
return self:_SetComparisonOperation("HASH_EQUAL", fields, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.InTable(self, field, value)
|
||||
return self:_SetComparisonOperation("IN_TABLE", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.NotInTable(self, field, value)
|
||||
return self:_SetComparisonOperation("NOT_IN_TABLE", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Or(self)
|
||||
return self:_SetSubClauseOperation("OR")
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.And(self)
|
||||
return self:_SetSubClauseOperation("AND")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause._GetParent(self)
|
||||
return self._parent
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._IsTrue(self, row)
|
||||
local value = self._value
|
||||
if value == Constants.BOUND_QUERY_PARAM then
|
||||
value = self._boundValue
|
||||
elseif value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
value = row:GetField(self._otherField)
|
||||
end
|
||||
local operation = self._operation
|
||||
if operation == "EQUAL" then
|
||||
return row[self._field] == value
|
||||
elseif operation == "NOT_EQUAL" then
|
||||
return row[self._field] ~= value
|
||||
elseif operation == "LESS" then
|
||||
return row[self._field] < value
|
||||
elseif operation == "LESS_OR_EQUAL" then
|
||||
return row[self._field] <= value
|
||||
elseif operation == "GREATER" then
|
||||
return row[self._field] > value
|
||||
elseif operation == "GREATER_OR_EQUAL" then
|
||||
return row[self._field] >= value
|
||||
elseif operation == "MATCHES" then
|
||||
return strfind(strlower(row[self._field]), value) and true or false
|
||||
elseif operation == "CONTAINS" then
|
||||
return strfind(strlower(row[self._field]), value, 1, true) and true or false
|
||||
elseif operation == "STARTS_WITH" then
|
||||
return strsub(strlower(row[self._field]), 1, #value) == value
|
||||
elseif operation == "IS_NIL" then
|
||||
return row[self._field] == nil
|
||||
elseif operation == "IS_NOT_NIL" then
|
||||
return row[self._field] ~= nil
|
||||
elseif operation == "CUSTOM" then
|
||||
return self._field(row, value) and true or false
|
||||
elseif operation == "HASH_EQUAL" then
|
||||
return row:CalculateHash(self._field) == value
|
||||
elseif operation == "IN_TABLE" then
|
||||
return value[row[self._field]] ~= nil
|
||||
elseif operation == "NOT_IN_TABLE" then
|
||||
return value[row[self._field]] == nil
|
||||
elseif operation == "OR" then
|
||||
for i = 1, #self._subClauses do
|
||||
if self._subClauses[i]:_IsTrue(row) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
elseif operation == "AND" then
|
||||
for i = 1, #self._subClauses do
|
||||
if not self._subClauses[i]:_IsTrue(row) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
error("Invalid operation: " .. tostring(operation))
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._GetIndexValue(self, indexField)
|
||||
if self._operation == "EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
local result = Util.ToIndexValue(self._boundValue)
|
||||
return result, result
|
||||
else
|
||||
local result = Util.ToIndexValue(self._value)
|
||||
return result, result
|
||||
end
|
||||
elseif self._operation == "LESS_OR_EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return nil, Util.ToIndexValue(self._boundValue)
|
||||
else
|
||||
return nil, Util.ToIndexValue(self._value)
|
||||
end
|
||||
elseif self._operation == "GREATER_OR_EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue), nil
|
||||
else
|
||||
return Util.ToIndexValue(self._value), nil
|
||||
end
|
||||
elseif self._operation == "STARTS_WITH" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
local minValue = nil
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
minValue = Util.ToIndexValue(self._boundValue)
|
||||
else
|
||||
minValue = Util.ToIndexValue(self._value)
|
||||
end
|
||||
-- calculate the max value
|
||||
assert(gsub(minValue, "\255", "") ~= "")
|
||||
local maxValue = nil
|
||||
for i = #minValue, 1, -1 do
|
||||
if strsub(minValue, i, i) ~= "\255" then
|
||||
maxValue = strsub(minValue, 1, i - 1)..strrep("\255", #minValue - i + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
return minValue, maxValue
|
||||
elseif self._operation == "OR" then
|
||||
local numSubClauses = #self._subClauses
|
||||
if numSubClauses == 0 then
|
||||
return
|
||||
end
|
||||
-- all of the subclauses need to support the same index
|
||||
local valueMin, valueMax = self._subClauses[1]:_GetIndexValue(indexField)
|
||||
for i = 2, numSubClauses do
|
||||
local subClauseValueMin, subClauseValueMax = self._subClauses[i]:_GetIndexValue(indexField)
|
||||
if subClauseValueMin ~= valueMin or subClauseValueMax ~= valueMax then
|
||||
return
|
||||
end
|
||||
end
|
||||
return valueMin, valueMax
|
||||
elseif self._operation == "AND" then
|
||||
-- get the most constrained range of index values from the subclauses
|
||||
local valueMin, valueMax = nil, nil
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
local subClauseValueMin, subClauseValueMax = subClause:_GetIndexValue(indexField)
|
||||
if subClauseValueMin ~= nil and (valueMin == nil or subClauseValueMin > valueMin) then
|
||||
valueMin = subClauseValueMin
|
||||
end
|
||||
if subClauseValueMax ~= nil and (valueMax == nil or subClauseValueMax < valueMax) then
|
||||
valueMax = subClauseValueMax
|
||||
end
|
||||
end
|
||||
return valueMin, valueMax
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._GetTrigramIndexValue(self, indexField)
|
||||
if self._operation == "EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return self._boundValue
|
||||
else
|
||||
return self._value
|
||||
end
|
||||
elseif self._operation == "CONTAINS" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return self._boundValue
|
||||
else
|
||||
return self._value
|
||||
end
|
||||
elseif self._operation == "OR" then
|
||||
-- all of the subclauses need to support the same trigram value
|
||||
local value = nil
|
||||
for i = 1, #self._subClauses do
|
||||
local subClause = self._subClauses[i]
|
||||
local subClauseValue = subClause:_GetTrigramIndexValue(indexField)
|
||||
if not subClauseValue then
|
||||
return
|
||||
end
|
||||
if i == 1 then
|
||||
value = subClauseValue
|
||||
elseif subClauseValue ~= value then
|
||||
return
|
||||
end
|
||||
end
|
||||
return value
|
||||
elseif self._operation == "AND" then
|
||||
-- at least one of the subclauses need to support the trigram
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
local value = subClause:_GetTrigramIndexValue(indexField)
|
||||
if value then
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._IsStrictIndex(self, indexField, indexValueMin, indexValueMax)
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return false
|
||||
end
|
||||
if self._operation == "EQUAL" and self._field == indexField and indexValueMin == indexValueMax then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMin
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMin
|
||||
end
|
||||
elseif self._operation == "GREATER_OR_EQUAL" and self._field == indexField then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMin
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMin
|
||||
end
|
||||
elseif self._operation == "LESS_OR_EQUAL" and self._field == indexField then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMax
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMax
|
||||
end
|
||||
elseif self._operation == "OR" and #self._subClauses == 1 then
|
||||
return self._subClauses[1]:_IsStrictIndex(indexField, indexValueMin, indexValueMax)
|
||||
elseif self._operation == "AND" then
|
||||
-- must be strict for all subclauses
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
if not subClause:_IsStrictIndex(indexField, indexValueMin, indexValueMax) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._UsesField(self, field)
|
||||
if field == self._field or self._operation == "CUSTOM" then
|
||||
return true
|
||||
end
|
||||
if self._operation == "OR" or self._operation == "AND" then
|
||||
for i = 1, #self._subClauses do
|
||||
if self._subClauses[i]:_UsesField(field) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._InsertSubClause(self, subClause)
|
||||
assert(self._operation == "OR" or self._operation == "AND")
|
||||
tinsert(self._subClauses, subClause)
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._SetComparisonOperation(self, operation, field, value, otherField)
|
||||
assert(not self._operation)
|
||||
assert(value == Constants.OTHER_FIELD_QUERY_PARAM or not otherField)
|
||||
self._operation = operation
|
||||
self._field = field
|
||||
self._value = value
|
||||
self._otherField = otherField
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._SetSubClauseOperation(self, operation)
|
||||
assert(not self._operation)
|
||||
self._operation = operation
|
||||
assert(#self._subClauses == 0)
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._BindParams(self, ...)
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
self._boundValue = ...
|
||||
self._query:_MarkResultStale()
|
||||
return 1
|
||||
end
|
||||
local valuesUsed = 0
|
||||
for _, clause in ipairs(self._subClauses) do
|
||||
valuesUsed = valuesUsed + clause:_BindParams(select(valuesUsed + 1, ...))
|
||||
end
|
||||
self._query:_MarkResultStale()
|
||||
return valuesUsed
|
||||
end
|
||||
297
LibTSM/Util/DatabaseClasses/QueryResultRow.lua
Normal file
297
LibTSM/Util/DatabaseClasses/QueryResultRow.lua
Normal file
@@ -0,0 +1,297 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryResultRow = TSM.Init("Util.DatabaseClasses.QueryResultRow")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local private = {
|
||||
context = {},
|
||||
objectPool = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local ROW_PROTOTYPE = {
|
||||
_Acquire = function(self, db, query, newRowUUID)
|
||||
local context = private.context[self]
|
||||
context.db = db
|
||||
context.query = query
|
||||
context.isNewRow = newRowUUID and true or false
|
||||
if newRowUUID then
|
||||
context.uuid = newRowUUID
|
||||
end
|
||||
end,
|
||||
|
||||
_Release = function(self)
|
||||
local context = private.context[self]
|
||||
context.db = nil
|
||||
context.query = nil
|
||||
context.isNewRow = nil
|
||||
context.uuid = nil
|
||||
assert(not context.pendingChanges)
|
||||
wipe(self)
|
||||
end,
|
||||
|
||||
Release = function(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end,
|
||||
|
||||
_SetUUID = function(self, uuid)
|
||||
local context = private.context[self]
|
||||
context.uuid = uuid
|
||||
wipe(self)
|
||||
end,
|
||||
|
||||
GetUUID = function(self)
|
||||
local uuid = private.context[self].uuid
|
||||
assert(uuid)
|
||||
return uuid
|
||||
end,
|
||||
|
||||
GetQuery = function(self)
|
||||
local query = private.context[self].query
|
||||
assert(query)
|
||||
return query
|
||||
end,
|
||||
|
||||
GetField = function(self, field, ...)
|
||||
if ... then
|
||||
error("GetField() only supports 1 field")
|
||||
end
|
||||
return self[field]
|
||||
end,
|
||||
|
||||
GetFields = function(self, ...)
|
||||
local numFields = select("#", ...)
|
||||
local field1, field2, field3, field4, field5, field6, field7, field8, field9, field10 = ...
|
||||
if numFields == 0 then
|
||||
return
|
||||
elseif numFields == 1 then
|
||||
return self[field1]
|
||||
elseif numFields == 2 then
|
||||
return self[field1], self[field2]
|
||||
elseif numFields == 3 then
|
||||
return self[field1], self[field2], self[field3]
|
||||
elseif numFields == 4 then
|
||||
return self[field1], self[field2], self[field3], self[field4]
|
||||
elseif numFields == 5 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5]
|
||||
elseif numFields == 6 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6]
|
||||
elseif numFields == 7 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7]
|
||||
elseif numFields == 8 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8]
|
||||
elseif numFields == 9 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8], self[field9]
|
||||
elseif numFields == 10 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8], self[field9], self[field10]
|
||||
else
|
||||
error("GetFields() only supports up to 10 fields")
|
||||
end
|
||||
end,
|
||||
|
||||
CalculateHash = function(self, fields)
|
||||
local hash = nil
|
||||
for _, field in ipairs(fields) do
|
||||
hash = Math.CalculateHash(self[field], hash)
|
||||
end
|
||||
return hash
|
||||
end,
|
||||
|
||||
SetField = function(self, field, value)
|
||||
local context = private.context[self]
|
||||
local isSameValue = not context.isNewRow and value == self[field]
|
||||
if isSameValue and not context.pendingChanges then
|
||||
-- setting to the same value, so ignore this call
|
||||
return self
|
||||
end
|
||||
if context.db:_IsSmartMapField(field) then
|
||||
error(format("Cannot set smart map field (%s)", tostring(field)), 3)
|
||||
end
|
||||
local fieldType = context.db:_GetFieldType(field)
|
||||
if not fieldType then
|
||||
error(format("Field %s doesn't exist", tostring(field)), 3)
|
||||
elseif fieldType ~= type(value) then
|
||||
error(format("Field %s should be a %s, got %s", tostring(field), tostring(fieldType), type(value)), 2)
|
||||
end
|
||||
if isSameValue then
|
||||
-- setting the field to its original value, so clear any pending change
|
||||
context.pendingChanges[field] = nil
|
||||
if not next(context.pendingChanges) then
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
end
|
||||
else
|
||||
context.pendingChanges = context.pendingChanges or TempTable.Acquire()
|
||||
context.pendingChanges[field] = value
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
_CreateHelper = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.isNewRow and context.pendingChanges)
|
||||
|
||||
-- make sure all the fields are set
|
||||
for field in context.db:FieldIterator() do
|
||||
assert(context.pendingChanges[field] ~= nil)
|
||||
end
|
||||
|
||||
-- apply all the pending changes
|
||||
for field, value in pairs(context.pendingChanges) do
|
||||
-- cache this new value
|
||||
rawset(self, field, value)
|
||||
end
|
||||
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
context.isNewRow = nil
|
||||
end,
|
||||
|
||||
Create = function(self)
|
||||
self:_CreateHelper()
|
||||
private.context[self].db:_InsertRow(self)
|
||||
end,
|
||||
|
||||
CreateAndClone = function(self)
|
||||
self:_CreateHelper()
|
||||
local clonedRow = self:Clone()
|
||||
private.context[self].db:_InsertRow(self)
|
||||
return clonedRow
|
||||
end,
|
||||
|
||||
Update = function(self)
|
||||
local context = private.context[self]
|
||||
assert(not context.isNewRow)
|
||||
if not context.pendingChanges then
|
||||
return
|
||||
end
|
||||
|
||||
-- apply all the pending changes
|
||||
local oldValues = TempTable.Acquire()
|
||||
for field, value in pairs(context.pendingChanges) do
|
||||
oldValues[field] = self[field]
|
||||
-- cache this new value
|
||||
rawset(self, field, value)
|
||||
end
|
||||
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
context.db:_UpdateRow(self, oldValues)
|
||||
TempTable.Release(oldValues)
|
||||
return self
|
||||
end,
|
||||
|
||||
CreateOrUpdateAndRelease = function(self)
|
||||
local context = private.context[self]
|
||||
if context.isNewRow then
|
||||
self:Create()
|
||||
else
|
||||
self:Update()
|
||||
self:Release()
|
||||
end
|
||||
end,
|
||||
|
||||
Clone = function(self)
|
||||
local context = private.context[self]
|
||||
assert(not context.isNewRow and not context.pendingChanges)
|
||||
local newRow = QueryResultRow.Get()
|
||||
newRow:_Acquire(context.db)
|
||||
newRow:_SetUUID(context.uuid)
|
||||
return newRow
|
||||
end,
|
||||
}
|
||||
|
||||
local ROW_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
if key == nil then
|
||||
error("Attempt to get nil key")
|
||||
end
|
||||
if ROW_PROTOTYPE[key] then
|
||||
return ROW_PROTOTYPE[key]
|
||||
end
|
||||
-- cache the value
|
||||
local context = private.context[self]
|
||||
if context.isNewRow then
|
||||
error("Getting value on a new row: "..tostring(key))
|
||||
end
|
||||
local result = nil
|
||||
if context.query then
|
||||
-- use the query to lookup the result
|
||||
result = context.query:_GetResultRowData(context.uuid, key)
|
||||
else
|
||||
-- we're not tied to a query so this should be a local DB field
|
||||
if not context.db:_GetFieldType(key) then
|
||||
error("Invalid field: "..tostring(key), 2)
|
||||
end
|
||||
result = context.db:GetRowFieldByUUID(context.uuid, key)
|
||||
end
|
||||
if result ~= nil then
|
||||
rawset(self, key, result)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Table is read-only", 2)
|
||||
end,
|
||||
__eq = function(self, other)
|
||||
local uuid = private.context[self].uuid
|
||||
local uuidOther = private.context[other].uuid
|
||||
return uuid and uuidOther and uuid == uuidOther
|
||||
end,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
return "QueryResultRow:"..strmatch(tostring(context), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")..":"..self:GetUUID()
|
||||
end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
QueryResultRow:OnModuleLoad(function()
|
||||
private.objectPool = ObjectPool.New("DATABASE_QUERY_RESULT_ROWS", private.CreateNew, 2)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryResultRow.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateNew()
|
||||
local row = setmetatable({}, ROW_MT)
|
||||
private.context[row] = {
|
||||
db = nil,
|
||||
query = nil,
|
||||
isNewRow = nil,
|
||||
uuid = nil,
|
||||
}
|
||||
return row
|
||||
end
|
||||
198
LibTSM/Util/DatabaseClasses/Schema.lua
Normal file
198
LibTSM/Util/DatabaseClasses/Schema.lua
Normal file
@@ -0,0 +1,198 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Schema = TSM.Init("Util.DatabaseClasses.Schema")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local DBTable = TSM.Include("Util.DatabaseClasses.DBTable")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local DatabaseSchema = LibTSMClass.DefineClass("DatabaseSchema")
|
||||
local private = {
|
||||
objectPool = nil,
|
||||
}
|
||||
local FIELD_TYPE_IS_VALID = {
|
||||
string = true,
|
||||
number = true,
|
||||
boolean = true,
|
||||
}
|
||||
local MAX_MULTI_FIELD_INDEX_PARTS = 2
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Modules Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Schema.Get(name)
|
||||
if not private.objectPool then
|
||||
private.objectPool = ObjectPool.New("DATABASE_SCHEMAS", DatabaseSchema, 2)
|
||||
end
|
||||
local schema = private.objectPool:Get()
|
||||
schema:_Acquire(name)
|
||||
return schema
|
||||
end
|
||||
|
||||
function Schema.IsClass(obj)
|
||||
return obj:__isa(DatabaseSchema)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Method Methods
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema.__init(self)
|
||||
self._name = nil
|
||||
self._fieldList = {}
|
||||
self._fieldTypeLookup = {}
|
||||
self._isIndex = {}
|
||||
self._isUnique = {}
|
||||
self._smartMapLookup = {}
|
||||
self._smartMapInputLookup = {}
|
||||
self._trigramIndexField = nil
|
||||
end
|
||||
|
||||
function DatabaseSchema._Acquire(self, name)
|
||||
assert(type(name) == "string")
|
||||
self._name = name
|
||||
end
|
||||
|
||||
function DatabaseSchema._Release(self)
|
||||
self._name = nil
|
||||
wipe(self._fieldList)
|
||||
wipe(self._fieldTypeLookup)
|
||||
wipe(self._isIndex)
|
||||
wipe(self._isUnique)
|
||||
wipe(self._smartMapLookup)
|
||||
wipe(self._smartMapInputLookup)
|
||||
self._trigramIndexField = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema.Release(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddStringField(self, fieldName)
|
||||
self:_AddField("string", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddNumberField(self, fieldName)
|
||||
self:_AddField("number", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddBooleanField(self, fieldName)
|
||||
self:_AddField("boolean", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddUniqueStringField(self, fieldName)
|
||||
self:_AddField("string", fieldName, true)
|
||||
self._isUnique[fieldName] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddUniqueNumberField(self, fieldName)
|
||||
self:_AddField("number", fieldName, true)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddSmartMapField(self, fieldName, map, inputFieldName)
|
||||
assert(self._fieldTypeLookup[inputFieldName] == map:GetKeyType())
|
||||
self:_AddField(map:GetValueType(), fieldName)
|
||||
self._smartMapLookup[fieldName] = map
|
||||
self._smartMapInputLookup[fieldName] = inputFieldName
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddIndex(self, ...)
|
||||
local numFields = select("#", ...)
|
||||
assert(numFields > 0)
|
||||
assert(numFields <= MAX_MULTI_FIELD_INDEX_PARTS, "Unsupported number of fields in index")
|
||||
for i = 1, numFields do
|
||||
local fieldName = select(i, ...)
|
||||
assert(self._fieldTypeLookup[fieldName])
|
||||
end
|
||||
self._isIndex[strjoin(Constants.DB_INDEX_FIELD_SEP, ...)] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddTrigramIndex(self, fieldName)
|
||||
assert(not self._trigramIndexField)
|
||||
self._trigramIndexField = fieldName
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.Commit(self)
|
||||
local db = DBTable.Create(self)
|
||||
self:Release()
|
||||
return db
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema._GetName(self)
|
||||
return self._name
|
||||
end
|
||||
|
||||
function DatabaseSchema._AddField(self, fieldType, fieldName, isUnique)
|
||||
assert(FIELD_TYPE_IS_VALID[fieldType])
|
||||
assert(type(fieldName) == "string" and strsub(fieldName, 1, 1) ~= "_" and not strmatch(fieldName, Constants.DB_INDEX_FIELD_SEP))
|
||||
assert(not self._fieldTypeLookup[fieldName])
|
||||
tinsert(self._fieldList, fieldName)
|
||||
self._fieldTypeLookup[fieldName] = fieldType
|
||||
if isUnique then
|
||||
self._isUnique[fieldName] = true
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseSchema._FieldIterator(self)
|
||||
return private.FieldIterator, self, 0
|
||||
end
|
||||
|
||||
function DatabaseSchema._MultiFieldIndexIterator(self)
|
||||
return private.MultiFieldIndexIterator, self, nil
|
||||
end
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.FieldIterator(self, index)
|
||||
index = index + 1
|
||||
if index > #self._fieldList then
|
||||
return
|
||||
end
|
||||
local fieldName = self._fieldList[index]
|
||||
return index, fieldName, self._fieldTypeLookup[fieldName], self._isIndex[fieldName], self._isUnique[fieldName], self._smartMapLookup[fieldName], self._smartMapInputLookup[fieldName]
|
||||
end
|
||||
|
||||
function private.MultiFieldIndexIterator(self, fieldName)
|
||||
while true do
|
||||
fieldName = next(self._isIndex, fieldName)
|
||||
if not fieldName then
|
||||
return
|
||||
end
|
||||
if strmatch(fieldName, Constants.DB_INDEX_FIELD_SEP) then
|
||||
return fieldName
|
||||
end
|
||||
end
|
||||
end
|
||||
31
LibTSM/Util/DatabaseClasses/Util.lua
Normal file
31
LibTSM/Util/DatabaseClasses/Util.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Util = TSM.Init("Util.DatabaseClasses.Util")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Util.ToIndexValue(value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
local valueType = type(value)
|
||||
if valueType == "string" then
|
||||
return strlower(value)
|
||||
elseif valueType == "boolean" then
|
||||
return value and 1 or 0
|
||||
elseif valueType == "number" and Math.IsNan(value) then
|
||||
return nil
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
85
LibTSM/Util/Debug.lua
Normal file
85
LibTSM/Util/Debug.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Debug Functions
|
||||
-- @module Debug
|
||||
|
||||
local _, TSM = ...
|
||||
local Debug = TSM.Init("Util.Debug")
|
||||
local private = {
|
||||
startSystemTimeMs = floor(GetTime() * 1000),
|
||||
startTimeMs = time() * 1000 + (floor(GetTime() * 1000) % 1000),
|
||||
}
|
||||
local ADDON_NAME_SHORTEN_PATTERN = {
|
||||
-- shorten "TradeSkillMaster" to "TSM"
|
||||
[".-lMaster\\"] = "TSM\\",
|
||||
[".-r\\LibTSM"] = "TSM\\LibTSM",
|
||||
}
|
||||
local IGNORED_STACK_LEVEL_MATCHERS = {
|
||||
-- ignore wrapper code from LibTSMClass
|
||||
"LibTSMClass%.lua:",
|
||||
}
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Gets the current time in milliseconds since epoch
|
||||
-- The time returned could be up to a second off absolutely, but relative times are guarenteed to be accurate.
|
||||
-- @treturn number The current time in milliseconds since epoch
|
||||
function Debug.GetTimeMilliseconds()
|
||||
local systemTimeMs = floor(GetTime() * 1000)
|
||||
return private.startTimeMs + (systemTimeMs - private.startSystemTimeMs)
|
||||
end
|
||||
|
||||
--- Gets the location string for the specified stack level
|
||||
-- @tparam number targetLevel The stack level to get the location for
|
||||
-- @tparam[opt] thread thread The thread to get the location for
|
||||
-- @treturn string The location string
|
||||
function Debug.GetStackLevelLocation(targetLevel, thread)
|
||||
targetLevel = targetLevel + 1
|
||||
assert(targetLevel > 0)
|
||||
local level = 1
|
||||
while true do
|
||||
local stackLine = nil
|
||||
if thread then
|
||||
stackLine = debugstack(thread, level, 1, 0)
|
||||
else
|
||||
stackLine = debugstack(level, 1, 0)
|
||||
end
|
||||
if not stackLine or stackLine == "" then
|
||||
return
|
||||
end
|
||||
if TSM.IsWowClassic() or TSM.__IS_TEST_ENV then
|
||||
stackLine = strmatch(stackLine, "^%.*([^:]+:%d+):")
|
||||
else
|
||||
local numSubs = nil
|
||||
stackLine, numSubs = gsub(stackLine, "^%[string \"@([^%.]+%.lua)\"%](:%d+).*$", "%1%2")
|
||||
stackLine = numSubs > 0 and stackLine or nil
|
||||
end
|
||||
if stackLine then
|
||||
local ignored = false
|
||||
for _, matchStr in ipairs(IGNORED_STACK_LEVEL_MATCHERS) do
|
||||
if strmatch(stackLine, matchStr) then
|
||||
ignored = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ignored then
|
||||
targetLevel = targetLevel - 1
|
||||
if targetLevel == 0 then
|
||||
stackLine = gsub(stackLine, "/", "\\")
|
||||
for matchStr, replaceStr in pairs(ADDON_NAME_SHORTEN_PATTERN) do
|
||||
stackLine = gsub(stackLine, matchStr, replaceStr)
|
||||
end
|
||||
return stackLine
|
||||
end
|
||||
end
|
||||
end
|
||||
level = level + 1
|
||||
end
|
||||
end
|
||||
177
LibTSM/Util/Delay.lua
Normal file
177
LibTSM/Util/Delay.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Delay Functions
|
||||
-- @module Delay
|
||||
|
||||
local _, TSM = ...
|
||||
local Delay = TSM.Init("Util.Delay")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local private = {
|
||||
delays = {},
|
||||
frameNumber = 0,
|
||||
frame = nil,
|
||||
}
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
local MIN_TIME_DURATION = 0.0001
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Delay:OnModuleLoad(function()
|
||||
private.frame = CreateFrame("Frame")
|
||||
private.frame:SetScript("OnUpdate", private.ProcessDelays)
|
||||
private.frame:Show()
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Call a callback after a set amount of time.
|
||||
-- Note that the delay may be up to 1 frame time longer than requested.
|
||||
-- @tparam[opt] string label A label for the delay (to allow it to be cancelled)
|
||||
-- @tparam number duration The amount of time to delay for
|
||||
-- @tparam function callback The function called when the delay is finished
|
||||
-- @tparam[opt] number repeatDelay The amount of time to set this delay for once it completes
|
||||
-- @param[opt=nil] context A context value to pass along to the callback (ignored if the delay was previously started)
|
||||
function Delay.AfterTime(label, duration, callback, repeatDelay, context)
|
||||
if type(label) == "number" then
|
||||
-- no label specified
|
||||
assert(not repeatDelay)
|
||||
duration, callback, repeatDelay, context = label, duration, callback, repeatDelay
|
||||
label = nil
|
||||
end
|
||||
assert(type(duration) == "number" and type(callback) == "function" and (not repeatDelay or type(repeatDelay) == "number"))
|
||||
repeatDelay = repeatDelay and max(repeatDelay, MIN_TIME_DURATION) or nil
|
||||
duration = max(duration, MIN_TIME_DURATION)
|
||||
|
||||
if label then
|
||||
for _, delay in ipairs(private.delays) do
|
||||
if delay.label == label then
|
||||
-- delay is already running, so just return
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
label = Debug.GetStackLevelLocation(2)
|
||||
end
|
||||
|
||||
local delayTbl = TempTable.Acquire()
|
||||
delayTbl.endTime = GetTime() + duration
|
||||
delayTbl.callback = callback
|
||||
delayTbl.label = label
|
||||
delayTbl.repeatDelay = repeatDelay
|
||||
delayTbl.context = context
|
||||
tinsert(private.delays, delayTbl)
|
||||
end
|
||||
|
||||
--- Call a callback after a set number of frames.
|
||||
-- Note that the delay may be up to 1 frame time longer than requested.
|
||||
-- @tparam[opt] string label A label for the delay (to allow it to be cancelled)
|
||||
-- @tparam number duration The number of frames to delay for
|
||||
-- @tparam function callback The function called when the delay is finished
|
||||
-- @tparam[opt] number repeatDelay The number of frames to set this delay for once it completes
|
||||
-- @param[opt=nil] context A context value to pass along to the callback (ignored if the delay was previously started)
|
||||
function Delay.AfterFrame(label, duration, callback, repeatDelay, context)
|
||||
if type(label) == "number" then
|
||||
-- no label specified
|
||||
assert(not repeatDelay)
|
||||
duration, callback, repeatDelay, context = label, duration, callback, repeatDelay
|
||||
label = nil
|
||||
end
|
||||
assert(type(duration) == "number" and type(callback) == "function" and (not repeatDelay or type(repeatDelay) == "number"))
|
||||
repeatDelay = repeatDelay and max(repeatDelay, 1) or nil
|
||||
duration = max(duration, 1)
|
||||
|
||||
if label then
|
||||
for _, delay in ipairs(private.delays) do
|
||||
if delay.label == label then
|
||||
-- delay is already running, so just return
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
label = Debug.GetStackLevelLocation(2)
|
||||
assert(label)
|
||||
end
|
||||
|
||||
local delayTbl = TempTable.Acquire()
|
||||
delayTbl.endFrame = private.frameNumber + duration
|
||||
delayTbl.callback = callback
|
||||
delayTbl.label = label
|
||||
delayTbl.repeatDelay = repeatDelay
|
||||
delayTbl.context = context
|
||||
tinsert(private.delays, delayTbl)
|
||||
end
|
||||
|
||||
--- Cancel a delay.
|
||||
-- This works for both time and frame delays.
|
||||
-- @tparam string label The label the delay was created with
|
||||
function Delay.Cancel(label)
|
||||
for i, delay in ipairs(private.delays) do
|
||||
if delay.label == label then
|
||||
TempTable.Release(tremove(private.delays, i))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Main Delay Callback
|
||||
-- ============================================================================
|
||||
|
||||
function private.ProcessDelays()
|
||||
private.frameNumber = private.frameNumber + 1
|
||||
-- the delays can change as we do our callbacks, so keep looping through them until there are no more pending
|
||||
while true do
|
||||
local pendingLabel, pendingCallback, pendingContext = nil, nil, nil
|
||||
for i, delay in ipairs(private.delays) do
|
||||
assert(delay.endFrame or delay.endTime)
|
||||
if delay.endFrame and delay.endFrame <= private.frameNumber then
|
||||
pendingLabel = delay.label
|
||||
pendingCallback = delay.callback
|
||||
pendingContext = delay.context
|
||||
if delay.repeatDelay then
|
||||
delay.endFrame = private.frameNumber + delay.repeatDelay
|
||||
else
|
||||
TempTable.Release(tremove(private.delays, i))
|
||||
end
|
||||
break
|
||||
elseif delay.endTime and delay.endTime <= GetTime() then
|
||||
pendingLabel = delay.label
|
||||
pendingCallback = delay.callback
|
||||
pendingContext = delay.context
|
||||
if delay.repeatDelay then
|
||||
delay.endTime = GetTime() + delay.repeatDelay
|
||||
else
|
||||
TempTable.Release(tremove(private.delays, i))
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if not pendingLabel then
|
||||
-- no more pending delays to process
|
||||
assert(not pendingCallback)
|
||||
break
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
pendingCallback(pendingContext)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Delay callback (%s) took %0.2fms", pendingLabel, timeTaken)
|
||||
end
|
||||
end
|
||||
end
|
||||
112
LibTSM/Util/Event.lua
Normal file
112
LibTSM/Util/Event.lua
Normal file
@@ -0,0 +1,112 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Event Functions
|
||||
-- @module Event
|
||||
|
||||
local _, TSM = ...
|
||||
local Event = TSM.Init("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
registry = {
|
||||
event = {},
|
||||
callback = {},
|
||||
},
|
||||
eventFrame = nil,
|
||||
temp = {},
|
||||
eventQueue = {},
|
||||
processingEvent = false,
|
||||
}
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Registers an event callback.
|
||||
-- @tparam string event The WoW event to register for (i.e. BAG_UPDATE)
|
||||
-- @tparam function callback The function to be called from the event handler
|
||||
function Event.Register(event, callback)
|
||||
assert(type(event) == "string" and event == strupper(event) and type(callback) == "function")
|
||||
-- make sure this event/callback isn't already registered
|
||||
for i = 1, #private.registry.event do
|
||||
assert(private.registry.event[i] ~= event or private.registry.callback[i] ~= callback)
|
||||
end
|
||||
private.eventFrame:RegisterEvent(event)
|
||||
tinsert(private.registry.event, event)
|
||||
tinsert(private.registry.callback, callback)
|
||||
end
|
||||
|
||||
--- Unregisters an event callback.
|
||||
-- @tparam string event The WoW event which the callback was registered for
|
||||
-- @tparam function callback The function which was passed to @{Event.Register} for this event
|
||||
function Event.Unregister(event, callback)
|
||||
assert(type(event) == "string" and event == strupper(event) and type(callback) == "function")
|
||||
local index = nil
|
||||
local shouldUnregister = true
|
||||
for i = 1, #private.registry.event do
|
||||
if private.registry.event[i] == event and private.registry.callback[i] == callback then
|
||||
assert(not index)
|
||||
index = i
|
||||
elseif private.registry.event[i] == event then
|
||||
shouldUnregister = false
|
||||
end
|
||||
end
|
||||
assert(index)
|
||||
tremove(private.registry.event, index)
|
||||
tremove(private.registry.callback, index)
|
||||
if shouldUnregister then
|
||||
private.eventFrame:UnregisterEvent(event)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Frame
|
||||
-- ============================================================================
|
||||
|
||||
function private.ProcessEvent(event, ...)
|
||||
-- NOTE: the registered events may change within the callback, so copy them to a temp table
|
||||
wipe(private.temp)
|
||||
for i = 1, #private.registry.event do
|
||||
if private.registry.event[i] == event then
|
||||
tinsert(private.temp, private.registry.callback[i])
|
||||
end
|
||||
end
|
||||
for _, callback in ipairs(private.temp) do
|
||||
local startTime = debugprofilestop()
|
||||
callback(event, ...)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Event (%s) callback took %.2fms", event, timeTaken)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.EventHandler(_, event, ...)
|
||||
if private.processingEvent then
|
||||
-- we are already in the middle of processing another event, so queue this one up
|
||||
tinsert(private.eventQueue, TempTable.Acquire(event, ...))
|
||||
assert(#private.eventQueue < 50)
|
||||
return
|
||||
end
|
||||
private.processingEvent = true
|
||||
private.ProcessEvent(event, ...)
|
||||
-- process queued events
|
||||
while #private.eventQueue > 0 do
|
||||
local tbl = tremove(private.eventQueue, 1)
|
||||
private.ProcessEvent(TempTable.UnpackAndRelease(tbl))
|
||||
end
|
||||
private.processingEvent = false
|
||||
end
|
||||
|
||||
private.eventFrame = CreateFrame("Frame")
|
||||
private.eventFrame:SetScript("OnEvent", private.EventHandler)
|
||||
private.eventFrame:Show()
|
||||
33
LibTSM/Util/FSM.lua
Normal file
33
LibTSM/Util/FSM.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FSM Functions.
|
||||
-- @module FSM
|
||||
|
||||
local _, TSM = ...
|
||||
local FSM = TSM.Init("Util.FSM")
|
||||
local Machine = TSM.Include("Util.FSMClasses.Machine")
|
||||
local State = TSM.Include("Util.FSMClasses.State")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new FSM.
|
||||
-- @tparam string name The name of the FSM (for debugging purposes)
|
||||
-- @treturn Machine The FSM object
|
||||
function FSM.New(name)
|
||||
return Machine.Create(name)
|
||||
end
|
||||
|
||||
--- Create a new FSM state.
|
||||
-- @tparam string state The name of the state
|
||||
-- @treturn State The State object
|
||||
function FSM.NewState(state)
|
||||
return State.Create(state)
|
||||
end
|
||||
182
LibTSM/Util/FSMClasses/Machine.lua
Normal file
182
LibTSM/Util/FSMClasses/Machine.lua
Normal file
@@ -0,0 +1,182 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FSMMachine Class.
|
||||
-- This class allows implementing event-driving finite state machines.
|
||||
-- @classmod FSMMachine
|
||||
|
||||
local _, TSM = ...
|
||||
local Machine = TSM.Init("Util.FSMClasses.Machine")
|
||||
local State = TSM.Include("Util.FSMClasses.State")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local FSMMachine = LibTSMClass.DefineClass("FSMMachine")
|
||||
local private = {
|
||||
eventTransitionHandlerCache = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Machine.Create(name)
|
||||
return FSMMachine(name)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMMachine.__init(self, name)
|
||||
self._name = name
|
||||
self._currentState = nil
|
||||
self._context = nil
|
||||
self._loggingDisabledCount = 0
|
||||
self._stateObjs = {}
|
||||
self._defaultEvents = {}
|
||||
self._handlingEvent = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
--- Add an FSM state.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam FSMState stateObj The FSM state object to add
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.AddState(self, stateObj)
|
||||
assert(State.IsInstance(stateObj))
|
||||
local name = stateObj:_GetName()
|
||||
assert(not self._stateObjs[name], "state already exists")
|
||||
self._stateObjs[stateObj:_GetName()] = stateObj
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a default event handler.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam string event The event name
|
||||
-- @tparam function handler The default event handler
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.AddDefaultEvent(self, event, handler)
|
||||
assert(not self._defaultEvents[event], "event already exists")
|
||||
self._defaultEvents[event] = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a simple default event-based transition.
|
||||
-- @tparam FSMMachine self The FSMMachine object
|
||||
-- @tparam string event The event name
|
||||
-- @tparam string toState The state to transition to
|
||||
-- @treturn FSMMachine The FSMMachine object
|
||||
function FSMMachine.AddDefaultEventTransition(self, event, toState)
|
||||
if not private.eventTransitionHandlerCache[toState] then
|
||||
private.eventTransitionHandlerCache[toState] = function(context, ...)
|
||||
return toState, ...
|
||||
end
|
||||
end
|
||||
return self:AddDefaultEvent(event, private.eventTransitionHandlerCache[toState])
|
||||
end
|
||||
|
||||
--- Initialize the FSM.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam string initialState The name of the initial state
|
||||
-- @param[opt={}] context The FSM context table which gets passed to all state and event handlers
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.Init(self, initialState, context)
|
||||
assert(self._stateObjs[initialState], "invalid initial state")
|
||||
self._currentState = initialState
|
||||
self._context = context or {}
|
||||
-- validate all the transitions
|
||||
for name, obj in pairs(self._stateObjs) do
|
||||
for _, toState in obj:_ToStateIterator() do
|
||||
assert(self._stateObjs[toState], format("toState doesn't exist (%s -> %s)", name, toState))
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Process an event.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam string event The name of the event
|
||||
-- @tparam[opt] vararg ... Additional arguments to pass to the handler function
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.ProcessEvent(self, event, ...)
|
||||
assert(self._currentState, "FSM not initialized")
|
||||
if self._handlingEvent then
|
||||
Log.RaiseStackLevel()
|
||||
Log.Warn("[%s] %s (ignored - handling event - %s)", self._name, event, self._handlingEvent)
|
||||
Log.LowerStackLevel()
|
||||
return self
|
||||
elseif self._inTransition then
|
||||
Log.RaiseStackLevel()
|
||||
Log.Warn("[%s] %s (ignored - in transition)", self._name, event)
|
||||
Log.LowerStackLevel()
|
||||
return self
|
||||
end
|
||||
|
||||
if self._loggingDisabledCount == 0 then
|
||||
Log.RaiseStackLevel()
|
||||
Log.Info("[%s] %s", self._name, event)
|
||||
Log.LowerStackLevel()
|
||||
end
|
||||
self._handlingEvent = event
|
||||
local currentStateObj = self._stateObjs[self._currentState]
|
||||
if currentStateObj:_HasEventHandler(event) then
|
||||
self:_Transition(TempTable.Acquire(currentStateObj:_ProcessEvent(event, self._context, ...)))
|
||||
elseif self._defaultEvents[event] then
|
||||
self:_Transition(TempTable.Acquire(self._defaultEvents[event](self._context, ...)))
|
||||
end
|
||||
self._handlingEvent = nil
|
||||
return self
|
||||
end
|
||||
|
||||
--- Enable or disable event and state transition logs (can be called recursively).
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam boolean enabled Whether or not logging should be enabled
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.SetLoggingEnabled(self, enabled)
|
||||
self._loggingDisabledCount = self._loggingDisabledCount + (enabled and -1 or 1)
|
||||
assert(self._loggingDisabledCount >= 0)
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMMachine._Transition(self, eventResult)
|
||||
local result = eventResult
|
||||
while result[1] do
|
||||
-- perform the transition
|
||||
local currentStateObj = self._stateObjs[self._currentState]
|
||||
local toState = tremove(result, 1)
|
||||
local toStateObj = self._stateObjs[toState]
|
||||
if self._loggingDisabledCount == 0 then
|
||||
Log.RaiseStackLevel()
|
||||
Log.RaiseStackLevel()
|
||||
Log.Info("[%s] %s -> %s", self._name, self._currentState, toState)
|
||||
Log.LowerStackLevel()
|
||||
Log.LowerStackLevel()
|
||||
end
|
||||
assert(toStateObj and currentStateObj:_IsTransitionValid(toState), "invalid transition")
|
||||
self._inTransition = true
|
||||
currentStateObj:_Exit(self._context)
|
||||
self._currentState = toState
|
||||
result = TempTable.Acquire(toStateObj:_Enter(self._context, TempTable.UnpackAndRelease(result)))
|
||||
self._inTransition = false
|
||||
end
|
||||
TempTable.Release(result)
|
||||
end
|
||||
157
LibTSM/Util/FSMClasses/State.lua
Normal file
157
LibTSM/Util/FSMClasses/State.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FSMState Class.
|
||||
-- This class represents a single state within an @{FSMMachine}.
|
||||
-- @classmod FSMState
|
||||
|
||||
local _, TSM = ...
|
||||
local State = TSM.Init("Util.FSMClasses.State")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local FSMState = LibTSMClass.DefineClass("FSMState")
|
||||
local private = {
|
||||
eventTransitionHandlerCache = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function State.Create(name)
|
||||
return FSMState(name)
|
||||
end
|
||||
|
||||
function State.IsInstance(obj)
|
||||
return obj:__isa(FSMState)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMState.__init(self, name)
|
||||
self._name = name
|
||||
self._onEnterHandler = nil
|
||||
self._onExitHandler = nil
|
||||
self._transitionValid = {}
|
||||
self._events = {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
--- Set the OnEnter handler.
|
||||
-- This function is called upon entering the state.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam ?function|string handler The handler function or a method name to call on the context object
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.SetOnEnter(self, handler)
|
||||
assert(type(handler) == "function" or type(handler) == "string")
|
||||
self._onEnterHandler = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set the OnExit handler.
|
||||
-- This function is called upon existing the state.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam ?function|string handler The handler function or a method name to call on the context object
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.SetOnExit(self, handler)
|
||||
assert(type(handler) == "function" or type(handler) == "string")
|
||||
self._onExitHandler = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a transition.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam string toState The state this transition goes to
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.AddTransition(self, toState)
|
||||
assert(not self._transitionValid[toState], "transition already exists")
|
||||
self._transitionValid[toState] = true
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a handled event.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam string event The name of the event
|
||||
-- @tparam function handler The function called when the event occurs
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.AddEvent(self, event, handler)
|
||||
assert(not self._events[event], "event already exists")
|
||||
self._events[event] = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a simple event-based transition.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam string event The event name
|
||||
-- @tparam string toState The state to transition to
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.AddEventTransition(self, event, toState)
|
||||
if not private.eventTransitionHandlerCache[toState] then
|
||||
private.eventTransitionHandlerCache[toState] = function(context, ...)
|
||||
return toState, ...
|
||||
end
|
||||
end
|
||||
return self:AddEvent(event, private.eventTransitionHandlerCache[toState])
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMState._GetName(self)
|
||||
return self._name
|
||||
end
|
||||
|
||||
function FSMState._ToStateIterator(self)
|
||||
local temp = TempTable.Acquire()
|
||||
for toState in pairs(self._transitionValid) do
|
||||
tinsert(temp, toState)
|
||||
end
|
||||
return TempTable.Iterator(temp)
|
||||
end
|
||||
|
||||
function FSMState._IsTransitionValid(self, toState)
|
||||
return self._transitionValid[toState]
|
||||
end
|
||||
|
||||
function FSMState._HasEventHandler(self, event)
|
||||
return self._events[event] and true or false
|
||||
end
|
||||
|
||||
function FSMState._ProcessEvent(self, event, context, ...)
|
||||
return self:_HandlerHelper(self._events[event], context, ...)
|
||||
end
|
||||
|
||||
function FSMState._Enter(self, context, ...)
|
||||
return self:_HandlerHelper(self._onEnterHandler, context, ...)
|
||||
end
|
||||
|
||||
function FSMState._Exit(self, context)
|
||||
return self:_HandlerHelper(self._onExitHandler, context)
|
||||
end
|
||||
|
||||
function FSMState._HandlerHelper(self, handler, context, ...)
|
||||
if type(handler) == "function" then
|
||||
return handler(context, ...)
|
||||
elseif type(handler) == "string" then
|
||||
return context[handler](context, ...)
|
||||
elseif handler ~= nil then
|
||||
error("Invalid handler: "..tostring(handler))
|
||||
end
|
||||
end
|
||||
94
LibTSM/Util/FontObject.lua
Normal file
94
LibTSM/Util/FontObject.lua
Normal file
@@ -0,0 +1,94 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FontObject Functions.
|
||||
-- @module FontObject
|
||||
|
||||
local _, TSM = ...
|
||||
local FontObject = TSM.Init("Util.FontObject")
|
||||
local private = {
|
||||
context = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local FONT_OBJECT_MT = {
|
||||
__index = {
|
||||
SetPath = function(self, path)
|
||||
assert(type(path) == "string")
|
||||
local context = private.context[self]
|
||||
context.path = path
|
||||
return self
|
||||
end,
|
||||
SetSize = function(self, size)
|
||||
assert(type(size) == "number")
|
||||
local context = private.context[self]
|
||||
context.size = size
|
||||
return self
|
||||
end,
|
||||
SetLineHeight = function(self, lineHeight)
|
||||
assert(type(lineHeight) == "number")
|
||||
local context = private.context[self]
|
||||
context.lineHeight = lineHeight
|
||||
return self
|
||||
end,
|
||||
GetWowFont = function(self)
|
||||
local context = private.context[self]
|
||||
-- wow renders the font slightly bigger than the designs would indicate, so subtract one from the font height
|
||||
if context.path == "Fonts\\ARKai_C.ttf" then
|
||||
-- this font is a bit smaller than it should be, so increase it by 1
|
||||
return context.path, context.size + 1
|
||||
else
|
||||
-- wow renders other fonts slightly bigger than the designs would indicate, so decrease the height by 1
|
||||
return context.path, context.size - 1
|
||||
end
|
||||
end,
|
||||
GetSpacing = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.lineHeight >= context.size)
|
||||
return context.lineHeight - context.size
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("FontObject cannot be modified") end,
|
||||
__metatable = false,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
local shortPath = strmatch(context.path, "([^/\\]+)%.[A-Za-z]+$")
|
||||
return "FontObject:"..tostring(shortPath)..":"..tostring(context.size)..":"..tostring(context.lineHeight)
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create an font object from a path and height.
|
||||
-- @tparam string path The path to the font file
|
||||
-- @tparam number size The size of the font in pixels
|
||||
-- @tparam number lineHeight The height of each line of text in pixels
|
||||
-- @treturn FontObject The font object
|
||||
function FontObject.New(path, size, lineHeight)
|
||||
local obj = setmetatable({}, FONT_OBJECT_MT)
|
||||
private.context[obj] = {
|
||||
path = nil,
|
||||
size = nil,
|
||||
lineHeight = nil,
|
||||
}
|
||||
obj:SetPath(path)
|
||||
obj:SetSize(size)
|
||||
obj:SetLineHeight(lineHeight)
|
||||
return obj
|
||||
end
|
||||
|
||||
function FontObject.IsInstance(obj)
|
||||
return type(obj) == "table" and private.context[obj] and true or false
|
||||
end
|
||||
110
LibTSM/Util/Future.lua
Normal file
110
LibTSM/Util/Future.lua
Normal file
@@ -0,0 +1,110 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Future Functions.
|
||||
-- @module Future
|
||||
|
||||
local _, TSM = ...
|
||||
local Future = TSM.Init("Util.Future")
|
||||
local private = {
|
||||
context = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local FUTURE_MT = {
|
||||
__index = {
|
||||
GetName = function(self)
|
||||
local context = private.context[self]
|
||||
return context.name
|
||||
end,
|
||||
SetScript = function(self, script, handler)
|
||||
assert(type(handler) == "function")
|
||||
local context = private.context[self]
|
||||
if script == "OnDone" then
|
||||
assert(context.state ~= "DONE")
|
||||
assert(not context.onDone)
|
||||
context.onDone = handler
|
||||
elseif script == "OnCleanup" then
|
||||
assert(not context.onCleanup)
|
||||
context.onCleanup = handler
|
||||
else
|
||||
error("Unknown script: "..tostring(script))
|
||||
end
|
||||
end,
|
||||
Start = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state == "RESET")
|
||||
context.state = "STARTED"
|
||||
end,
|
||||
Cancel = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state ~= "RESET")
|
||||
private.Reset(self)
|
||||
end,
|
||||
Done = function(self, value)
|
||||
local context = private.context[self]
|
||||
assert(context.state == "STARTED")
|
||||
context.state = "DONE"
|
||||
context.value = value
|
||||
if context.onDone then
|
||||
context.onDone(self)
|
||||
end
|
||||
end,
|
||||
IsDone = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state ~= "RESET")
|
||||
return context.state == "DONE"
|
||||
end,
|
||||
GetValue = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state == "DONE")
|
||||
local value = context.value
|
||||
private.Reset(self)
|
||||
return value
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("Future cannot be modified") end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new future.
|
||||
-- @tparam string name The name of the future for debugging purposes
|
||||
-- @treturn Future The future object
|
||||
function Future.New(name)
|
||||
local future = setmetatable({}, FUTURE_MT)
|
||||
private.context[future] = {
|
||||
name = name,
|
||||
}
|
||||
private.Reset(future)
|
||||
return future
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.Reset(future)
|
||||
local context = private.context[future]
|
||||
context.state = "RESET"
|
||||
context.value = nil
|
||||
context.onDone = nil
|
||||
if context.onCleanup then
|
||||
context.onCleanup()
|
||||
end
|
||||
end
|
||||
253
LibTSM/Util/HSLuv.lua
Normal file
253
LibTSM/Util/HSLuv.lua
Normal file
@@ -0,0 +1,253 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- NOTE: The following code is heavily based on https://github.com/hsluv/hsluv-lua, with some
|
||||
-- modifications to work properly with TSM. Its original license is below:
|
||||
|
||||
--[[
|
||||
Lua implementation of HSLuv and HPLuv color spaces
|
||||
Homepage: http://www.hsluv.org/
|
||||
|
||||
Copyright (C) 2019 Alexei Boronine
|
||||
|
||||
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.
|
||||
]]
|
||||
|
||||
local _, TSM = ...
|
||||
local HSLuv = TSM.Init("Util.HSLuv")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local private = {}
|
||||
local M = {
|
||||
{ 3.240969941904521, -1.537383177570093, -0.498610760293 },
|
||||
{ -0.96924363628087, 1.87596750150772, 0.041555057407175 },
|
||||
{ 0.055630079696993, -0.20397695888897, 1.056971514242878 }
|
||||
}
|
||||
local M_INV = {
|
||||
{ 0.41239079926595, 0.35758433938387, 0.18048078840183 },
|
||||
{ 0.21263900587151, 0.71516867876775, 0.072192315360733 },
|
||||
{ 0.019330818715591, 0.11919477979462, 0.95053215224966 }
|
||||
}
|
||||
local REF_Y = 1.0
|
||||
local REF_U = 0.19783000664283
|
||||
local REF_V = 0.46831999493879
|
||||
local KAPPA = 903.2962962
|
||||
local EPSILON = 0.0088564516
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function HSLuv.ToRGB(h, s, l)
|
||||
return private.HSLuvToRGB(h, s, l)
|
||||
end
|
||||
|
||||
function HSLuv.FromRGB(r, g, b)
|
||||
return private.RGBToHSLuv(r, g, b)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.MaxSafeChromaForLH(l, h)
|
||||
local hrad = h / 360 * math.pi * 2
|
||||
local chroma = 1.7976931348623157e+308
|
||||
local sub1 = ((l + 16) ^ 3) / 1560896
|
||||
local sub2 = nil
|
||||
if sub1 > EPSILON then
|
||||
sub2 = sub1
|
||||
else
|
||||
sub2 = l / KAPPA
|
||||
end
|
||||
for i = 1, 3 do
|
||||
for t = 0, 1 do
|
||||
local top1 = (284517 * M[i][1] - 94839 * M[i][3]) * sub2
|
||||
local top2 = (838422 * M[i][3] + 769860 * M[i][2] + 731718 * M[i][1]) * l * sub2 - 769860 * t * l
|
||||
local bottom = (632260 * M[i][3] - 126452 * M[i][2]) * sub2 + 126452 * t
|
||||
if bottom ~= 0 then
|
||||
local slope = top1 / bottom
|
||||
local intercept = top2 / bottom
|
||||
if hrad ~= 0 or slope ~= 0 then
|
||||
local length = intercept / (math.sin(hrad) - slope * math.cos(hrad))
|
||||
if length >= 0 then
|
||||
chroma = min(chroma, length)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return chroma
|
||||
end
|
||||
|
||||
function private.DotProduct(a, b1, b2, b3)
|
||||
return a[1] * b1 + a[2] * b2 + a[3] * b3
|
||||
end
|
||||
|
||||
function private.FromLinear(c)
|
||||
if c <= 0.0031308 then
|
||||
return 12.92 * c
|
||||
else
|
||||
return 1.055 * (c ^ 0.416666666666666685) - 0.055
|
||||
end
|
||||
end
|
||||
|
||||
function private.ToLinear(c)
|
||||
if c > 0.04045 then
|
||||
return ((c + 0.055) / 1.055) ^ 2.4
|
||||
else
|
||||
return c / 12.92
|
||||
end
|
||||
end
|
||||
|
||||
function private.XYZToRGB(x, y, z)
|
||||
local r = private.FromLinear(private.DotProduct(M[1], x, y, z))
|
||||
local g = private.FromLinear(private.DotProduct(M[2], x, y, z))
|
||||
local b = private.FromLinear(private.DotProduct(M[3], x, y, z))
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
function private.RGBToXYZ(r, g, b)
|
||||
r = private.ToLinear(r)
|
||||
g = private.ToLinear(g)
|
||||
b = private.ToLinear(b)
|
||||
local x = private.DotProduct(M_INV[1], r, g, b)
|
||||
local y = private.DotProduct(M_INV[2], r, g, b)
|
||||
local z = private.DotProduct(M_INV[3], r, g, b)
|
||||
return x, y, z
|
||||
end
|
||||
|
||||
function private.YToL(Y)
|
||||
if Y <= EPSILON then
|
||||
return Y / REF_Y * KAPPA
|
||||
else
|
||||
return 116 * ((Y / REF_Y) ^ 0.333333333333333315) - 16
|
||||
end
|
||||
end
|
||||
|
||||
function private.LToY(L)
|
||||
if L <= 8 then
|
||||
return REF_Y * L / KAPPA
|
||||
else
|
||||
return REF_Y * (((L + 16) / 116) ^ 3)
|
||||
end
|
||||
end
|
||||
|
||||
function private.XYZToLUV(x, y, z)
|
||||
local divider = x + 15 * y + 3 * z
|
||||
local varU = 4 * x
|
||||
local varV = 9 * y
|
||||
if divider ~= 0 then
|
||||
varU = varU / divider
|
||||
varV = varV / divider
|
||||
else
|
||||
varU = 0
|
||||
varV = 0
|
||||
end
|
||||
local L = private.YToL(y)
|
||||
if L == 0 then
|
||||
return 0, 0, 0
|
||||
end
|
||||
return L, 13 * L * (varU - REF_U), 13 * L * (varV - REF_V)
|
||||
end
|
||||
|
||||
function private.LUVToXYZ(l, u, v)
|
||||
if l == 0 then
|
||||
return 0, 0, 0
|
||||
end
|
||||
local varU = u / (13 * l) + REF_U
|
||||
local varV = v / (13 * l) + REF_V
|
||||
local Y = private.LToY(l)
|
||||
local X = 0 - (9 * Y * varU) / ((((varU - 4) * varV) - varU * varV))
|
||||
return X, Y, (9 * Y - 15 * varV * Y - varV * X) / (3 * varV)
|
||||
end
|
||||
|
||||
function private.LUVToLCH(l, u, v)
|
||||
local C = math.sqrt(u * u + v * v)
|
||||
local H
|
||||
if C < 0.00000001 then
|
||||
H = 0
|
||||
else
|
||||
H = math.atan2(v, u) * 180.0 / 3.1415926535897932
|
||||
if H < 0 then
|
||||
H = 360 + H
|
||||
end
|
||||
end
|
||||
return l, C, H
|
||||
end
|
||||
|
||||
function private.LCHToLUV(l, c, h)
|
||||
local hrad = h / 360.0 * 2 * math.pi
|
||||
return l, math.cos(hrad) * c, math.sin(hrad) * c
|
||||
end
|
||||
|
||||
function private.HSLuvToLCH(h, s, l)
|
||||
if l > 99.9999999 then
|
||||
return 100, 0, h
|
||||
end
|
||||
if l < 0.00000001 then
|
||||
return 0, 0, h
|
||||
end
|
||||
return l, private.MaxSafeChromaForLH(l, h) / 100 * s, h
|
||||
end
|
||||
|
||||
function private.LCHToHSLuv(l, c, h)
|
||||
local max_chroma = private.MaxSafeChromaForLH(l, h)
|
||||
if l > 99.9999999 then
|
||||
return h, 0, 100
|
||||
end
|
||||
if l < 0.00000001 then
|
||||
return h, 0, 0
|
||||
end
|
||||
|
||||
return h, c / max_chroma * 100, l
|
||||
end
|
||||
|
||||
function private.HSLuvToRGB(h, s, l)
|
||||
local v1, v2, v3 = h, s, l
|
||||
v1, v2, v3 = private.HSLuvToLCH(v1, v2, v3)
|
||||
v1, v2, v3 = private.LCHToLUV(v1, v2, v3)
|
||||
v1, v2, v3 = private.LUVToXYZ(v1, v2, v3)
|
||||
local r, g, b = private.XYZToRGB(v1, v2, v3)
|
||||
r = Math.Round(r * 255)
|
||||
g = Math.Round(g * 255)
|
||||
b = Math.Round(b * 255)
|
||||
assert(r >= 0 and r <= 255)
|
||||
assert(g >= 0 and g <= 255)
|
||||
assert(b >= 0 and b <= 255)
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
function private.RGBToHSLuv(r, g, b)
|
||||
local v1, v2, v3 = r / 255, g / 255, b / 255
|
||||
v1, v2, v3 = private.RGBToXYZ(v1, v2, v3)
|
||||
v1, v2, v3 = private.XYZToLUV(v1, v2, v3)
|
||||
v1, v2, v3 = private.LUVToLCH(v1, v2, v3)
|
||||
local h, s, l = private.LCHToHSLuv(v1, v2, v3)
|
||||
h = Math.Round(h) % 360
|
||||
s = Math.Round(s)
|
||||
l = Math.Round(l)
|
||||
assert(h >= 0 and h < 360)
|
||||
assert(s >= 0 and s <= 100)
|
||||
assert(l >= 0 and l <= 100)
|
||||
return h, s, l
|
||||
end
|
||||
328
LibTSM/Util/ItemString.lua
Normal file
328
LibTSM/Util/ItemString.lua
Normal file
@@ -0,0 +1,328 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Item String functions
|
||||
-- @module ItemString
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemString = TSM.Init("Util.ItemString")
|
||||
local BonusIds = TSM.Include("Data.BonusIds")
|
||||
local SmartMap = TSM.Include("Util.SmartMap")
|
||||
local private = {
|
||||
filteredItemStringCache = {},
|
||||
itemStringCache = {},
|
||||
baseItemStringMap = nil,
|
||||
baseItemStringReader = nil,
|
||||
hasNonBaseItemStrings = {},
|
||||
bonusIdsTemp = {},
|
||||
modifiersTemp = {},
|
||||
}
|
||||
local ITEM_MAX_ID = 999999
|
||||
local UNKNOWN_ITEM_STRING = "i:0"
|
||||
local PLACEHOLDER_ITEM_STRING = "i:1"
|
||||
local PET_CAGE_ITEM_STRING = "i:82800"
|
||||
local MINIMUM_VARIANT_ITEM_ID = 152632
|
||||
local IMPORTANT_MODIFIER_TYPES = {
|
||||
[9] = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
ItemString:OnModuleLoad(function()
|
||||
private.baseItemStringMap = SmartMap.New("string", "string", private.ToBaseItemString)
|
||||
private.baseItemStringReader = private.baseItemStringMap:CreateReader()
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Gets the constant unknown item string for places where the itemString is not known.
|
||||
-- @treturn string The itemString
|
||||
function ItemString.GetUnknown()
|
||||
return UNKNOWN_ITEM_STRING
|
||||
end
|
||||
|
||||
--- Gets the constant placeholder item string.
|
||||
-- @treturn string The itemString
|
||||
function ItemString.GetPlaceholder()
|
||||
return PLACEHOLDER_ITEM_STRING
|
||||
end
|
||||
|
||||
--- Gets the battlepet cage item string.
|
||||
-- @treturn string The itemString
|
||||
function ItemString.GetPetCage()
|
||||
return PET_CAGE_ITEM_STRING
|
||||
end
|
||||
|
||||
--- Gets the base itemString smart map.
|
||||
-- @treturn SmartMap The smart map
|
||||
function ItemString.GetBaseMap()
|
||||
return private.baseItemStringMap
|
||||
end
|
||||
|
||||
--- Converts the parameter into an itemString.
|
||||
-- @tparam ?number|string item Either an itemId, itemLink, or itemString to be converted
|
||||
-- @treturn string The itemString
|
||||
function ItemString.Get(item)
|
||||
if not item then
|
||||
return nil
|
||||
end
|
||||
if not private.itemStringCache[item] then
|
||||
private.itemStringCache[item] = private.ToItemString(item)
|
||||
end
|
||||
return private.itemStringCache[item]
|
||||
end
|
||||
|
||||
function ItemString.Filter(itemString)
|
||||
if not private.filteredItemStringCache[itemString] then
|
||||
private.filteredItemStringCache[itemString] = private.FilterBonusIdsAndModifiers(itemString, true, strsplit(":", itemString))
|
||||
end
|
||||
return private.filteredItemStringCache[itemString]
|
||||
end
|
||||
|
||||
--- Converts the parameter into an itemId.
|
||||
-- @tparam string item An item to get the id of
|
||||
-- @treturn number The itemId
|
||||
function ItemString.ToId(item)
|
||||
local itemString = ItemString.Get(item)
|
||||
if type(itemString) ~= "string" then
|
||||
return
|
||||
end
|
||||
return tonumber(strmatch(itemString, "^[ip]:(%d+)"))
|
||||
end
|
||||
|
||||
--- Converts the parameter into a base itemString.
|
||||
-- @tparam string itemString An itemString to get the base itemString of
|
||||
-- @treturn string The base itemString
|
||||
function ItemString.GetBaseFast(itemString)
|
||||
if not itemString then
|
||||
return nil
|
||||
end
|
||||
return private.baseItemStringReader[itemString]
|
||||
end
|
||||
|
||||
--- Converts the parameter into a base itemString.
|
||||
-- @tparam string item An item to get the base itemString of
|
||||
-- @treturn string The base itemString
|
||||
function ItemString.GetBase(item)
|
||||
-- make sure it's a valid itemString
|
||||
local itemString = ItemString.Get(item)
|
||||
if not itemString then return end
|
||||
|
||||
-- quickly return if we're certain it's already a valid baseItemString
|
||||
if type(itemString) == "string" and strmatch(itemString, "^[ip]:[0-9]+$") then return itemString end
|
||||
return ItemString.GetBaseFast(itemString)
|
||||
end
|
||||
|
||||
--- Converts an itemKey from WoW into a base itemString.
|
||||
-- @tparam table itemKey An itemKey to get the itemString of
|
||||
-- @treturn string The base itemString
|
||||
function ItemString.GetBaseFromItemKey(itemKey)
|
||||
if itemKey.battlePetSpeciesID > 0 then
|
||||
return "p:"..itemKey.battlePetSpeciesID
|
||||
else
|
||||
return "i:"..itemKey.itemID
|
||||
end
|
||||
end
|
||||
|
||||
function ItemString.HasNonBase(baseItemString)
|
||||
return private.hasNonBaseItemStrings[baseItemString] or false
|
||||
end
|
||||
|
||||
--- Converts the parameter into a WoW itemString.
|
||||
-- @tparam string itemString An itemString to get the WoW itemString of
|
||||
-- @treturn number The WoW itemString
|
||||
function ItemString.ToWow(itemString)
|
||||
local _, itemId, rand, extra = strsplit(":", itemString)
|
||||
local level = UnitLevel("player")
|
||||
local spec = not TSM.IsWowClassic() and GetSpecialization() or nil
|
||||
spec = spec and GetSpecializationInfo(spec) or ""
|
||||
local extraPart = extra and strmatch(itemString, "i:[0-9]+:[0-9%-]*:(.+)") or ""
|
||||
return "item:"..itemId.."::::::"..(rand or "").."::"..level..":"..spec..":::"..extraPart..":::"
|
||||
end
|
||||
|
||||
function ItemString.IsItem(itemString)
|
||||
return strmatch(itemString, "^i:[%-:0-9]+$") and true or false
|
||||
end
|
||||
|
||||
function ItemString.IsPet(itemString)
|
||||
return strmatch(itemString, "^p:[%-:0-9]+$") and true or false
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ToItemString(item)
|
||||
local paramType = type(item)
|
||||
if paramType == "string" then
|
||||
item = strtrim(item)
|
||||
local itemId = strmatch(item, "^[ip]:([0-9]+)$")
|
||||
if itemId then
|
||||
if tonumber(itemId) > ITEM_MAX_ID then
|
||||
return nil
|
||||
end
|
||||
-- this is already an itemString
|
||||
return item
|
||||
end
|
||||
itemId = strmatch(item, "item:(%d+)")
|
||||
if itemId and tonumber(itemId) > ITEM_MAX_ID then
|
||||
return nil
|
||||
end
|
||||
elseif paramType == "number" or tonumber(item) then
|
||||
local itemId = tonumber(item)
|
||||
if itemId > ITEM_MAX_ID then
|
||||
return nil
|
||||
end
|
||||
-- assume this is an itemId
|
||||
return "i:"..item
|
||||
else
|
||||
error("Invalid item parameter type: "..tostring(item))
|
||||
end
|
||||
|
||||
-- test if it's already (likely) an item string or battle pet string
|
||||
if strmatch(item, "^i:([0-9%-:]+)$") then
|
||||
return private.FixItemString(item)
|
||||
elseif strmatch(item, "^p:([0-9:]+)$") then
|
||||
return private.FixPet(item)
|
||||
end
|
||||
|
||||
local result = strmatch(item, "^\124cff[0-9a-z]+\124[Hh](.+)\124h%[.+%]\124h\124r$")
|
||||
if result then
|
||||
-- it was a full item link which we've extracted the itemString from
|
||||
item = result
|
||||
end
|
||||
|
||||
-- test if it's an old style item string
|
||||
result = strjoin(":", strmatch(item, "^(i)tem:([0-9%-]+):[0-9%-]+:[0-9%-]+:[0-9%-]+:[0-9%-]+:[0-9%-]+:([0-9%-]+)$"))
|
||||
if result then
|
||||
return private.FixItemString(result)
|
||||
end
|
||||
|
||||
-- test if it's an old style battle pet string (or if it was a link)
|
||||
result = strjoin(":", strmatch(item, "^battle(p)et:(%d+:%d+:%d+)"))
|
||||
if result then
|
||||
return private.FixPet(result)
|
||||
end
|
||||
result = strjoin(":", strmatch(item, "^battle(p)et:(%d+)[:]*$"))
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
result = strjoin(":", strmatch(item, "^(p):(%d+:%d+:%d+)"))
|
||||
if result then
|
||||
return private.FixPet(result)
|
||||
end
|
||||
|
||||
-- test if it's a long item string
|
||||
result = strjoin(":", strmatch(item, "(i)tem:([0-9%-]+):[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:([0-9%-]*):[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:([0-9%-:]+)"))
|
||||
if result and result ~= "" then
|
||||
return private.FixItemString(result)
|
||||
end
|
||||
|
||||
-- test if it's a shorter item string (without bonuses)
|
||||
result = strjoin(":", strmatch(item, "(i)tem:([0-9%-]+):[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:([0-9%-]*)"))
|
||||
if result and result ~= "" then
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
function private.RemoveExtra(itemString)
|
||||
local num = 1
|
||||
while num > 0 do
|
||||
itemString, num = gsub(itemString, ":0?$", "")
|
||||
end
|
||||
return itemString
|
||||
end
|
||||
|
||||
function private.FixItemString(itemString)
|
||||
itemString = gsub(itemString, ":0:", "::") -- remove 0s which are in the middle
|
||||
itemString = private.RemoveExtra(itemString)
|
||||
return private.FilterBonusIdsAndModifiers(itemString, false, strsplit(":", itemString))
|
||||
end
|
||||
|
||||
function private.FixPet(itemString)
|
||||
itemString = private.RemoveExtra(itemString)
|
||||
local result = strmatch(itemString, "^(p:%d+:%d+:%d+)$")
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
return strmatch(itemString, "^(p:%d+)")
|
||||
end
|
||||
|
||||
function private.FilterBonusIdsAndModifiers(itemString, importantBonusIdsOnly, itemType, itemId, rand, numBonusIds, ...)
|
||||
numBonusIds = tonumber(numBonusIds) or 0
|
||||
local numParts = select("#", ...)
|
||||
if numParts == 0 then
|
||||
return itemString
|
||||
end
|
||||
|
||||
-- grab the modifiers and filter them
|
||||
local numModifiers = numParts - numBonusIds
|
||||
local modifiersStr = (numModifiers > 0 and numModifiers > 1 and numModifiers % 2 == 1) and strjoin(":", select(numBonusIds + 1, ...)) or ""
|
||||
if modifiersStr ~= "" then
|
||||
wipe(private.modifiersTemp)
|
||||
local num, modifierType = nil, nil
|
||||
for modifier in gmatch(modifiersStr, "[0-9]+") do
|
||||
modifier = tonumber(modifier)
|
||||
if not num then
|
||||
num = modifier
|
||||
elseif not modifierType then
|
||||
modifierType = modifier
|
||||
else
|
||||
if IMPORTANT_MODIFIER_TYPES[modifierType] then
|
||||
tinsert(private.modifiersTemp, modifierType)
|
||||
tinsert(private.modifiersTemp, modifier)
|
||||
end
|
||||
modifierType = nil
|
||||
end
|
||||
end
|
||||
if #private.modifiersTemp > 0 then
|
||||
assert(#private.modifiersTemp % 2 == 0)
|
||||
tinsert(private.modifiersTemp, 1, #private.modifiersTemp / 2)
|
||||
modifiersStr = table.concat(private.modifiersTemp, ":")
|
||||
end
|
||||
end
|
||||
|
||||
-- filter the bonusIds
|
||||
local bonusIdsStr = ""
|
||||
if numBonusIds > 0 then
|
||||
-- get the list of bonusIds and filter them
|
||||
wipe(private.bonusIdsTemp)
|
||||
for i = 1, numBonusIds do
|
||||
private.bonusIdsTemp[i] = select(i, ...)
|
||||
end
|
||||
if importantBonusIdsOnly then
|
||||
-- Only track bonusIds if the itemId is above our minimum
|
||||
if tonumber(itemId) >= MINIMUM_VARIANT_ITEM_ID then
|
||||
bonusIdsStr = BonusIds.FilterImportant(table.concat(private.bonusIdsTemp, ":"))
|
||||
end
|
||||
else
|
||||
bonusIdsStr = BonusIds.FilterAll(table.concat(private.bonusIdsTemp, ":"))
|
||||
end
|
||||
end
|
||||
|
||||
-- rebuild the itemString
|
||||
itemString = strjoin(":", itemType, itemId, rand, bonusIdsStr, modifiersStr)
|
||||
itemString = gsub(itemString, ":0:", "::") -- remove 0s which are in the middle
|
||||
return private.RemoveExtra(itemString)
|
||||
end
|
||||
|
||||
function private.ToBaseItemString(itemString)
|
||||
local baseItemString = strmatch(itemString, "[ip]:%d+")
|
||||
if baseItemString ~= itemString then
|
||||
private.hasNonBaseItemStrings[baseItemString] = true
|
||||
end
|
||||
return baseItemString
|
||||
end
|
||||
58
LibTSM/Util/JSON.lua
Normal file
58
LibTSM/Util/JSON.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- JSON Functions
|
||||
-- @module JSON
|
||||
|
||||
local _, TSM = ...
|
||||
local JSON = TSM.Init("Util.JSON")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function JSON.Encode(value)
|
||||
if type(value) == "string" then
|
||||
return "\""..private.SanitizeString(value).."\""
|
||||
elseif type(value) == "number" or type(value) == "boolean" then
|
||||
return tostring(value)
|
||||
elseif type(value) == "table" then
|
||||
local absCount = 0
|
||||
for _ in pairs(value) do
|
||||
absCount = absCount + 1
|
||||
end
|
||||
local tblParts = {}
|
||||
if #value == absCount then
|
||||
for _, v in ipairs(value) do
|
||||
tinsert(tblParts, JSON.Encode(v))
|
||||
end
|
||||
return "["..table.concat(tblParts, ",").."]"
|
||||
else
|
||||
for k, v in pairs(value) do
|
||||
tinsert(tblParts, "\""..private.SanitizeString(k).."\":"..JSON.Encode(v))
|
||||
end
|
||||
return "{"..table.concat(tblParts, ",").."}"
|
||||
end
|
||||
else
|
||||
error("Invalid type: "..type(value))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SanitizeString(str)
|
||||
str = gsub(str, "\124cff[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]([^\124]+)\124r", "%1")
|
||||
str = gsub(str, "[\\]+", "/")
|
||||
str = gsub(str, "\"", "'")
|
||||
return str
|
||||
end
|
||||
189
LibTSM/Util/Log.lua
Normal file
189
LibTSM/Util/Log.lua
Normal file
@@ -0,0 +1,189 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Log = TSM.Init("Util.Log")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local private = {
|
||||
severity = {},
|
||||
location = {},
|
||||
timeStr = {},
|
||||
msg = {},
|
||||
writeIndex = 1,
|
||||
len = 0,
|
||||
temp = {},
|
||||
logToChat = TSM.__IS_TEST_ENV or false,
|
||||
currentThreadNameFunc = nil,
|
||||
stackLevel = 3,
|
||||
chatFrame = nil,
|
||||
}
|
||||
local MAX_ROWS = 200
|
||||
local MAX_MSG_LEN = 150
|
||||
local CHAT_LOG_COLOR_KEYS = {
|
||||
TRACE = "BLUE",
|
||||
INFO = "GREEN",
|
||||
WARN = "YELLOW",
|
||||
ERR = "RED",
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Log.SetChatFrame(chatFrame)
|
||||
private.chatFrame = strlower(chatFrame)
|
||||
end
|
||||
|
||||
function Log.SetLoggingToChatEnabled(enabled)
|
||||
if TSM.__IS_TEST_ENV then
|
||||
enabled = true
|
||||
end
|
||||
if private.logToChat == enabled then
|
||||
return
|
||||
end
|
||||
private.logToChat = enabled
|
||||
if enabled then
|
||||
-- dump our buffer
|
||||
local len = Log.Length()
|
||||
print(format("Printing %d buffered logs:", len))
|
||||
for i = 1, len do
|
||||
private.LogToChat(Log.Get(i))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Log.SetCurrentThreadNameFunction(func)
|
||||
private.currentThreadNameFunc = func
|
||||
end
|
||||
|
||||
function Log.Length()
|
||||
return private.len
|
||||
end
|
||||
|
||||
function Log.Get(index)
|
||||
assert(index <= private.len)
|
||||
local readIndex = (private.writeIndex - private.len + index - 2) % MAX_ROWS + 1
|
||||
return private.severity[readIndex], private.location[readIndex], private.timeStr[readIndex], private.msg[readIndex]
|
||||
end
|
||||
|
||||
function Log.RaiseStackLevel()
|
||||
private.stackLevel = private.stackLevel + 1
|
||||
end
|
||||
|
||||
function Log.LowerStackLevel()
|
||||
private.stackLevel = private.stackLevel - 1
|
||||
end
|
||||
|
||||
function Log.StackTrace()
|
||||
Log.RaiseStackLevel()
|
||||
Log.Trace("Stack Trace:")
|
||||
local level = 2
|
||||
local line = Debug.GetStackLevelLocation(level)
|
||||
while line do
|
||||
Log.Trace(" " .. line)
|
||||
level = level + 1
|
||||
line = Debug.GetStackLevelLocation(level)
|
||||
end
|
||||
Log.LowerStackLevel()
|
||||
end
|
||||
|
||||
function Log.Trace(...)
|
||||
private.Log("TRACE", ...)
|
||||
end
|
||||
|
||||
function Log.Info(...)
|
||||
private.Log("INFO", ...)
|
||||
end
|
||||
|
||||
function Log.Warn(...)
|
||||
private.Log("WARN", ...)
|
||||
end
|
||||
|
||||
function Log.Err(...)
|
||||
private.Log("ERR", ...)
|
||||
end
|
||||
|
||||
function Log.PrintUserRaw(str)
|
||||
private.GetChatFrame():AddMessage(str)
|
||||
end
|
||||
|
||||
function Log.PrintfUserRaw(...)
|
||||
Log.PrintUserRaw(format(...))
|
||||
end
|
||||
|
||||
function Log.PrintUser(str)
|
||||
Log.PrintUserRaw(Theme.GetColor("INDICATOR"):ColorText("TSM")..": "..str)
|
||||
end
|
||||
|
||||
function Log.PrintfUser(...)
|
||||
Log.PrintUser(format(...))
|
||||
end
|
||||
|
||||
function Log.ColorUserAccentText(text)
|
||||
return Theme.GetColor("INDICATOR_ALT"):ColorText(text)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetChatFrame()
|
||||
for i = 1, NUM_CHAT_WINDOWS do
|
||||
local name = strlower(GetChatWindowInfo(i) or "")
|
||||
if name ~= "" and name == private.chatFrame then
|
||||
return _G["ChatFrame" .. i]
|
||||
end
|
||||
end
|
||||
return DEFAULT_CHAT_FRAME
|
||||
end
|
||||
|
||||
function private.Log(severity, fmtStr, ...)
|
||||
assert(type(fmtStr) == "string" and CHAT_LOG_COLOR_KEYS[severity])
|
||||
wipe(private.temp)
|
||||
for i = 1, select("#", ...) do
|
||||
local arg = select(i, ...)
|
||||
if type(arg) == "boolean" then
|
||||
arg = arg and "T" or "F"
|
||||
elseif type(arg) ~= "string" and type(arg) ~= "number" then
|
||||
arg = tostring(arg)
|
||||
end
|
||||
private.temp[i] = arg
|
||||
end
|
||||
-- ignore anything after a newline in the log message
|
||||
local msg = strsplit("\n", format(fmtStr, unpack(private.temp)))
|
||||
if #msg > MAX_MSG_LEN then
|
||||
msg = strsub(msg, 1, -4).."..."
|
||||
end
|
||||
local location = Debug.GetStackLevelLocation(private.stackLevel)
|
||||
location = location and strmatch(location, "([^\\/]+%.lua:[0-9]+)") or "?:?"
|
||||
local threadName = private.currentThreadNameFunc and private.currentThreadNameFunc() or nil
|
||||
if threadName then
|
||||
location = location.."|"..threadName
|
||||
end
|
||||
local timeMs = Debug.GetTimeMilliseconds()
|
||||
local timeStr = format("%s.%03d", date("%H:%M:%S", floor(timeMs / 1000)), timeMs % 1000)
|
||||
|
||||
-- append the log
|
||||
private.severity[private.writeIndex] = severity
|
||||
private.location[private.writeIndex] = location
|
||||
private.timeStr[private.writeIndex] = timeStr
|
||||
private.msg[private.writeIndex] = msg
|
||||
private.writeIndex = (private.writeIndex < MAX_ROWS) and (private.writeIndex + 1) or 1
|
||||
private.len = min(private.len + 1, MAX_ROWS)
|
||||
|
||||
if private.logToChat then
|
||||
private.LogToChat(severity, location, timeStr, msg)
|
||||
end
|
||||
end
|
||||
|
||||
function private.LogToChat(severity, location, timeStr, msg)
|
||||
print(strjoin(" ", timeStr, Theme.GetFeedbackColor(CHAT_LOG_COLOR_KEYS[severity]):ColorText("{"..location.."}"), msg))
|
||||
end
|
||||
162
LibTSM/Util/Math.lua
Normal file
162
LibTSM/Util/Math.lua
Normal file
@@ -0,0 +1,162 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Math Functions
|
||||
-- @module Math
|
||||
|
||||
local _, TSM = ...
|
||||
local Math = TSM.Init("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local NAN = math.huge * 0
|
||||
local IS_NAN_GT_INF = NAN > math.huge
|
||||
local NAN_STR = tostring(NAN)
|
||||
local private = {
|
||||
keysTemp = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Returns NAN.
|
||||
-- @treturn number NAN
|
||||
function Math.GetNan()
|
||||
return NAN
|
||||
end
|
||||
|
||||
--- Checks if a value is NAN.
|
||||
-- @tparam number value The number to check
|
||||
-- @treturn boolean Whether or not the value is NAN
|
||||
function Math.IsNan(value)
|
||||
if IS_NAN_GT_INF then
|
||||
-- optimization if NAN > math.huge (which it is in Wow's version of lua)
|
||||
return value > math.huge and tostring(value) == NAN_STR
|
||||
else
|
||||
return tostring(value) == NAN_STR
|
||||
end
|
||||
end
|
||||
|
||||
--- Rounds a value to a specified significant value.
|
||||
-- @tparam number value The number to be rounded
|
||||
-- @tparam number sig The value to round to the nearest multiple of
|
||||
-- @treturn number The rounded value
|
||||
function Math.Round(value, sig)
|
||||
sig = sig or 1
|
||||
return floor((value / sig) + 0.5) * sig
|
||||
end
|
||||
|
||||
--- Rounds a value down to a specified significant value.
|
||||
-- @tparam number value The number to be rounded
|
||||
-- @tparam number sig The value to round down to the nearest multiple of
|
||||
-- @treturn number The rounded value
|
||||
function Math.Floor(value, sig)
|
||||
sig = sig or 1
|
||||
return floor(value / sig) * sig
|
||||
end
|
||||
|
||||
--- Rounds a value up to a specified significant value.
|
||||
-- @tparam number value The number to be rounded
|
||||
-- @tparam number sig The value to round up to the nearest multiple of
|
||||
-- @treturn number The rounded value
|
||||
function Math.Ceil(value, sig)
|
||||
sig = sig or 1
|
||||
return ceil(value / sig) * sig
|
||||
end
|
||||
|
||||
--- Scales a value from one range to another.
|
||||
-- @tparam number value The number to be scaled
|
||||
-- @tparam number fromStart The start value of the range to scale from
|
||||
-- @tparam number fromEnd The end value of the range to scale from (can be less than fromStart)
|
||||
-- @tparam number toStart The start value of the range to scale to
|
||||
-- @tparam number toEnd The end value of the range to scale to (can be less than toStart)
|
||||
-- @treturn number The scaled value
|
||||
function Math.Scale(value, fromStart, fromEnd, toStart, toEnd)
|
||||
assert(value >= min(fromStart, fromEnd) and value <= max(fromStart, fromEnd))
|
||||
return toStart + ((value - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart)
|
||||
end
|
||||
|
||||
--- Bounds a number between a min and max value.
|
||||
-- @tparam number value The number to be bounded
|
||||
-- @tparam number minValue The min value
|
||||
-- @tparam number maxValue The max value
|
||||
-- @treturn number The bounded value
|
||||
function Math.Bound(value, minValue, maxValue)
|
||||
return min(max(value, minValue), maxValue)
|
||||
end
|
||||
|
||||
--- Calculates the has of the specified data
|
||||
-- This data can handle data of type string or number. It can also handle a table being passed as the data assuming
|
||||
-- all keys and values of the table are also hashable (strings, numbers, or tables with the same restriction). This
|
||||
-- function uses the [djb2 algorithm](http://www.cse.yorku.ca/~oz/hash.html).
|
||||
-- @param data The data to be hased
|
||||
-- @tparam[opt] number hash The initial value of the hash
|
||||
-- @treturn number The hash value
|
||||
function Math.CalculateHash(data, hash)
|
||||
hash = hash or 5381
|
||||
local maxValue = 2 ^ 24
|
||||
local dataType = type(data)
|
||||
if dataType == "string" then
|
||||
-- iterate through 8 bytes at a time
|
||||
for i = 1, ceil(#data / 8) do
|
||||
local b1, b2, b3, b4, b5, b6, b7, b8 = strbyte(data, (i - 1) * 8 + 1, i * 8)
|
||||
hash = (hash * 33 + b1) % maxValue
|
||||
if not b2 then break end
|
||||
hash = (hash * 33 + b2) % maxValue
|
||||
if not b3 then break end
|
||||
hash = (hash * 33 + b3) % maxValue
|
||||
if not b4 then break end
|
||||
hash = (hash * 33 + b4) % maxValue
|
||||
if not b5 then break end
|
||||
hash = (hash * 33 + b5) % maxValue
|
||||
if not b6 then break end
|
||||
hash = (hash * 33 + b6) % maxValue
|
||||
if not b7 then break end
|
||||
hash = (hash * 33 + b7) % maxValue
|
||||
if not b8 then break end
|
||||
hash = (hash * 33 + b8) % maxValue
|
||||
end
|
||||
elseif dataType == "number" then
|
||||
assert(data == floor(data), "Invalid number")
|
||||
if data < 0 then
|
||||
data = data * -1
|
||||
hash = (hash * 33 + 59) % maxValue
|
||||
end
|
||||
while data > 0 do
|
||||
hash = (hash * 33 + data % 256) % maxValue
|
||||
data = floor(data / 256)
|
||||
end
|
||||
elseif dataType == "table" then
|
||||
local keys = nil
|
||||
if private.keysTemp.inUse then
|
||||
keys = TempTable.Acquire()
|
||||
else
|
||||
keys = private.keysTemp
|
||||
private.keysTemp.inUse = true
|
||||
end
|
||||
for k in pairs(data) do
|
||||
tinsert(keys, k)
|
||||
end
|
||||
sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
hash = Math.CalculateHash(key, hash)
|
||||
hash = Math.CalculateHash(data[key], hash)
|
||||
end
|
||||
if keys == private.keysTemp then
|
||||
wipe(private.keysTemp)
|
||||
else
|
||||
TempTable.Release(keys)
|
||||
end
|
||||
elseif dataType == "boolean" then
|
||||
hash = (hash * 33 + (data and 1 or 0)) % maxValue
|
||||
elseif dataType == "nil" then
|
||||
hash = (hash * 33 + 17) % maxValue
|
||||
else
|
||||
error("Invalid data")
|
||||
end
|
||||
return hash
|
||||
end
|
||||
177
LibTSM/Util/Money.lua
Normal file
177
LibTSM/Util/Money.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Money Functions
|
||||
-- @module Money
|
||||
|
||||
local _, TSM = ...
|
||||
local Money = TSM.Init("Util.Money")
|
||||
local String = TSM.Include("Util.String")
|
||||
local private = {
|
||||
textMoneyParts = {},
|
||||
}
|
||||
local GOLD_ICON = "|TInterface\\MoneyFrame\\UI-GoldIcon:0|t"
|
||||
local SILVER_ICON = "|TInterface\\MoneyFrame\\UI-SilverIcon:0|t"
|
||||
local COPPER_ICON = "|TInterface\\MoneyFrame\\UI-CopperIcon:0|t"
|
||||
local GOLD_ICON_DISABLED = "|TInterface\\MoneyFrame\\UI-GoldIcon:0:0:0:0:1:1:0:1:0:1:100:100:100|t"
|
||||
local SILVER_ICON_DISABLED = "|TInterface\\MoneyFrame\\UI-SilverIcon:0:0:0:0:1:1:0:1:0:1:100:100:100|t"
|
||||
local COPPER_ICON_DISABLED = "|TInterface\\MoneyFrame\\UI-CopperIcon:0:0:0:0:1:1:0:1:0:1:100:100:100|t"
|
||||
local GOLD_TEXT = "|cffffd70ag|r"
|
||||
local SILVER_TEXT = "|cffc7c7cfs|r"
|
||||
local COPPER_TEXT = "|cffeda55fc|r"
|
||||
local GOLD_TEXT_DISABLED = "|cff5d5222g|r"
|
||||
local SILVER_TEXT_DISABLED = "|cff464646s|r"
|
||||
local COPPER_TEXT_DISABLED = "|cff402d22c|r"
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Converts a numeric money value (in copper) to a string for display in the UI.
|
||||
-- Supported options:
|
||||
--
|
||||
-- * OPT\_ICON Use texture icons instead of g/s/c letters
|
||||
-- * OPT\_TRIM Remove any non-significant 0 valued denominations (i.e. "1g" instead of "1g 0s 0c")
|
||||
-- * OPT\_83\_NO\_COPPER Remove the copper value entirely if we're patch 8.3
|
||||
-- * OPT\_DISABLE Uses a muted color from the denomination text (not allowed with "OPT\_ICON" or "OPT\_NO\_COLOR")
|
||||
-- @tparam number value The money value to be converted in copper (100 copper per silver, 100 silver per gold)
|
||||
-- @tparam[opt] string color A color prefix to use for the numbers in the result (i.e. "|cff00ff00" for red)
|
||||
-- @param[opt] ... One or more options to modify the format of the result
|
||||
-- @return The string representation of the specified money value
|
||||
function Money.ToString(value, color, ...)
|
||||
value = tonumber(value)
|
||||
if not value then
|
||||
return
|
||||
end
|
||||
assert(not color or strmatch(color, "^\124cff[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$"))
|
||||
|
||||
-- parse the options
|
||||
local isIcon, trim, disabled, noCopper = false, false, false, false
|
||||
for i = 1, select('#', ...) do
|
||||
local opt = select(i, ...)
|
||||
if opt == nil then
|
||||
-- pass
|
||||
elseif opt == "OPT_ICON" then
|
||||
isIcon = true
|
||||
elseif opt == "OPT_TRIM" then
|
||||
trim = true
|
||||
elseif opt == "OPT_DISABLE" then
|
||||
disabled = true
|
||||
elseif opt == "OPT_83_NO_COPPER" then
|
||||
noCopper = not TSM.IsWowClassic()
|
||||
else
|
||||
error("Invalid option: "..tostring(opt))
|
||||
end
|
||||
end
|
||||
|
||||
local isNegative = value < 0
|
||||
value = abs(value)
|
||||
local gold = floor(value / COPPER_PER_GOLD)
|
||||
local silver = floor((value % COPPER_PER_GOLD) / COPPER_PER_SILVER)
|
||||
local copper = floor(value % COPPER_PER_SILVER)
|
||||
assert(not noCopper or copper == 0)
|
||||
local goldText, silverText, copperText = nil, nil, nil
|
||||
if isIcon then
|
||||
if disabled then
|
||||
goldText, silverText, copperText = GOLD_ICON_DISABLED, SILVER_ICON_DISABLED, COPPER_ICON_DISABLED
|
||||
else
|
||||
goldText, silverText, copperText = GOLD_ICON, SILVER_ICON, COPPER_ICON
|
||||
end
|
||||
else
|
||||
if disabled then
|
||||
goldText, silverText, copperText = GOLD_TEXT_DISABLED, SILVER_TEXT_DISABLED, COPPER_TEXT_DISABLED
|
||||
else
|
||||
goldText, silverText, copperText = GOLD_TEXT, SILVER_TEXT, COPPER_TEXT
|
||||
end
|
||||
end
|
||||
|
||||
if value == 0 then
|
||||
return private.FormatNumber(0, true, color)..(noCopper and silverText or copperText)
|
||||
end
|
||||
|
||||
wipe(private.textMoneyParts)
|
||||
-- add gold
|
||||
if gold > 0 then
|
||||
private.InsertMoneyPart(gold, color, goldText)
|
||||
end
|
||||
-- add silver
|
||||
if silver > 0 or (not trim and gold > 0) then
|
||||
private.InsertMoneyPart(silver, color, silverText)
|
||||
end
|
||||
-- add copper
|
||||
if copper > 0 or (not trim and not noCopper and (gold + silver) > 0) then
|
||||
private.InsertMoneyPart(copper, color, copperText)
|
||||
end
|
||||
local text = table.concat(private.textMoneyParts, " ")
|
||||
if isNegative then
|
||||
return (color and (color.."-|r") or "-")..text
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
--- Converts a string money value to a number value (in copper).
|
||||
-- The value passed to this function can contain colored text, but must use g/s/c for the denominations and not icons.
|
||||
-- @tparam string value The money value to be converted as a string
|
||||
-- @treturn string The numeric representation of the specified money value
|
||||
function Money.FromString(value)
|
||||
-- remove any colors
|
||||
value = gsub(gsub(strtrim(value), "\124c([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])", ""), "\124r", "")
|
||||
-- remove any separators
|
||||
value = gsub(value, String.Escape(LARGE_NUMBER_SEPERATOR), "")
|
||||
|
||||
-- extract gold/silver/copper values
|
||||
local gold = tonumber(strmatch(value, "([0-9]+)g"))
|
||||
local silver = tonumber(strmatch(value, "([0-9]+)s"))
|
||||
local copper = tonumber(strmatch(value, "([0-9]+)c"))
|
||||
if not gold and not silver and not copper then return end
|
||||
|
||||
-- test that there are no extra characters (other than spaces)
|
||||
value = gsub(value, "[0-9]+g", "", 1)
|
||||
value = gsub(value, "[0-9]+s", "", 1)
|
||||
value = gsub(value, "[0-9]+c", "", 1)
|
||||
if strtrim(value) ~= "" then return end
|
||||
|
||||
return ((gold or 0) * COPPER_PER_GOLD) + ((silver or 0) * COPPER_PER_SILVER) + (copper or 0)
|
||||
end
|
||||
|
||||
--- Returns the colored gold indicator text
|
||||
-- @treturn string The colored gold indicator text
|
||||
function Money.GetGoldText()
|
||||
return GOLD_TEXT
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.InsertMoneyPart(value, color, text)
|
||||
tinsert(private.textMoneyParts, private.FormatNumber(value, #private.textMoneyParts == 0, color)..text)
|
||||
end
|
||||
|
||||
function private.FormatNumber(num, isMostSignificant, color)
|
||||
if num < 10 and not isMostSignificant then
|
||||
num = "0"..num
|
||||
elseif isMostSignificant and num >= 1000 then
|
||||
num = tostring(num)
|
||||
local result = ""
|
||||
for i = 4, #num, 3 do
|
||||
result = LARGE_NUMBER_SEPERATOR..strsub(num, -(i - 1), -(i - 3))..result
|
||||
end
|
||||
result = strsub(num, 1, (#num % 3 == 0) and 3 or (#num % 3))..result
|
||||
num = result
|
||||
end
|
||||
|
||||
if color then
|
||||
return color..num.."|r"
|
||||
else
|
||||
return num
|
||||
end
|
||||
end
|
||||
215
LibTSM/Util/NineSlice.lua
Normal file
215
LibTSM/Util/NineSlice.lua
Normal file
@@ -0,0 +1,215 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- NineSlice Functions.
|
||||
-- @module NineSlice
|
||||
|
||||
local _, TSM = ...
|
||||
local NineSlice = TSM.Init("Util.NineSlice")
|
||||
local private = {
|
||||
styles = {},
|
||||
context = {},
|
||||
}
|
||||
local PART_INFO = {
|
||||
topLeft = {
|
||||
points = {
|
||||
{ "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
bottomLeft = {
|
||||
points = {
|
||||
{ "BOTTOMLEFT" },
|
||||
},
|
||||
},
|
||||
topRight = {
|
||||
points = {
|
||||
{ "TOPRIGHT" },
|
||||
},
|
||||
},
|
||||
bottomRight = {
|
||||
points = {
|
||||
{ "BOTTOMRIGHT" },
|
||||
},
|
||||
},
|
||||
left = {
|
||||
points = {
|
||||
{ "TOPLEFT", "topLeft", "BOTTOMLEFT" },
|
||||
{ "BOTTOMLEFT", "bottomLeft", "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
right = {
|
||||
points = {
|
||||
{ "TOPRIGHT", "topRight", "BOTTOMRIGHT" },
|
||||
{ "BOTTOMRIGHT", "bottomRight", "TOPRIGHT" },
|
||||
},
|
||||
},
|
||||
top = {
|
||||
points = {
|
||||
{ "TOPLEFT", "topLeft", "TOPRIGHT" },
|
||||
{ "TOPRIGHT", "topRight", "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
bottom = {
|
||||
points = {
|
||||
{ "BOTTOMLEFT", "bottomLeft", "BOTTOMRIGHT" },
|
||||
{ "BOTTOMRIGHT", "bottomRight", "BOTTOMLEFT" },
|
||||
},
|
||||
},
|
||||
center = {
|
||||
points = {
|
||||
{ "TOPLEFT", "topLeft", "BOTTOMRIGHT" },
|
||||
{ "BOTTOMRIGHT", "bottomRight", "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local NINE_SLICE_MT = {
|
||||
__index = {
|
||||
Hide = function(self)
|
||||
local context = private.context[self]
|
||||
for _, texture in pairs(context.parts) do
|
||||
texture:Hide()
|
||||
end
|
||||
end,
|
||||
SetStyle = function(self, key, inset)
|
||||
local context = private.context[self]
|
||||
local style = private.styles[key]
|
||||
for part, texture in pairs(context.parts) do
|
||||
local partStyle = style[part]
|
||||
if partStyle then
|
||||
texture:Show()
|
||||
else
|
||||
texture:Hide()
|
||||
end
|
||||
end
|
||||
if context.styleKey == key and context.inset == inset then
|
||||
return
|
||||
end
|
||||
context.styleKey = key
|
||||
context.inset = inset
|
||||
for part, texture in pairs(context.parts) do
|
||||
local partStyle = style[part]
|
||||
if partStyle then
|
||||
texture:ClearAllPoints()
|
||||
for i, point in ipairs(PART_INFO[part].points) do
|
||||
local anchor, relFrame, relAnchor, xOff, yOff = nil, nil, nil, 0, 0
|
||||
if partStyle.offset then
|
||||
xOff, yOff = unpack(partStyle.offset[i])
|
||||
end
|
||||
if #point == 1 then
|
||||
anchor = unpack(point)
|
||||
elseif #point == 3 then
|
||||
anchor, relFrame, relAnchor = unpack(point)
|
||||
relFrame = context.parts[relFrame]
|
||||
assert(relFrame)
|
||||
else
|
||||
error("Invalid point")
|
||||
end
|
||||
if relFrame then
|
||||
texture:SetPoint(anchor, relFrame, relAnchor, xOff, yOff)
|
||||
else
|
||||
if inset and xOff == 0 and strmatch(anchor, "LEFT") then
|
||||
xOff = inset
|
||||
elseif inset and xOff == 0 and strmatch(anchor, "RIGHT") then
|
||||
xOff = -inset
|
||||
end
|
||||
if inset and yOff == 0 and strmatch(anchor, "TOP") then
|
||||
yOff = -inset
|
||||
elseif inset and yOff == 0 and strmatch(anchor, "BOTTOM") then
|
||||
yOff = inset
|
||||
end
|
||||
texture:SetPoint(anchor, xOff, yOff)
|
||||
end
|
||||
end
|
||||
texture:SetSize(partStyle.width, partStyle.height)
|
||||
texture:SetTexture(partStyle.texture)
|
||||
texture:SetTexCoord(unpack(partStyle.coord))
|
||||
end
|
||||
end
|
||||
end,
|
||||
SetVertexColor = function(self, r, g, b, a)
|
||||
for part in pairs(PART_INFO) do
|
||||
self:SetPartVertexColor(part, r, g, b, a)
|
||||
end
|
||||
end,
|
||||
SetPartVertexColor = function(self, part, r, g, b, a)
|
||||
local context = private.context[self]
|
||||
context.parts[part]:SetVertexColor(r, g, b, a)
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("NineSlice cannot be modified") end,
|
||||
__tostring = function(self) return "NineSlice:"..strmatch(tostring(private.context[self]), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)") end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Registers a nine-slice style.
|
||||
-- @tparam string key The style key
|
||||
-- @tparam table info The style info table
|
||||
function NineSlice.RegisterStyle(key, info)
|
||||
assert(not private.styles[key])
|
||||
private.styles[key] = info
|
||||
for part in pairs(PART_INFO) do
|
||||
-- allowed to be missing the center part
|
||||
if part ~= "center" or info[part] ~= nil then
|
||||
assert(type(info[part].texture) == "string")
|
||||
assert(#info[part].coord == 4)
|
||||
assert(info[part].width > 0)
|
||||
assert(info[part].height > 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Create an nine-slice object.
|
||||
-- @tparam table frame The parent frame
|
||||
-- @tparam[opt=0] number subLayer The texture subLayer
|
||||
-- @treturn NineSlice The nine-slice object
|
||||
function NineSlice.New(frame, subLayer)
|
||||
local obj = setmetatable({}, NINE_SLICE_MT)
|
||||
local context = {
|
||||
frame = frame,
|
||||
parts = {},
|
||||
styleKey = nil,
|
||||
inset = nil,
|
||||
}
|
||||
private.context[obj] = context
|
||||
|
||||
-- create all the textures
|
||||
for part in pairs(PART_INFO) do
|
||||
local texture = frame:CreateTexture(nil, "BACKGROUND", nil, subLayer or 0)
|
||||
texture:SetBlendMode("BLEND")
|
||||
context.parts[part] = texture
|
||||
end
|
||||
|
||||
-- set the points for all the textures
|
||||
for part, info in pairs(PART_INFO) do
|
||||
for _, point in ipairs(info.points) do
|
||||
if #point == 1 then
|
||||
context.parts[part]:SetPoint(unpack(point))
|
||||
elseif #point == 3 then
|
||||
local anchor, relFrame, relAnchor = unpack(point)
|
||||
relFrame = context.parts[relFrame]
|
||||
assert(relFrame)
|
||||
context.parts[part]:SetPoint(anchor, relFrame, relAnchor)
|
||||
else
|
||||
error("Invalid point")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
120
LibTSM/Util/ObjectPool.lua
Normal file
120
LibTSM/Util/ObjectPool.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- ObjectPool Functions.
|
||||
-- @module ObjectPool
|
||||
|
||||
local _, TSM = ...
|
||||
local ObjectPool = TSM.Init("Util.ObjectPool")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local private = {
|
||||
debugLeaks = TSM.__IS_TEST_ENV or false,
|
||||
instances = {},
|
||||
context = {},
|
||||
}
|
||||
local DEBUG_STATS_MIN_COUNT = 1
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local OBJECT_POOL_MT = {
|
||||
__index = {
|
||||
Get = function(self)
|
||||
local context = private.context[self]
|
||||
local obj = tremove(context.freeList)
|
||||
if not obj then
|
||||
context.numCreated = context.numCreated + 1
|
||||
obj = context.createFunc()
|
||||
assert(obj)
|
||||
end
|
||||
if private.debugLeaks then
|
||||
context.state[obj] = (Debug.GetStackLevelLocation(2 + context.extraStackOffset) or "?").." -> "..(Debug.GetStackLevelLocation(3 + context.extraStackOffset) or "?")
|
||||
else
|
||||
context.state[obj] = "???"
|
||||
end
|
||||
return obj
|
||||
end,
|
||||
Recycle = function(self, obj)
|
||||
local context = private.context[self]
|
||||
assert(context.state[obj])
|
||||
context.state[obj] = nil
|
||||
tinsert(context.freeList, obj)
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("Object pool cannot be modified") end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new object pool.
|
||||
-- @tparam string name The name of the object pool for debug purposes
|
||||
-- @tparam function createFunc The function which is called to create a new object
|
||||
-- @tparam[opt=0] number extraStackOffset The extra stack offset for tracking where objects are being used from or nil to disable stack info
|
||||
-- @treturn ObjectPool The object pool object
|
||||
function ObjectPool.New(name, createFunc, extraStackOffset)
|
||||
assert(createFunc)
|
||||
assert(not private.instances[name])
|
||||
local pool = setmetatable({}, OBJECT_POOL_MT)
|
||||
private.context[pool] = {
|
||||
createFunc = createFunc,
|
||||
extraStackOffset = extraStackOffset or 0,
|
||||
freeList = {},
|
||||
state = {},
|
||||
numCreated = 0,
|
||||
}
|
||||
private.instances[name] = private.context[pool]
|
||||
return pool
|
||||
end
|
||||
|
||||
function ObjectPool.EnableLeakDebug()
|
||||
private.debugLeaks = true
|
||||
end
|
||||
|
||||
function ObjectPool.GetDebugInfo()
|
||||
local debugInfo = {}
|
||||
for name, context in pairs(private.instances) do
|
||||
local numCreated, numInUse, info = private.GetDebugStats(context)
|
||||
debugInfo[name] = {
|
||||
numCreated = numCreated,
|
||||
numInUse = numInUse,
|
||||
info = info,
|
||||
}
|
||||
end
|
||||
return debugInfo
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetDebugStats(context)
|
||||
local counts = {}
|
||||
local totalCount = 0
|
||||
for _, caller in pairs(context.state) do
|
||||
counts[caller] = (counts[caller] or 0) + 1
|
||||
totalCount = totalCount + 1
|
||||
end
|
||||
local debugInfo = {}
|
||||
for info, count in pairs(counts) do
|
||||
if count > DEBUG_STATS_MIN_COUNT then
|
||||
tinsert(debugInfo, format("[%d] %s", count, info))
|
||||
end
|
||||
end
|
||||
if #debugInfo == 0 then
|
||||
tinsert(debugInfo, "<none>")
|
||||
end
|
||||
return context.numCreated, totalCount, debugInfo
|
||||
end
|
||||
92
LibTSM/Util/ScriptWrapper.lua
Normal file
92
LibTSM/Util/ScriptWrapper.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ScriptWrapper = TSM.Init("Util.ScriptWrapper")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
handlers = {},
|
||||
objLookup = {},
|
||||
wrappers = {},
|
||||
propagateWrappers = {},
|
||||
nestedLevel = 0,
|
||||
}
|
||||
local SCRIPT_CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Sets the handler for a script on a frame.
|
||||
-- @tparam table frame The frame to call SetScript() on
|
||||
-- @tparam string script The script to set
|
||||
-- @tparam function handler The handler to set
|
||||
-- @tparam[opt=frame] table obj The object to pass to the handler as the first parameter (instead of frame)
|
||||
function ScriptWrapper.Set(frame, script, handler, obj)
|
||||
assert(type(frame) == "table" and type(script) == "string" and type(handler) == "function")
|
||||
local key = private.GetFrameScriptKey(frame, script)
|
||||
private.handlers[key] = handler
|
||||
private.objLookup[key] = obj or frame
|
||||
if not private.wrappers[script] then
|
||||
private.wrappers[script] = function(...)
|
||||
private.ScriptHandlerCommon(script, ...)
|
||||
end
|
||||
end
|
||||
frame:SetScript(script, private.wrappers[script])
|
||||
end
|
||||
|
||||
--- Sets the script handler to simply propogate the script to the parent element.
|
||||
-- @tparam table frame The frame to call SetScript() on
|
||||
-- @tparam string script The script which should be propagated
|
||||
-- @tparam[opt=frame] table obj The object to pass to the handler as the first parameter (instead of frame)
|
||||
function ScriptWrapper.SetPropagate(frame, script, obj)
|
||||
if not private.propagateWrappers[script] then
|
||||
private.propagateWrappers[script] = function(f, ...)
|
||||
local parentFrame = f:GetParent()
|
||||
local parentScript = parentFrame:GetScript(script)
|
||||
if not parentScript then
|
||||
return
|
||||
end
|
||||
parentScript(parentFrame, ...)
|
||||
end
|
||||
end
|
||||
ScriptWrapper.Set(frame, script, private.propagateWrappers[script], obj)
|
||||
end
|
||||
|
||||
--- Clears a previously-registered script handler.
|
||||
-- @tparam table frame The frame to clear the scrip ton
|
||||
-- @tparam string script The script which should be cleared
|
||||
function ScriptWrapper.Clear(frame, script)
|
||||
local key = private.GetFrameScriptKey(frame, script)
|
||||
private.handlers[key] = nil
|
||||
private.objLookup[key] = nil
|
||||
frame:SetScript(script, nil)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetFrameScriptKey(frame, script)
|
||||
return tostring(frame)..":"..script
|
||||
end
|
||||
|
||||
function private.ScriptHandlerCommon(script, frame, ...)
|
||||
local key = private.GetFrameScriptKey(frame, script)
|
||||
local obj = private.objLookup[key]
|
||||
private.nestedLevel = private.nestedLevel + 1
|
||||
local startTime = debugprofilestop()
|
||||
private.handlers[key](obj, ...)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
private.nestedLevel = private.nestedLevel - 1
|
||||
if private.nestedLevel == 0 and timeTaken > SCRIPT_CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Script handler (%s) for frame (%s) took %0.2fms", script, tostring(obj), timeTaken)
|
||||
end
|
||||
end
|
||||
36
LibTSM/Util/SlotId.lua
Normal file
36
LibTSM/Util/SlotId.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- SlotId Functions
|
||||
-- @module SlotId
|
||||
|
||||
local _, TSM = ...
|
||||
local SlotId = TSM.Init("Util.SlotId")
|
||||
local SLOT_ID_MULTIPLIER = 1000
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Combines a container and slot into a slotId.
|
||||
-- @tparam number container The container
|
||||
-- @tparam number slot The slot
|
||||
-- @treturn number The slotId
|
||||
function SlotId.Join(container, slot)
|
||||
return container * SLOT_ID_MULTIPLIER + slot
|
||||
end
|
||||
|
||||
--- Splits a slotId into a container and slot
|
||||
-- @tparam number slotId The slotId
|
||||
-- @treturn number container The container
|
||||
-- @treturn number slot The slot
|
||||
function SlotId.Split(slotId)
|
||||
local container = floor(slotId / SLOT_ID_MULTIPLIER)
|
||||
local slot = slotId % SLOT_ID_MULTIPLIER
|
||||
return container, slot
|
||||
end
|
||||
217
LibTSM/Util/SmartMap.lua
Normal file
217
LibTSM/Util/SmartMap.lua
Normal file
@@ -0,0 +1,217 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Smart Map.
|
||||
-- @module SmartMap
|
||||
|
||||
local _, TSM = ...
|
||||
local SmartMap = TSM.Init("Util.SmartMap")
|
||||
local private = {
|
||||
mapContext = {},
|
||||
readerContext = {},
|
||||
}
|
||||
local VALID_FIELD_TYPES = {
|
||||
string = true,
|
||||
number = true,
|
||||
boolean = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable Methods
|
||||
-- ============================================================================
|
||||
|
||||
local SMART_MAP_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
if key == nil then
|
||||
error("Attempt to get nil key")
|
||||
end
|
||||
if key == "ValueChanged" then
|
||||
return private.MapValueChanged
|
||||
elseif key == "SetCallbacksPaused" then
|
||||
return private.MapSetCallbacksPaused
|
||||
elseif key == "CreateReader" then
|
||||
return private.MapCreateReader
|
||||
elseif key == "GetKeyType" then
|
||||
return private.MapGetKeyType
|
||||
elseif key == "GetValueType" then
|
||||
return private.MapGetValueType
|
||||
elseif key == "Iterator" then
|
||||
return private.MapIterator
|
||||
else
|
||||
error("Invalid map method: "..tostring(key), 2)
|
||||
end
|
||||
end,
|
||||
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Map cannot be written to directly", 2)
|
||||
end,
|
||||
|
||||
__tostring = function(self)
|
||||
return "SmartMap:"..strmatch(tostring(private.mapContext[self]), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")
|
||||
end,
|
||||
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
local READER_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
-- check if the map already has the value for this key cached
|
||||
local readerContext = private.readerContext[self]
|
||||
local map = readerContext.map
|
||||
local mapContext = private.mapContext[map]
|
||||
if mapContext.data[key] ~= nil then
|
||||
return mapContext.data[key]
|
||||
end
|
||||
|
||||
-- get the value for this key
|
||||
local value = mapContext.func(key)
|
||||
if value == nil then
|
||||
error(format("No value for key (%s)", tostring(key)))
|
||||
elseif type(value) ~= mapContext.valueType then
|
||||
error(format("Invalid type of value (got %s, expected %s): %s", type(value), mapContext.valueType, tostring(value)))
|
||||
end
|
||||
|
||||
-- cache the value both on the map and on this reader
|
||||
mapContext.data[key] = value
|
||||
rawset(self, key, value)
|
||||
|
||||
return value
|
||||
end,
|
||||
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Reader is read-only", 2)
|
||||
end,
|
||||
|
||||
__tostring = function(self)
|
||||
return "SmartMapReader:"..strmatch(tostring(private.readerContext[self]), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")
|
||||
end,
|
||||
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function SmartMap.New(keyType, valueType, callable)
|
||||
assert(VALID_FIELD_TYPES[keyType] and VALID_FIELD_TYPES[valueType])
|
||||
local map = setmetatable({}, SMART_MAP_MT)
|
||||
private.mapContext[map] = {
|
||||
keyType = keyType,
|
||||
valueType = valueType,
|
||||
func = callable,
|
||||
data = {},
|
||||
readers = {},
|
||||
callbacksPaused = 0,
|
||||
hasReaderCallback = false,
|
||||
}
|
||||
return map
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.MapValueChanged(self, key)
|
||||
local mapContext = private.mapContext[self]
|
||||
local oldValue = mapContext.data[key]
|
||||
if oldValue == nil then
|
||||
-- nobody cares about this value
|
||||
return
|
||||
end
|
||||
|
||||
if not mapContext.hasReaderCallback then
|
||||
-- no reader has registered a callback, so just clear the value
|
||||
mapContext.data[key] = nil
|
||||
for _, reader in ipairs(mapContext.readers) do
|
||||
rawset(reader, key, nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- get the new value
|
||||
local newValue = mapContext.func(key)
|
||||
if type(newValue) ~= mapContext.valueType then
|
||||
error(format("Invalid type (got %s, expected %s)", type(newValue), mapContext.valueType))
|
||||
end
|
||||
if oldValue == newValue then
|
||||
-- the value didn't change
|
||||
return
|
||||
end
|
||||
|
||||
-- update the data
|
||||
mapContext.data[key] = newValue
|
||||
|
||||
for _, reader in ipairs(mapContext.readers) do
|
||||
local readerContext = private.readerContext[reader]
|
||||
local prevValue = rawget(reader, key)
|
||||
if prevValue ~= nil then
|
||||
rawset(reader, key, newValue)
|
||||
if readerContext.callback then
|
||||
readerContext.pendingChanges[key] = prevValue
|
||||
if mapContext.callbacksPaused == 0 then
|
||||
readerContext.callback(reader, readerContext.pendingChanges)
|
||||
wipe(readerContext.pendingChanges)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.MapSetCallbacksPaused(self, paused)
|
||||
local mapContext = private.mapContext[self]
|
||||
if paused then
|
||||
mapContext.callbacksPaused = mapContext.callbacksPaused + 1
|
||||
else
|
||||
mapContext.callbacksPaused = mapContext.callbacksPaused - 1
|
||||
assert(mapContext.callbacksPaused >= 0)
|
||||
if mapContext.callbacksPaused == 0 then
|
||||
for _, reader in ipairs(mapContext.readers) do
|
||||
local readerContext = private.readerContext[reader]
|
||||
if readerContext.callback and next(readerContext.pendingChanges) then
|
||||
readerContext.callback(reader, readerContext.pendingChanges)
|
||||
wipe(readerContext.pendingChanges)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.MapCreateReader(self, callback)
|
||||
assert(callback == nil or type(callback) == "function")
|
||||
local reader = setmetatable({}, READER_MT)
|
||||
local mapContext = private.mapContext[self]
|
||||
tinsert(mapContext.readers, reader)
|
||||
mapContext.hasReaderCallback = mapContext.hasReaderCallback or (callback and true or false)
|
||||
private.readerContext[reader] = {
|
||||
map = self,
|
||||
callback = callback,
|
||||
pendingChanges = {},
|
||||
}
|
||||
return reader
|
||||
end
|
||||
|
||||
function private.MapGetKeyType(self)
|
||||
return private.mapContext[self].keyType
|
||||
end
|
||||
|
||||
function private.MapGetValueType(self)
|
||||
return private.mapContext[self].valueType
|
||||
end
|
||||
|
||||
function private.MapIterator(self)
|
||||
return pairs(private.mapContext[self].data)
|
||||
end
|
||||
84
LibTSM/Util/Sound.lua
Normal file
84
LibTSM/Util/Sound.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Sound Functions
|
||||
-- @module Sound
|
||||
|
||||
local _, TSM = ...
|
||||
local Sound = TSM.Init("Util.Sound")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local NO_SOUND_KEY = "TSM_NO_SOUND" -- this can never change
|
||||
local SOUNDS = {
|
||||
[NO_SOUND_KEY] = "<"..L["No Sound"]..">",
|
||||
["AuctionWindowOpen"] = L["Auction Window Open"],
|
||||
["AuctionWindowClose"] = L["Auction Window Close"],
|
||||
["alarmclockwarning3"] = L["Alarm Clock"],
|
||||
["UI_AutoQuestComplete"] = L["Auto Quest Complete"],
|
||||
["TSM_CASH_REGISTER"] = L["Cash Register"],
|
||||
["HumanExploration"] = L["Exploration"],
|
||||
["Fishing Reel in"] = L["Fishing Reel In"],
|
||||
["LevelUp"] = L["Level Up"],
|
||||
["MapPing"] = L["Map Ping"],
|
||||
["MONEYFRAMEOPEN"] = L["Money Frame Open"],
|
||||
["IgPlayerInviteAccept"] = L["Player Invite Accept"],
|
||||
["QUESTADDED"] = L["Quest Added"],
|
||||
["QUESTCOMPLETED"] = L["Quest Completed"],
|
||||
["UI_QuestObjectivesComplete"] = L["Quest Objectives Complete"],
|
||||
["RaidWarning"] = L["Raid Warning"],
|
||||
["ReadyCheck"] = L["Ready Check"],
|
||||
["UnwrapGift"] = L["Unwrap Gift"],
|
||||
}
|
||||
local SOUNDKITIDS = {
|
||||
["AuctionWindowOpen"] = 5274,
|
||||
["AuctionWindowClose"] = 5275,
|
||||
["alarmclockwarning3"] = 12889,
|
||||
["UI_AutoQuestComplete"] = 23404,
|
||||
["HumanExploration"] = 4140,
|
||||
["Fishing Reel in"] = 3407,
|
||||
["LevelUp"] = 888,
|
||||
["MapPing"] = 3175,
|
||||
["MONEYFRAMEOPEN"] = 891,
|
||||
["IgPlayerInviteAccept"] = 880,
|
||||
["QUESTADDED"] = 618,
|
||||
["QUESTCOMPLETED"] = 878,
|
||||
["UI_QuestObjectivesComplete"] = 26905,
|
||||
["RaidWarning"] = 8959,
|
||||
["ReadyCheck"] = 8960,
|
||||
["UnwrapGift"] = 64329,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Gets the key used to represent no sound.
|
||||
-- @return The key used to represent no sound
|
||||
function Sound.GetNoSoundKey()
|
||||
return NO_SOUND_KEY
|
||||
end
|
||||
|
||||
--- Gets the key-value table containing all supported sounds.
|
||||
-- The key is the what gets passed to @{Sound.PlaySound} and the value is a localized string describing the sound.
|
||||
-- @return The sounds table
|
||||
function Sound.GetSounds()
|
||||
return SOUNDS
|
||||
end
|
||||
|
||||
--- Plays a sound and flashes the client icon.
|
||||
-- @param soundKey The key of the sound (from @{Sound.GetSounds}) to play
|
||||
function Sound.PlaySound(soundKey)
|
||||
if soundKey == NO_SOUND_KEY then
|
||||
-- do nothing
|
||||
elseif soundKey == "TSM_CASH_REGISTER" then
|
||||
PlaySoundFile("Interface\\Addons\\TradeSkillMaster\\Media\\register.mp3", "Master")
|
||||
FlashClientIcon()
|
||||
else
|
||||
PlaySound(SOUNDKITIDS[soundKey], "Master")
|
||||
FlashClientIcon()
|
||||
end
|
||||
end
|
||||
93
LibTSM/Util/String.lua
Normal file
93
LibTSM/Util/String.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- String Functions
|
||||
-- @module String
|
||||
|
||||
local _, TSM = ...
|
||||
local String = TSM.Init("Util.String")
|
||||
local MAGIC_CHARACTERS = {
|
||||
["["] = true,
|
||||
["]"] = true,
|
||||
["("] = true,
|
||||
[")"] = true,
|
||||
["."] = true,
|
||||
["+"] = true,
|
||||
["-"] = true,
|
||||
["*"] = true,
|
||||
["?"] = true,
|
||||
["^"] = true,
|
||||
["$"] = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Splits a string in a way which won't cause stack overflows for large inputs.
|
||||
-- The lua strsplit function causes a stack overflow if passed large inputs. This API fixes that issue and also supports
|
||||
-- separators which are more than one character in length.
|
||||
-- @tparam string str The string to be split
|
||||
-- @tparam string sep The separator to use to split the string
|
||||
-- @tparam[opt=nil] table resultTbl An optional table to store the result in
|
||||
-- @treturn table The result as a list of substrings
|
||||
function String.SafeSplit(str, sep, resultTbl)
|
||||
resultTbl = resultTbl or {}
|
||||
local s = 1
|
||||
local sepLength = #sep
|
||||
if sepLength == 0 then
|
||||
tinsert(resultTbl, str)
|
||||
return resultTbl
|
||||
end
|
||||
while true do
|
||||
local e = strfind(str, sep, s)
|
||||
if not e then
|
||||
tinsert(resultTbl, strsub(str, s))
|
||||
break
|
||||
end
|
||||
tinsert(resultTbl, strsub(str, s, e - 1))
|
||||
s = e + sepLength
|
||||
end
|
||||
return resultTbl
|
||||
end
|
||||
|
||||
--- Escapes any magic characters used by lua's pattern matching.
|
||||
-- @tparam string str The string to be escaped
|
||||
-- @treturn string The escaped string
|
||||
function String.Escape(str)
|
||||
assert(not strmatch(str, "\001"), "Input string must not contain '\\001' characters")
|
||||
str = gsub(str, "%%", "\001")
|
||||
for char in pairs(MAGIC_CHARACTERS) do
|
||||
str = gsub(str, "%"..char, "%%"..char)
|
||||
end
|
||||
str = gsub(str, "\001", "%%%%")
|
||||
return str
|
||||
end
|
||||
|
||||
--- Check if a string which contains multiple values separated by a specific string contains the value.
|
||||
-- @tparam string str The string to be searched
|
||||
-- @tparam string sep The separating string
|
||||
-- @tparam string value The value to search for
|
||||
-- @treturn boolean Whether or not the value was found
|
||||
-- @within String
|
||||
function String.SeparatedContains(str, sep, value)
|
||||
return str == value or strmatch(str, "^"..value..sep) or strmatch(str, sep..value..sep) or strmatch(str, sep..value.."$")
|
||||
end
|
||||
|
||||
--- Iterates over the parts of a string which are separated by a character.
|
||||
-- @tparam string str The string to be split
|
||||
-- @tparam string sep The separator to use to split the string
|
||||
-- @return An iterator with fields: `part`
|
||||
-- @within String
|
||||
function String.SplitIterator(str, sep)
|
||||
assert(#sep == 1)
|
||||
if MAGIC_CHARACTERS[sep] then
|
||||
sep = "%"..sep
|
||||
end
|
||||
return gmatch(str, "([^"..sep.."]+)")
|
||||
end
|
||||
484
LibTSM/Util/Table.lua
Normal file
484
LibTSM/Util/Table.lua
Normal file
@@ -0,0 +1,484 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Table Functions
|
||||
-- @module Table
|
||||
|
||||
local _, TSM = ...
|
||||
local Table = TSM.Init("Util.Table")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local private = {
|
||||
filterTemp = {},
|
||||
sortValueLookup = nil,
|
||||
sortValueReverse = false,
|
||||
sortValueUnstable = false,
|
||||
iterContext = { arg = {}, index = {}, helperFunc = {}, cleanupFunc = {} },
|
||||
inwardIteratorContext = {},
|
||||
}
|
||||
setmetatable(private.iterContext.arg, { __mode = "k" })
|
||||
setmetatable(private.iterContext.index, { __mode = "k" })
|
||||
setmetatable(private.iterContext.helperFunc, { __mode = "k" })
|
||||
setmetatable(private.iterContext.cleanupFunc, { __mode = "k" })
|
||||
local READ_ONLY_TABLE_MT = {
|
||||
__index = function(_, key) error(format("Key (%s) does not exist in read-only table", tostring(key)), 2) end,
|
||||
__newindex = function(_, key) error(format("Writing (%s) to read-only table", tostring(key)), 2) end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Creates an iterator from a table.
|
||||
-- NOTE: This iterator must be run to completion and not interrupted (i.e. with a `break` or `return`).
|
||||
-- @tparam table tbl The table (numerically-indexed) to iterate over
|
||||
-- @tparam[opt] function helperFunc A helper function which gets passed the current index, value, and user-specified arg
|
||||
-- and returns nothing if an entry in the table should be skipped or the result of an iteration loop
|
||||
-- @param[opt] arg A value to be passed to the helper function
|
||||
-- @tparam[opt] function cleanupFunc A function to be called (passed `tbl`) to cleanup at the end of iterator
|
||||
-- @return An iterator with fields: `index, value` or the return of `helperFunc`
|
||||
function Table.Iterator(tbl, helperFunc, arg, cleanupFunc)
|
||||
local iterContext = TempTable.Acquire()
|
||||
iterContext.data = tbl
|
||||
iterContext.arg = arg
|
||||
iterContext.index = 0
|
||||
iterContext.helperFunc = helperFunc
|
||||
iterContext.cleanupFunc = cleanupFunc
|
||||
return private.TableIterator, iterContext
|
||||
end
|
||||
|
||||
--- Creates an iterator from the keys of a table.
|
||||
-- @tparam table tbl The table to iterate over the keys of
|
||||
-- @return An iterator with fields: `key`
|
||||
function Table.KeyIterator(tbl)
|
||||
return private.TableKeyIterator, tbl, nil
|
||||
end
|
||||
|
||||
--- Uses a function to filter the entries in a table.
|
||||
-- @tparam table tbl The table to be filtered
|
||||
-- @tparam function func The filter function which gets passed `key, value, ...` and returns true if that entry should
|
||||
-- be removed from the table
|
||||
-- @param[opt] ... Optional arguments to be passed to the filter function
|
||||
function Table.Filter(tbl, func, ...)
|
||||
assert(not next(private.filterTemp))
|
||||
for k, v in pairs(tbl) do
|
||||
if func(k, v, ...) then
|
||||
tinsert(private.filterTemp, k)
|
||||
end
|
||||
end
|
||||
for _, k in ipairs(private.filterTemp) do
|
||||
tbl[k] = nil
|
||||
end
|
||||
wipe(private.filterTemp)
|
||||
end
|
||||
|
||||
--- Removes all occurences of the value in the table.
|
||||
-- Only the numerically-indexed entries are checked.
|
||||
-- @tparam table tbl The table to remove the value from
|
||||
-- @param value The value to remove
|
||||
-- @treturn number The number of values removed
|
||||
function Table.RemoveByValue(tbl, value)
|
||||
local numRemoved = 0
|
||||
for i = #tbl, 1, -1 do
|
||||
if tbl[i] == value then
|
||||
tremove(tbl, i)
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
end
|
||||
return numRemoved
|
||||
end
|
||||
|
||||
--- Gets the table key by value.
|
||||
-- @tparam table tbl The table to look through
|
||||
-- @param value The value to get the key of
|
||||
-- @return The key for the specified value or `nil`
|
||||
function Table.KeyByValue(tbl, value)
|
||||
for k, v in pairs(tbl) do
|
||||
if v == value then
|
||||
return k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets the number of entries in the table.
|
||||
-- This can be used when the count of a non-numerically-indexed table is desired (i.e. `#tbl` wouldn't work).
|
||||
-- @tparam table tbl The table to get the number of entries in
|
||||
-- @treturn number The number of entries
|
||||
function Table.Count(tbl)
|
||||
local count = 0
|
||||
for _ in pairs(tbl) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--- Gets the distinct table key by value.
|
||||
-- This function will assert if the value is not found in the table or if more than one key is found.
|
||||
-- @tparam table tbl The table to look through
|
||||
-- @param value The value to get the key of
|
||||
-- @return The key for the specified value
|
||||
function Table.GetDistinctKey(tbl, value)
|
||||
local key = nil
|
||||
for k, v in pairs(tbl) do
|
||||
if v == value then
|
||||
assert(not key)
|
||||
key = k
|
||||
end
|
||||
end
|
||||
assert(key)
|
||||
return key
|
||||
end
|
||||
|
||||
--- Checks if two tables have the same entries (non-recursively).
|
||||
-- @tparam table tbl1 The first table to check
|
||||
-- @tparam table tbl2 The second table to check
|
||||
-- @treturn boolean Whether or not the tables are equal
|
||||
function Table.Equal(tbl1, tbl2)
|
||||
if Table.Count(tbl1) ~= Table.Count(tbl2) then
|
||||
return false
|
||||
end
|
||||
for k, v in pairs(tbl1) do
|
||||
if tbl2[k] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Returns whether or not the table is currently sorted.
|
||||
-- @tparam table tbl The table to check
|
||||
-- @tparam[opt] function sortFunc The helper function to use to determine sort order (same prototype as is used for `sort()`)
|
||||
-- @tparam[opt=1] number firstIndex The first index to check
|
||||
-- @tparam[opt=#tbl] number lastIndex The last index to check
|
||||
-- @treturn boolean Whether or not the table is sorted
|
||||
function Table.IsSorted(tbl, sortFunc, firstIndex, lastIndex)
|
||||
sortFunc = sortFunc or private.DefaultSortFunc
|
||||
firstIndex = firstIndex or 1
|
||||
lastIndex = lastIndex or #tbl
|
||||
local prevValue = tbl[firstIndex]
|
||||
for i = firstIndex + 1, lastIndex do
|
||||
local value = tbl[i]
|
||||
if sortFunc(value, prevValue) then
|
||||
return false
|
||||
end
|
||||
prevValue = value
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Sorts a table with some optimizations over lua's sort().
|
||||
-- @tparam table tbl The table to sort
|
||||
-- @tparam[opt] function sortFunc The helper function to use to determine sort order (same prototype as is used for `sort()`)
|
||||
function Table.Sort(tbl, sortFunc)
|
||||
if Table.IsSorted(tbl, sortFunc) then
|
||||
return
|
||||
end
|
||||
sort(tbl, sortFunc)
|
||||
end
|
||||
|
||||
--- Returns whether or not the table is currently sorted with a value lookup table.
|
||||
-- @tparam table tbl The table to sort
|
||||
-- @tparam table valueLookup The sort value lookup table
|
||||
-- @tparam[opt=1] number firstIndex The first index to check
|
||||
-- @tparam[opt=#tbl] number lastIndex The last index to check
|
||||
function Table.IsSortedWithValueLookup(tbl, valueLookup, firstIndex, lastIndex)
|
||||
assert(not private.sortValueLookup and valueLookup)
|
||||
private.sortValueLookup = valueLookup
|
||||
private.sortValueReverse = false
|
||||
private.sortValueUnstable = false
|
||||
local result = Table.IsSorted(tbl, private.TableSortWithValueLookupHelper, firstIndex, lastIndex)
|
||||
private.sortValueLookup = nil
|
||||
return result
|
||||
end
|
||||
|
||||
--- Merges two sorted tables with a value lookup table.
|
||||
-- @tparam table tbl1 The first table to merge
|
||||
-- @tparam table tbl2 The second table to merge
|
||||
-- @tparam table result The result table
|
||||
-- @tparam table valueLookup The sort value lookup table
|
||||
function Table.MergeSortedWithValueLookup(tbl1, tbl2, result, valueLookup)
|
||||
assert(not private.sortValueLookup and valueLookup)
|
||||
private.sortValueLookup = valueLookup
|
||||
private.sortValueReverse = false
|
||||
private.sortValueUnstable = false
|
||||
|
||||
local index1, index2, resultIndex = 1, 1, 1
|
||||
while true do
|
||||
local value1 = tbl1[index1]
|
||||
local value2 = tbl2[index2]
|
||||
if value1 == nil and value2 == nil then
|
||||
-- we're done
|
||||
break
|
||||
elseif value1 == nil then
|
||||
result[resultIndex] = value2
|
||||
index2 = index2 + 1
|
||||
elseif value2 == nil then
|
||||
result[resultIndex] = value1
|
||||
index1 = index1 + 1
|
||||
elseif private.TableSortWithValueLookupHelper(value1, value2) then
|
||||
result[resultIndex] = value1
|
||||
index1 = index1 + 1
|
||||
else
|
||||
result[resultIndex] = value2
|
||||
index2 = index2 + 1
|
||||
end
|
||||
resultIndex = resultIndex + 1
|
||||
end
|
||||
private.sortValueLookup = nil
|
||||
end
|
||||
|
||||
--- Does a table sort with an extra value lookup step.
|
||||
-- @tparam table tbl The table to sort
|
||||
-- @tparam table valueLookup The sort value lookup table
|
||||
-- @tparam[opt=false] boolean reverse Reverse the sort order
|
||||
-- @tparam[opt=false] boolean unstable Don't try to make the sort stable
|
||||
function Table.SortWithValueLookup(tbl, valueLookup, reverse, unstable)
|
||||
assert(not private.sortValueLookup and valueLookup)
|
||||
private.sortValueLookup = valueLookup
|
||||
private.sortValueReverse = reverse
|
||||
private.sortValueUnstable = unstable
|
||||
Table.Sort(tbl, private.TableSortWithValueLookupHelper)
|
||||
private.sortValueLookup = nil
|
||||
end
|
||||
|
||||
--- Creates an iterator which iterates through a numerically-indexed table (list) from the ends inward.
|
||||
-- @tparam table tbl The table to iterate over
|
||||
-- @return An iterator with fields: `index`, `value`, `isAscending`
|
||||
function Table.InwardIterator(tbl)
|
||||
assert(not private.inwardIteratorContext[tbl])
|
||||
local context = TempTable.Acquire()
|
||||
private.inwardIteratorContext[tbl] = context
|
||||
context.inUse = true
|
||||
context.tbl = tbl
|
||||
context.leftIndex = 1
|
||||
context.rightIndex = #tbl
|
||||
context.isAscending = true
|
||||
return private.InwardIteratorHelper, context, 0
|
||||
end
|
||||
|
||||
--- Reverses the direction of the current inward iterator.
|
||||
-- @tparam table tbl The table being iterated over
|
||||
function Table.InwardIteratorReverse(tbl)
|
||||
local context = private.inwardIteratorContext[tbl]
|
||||
assert(context and context.tbl == tbl)
|
||||
context.isAscending = not context.isAscending
|
||||
end
|
||||
|
||||
--- Sets a table as read-only (modifications aren't checked).
|
||||
-- @tparam table tbl The table to make read-only
|
||||
function Table.SetReadOnly(tbl)
|
||||
setmetatable(tbl, READ_ONLY_TABLE_MT)
|
||||
end
|
||||
|
||||
--- Appends all values passed in to the end of the table.
|
||||
-- @tparam table tbl The table to insert the data into
|
||||
-- @param ... The values to insert
|
||||
function Table.Append(tbl, ...)
|
||||
local len = #tbl
|
||||
for i = 1, select("#", ...) do
|
||||
tbl[len + i] = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
--- Performs a binary search on a sorted table and returns the index of the search value.
|
||||
-- @tparam table tbl The table to search
|
||||
-- @tparam number|string searchValue The value to search for
|
||||
-- @tparam[opt=nil] function valueFunc A function to call to get the value to compare
|
||||
-- @param ... Extra values to pass to valueFunc
|
||||
-- @treturn ?number The index of the value or nil if it wasn't found
|
||||
-- @treturn ?number The insert index
|
||||
function Table.BinarySearch(tbl, searchValue, valueFunc, ...)
|
||||
if valueFunc then
|
||||
searchValue = valueFunc(searchValue, ...)
|
||||
end
|
||||
local insertIndex = 1
|
||||
local low, mid, high = 1, 0, #tbl
|
||||
while low <= high do
|
||||
mid = floor((low + high) / 2)
|
||||
local value = tbl[mid]
|
||||
if valueFunc then
|
||||
value = valueFunc(tbl[mid], ...)
|
||||
end
|
||||
if value == searchValue then
|
||||
return mid, mid
|
||||
elseif value < searchValue then
|
||||
-- we're too low
|
||||
low = mid + 1
|
||||
else
|
||||
-- we're too high
|
||||
high = mid - 1
|
||||
end
|
||||
insertIndex = low
|
||||
end
|
||||
return nil, insertIndex
|
||||
end
|
||||
|
||||
--- Inserts a value into a sorted table by using the insertIndex returned by Table.BinarySearch().
|
||||
-- @see Table.BinarySearch
|
||||
-- @tparam table tbl The table
|
||||
-- @tparam number|string value The value to insert
|
||||
-- @tparam[opt=nil] function valueFunc A function to call to get the value to compare
|
||||
-- @param ... Extra values to pass to valueFunc
|
||||
function Table.InsertSorted(tbl, value, valueFunc, ...)
|
||||
local _, insertIndex = Table.BinarySearch(tbl, value, valueFunc, ...)
|
||||
tinsert(tbl, insertIndex, value)
|
||||
end
|
||||
|
||||
--- Gets the common values from two or more sorted tables.
|
||||
-- @tparam table tbls The tables to compare
|
||||
-- @tparam table result The result table
|
||||
-- @tparam[opt=nil] function valueFunc A function to call to get the value to compare
|
||||
-- @param ... Extra values to pass to valueFunc
|
||||
function Table.GetCommonValuesSorted(tbls, result, valueFunc, ...)
|
||||
local numTbls = #tbls
|
||||
if numTbls == 0 then
|
||||
return
|
||||
elseif numTbls == 1 then
|
||||
for i = 1, #tbls[1] do
|
||||
result[i] = tbls[1][i]
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- initialize our iterator indexes
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
assert(t._index == nil)
|
||||
t._index = 1
|
||||
end
|
||||
|
||||
while true do
|
||||
-- go through each list and check if the current values are equal and get the max value
|
||||
local isDone, isEqual = false, true
|
||||
local equalValue, maxValue = nil, nil
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
local value = t[t._index]
|
||||
value = value and valueFunc and valueFunc(value, ...)
|
||||
if not value then
|
||||
isDone = true
|
||||
break
|
||||
elseif i == 1 then
|
||||
equalValue = value
|
||||
maxValue = value
|
||||
else
|
||||
if value ~= equalValue then
|
||||
isEqual = false
|
||||
end
|
||||
if value > maxValue then
|
||||
maxValue = value
|
||||
end
|
||||
end
|
||||
end
|
||||
if isDone then
|
||||
break
|
||||
end
|
||||
if isEqual then
|
||||
-- all lists contained the same value, so insert it into our result and advance all the indexes
|
||||
tinsert(result, tbls[1][tbls[1]._index])
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
t._index = t._index + 1
|
||||
end
|
||||
else
|
||||
-- all lists aren't on the same value, so advanced each one to at least the current max value
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
local value = t[t._index]
|
||||
value = value and valueFunc and valueFunc(value, ...)
|
||||
while value and value < maxValue do
|
||||
t._index = t._index + 1
|
||||
value = t[t._index]
|
||||
value = value and valueFunc and valueFunc(value, ...)
|
||||
end
|
||||
end
|
||||
if isDone then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- clear all our iterator indexes
|
||||
for i = 1, numTbls do
|
||||
tbls[i]._index = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TableKeyIterator(tbl, prevKey)
|
||||
local key = next(tbl, prevKey)
|
||||
return key
|
||||
end
|
||||
|
||||
function private.TableIterator(iterContext)
|
||||
iterContext.index = iterContext.index + 1
|
||||
if iterContext.index > #iterContext.data then
|
||||
local data = iterContext.data
|
||||
local cleanupFunc = iterContext.cleanupFunc
|
||||
TempTable.Release(iterContext)
|
||||
if cleanupFunc then
|
||||
cleanupFunc(data)
|
||||
end
|
||||
return
|
||||
end
|
||||
if iterContext.helperFunc then
|
||||
local result = TempTable.Acquire(iterContext.helperFunc(iterContext.index, iterContext.data[iterContext.index], iterContext.arg))
|
||||
if #result == 0 then
|
||||
TempTable.Release(result)
|
||||
return private.TableIterator(iterContext)
|
||||
end
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
else
|
||||
return iterContext.index, iterContext.data[iterContext.index]
|
||||
end
|
||||
end
|
||||
|
||||
function private.TableSortWithValueLookupHelper(a, b)
|
||||
local aValue = private.sortValueLookup[a]
|
||||
local bValue = private.sortValueLookup[b]
|
||||
if aValue == bValue then
|
||||
if private.sortValueUnstable then
|
||||
return false
|
||||
else
|
||||
return a > b
|
||||
end
|
||||
end
|
||||
if private.sortValueReverse then
|
||||
return aValue > bValue
|
||||
else
|
||||
return aValue < bValue
|
||||
end
|
||||
end
|
||||
|
||||
function private.InwardIteratorHelper(context)
|
||||
if context.leftIndex > context.rightIndex then
|
||||
private.inwardIteratorContext[context.tbl] = nil
|
||||
TempTable.Release(context)
|
||||
return
|
||||
end
|
||||
local index = nil
|
||||
if context.isAscending then
|
||||
-- iterating in ascending order
|
||||
index = context.leftIndex
|
||||
context.leftIndex = context.leftIndex + 1
|
||||
else
|
||||
-- iterating in descending order
|
||||
index = context.rightIndex
|
||||
context.rightIndex = context.rightIndex - 1
|
||||
end
|
||||
return index, context.tbl[index], context.isAscending
|
||||
end
|
||||
|
||||
function private.DefaultSortFunc(a, b)
|
||||
return a < b
|
||||
end
|
||||
141
LibTSM/Util/TempTable.lua
Normal file
141
LibTSM/Util/TempTable.lua
Normal file
@@ -0,0 +1,141 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- TempTable Functions
|
||||
-- @module TempTable
|
||||
|
||||
local _, TSM = ...
|
||||
local TempTable = TSM.Init("Util.TempTable")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local private = {
|
||||
debugLeaks = TSM.__IS_TEST_ENV or false,
|
||||
freeTempTables = {},
|
||||
tempTableState = {},
|
||||
}
|
||||
local NUM_TEMP_TABLES = 100
|
||||
local RELEASED_TEMP_TABLE_MT = {
|
||||
__newindex = function(self, key, value)
|
||||
error("Attempt to access temp table after release")
|
||||
end,
|
||||
__index = function(self, key)
|
||||
error("Attempt to access temp table after release")
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Acquires a temporary table.
|
||||
-- Temporary tables are recycled tables which can be used instead of creating a new table every time one is needed for a
|
||||
-- defined lifecycle. This avoids relying on the garbage collector and improves overall performance.
|
||||
-- @param ... Any number of valuse to insert into the table initially
|
||||
-- @treturn table The temporary table
|
||||
function TempTable.Acquire(...)
|
||||
local tbl = tremove(private.freeTempTables, 1)
|
||||
assert(tbl, "Could not acquire temp table")
|
||||
setmetatable(tbl, nil)
|
||||
if private.debugLeaks then
|
||||
private.tempTableState[tbl] = (Debug.GetStackLevelLocation(2) or "?").." -> "..(Debug.GetStackLevelLocation(3) or "?")
|
||||
else
|
||||
private.tempTableState[tbl] = true
|
||||
end
|
||||
for i = 1, select("#", ...) do
|
||||
tbl[i] = select(i, ...)
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
--- Iterators over a temporary table, releasing it when done.
|
||||
-- NOTE: This iterator must be run to completion and not interrupted (i.e. with a `break` or `return`).
|
||||
-- @tparam table tbl The temporary table to iterator over
|
||||
-- @tparam[opt=1] number numFields The number of fields to unpack with each iteration
|
||||
-- @return An iterator with fields: `index, {numFields...}`
|
||||
function TempTable.Iterator(tbl, numFields)
|
||||
numFields = numFields or 1
|
||||
assert(numFields >= 1 and #tbl % numFields == 0)
|
||||
assert(private.tempTableState[tbl])
|
||||
tbl.__iterNumFields = numFields
|
||||
return private.TempTableIteratorHelper, tbl, 1 - numFields
|
||||
end
|
||||
|
||||
--- Releases a temporary table.
|
||||
-- The temporary table will be returned to the pool and must not be accessed after being released.
|
||||
-- @tparam table tbl The temporary table to release
|
||||
function TempTable.Release(tbl)
|
||||
private.TempTableReleaseHelper(tbl)
|
||||
end
|
||||
|
||||
--- Releases a temporary table and returns its values.
|
||||
-- Releases the temporary table (see @{TempTable.Release}) and returns its unpacked values.
|
||||
-- @tparam table tbl The temporary table to release and unpack
|
||||
-- @return The result of calling `unpack` on the table
|
||||
function TempTable.UnpackAndRelease(tbl)
|
||||
return private.TempTableReleaseHelper(tbl, unpack(tbl))
|
||||
end
|
||||
|
||||
function TempTable.EnableLeakDebug()
|
||||
private.debugLeaks = true
|
||||
end
|
||||
|
||||
function TempTable.GetDebugInfo()
|
||||
local debugInfo = {}
|
||||
local counts = {}
|
||||
for _, info in pairs(private.tempTableState) do
|
||||
counts[info] = (counts[info] or 0) + 1
|
||||
end
|
||||
for info, count in pairs(counts) do
|
||||
tinsert(debugInfo, format("[%d] %s", count, type(info) == "string" and info or "?"))
|
||||
end
|
||||
if #debugInfo == 0 then
|
||||
tinsert(debugInfo, "<none>")
|
||||
end
|
||||
return debugInfo
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TempTableIteratorHelper(tbl, index)
|
||||
local numFields = tbl.__iterNumFields
|
||||
index = index + numFields
|
||||
if index > #tbl then
|
||||
TempTable.Release(tbl)
|
||||
return
|
||||
end
|
||||
if numFields == 1 then
|
||||
return index, tbl[index]
|
||||
else
|
||||
return index, unpack(tbl, index, index + numFields - 1)
|
||||
end
|
||||
end
|
||||
|
||||
function private.TempTableReleaseHelper(tbl, ...)
|
||||
assert(private.tempTableState[tbl])
|
||||
wipe(tbl)
|
||||
tinsert(private.freeTempTables, tbl)
|
||||
private.tempTableState[tbl] = nil
|
||||
setmetatable(tbl, RELEASED_TEMP_TABLE_MT)
|
||||
return ...
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Temp Table Setup
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
for _ = 1, NUM_TEMP_TABLES do
|
||||
local tempTbl = setmetatable({}, RELEASED_TEMP_TABLE_MT)
|
||||
tinsert(private.freeTempTables, tempTbl)
|
||||
end
|
||||
end
|
||||
318
LibTSM/Util/Theme.lua
Normal file
318
LibTSM/Util/Theme.lua
Normal file
@@ -0,0 +1,318 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Theme Functions.
|
||||
-- @module Theme
|
||||
|
||||
local _, TSM = ...
|
||||
local Theme = TSM.Init("Util.Theme")
|
||||
local FontPaths = TSM.Include("Data.FontPaths")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local Color = TSM.Include("Util.Color")
|
||||
local FontObject = TSM.Include("Util.FontObject")
|
||||
local private = {
|
||||
callbacks = {},
|
||||
names = {},
|
||||
colorSets = {},
|
||||
currentColorSet = nil,
|
||||
fontFrame = nil,
|
||||
currentFontSet = nil,
|
||||
}
|
||||
local THEME_COLOR_KEYS = {
|
||||
FRAME_BG = true,
|
||||
PRIMARY_BG = true,
|
||||
PRIMARY_BG_ALT = true,
|
||||
ACTIVE_BG = true,
|
||||
ACTIVE_BG_ALT = true,
|
||||
}
|
||||
local STATIC_COLORS = {
|
||||
INDICATOR = Color.NewFromHex("#ffd839"),
|
||||
INDICATOR_ALT = Color.NewFromHex("#79a2ff"),
|
||||
INDICATOR_DISABLED = Color.NewFromHex("#6f5819"),
|
||||
|
||||
TEXT = Color.NewFromHex("#ffffff"),
|
||||
TEXT_ALT = Color.NewFromHex("#e2e2e2"),
|
||||
TEXT_DISABLED = Color.NewFromHex("#424242"),
|
||||
}
|
||||
local FEEDBACK_COLORS = {
|
||||
RED = Color.NewFromHex("#f72d20"),
|
||||
YELLOW = Color.NewFromHex("#e1f720"),
|
||||
GREEN = Color.NewFromHex("#4ff720"),
|
||||
BLUE = Color.NewFromHex("#2076f7"),
|
||||
ORANGE = Color.NewFromHex("#f77a20"),
|
||||
}
|
||||
local BLIZZARD_COLOR = Color.NewFromHex("#00b4ff")
|
||||
local GROUP_COLORS = {
|
||||
Color.NewFromHex("#fcf141"),
|
||||
Color.NewFromHex("#bdaec6"),
|
||||
Color.NewFromHex("#06a2cb"),
|
||||
Color.NewFromHex("#ffb85c"),
|
||||
Color.NewFromHex("#51b599"),
|
||||
}
|
||||
local PROFESSION_DIFFICULTY_COLORS = {
|
||||
optimal = Color.NewFromHex("#ff8040"),
|
||||
medium = Color.NewFromHex("#ffff00"),
|
||||
easy = Color.NewFromHex("#40c040"),
|
||||
trivial = Color.NewFromHex("#808080"),
|
||||
header = Color.NewFromHex("#ffd100"),
|
||||
subheader = Color.NewFromHex("#ffd100"),
|
||||
nodifficulty = Color.NewFromHex("#f5f5f5"),
|
||||
}
|
||||
-- NOTE: there is a global ITEM_QUALITY_COLORS so we need to use another name
|
||||
local TSM_ITEM_QUALITY_COLORS = {
|
||||
[0] = Color.NewFromHex("#9d9d9d"),
|
||||
[1] = Color.NewFromHex("#ffffff"),
|
||||
[2] = Color.NewFromHex("#1eff00"),
|
||||
[3] = Color.NewFromHex("#0070dd"),
|
||||
[4] = Color.NewFromHex("#a334ee"),
|
||||
[5] = Color.NewFromHex("#ff8000"),
|
||||
[6] = Color.NewFromHex("#e6cc80"),
|
||||
[7] = Color.NewFromHex("#00ccff"),
|
||||
[8] = Color.NewFromHex("#00ccff"),
|
||||
}
|
||||
local AUCTION_PCT_COLORS = {
|
||||
{ -- blue
|
||||
color = "BLUE",
|
||||
value = 50,
|
||||
},
|
||||
{ -- green
|
||||
color = "GREEN",
|
||||
value = 80,
|
||||
},
|
||||
{ -- yellow
|
||||
color = "YELLOW",
|
||||
value = 110,
|
||||
},
|
||||
{ -- orange
|
||||
color = "ORANGE",
|
||||
value = 135,
|
||||
},
|
||||
{ -- red
|
||||
color = "RED",
|
||||
value = math.huge,
|
||||
},
|
||||
default = "TEXT",
|
||||
bid = "TEXT_ALT",
|
||||
}
|
||||
local CONSTANTS = {
|
||||
COL_SPACING = 8,
|
||||
SCROLLBAR_MARGIN = 4,
|
||||
SCROLLBAR_WIDTH = 4,
|
||||
MOUSE_WHEEL_SCROLL_AMOUNT = 60,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Theme:OnModuleLoad(function()
|
||||
Table.SetReadOnly(STATIC_COLORS)
|
||||
Table.SetReadOnly(FEEDBACK_COLORS)
|
||||
Table.SetReadOnly(GROUP_COLORS)
|
||||
Table.SetReadOnly(PROFESSION_DIFFICULTY_COLORS)
|
||||
Table.SetReadOnly(TSM_ITEM_QUALITY_COLORS)
|
||||
Table.SetReadOnly(CONSTANTS)
|
||||
|
||||
-- create a frame to load fonts
|
||||
private.fontFrame = CreateFrame("Frame", nil, UIParent)
|
||||
private.fontFrame.texts = {}
|
||||
private.fontFrame:SetAllPoints()
|
||||
private.fontFrame:SetScript("OnUpdate", private.FontFrameOnUpdate)
|
||||
|
||||
-- TODO: eventually allow for different font sets?
|
||||
private.currentFontSet = {
|
||||
HEADING_H5 = FontObject.New(FontPaths.GetBodyRegular(), 20, 28),
|
||||
BODY_BODY1 = FontObject.New(FontPaths.GetBodyRegular(), 16, 24),
|
||||
BODY_BODY1_BOLD = FontObject.New(FontPaths.GetBodyBold(), 16, 24),
|
||||
BODY_BODY2 = FontObject.New(FontPaths.GetBodyRegular(), 14, 20),
|
||||
BODY_BODY2_MEDIUM = FontObject.New(FontPaths.GetBodyMedium(), 14, 20),
|
||||
BODY_BODY2_BOLD = FontObject.New(FontPaths.GetBodyBold(), 14, 20),
|
||||
BODY_BODY3 = FontObject.New(FontPaths.GetBodyRegular(), 12, 20),
|
||||
BODY_BODY3_MEDIUM = FontObject.New(FontPaths.GetBodyMedium(), 12, 20),
|
||||
ITEM_BODY1 = FontObject.New(FontPaths.GetItem(), 16, 24),
|
||||
ITEM_BODY2 = FontObject.New(FontPaths.GetItem(), 14, 20),
|
||||
ITEM_BODY3 = FontObject.New(FontPaths.GetItem(), 12, 20),
|
||||
TABLE_TABLE1 = FontObject.New(FontPaths.GetTable(), 12, 20),
|
||||
}
|
||||
|
||||
-- load the fonts
|
||||
for _, obj in pairs(private.currentFontSet) do
|
||||
local fontPath = obj:GetWowFont()
|
||||
private.QueueFontLoad(fontPath)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Registers a callback when the theme changes.
|
||||
-- @tparam function callback The callback function
|
||||
function Theme.RegisterChangeCallback(callback)
|
||||
assert(type(callback) == "function")
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
--- Registers a new color set.
|
||||
-- @tparam string key The key which represents the color set
|
||||
-- @tparam string name The name of the color set
|
||||
-- @tparam table colorSet The colors which make up the color set (with keys specified in `THEME_COLOR_KEYS`)
|
||||
function Theme.RegisterColorSet(key, name, colorSet)
|
||||
assert(not private.colorSets[key])
|
||||
for k in pairs(THEME_COLOR_KEYS) do
|
||||
assert(Color.IsInstance(colorSet[k]))
|
||||
end
|
||||
private.names[key] = name
|
||||
private.colorSets[key] = colorSet
|
||||
end
|
||||
|
||||
--- Sets the active color set.
|
||||
-- @tparam string key The key which represents the color set
|
||||
function Theme.SetActiveColorSet(key)
|
||||
assert(private.colorSets[key])
|
||||
if private.currentColorSet == private.colorSets[key] then
|
||||
return
|
||||
end
|
||||
private.currentColorSet = private.colorSets[key]
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function Theme.GetThemeName(key)
|
||||
return private.names[key]
|
||||
end
|
||||
|
||||
--- Gets the color object from the current active color set.
|
||||
-- @tparam string key The key of the color to get
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetColor(key, themeKey)
|
||||
local colorKey, tintPct, opacityPct = strmatch(key, "^([A-Z_]+)([%-%+]?[0-9A-Z_]*)%%?([0-9A-Z_]*)$")
|
||||
tintPct = tonumber(tintPct) or (tintPct ~= "" and tintPct or nil)
|
||||
opacityPct = tonumber(opacityPct) or (opacityPct ~= "" and opacityPct or nil)
|
||||
assert(colorKey)
|
||||
local color = nil
|
||||
if THEME_COLOR_KEYS[colorKey] then
|
||||
color = themeKey and private.colorSets[themeKey][colorKey] or private.currentColorSet[colorKey]
|
||||
else
|
||||
color = STATIC_COLORS[colorKey]
|
||||
end
|
||||
assert(color)
|
||||
if tintPct then
|
||||
color = color:GetTint(tintPct)
|
||||
end
|
||||
if opacityPct then
|
||||
color = color:GetOpacity(opacityPct)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
--- Gets the color object for a given feedback color key.
|
||||
-- @tparam string key The key of the feedback color to get
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetFeedbackColor(key)
|
||||
return FEEDBACK_COLORS[key]
|
||||
end
|
||||
|
||||
--- Gets the color object for Blizzard GMs.
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetBlizzardColor()
|
||||
return BLIZZARD_COLOR
|
||||
end
|
||||
|
||||
--- Gets the color object for a given group level.
|
||||
-- @tparam number level The level of the group (1-based)
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetGroupColor(level)
|
||||
level = ((level - 1) % #GROUP_COLORS) + 1
|
||||
return GROUP_COLORS[level]
|
||||
end
|
||||
|
||||
function Theme.GetProfessionDifficultyColor(difficulty)
|
||||
return PROFESSION_DIFFICULTY_COLORS[difficulty]
|
||||
end
|
||||
|
||||
function Theme.GetItemQualityColor(quality)
|
||||
return TSM_ITEM_QUALITY_COLORS[quality]
|
||||
end
|
||||
|
||||
function Theme.GetAuctionPercentColor(pct)
|
||||
if pct == "BID" then
|
||||
return Theme.GetColor(AUCTION_PCT_COLORS.bid)
|
||||
end
|
||||
for _, info in ipairs(AUCTION_PCT_COLORS) do
|
||||
if pct < info.value then
|
||||
return Theme.GetFeedbackColor(info.color)
|
||||
end
|
||||
end
|
||||
return Theme.GetColor(AUCTION_PCT_COLORS.default)
|
||||
end
|
||||
|
||||
--- Gets the font object from the current active font set.
|
||||
-- @tparam string key The key of the font to get
|
||||
-- @treturn FontObject The font object
|
||||
function Theme.GetFont(key)
|
||||
local fontObj = private.currentFontSet[key]
|
||||
assert(fontObj)
|
||||
return fontObj
|
||||
end
|
||||
|
||||
--- Gets the column spacing constant value.
|
||||
-- @treturn number The column spacing
|
||||
function Theme.GetColSpacing()
|
||||
return CONSTANTS.COL_SPACING
|
||||
end
|
||||
|
||||
--- Gets the scrollbar margin constant value.
|
||||
-- @treturn number The scrollbar margin
|
||||
function Theme.GetScrollbarMargin()
|
||||
return CONSTANTS.SCROLLBAR_MARGIN
|
||||
end
|
||||
|
||||
--- Gets the scrollbar width constant value.
|
||||
-- @treturn number The scrollbar width
|
||||
function Theme.GetScrollbarWidth()
|
||||
return CONSTANTS.SCROLLBAR_WIDTH
|
||||
end
|
||||
|
||||
--- Gets the scrollbar width constant value.
|
||||
-- @treturn number The scrollbar width
|
||||
function Theme.GetMouseWheelScrollAmount()
|
||||
return CONSTANTS.MOUSE_WHEEL_SCROLL_AMOUNT
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.QueueFontLoad(path)
|
||||
if private.fontFrame.texts[path] then
|
||||
return
|
||||
end
|
||||
local fontString = private.fontFrame:CreateFontString()
|
||||
fontString:SetPoint("CENTER")
|
||||
fontString:SetWidth(10000)
|
||||
fontString:SetHeight(6)
|
||||
fontString:SetFont(path, 6)
|
||||
fontString:SetText("1")
|
||||
private.fontFrame.texts[path] = fontString
|
||||
private.fontFrame:Show()
|
||||
end
|
||||
|
||||
function private.FontFrameOnUpdate(frame)
|
||||
for _, fontString in pairs(frame.texts) do
|
||||
if fontString:IsVisible() then
|
||||
assert(fontString:GetStringWidth() > 0, "Text not loaded: "..tostring(fontString:GetFont()))
|
||||
fontString:Hide()
|
||||
end
|
||||
end
|
||||
frame:Hide()
|
||||
end
|
||||
48
LibTSM/Util/Vararg.lua
Normal file
48
LibTSM/Util/Vararg.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Vararg Functions
|
||||
-- @module Vararg
|
||||
|
||||
local _, TSM = ...
|
||||
local Vararg = TSM.Init("Util.Vararg")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Stores a varag into a table.
|
||||
-- @tparam table tbl The table to store the values in
|
||||
-- @param ... Zero or more values to store in the table
|
||||
function Vararg.IntoTable(tbl, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
tbl[i] = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates an iterator from a vararg.
|
||||
-- NOTE: This iterator must be run to completion and not interrupted (i.e. with a `break` or `return`).
|
||||
-- @param ... The values to iterate over
|
||||
-- @return An iterator with fields: `index, value`
|
||||
function Vararg.Iterator(...)
|
||||
return TempTable.Iterator(TempTable.Acquire(...))
|
||||
end
|
||||
|
||||
--- Returns whether not the value exists within the vararg.
|
||||
-- @param value The value to search for
|
||||
-- @param ... Any number of values to search in
|
||||
-- @treturn boolean Whether or not the value was found in the vararg
|
||||
function Vararg.In(value, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
if value == select(i, ...) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
67
LibTSM/Util/Wow.lua
Normal file
67
LibTSM/Util/Wow.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Wow Functions
|
||||
-- @module Wow
|
||||
|
||||
local _, TSM = ...
|
||||
local Wow = TSM.Init("Util.Wow")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Shows a basic Wow message popup.
|
||||
-- @tparam string text The text to display
|
||||
function Wow.ShowBasicMessage(text)
|
||||
if BasicMessageDialog:IsShown() then
|
||||
return
|
||||
end
|
||||
BasicMessageDialog.Text:SetText(text)
|
||||
BasicMessageDialog:Show()
|
||||
end
|
||||
|
||||
--- Shows a WoW static popup dialog.
|
||||
-- @tparam string name The unique (global) name of the dialog to be shown
|
||||
function Wow.ShowStaticPopupDialog(name)
|
||||
StaticPopupDialogs[name].preferredIndex = 4
|
||||
StaticPopup_Show(name)
|
||||
for i = 1, 100 do
|
||||
if _G["StaticPopup" .. i] and _G["StaticPopup" .. i].which == name then
|
||||
_G["StaticPopup" .. i]:SetFrameStrata("TOOLTIP")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Sets the WoW item ref frame to the specified link.
|
||||
-- @tparam string link The itemLink to show the item ref frame for
|
||||
function Wow.SafeItemRef(link)
|
||||
if type(link) ~= "string" then return end
|
||||
-- extract the Blizzard itemString for both items and pets
|
||||
local blizzItemString = strmatch(link, "^\124c[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\124H(item:[^\124]+)\124.+$")
|
||||
blizzItemString = blizzItemString or strmatch(link, "^\124c[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\124H(battlepet:[^\124]+)\124.+$")
|
||||
if blizzItemString then
|
||||
SetItemRef(blizzItemString, link, "LeftButton")
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks if an addon is installed.
|
||||
-- This function only checks if the addon is installed, not if it's enabled.
|
||||
-- @tparam string name The name of the addon
|
||||
-- @treturn boolean Whether or not the addon is installed
|
||||
function Wow.IsAddonInstalled(name)
|
||||
return select(2, GetAddOnInfo(name)) and true or false
|
||||
end
|
||||
|
||||
--- Checks if an addon is currently enabled.
|
||||
-- @tparam string name The name of the addon
|
||||
-- @treturn boolean Whether or not the addon is enabled
|
||||
function Wow.IsAddonEnabled(name)
|
||||
return GetAddOnEnableState(UnitName("player"), name) == 2 and select(4, GetAddOnInfo(name)) and true or false
|
||||
end
|
||||
Reference in New Issue
Block a user