* fix: enable Unicode separators and caret motions As the runtime is going to support Unicode installation locations and build directories, some UTF-8 text is going to reach the Lua side of the project. This includes the script path, the user path, any paths yielded from file searches and also imported character names from accounts. Care needs to be taken in many places where string operations are performed as no longer does a byte necessarily correspond to a single character and anything that truncates, reverses or otherwise slices strings could need an audit. This change fixes cursor movement in `EditControl`s with the arrow keys as those historically used string matching and byte offsets. It also ensures that the use of arbitrary Unicode codepoints as decimal and thousands separators works correctly as the previous code used unaware reversing and slicing. * fix: turn update paths relative for wide installs The updater is a fixed piece of older code that uses a Lua runtime that only handles paths that are representable in the user's text codepage. As the software may be installed in a location that cannot be expressed in that way, to mitigate the problem we turn all the paths in the update op-files into relative paths. That way as long as we never use exotic codepoints in our own paths it should be able to apply them cleanly and restart Path of Building afterward with a relative path. The updater executable can ironically enough not be updated at all with the related type of runtime hacks we introduced in SimpleGraphic as the updater deadlocks in updating itself. We have to work around its shortcomings in how we produce the op-files and possibly the update application script that runs under that limited runtime. * fix: convert GIFs masquerading as PNG to PNG Upon removing support for several file formats like GIF and BLP from the SimpleGraphic runtime, we noticed that there were some assets that had incorrect file extensions and loaded only thanks to file format detection ignoring extensions. As the actual file format loader for GIF was removed, these stealth GIFs are now losslessly converted to PNG. * Add luautf8 to Dockerfile --------- Co-authored-by: Wires77 <Wires77@users.noreply.github.com>
953 lines
25 KiB
Lua
953 lines
25 KiB
Lua
-- Path of Building
|
|
--
|
|
-- Module: Common
|
|
-- Libraries, functions and classes used by various modules.
|
|
--
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local type = type
|
|
local t_insert = table.insert
|
|
local t_remove = table.remove
|
|
local m_abs = math.abs
|
|
local m_floor = math.floor
|
|
local m_min = math.min
|
|
local m_max = math.max
|
|
local m_ceil = math.ceil
|
|
local s_format = string.format
|
|
local s_char = string.char
|
|
local b_rshift = bit.rshift
|
|
local b_and = bit.band
|
|
local b_xor = bit.bxor
|
|
|
|
common = { }
|
|
|
|
-- External libraries
|
|
common.curl = require("lcurl.safe")
|
|
common.xml = require("xml")
|
|
common.base64 = require("base64")
|
|
common.sha1 = require("sha1")
|
|
local utf8 = require('lua-utf8')
|
|
|
|
-- Try to load a library return nil if failed. https://stackoverflow.com/questions/34965863/lua-require-fallback-error-handling
|
|
function prerequire(...)
|
|
local status, lib = pcall(require, ...)
|
|
if(status) then return lib end
|
|
return nil
|
|
end
|
|
|
|
profiler = prerequire("lua-profiler")
|
|
profiling = false
|
|
|
|
if launch.devMode and profiler == nil then
|
|
ConPrintf("Unable to Load Profiler")
|
|
end
|
|
|
|
-- Class library
|
|
common.classes = { }
|
|
local function addSuperParents(class, parent)
|
|
for _, superParent in pairs(parent._parents) do
|
|
class._superParents[superParent] = true
|
|
if superParent._parents then
|
|
addSuperParents(class, superParent)
|
|
end
|
|
end
|
|
end
|
|
local function getClass(className)
|
|
local class = common.classes[className]
|
|
if not class then
|
|
LoadModule("Classes/"..className)
|
|
class = common.classes[className]
|
|
assert(class, "Class '"..className.."' not defined in class file")
|
|
end
|
|
return class
|
|
end
|
|
-- newClass("<className>"[, "<parentClassName>"[, "<parentClassName>" ...]], constructorFunc)
|
|
function newClass(className, ...)
|
|
local class = { }
|
|
common.classes[className] = class
|
|
class.__index = class
|
|
class.__call = function(obj, mix)
|
|
for k, v in pairs(mix) do
|
|
obj[k] = v
|
|
end
|
|
return obj
|
|
end
|
|
class._className = className
|
|
local numVarArg = select("#", ...)
|
|
class._constructor = select(numVarArg, ...)
|
|
if numVarArg > 1 then
|
|
-- Build list of parent classes
|
|
class._parents = { }
|
|
for i = 1, numVarArg - 1 do
|
|
class._parents[i] = getClass(select(i, ...))
|
|
end
|
|
-- Build list of all classes directly or indirectly inherited by this class
|
|
class._superParents = { }
|
|
addSuperParents(class, class)
|
|
-- Set up inheritance
|
|
setmetatable(class, {
|
|
__index = function(self, key)
|
|
for _, parent in ipairs(class._parents) do
|
|
local val = parent[key]
|
|
if val ~= nil then
|
|
self[key] = val
|
|
return val
|
|
end
|
|
end
|
|
end
|
|
})
|
|
end
|
|
return class
|
|
end
|
|
function new(className, ...)
|
|
local class = getClass(className)
|
|
local object = setmetatable({ }, class)
|
|
object.Object = object
|
|
if class._parents then
|
|
-- Add parent and superparent class proxies
|
|
object._parentInit = { }
|
|
for parent in pairs(class._superParents) do
|
|
local proxyMeta = {
|
|
__index = function(self, key)
|
|
local v = rawget(object, key)
|
|
if v ~= nil then
|
|
return v
|
|
else
|
|
return parent[key]
|
|
end
|
|
end,
|
|
__newindex = object,
|
|
__call = function(...)
|
|
if not parent._constructor then
|
|
error("Parent class '"..parent._className.."' of class '"..class._className.."' has no constructor")
|
|
end
|
|
if object._parentInit[parent] then
|
|
error("Parent class '"..parent._className.."' of class '"..class._className.."' has already been initialised")
|
|
end
|
|
parent._constructor(...)
|
|
object._parentInit[parent] = true
|
|
end,
|
|
}
|
|
object[parent._className] = setmetatable(proxyMeta, proxyMeta)
|
|
end
|
|
end
|
|
if class._constructor then
|
|
class._constructor(object, ...)
|
|
end
|
|
if class._parents then
|
|
-- Check that the constructors for all parent and superparent classes have been called
|
|
for parent in pairs(class._superParents) do
|
|
if parent._constructor and not object._parentInit[parent] then
|
|
error("Parent class '"..parent._className.."' of class '"..className.."' must be initialised")
|
|
end
|
|
end
|
|
end
|
|
return object
|
|
end
|
|
|
|
function codePointToUTF8(codePoint)
|
|
if codePoint >= 0xD800 and codePoint <= 0xDFFF then
|
|
return "?"
|
|
elseif codePoint <= 0x7F then
|
|
return s_char(codePoint)
|
|
elseif codePoint <= 0x07FF then
|
|
return s_char(0xC0 + b_rshift(codePoint, 6), 0x80 + b_and(codePoint, 0x3F))
|
|
elseif codePoint <= 0xFFFF then
|
|
return s_char(0xE0 + b_rshift(codePoint, 12), 0x80 + b_and(b_rshift(codePoint, 6), 0x3F), 0x80 + b_and(codePoint, 0x3F))
|
|
elseif codePoint <= 0x10FFFF then
|
|
return s_char(0xF0 + b_rshift(codePoint, 18), 0x80 + b_and(b_rshift(codePoint, 12), 0x3F), 0x80 + b_and(b_rshift(codePoint, 6), 0x3F), 0x80 + b_and(codePoint, 0x3F))
|
|
else
|
|
return "?"
|
|
end
|
|
end
|
|
function convertUTF16to8(text, offset)
|
|
offset = offset or 1
|
|
local out = { }
|
|
local highSurr
|
|
for i = offset, #text - 1, 2 do
|
|
local codeUnit = text:byte(i) + text:byte(i+1) * 256
|
|
if codeUnit == 0 then
|
|
break
|
|
elseif codeUnit >= 0xD800 and codeUnit <= 0xDBFF then
|
|
highSurr = codeUnit - 0xD800
|
|
elseif codeUnit >= 0xDC00 and codeUnit <= 0xDFFF then
|
|
if highSurr then
|
|
t_insert(out, codePointToUTF8(highSurr * 1024 + codeUnit - 0xDC00 + 0x010000))
|
|
highSurr = nil
|
|
end
|
|
else
|
|
t_insert(out, codePointToUTF8(codeUnit))
|
|
end
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
function codePointToUTF16(codePoint)
|
|
if codePoint >= 0xD800 and codePoint <= 0xDFFF then
|
|
return "?\z"
|
|
elseif codePoint <= 0xFFFF then
|
|
return s_char(b_and(codePoint, 0xFF), b_rshift(codePoint, 8))
|
|
elseif codePoint <= 0x10FFFF then
|
|
local highSurr = 0xD800 + b_rshift(codePoint - 0x10000, 10)
|
|
local lowSurr = 0xDC00 + b_and(codePoint - 0x10000, 0x2FF)
|
|
return s_char(b_and(highSurr, 0xFF), b_rshift(highSurr, 8), b_and(lowSurr, 0xFF), b_rshift(lowSurr, 8))
|
|
else
|
|
return "?\z"
|
|
end
|
|
end
|
|
function convertUTF8to16(text, offset)
|
|
offset = offset or 1
|
|
local out = { }
|
|
local codePoint = 0
|
|
local codeUnitRemaining
|
|
for i = offset, #text do
|
|
local codeUnit = text:byte(i)
|
|
if codeUnit == 0 then
|
|
break
|
|
elseif codeUnit <= 0x7F then
|
|
table.insert(out, s_char(codeUnit, 0))
|
|
elseif codeUnit >= 0xC2 and codeUnit <= 0xDF then
|
|
codeUnitRemaining = 1
|
|
codePoint = b_and(codeUnit, 0x1F)
|
|
elseif codeUnit >= 0xE0 and codeUnit <= 0xEF then
|
|
codeUnitRemaining = 2
|
|
codePoint = b_and(codeUnit, 0x0F)
|
|
elseif codeUnit >= 0xF0 and codeUnit <= 0xF4 then
|
|
codeUnitRemaining = 3
|
|
codePoint = b_and(codeUnit, 0x03)
|
|
elseif codeUnit >= 0x80 and codeUnit <= 0xBF then
|
|
if codeUnitRemaining then
|
|
codePoint = bit.lshift(codePoint, 6) + b_and(codeUnit, 0x3F)
|
|
codeUnitRemaining = codeUnitRemaining - 1
|
|
if codeUnitRemaining == 0 then
|
|
t_insert(out, codePointToUTF16(codePoint))
|
|
codeUnitRemaining = nil
|
|
end
|
|
else
|
|
t_insert(out, "?\z")
|
|
end
|
|
else
|
|
t_insert(out, "?\z")
|
|
end
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
--- Clean item text by removing or replacing unsupported or redundant characters or sequences
|
|
---@param text string
|
|
---@return string
|
|
function sanitiseText(text)
|
|
if not text then return nil end
|
|
-- Something something unicode support something grumble
|
|
-- Only do these replacements if a char from 128-255 or '<' is found first
|
|
return text:find("[\128-\255<]") and text
|
|
:gsub("%b<>", "")
|
|
:gsub("\226\128\144", "-") -- U+2010 HYPHEN
|
|
:gsub("\226\128\145", "-") -- U+2011 NON-BREAKING HYPHEN
|
|
:gsub("\226\128\146", "-") -- U+2012 FIGURE DASH
|
|
:gsub("\226\128\147", "-") -- U+2013 EN DASH
|
|
:gsub("\226\128\148", "-") -- U+2014 EM DASH
|
|
:gsub("\226\128\149", "-") -- U+2015 HORIZONTAL BAR
|
|
:gsub("\226\136\146", "-") -- U+2212 MINUS SIGN
|
|
:gsub("\195\164", "a") -- U+00E4 LATIN SMALL LETTER A WITH DIAERESIS
|
|
:gsub("\195\182", "o") -- U+00F6 LATIN SMALL LETTER O WITH DIAERESIS
|
|
-- single-byte: Windows-1252 and similar
|
|
:gsub("\150", "-") -- U+2013 EN DASH
|
|
:gsub("\151", "-") -- U+2014 EM DASH
|
|
:gsub("\228", "a") -- U+00E4 LATIN SMALL LETTER A WITH DIAERESIS
|
|
:gsub("\246", "o") -- U+00F6 LATIN SMALL LETTER O WITH DIAERESIS
|
|
-- unsupported
|
|
:gsub("[\128-\255]", "?")
|
|
or text
|
|
end
|
|
|
|
do
|
|
local function toUnsigned(val)
|
|
return val < 0 and val + 0x100000000 or val
|
|
end
|
|
local function murmurMix(val)
|
|
val = toUnsigned(val)
|
|
return bit.tobit(val * 0xE995 + b_and(val * 0x5BD1, 0xFFFF) * 0x10000)
|
|
end
|
|
function murmurHash2(key, seed)
|
|
local len = #key
|
|
local h = b_xor(seed or 0, len)
|
|
local o = 1
|
|
while len >= 4 do
|
|
local k = bytesToInt(key, o)
|
|
k = murmurMix(k)
|
|
k = b_xor(k, b_rshift(k, 24))
|
|
k = murmurMix(k)
|
|
h = murmurMix(h)
|
|
h = b_xor(h, k)
|
|
o = o + 4
|
|
len = len - 4
|
|
end
|
|
if len > 0 then
|
|
h = b_xor(h, bytesToInt(key, o))
|
|
h = murmurMix(h)
|
|
end
|
|
h = b_xor(h, b_rshift(h, 13))
|
|
h = murmurMix(h)
|
|
h = b_xor(h, b_rshift(h, 15))
|
|
return toUnsigned(h)
|
|
end
|
|
end
|
|
|
|
local function bits(int, s, e)
|
|
return b_and(b_rshift(int, s), 2 ^ (e - s + 1) - 1)
|
|
end
|
|
function bytesToInt(b, o)
|
|
return bit.tobit(bytesToUInt(b, o))
|
|
end
|
|
function bytesToUInt(b, o)
|
|
o = o or 1
|
|
return (b:byte(o + 0) or 0) + (b:byte(o + 1) or 0) * 256 + (b:byte(o + 2) or 0) * 65536 + (b:byte(o + 3) or 0) * 16777216
|
|
end
|
|
function bytesToUShort(b, o)
|
|
o = o or 1
|
|
return (b:byte(o + 0) or 0) + (b:byte(o + 1) or 0) * 256
|
|
end
|
|
function bytesToULong(b, o)
|
|
o = o or 1
|
|
return bytesToUInt(b, o) + bytesToUInt(b, o + 4) * 4294967296
|
|
end
|
|
function bytesToFloat(b, o)
|
|
local int = bytesToInt(b, o)
|
|
local s = (-1) ^ bits(int, 31, 31)
|
|
local e = bits(int, 23, 30) - 127
|
|
if e == -127 then
|
|
return 0 * s
|
|
end
|
|
local m = 1
|
|
for i = 0, 22 do
|
|
m = m + bits(int, i, i) * 2 ^ (i - 23)
|
|
end
|
|
return s * m * 2 ^ e
|
|
end
|
|
|
|
-- Quick hack to convert JSON to valid lua
|
|
function jsonToLua(json)
|
|
return json:gsub("%[","{"):gsub("%]","}"):gsub('"(%d[%d%.]*)":','[%1]='):gsub('"([^"]+)":','["%1"]='):gsub("\\/","/"):gsub("{(%w+)}","{[0]=%1}")
|
|
:gsub("\\u(%x%x%x%x)",function(hex) return codePointToUTF8(tonumber(hex,16)) end)
|
|
end
|
|
|
|
-- Check if mouse is currently inside area defined by region.x, region.y, region.width, region.height
|
|
function isMouseInRegion(region)
|
|
local cursorX, cursorY = GetCursorPos()
|
|
return cursorX >= region.x and cursorX < region.x + region.width and cursorY >= region.y and cursorY < region.y + region.height
|
|
end
|
|
|
|
-- Write a Lua table to file
|
|
local function qFmt(s)
|
|
return '"'..s:gsub("\n","\\n"):gsub("\"","\\\"")..'"'
|
|
end
|
|
function writeLuaTable(out, t, indent)
|
|
out:write('{')
|
|
if indent then
|
|
out:write('\n')
|
|
end
|
|
local keyList = { }
|
|
for k, v in pairs(t) do
|
|
t_insert(keyList, k)
|
|
end
|
|
table.sort(keyList, function(a,b) if type(a) == type(b) then return a < b else return type(a) < type(b) end end)
|
|
for i, k in ipairs(keyList) do
|
|
local v = t[k]
|
|
if indent then
|
|
out:write(string.rep("\t", indent))
|
|
end
|
|
if type(k) == "string" and k:match("^%a[%a%d]*$") and k ~= "hexproof" then
|
|
out:write(k, '=')
|
|
else
|
|
out:write('[')
|
|
if type(k) == "number" then
|
|
out:write(k)
|
|
else
|
|
out:write(qFmt(k))
|
|
end
|
|
out:write(']=')
|
|
end
|
|
if type(v) == "table" then
|
|
writeLuaTable(out, v, indent and indent + 1)
|
|
elseif type(v) == "string" then
|
|
out:write(qFmt(v))
|
|
else
|
|
if v == math.huge then
|
|
out:write("math.huge")
|
|
else
|
|
out:write(tostring(v))
|
|
end
|
|
end
|
|
if i < #keyList then
|
|
out:write(',')
|
|
end
|
|
if indent then
|
|
out:write('\n')
|
|
end
|
|
end
|
|
if indent then
|
|
out:write(string.rep("\t", indent-1))
|
|
end
|
|
out:write('}')
|
|
end
|
|
|
|
-- Make a copy of a table and all subtables
|
|
function copyTable(tbl, noRecurse)
|
|
local out = {}
|
|
for k, v in pairs(tbl) do
|
|
if not noRecurse and type(v) == "table" then
|
|
out[k] = copyTable(v)
|
|
else
|
|
out[k] = v
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
do
|
|
local subTableMap = { }
|
|
function copyTableSafe(tbl, noRecurse, preserveMeta, isSubTable)
|
|
local out = {}
|
|
if not noRecurse then
|
|
subTableMap[tbl] = out
|
|
end
|
|
if preserveMeta then
|
|
setmetatable(out, getmetatable(tbl))
|
|
end
|
|
for k, v in pairs(tbl) do
|
|
if not noRecurse and type(v) == "table" then
|
|
out[k] = subTableMap[v] or copyTableSafe(v, false, preserveMeta, true)
|
|
else
|
|
out[k] = v
|
|
end
|
|
end
|
|
if not noRecurse and not isSubTable then
|
|
wipeTable(subTableMap)
|
|
end
|
|
return out
|
|
end
|
|
end
|
|
|
|
function mergeDB(srcDB, modDB)
|
|
if modDB then
|
|
srcDB:AddDB(modDB)
|
|
for k,v in pairs(modDB.conditions) do
|
|
srcDB.conditions[k] = v
|
|
end
|
|
for k,v in pairs(modDB.multipliers) do
|
|
srcDB.multipliers[k] = v
|
|
end
|
|
end
|
|
end
|
|
|
|
function specCopy(env)
|
|
local modDB = new("ModDB")
|
|
modDB:AddDB(env.modDB)
|
|
modDB.conditions = copyTable(env.modDB.conditions)
|
|
modDB.multipliers = copyTable(env.modDB.multipliers)
|
|
local enemyDB = new("ModDB")
|
|
if env.enemyDB then
|
|
enemyDB:AddDB(env.enemyDB)
|
|
enemyDB.conditions = copyTable(env.enemyDB.conditions)
|
|
enemyDB.multipliers = copyTable(env.enemyDB.multipliers)
|
|
end
|
|
local minionDB = nil
|
|
if env.minion then
|
|
minionDB = new("ModDB")
|
|
minionDB:AddDB(env.minion.modDB)
|
|
minionDB.conditions = copyTable(env.minion.modDB.conditions)
|
|
minionDB.multipliers = copyTable(env.minion.modDB.multipliers)
|
|
end
|
|
return modDB, enemyDB, minionDB
|
|
end
|
|
|
|
-- Wipe all keys from the table and return it, or return a new table if no table provided
|
|
function wipeTable(tbl)
|
|
if not tbl then
|
|
return { }
|
|
end
|
|
for k in pairs(tbl) do
|
|
tbl[k] = nil
|
|
end
|
|
return tbl
|
|
end
|
|
|
|
-- Search a table for a value, and return the corresponding key
|
|
function isValueInTable(tbl, val)
|
|
for k, v in pairs(tbl) do
|
|
if val == v then
|
|
return k
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Search an array for a value, and return the corresponding array index
|
|
function isValueInArray(tbl, val)
|
|
for i, v in ipairs(tbl) do
|
|
if val == v then
|
|
return i
|
|
end
|
|
end
|
|
end
|
|
|
|
---Returns the array index of the first element for which the predicate evaluates to true
|
|
---@param table table
|
|
---@param predicate fun(value:any):any
|
|
---@return number
|
|
function isValueInArrayPred(table, predicate)
|
|
for i, v in ipairs(table) do
|
|
if predicate(v) then
|
|
return i
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Pretty-prints a table
|
|
function prettyPrintTable(tbl, pre, outFile)
|
|
pre = pre or ""
|
|
local outNames = { }
|
|
for name in pairs(tbl) do
|
|
t_insert(outNames, name)
|
|
end
|
|
table.sort(outNames)
|
|
for _, name in ipairs(outNames) do
|
|
if type(tbl[name]) == "table" then
|
|
prettyPrintTable(tbl[name], pre .. name .. ".", outFile)
|
|
else
|
|
if outFile then
|
|
outFile:write(pre .. name .. " = " .. tostring(tbl[name]) .. "\n")
|
|
else
|
|
ConPrintf("%s%s = %s", pre, name, tostring(tbl[name]))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function tableConcat(t1,t2)
|
|
local t3 = {}
|
|
for i=1,#t1 do
|
|
t3[#t3+1] = t1[i]
|
|
end
|
|
for i=1,#t2 do
|
|
t3[#t3+1] = t2[i]
|
|
end
|
|
return t3
|
|
end
|
|
|
|
--- Simple table value equality
|
|
---@param t1 table
|
|
---@param t2 table
|
|
---@return boolean
|
|
function tableDeepEquals(t1, t2)
|
|
if t1 == t2 then
|
|
return true
|
|
end
|
|
if not t1 or not t2 or #t1 ~= #t2 then
|
|
return false
|
|
end
|
|
for k, v1 in pairs(t1) do
|
|
local v2 = t2[k]
|
|
local typeV1 = type(v1)
|
|
if not (typeV1 == type(v2) and (typeV1 == "table" and tableDeepEquals(v1, v2) or v1 == v2)) then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function pairsYield(t)
|
|
local k
|
|
local start = GetTime()
|
|
return function() -- iterator function
|
|
if coroutine.running() and GetTime() - start > 20 then
|
|
coroutine.yield()
|
|
start = GetTime()
|
|
end
|
|
local v
|
|
k, v = next(t, k)
|
|
return k, v
|
|
end
|
|
end
|
|
|
|
-- Based on https://www.lua.org/pil/19.3.html
|
|
function pairsSortByKey(t, f)
|
|
local sortedKeys = {}
|
|
for key in pairs(t) do t_insert(sortedKeys, key) end
|
|
table.sort(sortedKeys, f)
|
|
local i = 0
|
|
return function() -- iterator function
|
|
i = i + 1
|
|
if sortedKeys[i] == nil then
|
|
return nil
|
|
else
|
|
return sortedKeys[i], t[sortedKeys[i]]
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Natural sort comparator
|
|
function naturalSortCompare(a, b)
|
|
local aIndex, bIndex = 1, 1
|
|
while true do
|
|
local aStr, aNum, aEnd = a:match("(.-)(%d+)()", aIndex)
|
|
local bStr, bNum, bEnd = b:match("(.-)(%d+)()", bIndex)
|
|
if not aStr or not bStr then
|
|
aStr = a:sub(aIndex)
|
|
bStr = b:sub(bIndex)
|
|
end
|
|
local al = aStr:upper()
|
|
local bl = bStr:upper()
|
|
if al ~= bl then
|
|
return al < bl
|
|
end
|
|
if aStr ~= bStr then
|
|
return aStr < bStr
|
|
end
|
|
if not aNum then
|
|
return a < b
|
|
end
|
|
local an = tonumber(aNum)
|
|
local bn = tonumber(bNum)
|
|
if an ~= bn then
|
|
return an < bn
|
|
end
|
|
if aNum ~= bNum then
|
|
return aNum < bNum
|
|
end
|
|
aIndex = aEnd
|
|
bIndex = bEnd
|
|
end
|
|
end
|
|
|
|
-- Rounds a number to the nearest <dec> decimal places
|
|
function round(val, dec)
|
|
if dec then
|
|
return m_floor(val * 10 ^ dec + 0.5) / 10 ^ dec
|
|
else
|
|
return m_floor(val + 0.5)
|
|
end
|
|
end
|
|
|
|
--- Rounds down a number to the nearest <dec> decimal places
|
|
---@param val number
|
|
---@param dec number
|
|
---@return number
|
|
function floor(val, dec)
|
|
if dec then
|
|
local mult = 10 ^ dec
|
|
return m_floor(val * mult + 0.0001) / mult
|
|
else
|
|
return m_floor(val)
|
|
end
|
|
end
|
|
|
|
---@param n number
|
|
---@return number
|
|
function triangular(n)
|
|
--- Returns the n-th triangular number
|
|
--- See https://en.wikipedia.org/wiki/Triangular_number
|
|
return n * (n + 1) / 2
|
|
end
|
|
|
|
-- Formats "1234.56" -> "1,234.5"
|
|
function formatNumSep(str)
|
|
return string.gsub(str, "(%^?x?%x?%x?%x?%x?%x?%x?-?%d+%.?%d+)", function(m)
|
|
local colour = m:match("(^x%x%x%x%x%x%x)") or m:match("(%^%d)") or ""
|
|
local str = m:gsub("(^x%x%x%x%x%x%x)", ""):gsub("(%^%d)", "")
|
|
if str == "" or (colour == "" and m:match("%^")) then -- return if we have an invalid color code or a completely stripped number.
|
|
return m
|
|
end
|
|
local x, y, minus, integer, fraction = str:find("(-?)(%d+)(%.?%d*)")
|
|
if main.showThousandsSeparators then
|
|
rev1kSep = utf8.reverse(main.thousandsSeparator)
|
|
integer = utf8.reverse(utf8.gsub(utf8.reverse(integer), "(%d%d%d)", "%1"..rev1kSep))
|
|
-- There will be leading separators if the number of digits are divisible by 3
|
|
-- This checks for their presence and removes them
|
|
-- Don't use patterns here because thousandsSeparator can be a pattern control character, and will crash if used
|
|
if main.thousandsSeparator ~= "" then
|
|
local thousandsSeparator = utf8.find(integer, rev1kSep, 1, 2)
|
|
if thousandsSeparator and thousandsSeparator == 1 then
|
|
integer = utf8.sub(integer, 2)
|
|
end
|
|
end
|
|
else
|
|
integer = utf8.reverse(utf8.gsub(utf8.reverse(integer), "(%d%d%d)", "%1"))
|
|
end
|
|
return colour..minus..integer..utf8.gsub(fraction, "%.", main.decimalSeparator)
|
|
end)
|
|
end
|
|
|
|
function getFormatNumSep(dec)
|
|
return function(val)
|
|
return formatNumSep(val, dec)
|
|
end
|
|
end
|
|
|
|
-- Formats 1234.56 -> "1234.6" [dec=1]
|
|
function formatRound(val, dec)
|
|
dec = dec or 0
|
|
return m_floor(val * 10 ^ dec + 0.5) / 10 ^ dec
|
|
end
|
|
function getFormatRound(dec)
|
|
return function(val)
|
|
return formatRound(val, dec)
|
|
end
|
|
end
|
|
|
|
-- Formats 12.3456 -> "1234.6%" [dec=1]
|
|
function formatPercent(val, dec)
|
|
dec = dec or 0
|
|
return m_floor((val or 0) * 100 * 10 ^ dec + 0.5) / 10 ^ dec .. "%"
|
|
end
|
|
function getFormatPercent(dec)
|
|
return function(val)
|
|
return formatPercent(val, dec)
|
|
end
|
|
end
|
|
|
|
-- Formats 1234.56 -> "1234.5s" [dec=1]
|
|
function formatSec(val, dec)
|
|
dec = dec or 0
|
|
if val == 0 then
|
|
return "0s"
|
|
else
|
|
return s_format("%."..dec.."fs", val)
|
|
end
|
|
end
|
|
function getFormatSec(dec)
|
|
return function(val)
|
|
return formatSec(val, dec)
|
|
end
|
|
end
|
|
|
|
function copyFile(srcName, dstName)
|
|
local inFile, msg = io.open(srcName, "r")
|
|
if not inFile then
|
|
return nil, "Couldn't open '"..srcName.."': "..msg
|
|
end
|
|
local outFile, msg = io.open(dstName, "w")
|
|
if not outFile then
|
|
return nil, "Couldn't create '"..dstName.."': "..msg
|
|
end
|
|
outFile:write(inFile:read("*a"))
|
|
inFile:close()
|
|
outFile:close()
|
|
return true
|
|
end
|
|
|
|
function zip(a, b)
|
|
local zipped = { }
|
|
for i, _ in pairs(a) do
|
|
table.insert(zipped, { a[i], b[i] })
|
|
end
|
|
return zipped
|
|
end
|
|
|
|
-- Generate a UUID for a skill
|
|
function cacheSkillUUID(skill, env)
|
|
local strName = skill.activeEffect.grantedEffect.name:gsub("%s+", "") -- strip spaces
|
|
local strSlotName = (skill.socketGroup and skill.socketGroup.slot and skill.socketGroup.slot:upper() or "NO_SLOT"):gsub("%s+", "") -- strip spaces
|
|
local slotIndx = 1
|
|
local groupIdx = 1
|
|
if skill.socketGroup and skill.socketGroup.gemList and skill.activeEffect.srcInstance then
|
|
for idx, gem in ipairs(skill.socketGroup.gemList) do
|
|
-- we compare table addresses rather than names since two of the same gem
|
|
-- can be socketed in the same slot
|
|
if gem == skill.activeEffect.srcInstance then
|
|
slotIndx = idx
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
for i, group in ipairs(env.build.skillsTab.socketGroupList) do
|
|
if skill.socketGroup == group then
|
|
groupIdx = i
|
|
break
|
|
end
|
|
end
|
|
|
|
return strName.."_"..strSlotName.."_"..tostring(slotIndx) .. "_" .. tostring(groupIdx)
|
|
end
|
|
|
|
-- Global Cache related
|
|
function cacheData(uuid, env)
|
|
GlobalCache.cachedData[env.mode][uuid] = {
|
|
Name = env.player.mainSkill.activeEffect.grantedEffect.name,
|
|
Speed = env.player.output.Speed,
|
|
HitSpeed = env.player.output.HitSpeed,
|
|
ManaCost = env.player.output.ManaCost,
|
|
LifeCost = env.player.output.LifeCost,
|
|
ESCost = env.player.output.ESCost,
|
|
RageCost = env.player.output.RageCost,
|
|
HitChance = env.player.output.HitChance,
|
|
AccuracyHitChance = env.player.output.AccuracyHitChance,
|
|
PreEffectiveCritChance = env.player.output.PreEffectiveCritChance,
|
|
CritChance = env.player.output.CritChance,
|
|
TotalDPS = env.player.output.TotalDPS,
|
|
ActiveSkill = env.player.mainSkill,
|
|
Env = env,
|
|
}
|
|
end
|
|
|
|
-- Wipe all the tables associated with Global Cache
|
|
function wipeGlobalCache()
|
|
wipeTable(GlobalCache.cachedData.MAIN)
|
|
wipeTable(GlobalCache.cachedData.CALCS)
|
|
wipeTable(GlobalCache.cachedData.CALCULATOR)
|
|
end
|
|
|
|
-- Check if a specific named gem is enabled in a socket group belonging to a skill
|
|
function supportEnabled(skillName, activeSkill)
|
|
for _, gemInstance in ipairs(activeSkill.socketGroup.gemList) do
|
|
if gemInstance.skillId == skillName then
|
|
return gemInstance.enabled
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function stringify(thing)
|
|
if type(thing) == 'string' then
|
|
return thing
|
|
elseif type(thing) == 'number' then
|
|
return ""..thing;
|
|
elseif type(thing) == 'table' then
|
|
local s = "{";
|
|
local keys = { }
|
|
for key in pairs(thing) do table.insert(keys, key) end
|
|
table.sort(keys)
|
|
for _, k in ipairs(keys) do
|
|
local v = thing[k]
|
|
s = s.."\n\t"
|
|
if type(k) == 'number' then
|
|
s = s.."["..k.."] = "
|
|
else
|
|
s = s.."[\""..k.."\"] = "
|
|
end
|
|
if type(v) == 'string' then
|
|
s = s.."\""..stringify(v).."\", "
|
|
else
|
|
if type(v) == "boolean" then
|
|
v = v and "true" or "false"
|
|
end
|
|
val = stringify(v)..", "
|
|
if type(v) == "table" then
|
|
val = string.gsub(val, "\n", "\n\t")
|
|
end
|
|
s = s..val;
|
|
end
|
|
end
|
|
return s.."\n}"
|
|
end
|
|
end
|
|
|
|
-- Class function to split a string on a single character (??) separator.
|
|
-- returns a list of fields, not including the separator.
|
|
-- Will return the first field as blank if the first character of the string is the separator
|
|
-- Separator defaults to colon
|
|
function string:split(sep)
|
|
-- Initially from http://lua-users.org/wiki/SplitJoin
|
|
-- function will ignore duplicate separators
|
|
local sep, fields = sep or ":", {}
|
|
local pattern = s_format("([^%s]+)", sep)
|
|
-- inject a blank entry if self begins with a colon
|
|
if string.sub(self, 1, 1) == sep then t_insert(fields, "") end
|
|
self:gsub(pattern, function(c) fields[#fields+1] = c end)
|
|
return fields
|
|
end
|
|
|
|
-- Ceil function with optional base parameter
|
|
function ceil_b(x, base)
|
|
base = base or 1
|
|
return base * m_ceil(x/base)
|
|
end
|
|
|
|
-- Ceil function with optional base parameter
|
|
function floor_b(x, base)
|
|
base = base or 1
|
|
return base * m_floor(x/base)
|
|
end
|
|
|
|
function urlEncode(str)
|
|
local charToHex = function(c)
|
|
return s_format("%%%02X", string.byte(c))
|
|
end
|
|
return str:gsub("([^%w_%-.~])", charToHex)
|
|
end
|
|
|
|
function urlDecode(str)
|
|
local hexToChar = function(x)
|
|
return s_char(tonumber(x, 16))
|
|
end
|
|
return str:gsub("%%(%x%x)", hexToChar)
|
|
end
|
|
|
|
function string:matchOrPattern(pattern)
|
|
local function generateOrPatterns(pattern)
|
|
local subGroups = {}
|
|
local index = 1
|
|
-- find and call generate patterns on all subGroups
|
|
for subGroup in pattern:gmatch("%b()") do
|
|
local open, close = pattern:find(subGroup, (subGroups[index] and subGroups[index].close or 1), true)
|
|
t_insert(subGroups, { open = open, close = close, patterns = generateOrPatterns(subGroup:sub(2,-2)) })
|
|
index = index + 1
|
|
end
|
|
|
|
-- generate complete patterns from the subGroup patterns
|
|
local generatedPatterns = { pattern:sub(1, (subGroups[1] and subGroups[1].open or 0) - 1) }
|
|
for i, subGroup in ipairs(subGroups) do
|
|
local regularNextString = pattern:sub(subGroup.close + 1, (subGroups[i+1] and subGroups[i+1].open or 0) - 1)
|
|
local tempPatterns = {}
|
|
for _, subPattern in ipairs(generatedPatterns) do
|
|
for subGroupPattern in pairs(subGroup.patterns) do
|
|
t_insert(tempPatterns, subPattern..subGroupPattern..regularNextString)
|
|
end
|
|
end
|
|
generatedPatterns = tempPatterns
|
|
end
|
|
|
|
-- apply | operators
|
|
local orPatterns = { }
|
|
for _, generatedPattern in ipairs(generatedPatterns) do
|
|
for orPattern in generatedPattern:gmatch("[^|]+") do
|
|
orPatterns[orPattern] = true -- store string as key to avoid duplicates.
|
|
end
|
|
end
|
|
return orPatterns
|
|
end
|
|
|
|
local orPatterns = generateOrPatterns(pattern)
|
|
for orPattern in pairs(orPatterns) do
|
|
if self:match(orPattern) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function ImportBuild(importLink, callback)
|
|
local urlText = importLink:gsub("^[%s?]+", ""):gsub("[%s?]+$", "") -- Quick Trim
|
|
if urlText:match("youtube%.com/redirect%?") or urlText:match("google%.com/url%?") then
|
|
local nested_url = urlText:gsub(".*[?&]q=([^&]+).*", "%1")
|
|
urlText = UrlDecode(nested_url)
|
|
end
|
|
local websiteInfo = nil
|
|
for j = 1, #buildSites.websiteList do
|
|
if urlText and urlText:match(buildSites.websiteList[j].matchURL) then
|
|
websiteInfo = buildSites.websiteList[j]
|
|
end
|
|
end
|
|
|
|
-- its an import link
|
|
if websiteInfo then
|
|
buildSites.DownloadBuild(urlText, websiteInfo, function(isSuccess, data)
|
|
if isSuccess then
|
|
callback(Inflate(common.base64.decode(data:gsub("-", "+"):gsub("_", "/"))), urlText)
|
|
end
|
|
end)
|
|
else
|
|
-- try to decode input buffer
|
|
callback(Inflate(common.base64.decode(importLink:gsub("-", "+"):gsub("_", "/"))), nil)
|
|
end
|
|
end
|