-- 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(""[, ""[, "" ...]], 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 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 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