initial commit

This commit is contained in:
Gitea
2020-11-13 14:13:12 -05:00
commit 05df49ff60
368 changed files with 128754 additions and 0 deletions

90
LibTSM/Util/Analytics.lua Normal file
View 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
View 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
View 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
View 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

View 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()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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