-- Copyright ©2024-2025 by Todd Hundersmarck (ThundR) 
-- All Rights Reserved

--[[

Unauthorized use and/or distribution of this work entitles myself, the author, to unlimited free and unrestricted use, access, and distribution of any works related to the unauthorized user and/or distributor.

--]]

local g_thModName = g_currentModName
local g_thModPath = g_currentModDirectory
_G.THClassType = {
    OBJECT = "object",
    EVENT = "event"
}
_G.THValueType = {
    UNKNOWN = "unknown",
    STRING = "string",
    INTEGER = "integer",
    NUMBER = "number",
    BOOLEAN = "boolean",
    TABLE = "table",
    FUNCTION = "function",
    USERDATA = "userdata"
}
_G.THColor = {
    TRANSPARENT = { 0, 0, 0, 0 },
    WHITE = { 1, 1, 1, 1 },
    BLACK = { 0, 0, 0, 1 },
    BLUE = { 1, 1, 1, 1 },
    GREEN = { 0.2763, 0.7038, 0.0782, 1 },
    ORANGE = { 0.9900, 0.4640, 0.0010, 1 },
    RED = { 0.9069, 0.0097, 0.0097, 1 },
    BACKGROUND = { 0, 0, 0, 0.75 }
}
_G.THAxis = {
    X = 1,
    Y = 2,
    Z = 3
}
_G.THMessage = {
    INTERNAL_ERROR = "An internal error has occurred",
    ARGUMENT_INVALID = "Invalid argument %q (%s) in function call",
    FILE_NOT_FOUND = "File not found: %s",
    INVALID_FILL_TYPE = "Invalid fillType: %s",
    INVALID_FRUIT_TYPE = "Invalid fruitType: %s",
    INVALID_PARENT = "Invalid parent: %s",
    PRELOADING = "Pre-loading: %s",
    LOADING = "Loading: %s",
    LOADING_ERROR = "Failed to load: %s",
    DUPLICATE_ENTRY = "Duplicate %s: %s",
    INVALID_VALUE = "Invalid %s: %s",
    MISSING_XML_KEY = "Missing xml key: %s",
    VALUE_OUT_OF_RANGE = "Value %q (%s) must be between %s and %s",
    VALUE_GREATER = "Value %q (%s) must be greater than %s",
    VALUE_GREATER_EQUAL = "Value %q (%s) must be greater or equal to %s",
    VALUE_LESSER = "Value %q (%s) must be less than %s",
    VALUE_LESSER_EQUAL = "Value %q (%s) must be less than or equal to %s",
    EVENT_ADMIN_ONLY = "Event requires admin privileges",
    EVENT_NO_CLINET = "Event should not run on client machines!",
    EVENT_NO_CLIENT_READ = "Event should not read from client machines!",
    EVENT_NO_CLIENT_WRITE = "Event should not write from client machines!",
    EVENT_NO_SERVER = "Event should not run on the server!",
    EVENT_NO_SERVER_READ = "Event should not read from the server!",
    EVENT_NO_SERVER_WRITE = "Event should not write from the server!",
    NETWORK_ID_MISMATCH = "Network id mismatch: %s"
}
_G.THGameVersion = 25
if g_minModDescVersion ~= nil then
    if g_minModDescVersion < 90 then
        _G.THGameVersion = 22
    end
end
_G.g_thGlobalEnv = _G
local meta = getmetatable(_G)
if meta ~= nil then
    if type(meta.__index) == THValueType.TABLE then
        _G.g_thGlobalEnv = meta.__index
    else
        printf("[ERROR]: %s: Could not find global environment", g_thModName)
    end
end
THUtils = {
    modName = g_thModName,
    modPath = g_thModPath,
    networkObjects = {},
    networkObjectToId = {},
    networkIdToObject = {},
    functionCache = {},
}
THUtils.MESSAGE_PREFIX = string.format("%s: ", THUtils.modName)
function THUtils.getObjectName(objectId)
    local objectIdType = type(objectId)
    local objectName = nil
    if objectIdType == THValueType.TABLE then
        if type(objectId.getName) == THValueType.FUNCTION then
            objectName = objectId:getName()
        end
    elseif objectIdType == THValueType.NUMBER then
        if THUtils.floor(objectId) == objectId and objectId > 0 then
            objectName = getName(objectId)
        end
    elseif objectId ~= nil then
        THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "objectId", objectId)
    end
    return objectName
end
function THUtils.getObjectNamePrefixedText(object, text)
    local objectName = THUtils.getObjectName(object)
    if text ~= nil and text ~= ""
        and objectName ~= nil and objectName ~= ""
    then
        return string.format("%s: %s", objectName, text)
    end
    return text
end
function THUtils.displayMsg(text, ...)
    if text == "" then
        print(text)
    else
        local isError = false
        if type(text) == THValueType.STRING then
            if select("#", ...) > 0 then
                text = string.format(text, ...)
            end
        else
            text = string.format(THMessage.ARGUMENT_INVALID, "text", text)
            isError = true
        end
        local msgText = THUtils.MESSAGE_PREFIX .. text
        if isError then
            print("[ERROR] " .. msgText)
        else
            print("[INFO] " .. msgText)
        end
        if isError then
            printCallstack()
        end
    end
end
function THUtils.objectDisplayMsg(object, text, ...)
    if text ~= nil and text ~= "" then
        local prefixedText = THUtils.getObjectNamePrefixedText(object, text)
        if prefixedText ~= nil and prefixedText ~= "" then
            THUtils.displayMsg(prefixedText, ...)
            return
        end
    end
    THUtils.displayMsg(text, ...)
end
function THUtils.errorMsg(showStack, text, ...)
    local msgPrefix = THUtils.MESSAGE_PREFIX
    if not showStack or showStack == true then
        if type(text) == THValueType.STRING and text ~= "" then
            if showStack == nil or showStack == true then
                printf("[ERROR] " .. msgPrefix .. text, ...)
            else
                printf("[WARNING] " .. msgPrefix .. text, ...)
            end
        end
        if showStack == true then
            printCallstack()
        end
    else
        text = string.format(THMessage.ARGUMENT_INVALID, "showStack", showStack)
        printf("[ERROR] " .. msgPrefix .. text)
        printCallstack()
    end
end
function THUtils.objectErrorMsg(object, showStack, text, ...)
    if text ~= nil and text ~= "" then
        local prefixedText = THUtils.getObjectNamePrefixedText(object, text)
        if prefixedText ~= nil and prefixedText ~= "" then
            THUtils.errorMsg(showStack, prefixedText, ...)
            return
        end
    end
    THUtils.errorMsg(showStack, text, ...)
end
function THUtils.assert(expression, showStack, text, ...)
    if not expression then
        if showStack ~= nil then
            THUtils.errorMsg(showStack, text, ...)
        else
            THUtils.displayMsg(text, ...)
        end
    end
    return expression
end
function THUtils.assertValue(expression, showStack, yesValue, noValue, text, ...)
    if THUtils.assert(expression, showStack, text, ...) then
        return yesValue
    end
    return noValue
end
function THUtils.argIsValid(expression, argName, argValue)
    if not expression then
        THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, argName, argValue)
        return false
    end
    return true
end
function THUtils.validateArg(expression, argName, argValue, defValue)
    if THUtils.argIsValid(expression, argName, argValue) then
        return argValue
    end
    return defValue
end
function THUtils.xmlErrorMsg(xmlKey, showStack, text, ...)
    text = text or ""
    if THUtils.argIsValid(type(text) == THValueType.STRING, "text", text)
        and THUtils.argIsValid(not showStack or showStack == true, "showStack", showStack)
    then
        local msgPrefix = THUtils.MESSAGE_PREFIX
        if showStack == nil or showStack == true then
            printf("[XML ERROR] " .. msgPrefix .. xmlKey)
        else
            printf("XML WARNING] " .. msgPrefix .. xmlKey)
        end
        if text ~= nil and text ~= "" then
            THUtils.displayMsg("- " .. text, ...)
        end
        if showStack == true then
            printCallstack()
        end
    end
end
function THUtils.objectXMLErrorMsg(object, xmlKey, showStack, text, ...)
    local objectName = THUtils.getObjectName(object)
    if objectName ~= nil and objectName ~= "" then
        local oldMsgPrefix = THUtils.MESSAGE_PREFIX
        THUtils.MESSAGE_PREFIX = string.format("%s %s:", oldMsgPrefix, objectName)
        THUtils.xmlErrorMsg(xmlKey, showStack, text, ...)
        THUtils.MESSAGE_PREFIX = oldMsgPrefix
        return
    end
    THUtils.xmlErrorMsg(xmlKey, showStack, text, ...)
end
function THUtils.getNoNil(value, replacement)
    if value == nil then
        return replacement
    end
    return value
end
function THUtils.evaluate(expression, yesValue, noValue)
    if expression then
        return yesValue
    end
    return noValue
end
function THUtils.pcall(func, pa,pb,pc,pd,pe,pf,pg,ph,pi,pj,pk,pl,pm,pn,po,pp,pq,pr,ps,pt,pu,pv,pw,px,py,pz)
    local function errHandler(errMsg)
        errMsg = THUtils.getNoNil(errMsg, THMessage.INTERNAL_ERROR)
        if type(errMsg) == THValueType.STRING then
            THUtils.errorMsg(true, errMsg)
        else
            printCallstack()
        end
    end
    local function protectedFunc()
        return func(pa,pb,pc,pd,pe,pf,pg,ph,pi,pj,pk,pl,pm,pn,po,pp,pq,pr,ps,pt,pu,pv,pw,px,py,pz)
    end
    return xpcall(protectedFunc, errHandler)
end
function THUtils.call(func, ...)
    local function appendFunc(rSuccess, ret1, ...)
        if rSuccess == true then
            return ret1, ...
        end
    end
    return appendFunc(THUtils.pcall(func, ...))
end
function THUtils.getFilename(filename, filePath, verbose, ...)
    if THUtils.argIsValid(filename == nil or type(filename) == THValueType.STRING, "filename", filename)
        and THUtils.argIsValid(filePath == nil or type(filePath) == THValueType.STRING, "filePath", filePath)
        and THUtils.argIsValid(not verbose or verbose == true, "verbose", verbose)
    then
        if filename == nil then
            if verbose == true then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "filename", filename)
            end
        else
            local function appendFunc(rAbsFilename, ...)
                THUtils.pcall(function()
                    rAbsFilename = rAbsFilename or ""
                    if verbose then
                        if rAbsFilename == "" or not fileExists(rAbsFilename) then
                            THUtils.errorMsg(true, THMessage.FILE_NOT_FOUND, rAbsFilename)
                        end
                    end
                end)
                return rAbsFilename, ...
            end
            if filePath ~= nil and filePath ~= "" then
                return appendFunc(Utils.getFilename(filename, filePath, ...))
            end
            return appendFunc(Utils.getFilename(filename, nil, ...))
        end
    end
    return ""
end
function THUtils.getFilenameAndDirectory(absFilename)
    if THUtils.argIsValid(type(absFilename) == THValueType.STRING, "absFilename", absFilename) then
        absFilename = string.gsub(absFilename, "\\", "/")
        absFilename = string.gsub(absFilename, "//+", "/")
        if string.find(absFilename, "/", nil, true) then
            if string.sub(absFilename, -1) == "/" then
                return nil, absFilename
            end
            local filenameParts = THUtils.splitString(absFilename, "/")
            if filenameParts ~= nil then
                local numFileParts = #filenameParts
                local filePath = nil
                if numFileParts > 0 then
                    for i = 1, numFileParts do
                        if i == numFileParts then
                            return filenameParts[i], filePath
                        end
                        if filePath == nil then
                            filePath = filenameParts[i]
                        else
                            filePath = filePath .. filenameParts[i]
                        end
                        filePath = filePath .. "/"
                    end
                end
            end
        else
            return absFilename
        end
    end
end
function THUtils.getNormalizedFilePath(filePath)
    if type(filePath) == THValueType.STRING then
        if filePath ~= "" then
            local normalFilePath = string.gsub(filePath, "\\", "/")
            if string.sub(normalFilePath, -1) ~= "/" then
                normalFilePath = normalFilePath .. "/"
            end
            return normalFilePath
        end
        return filePath
    end
end
function THUtils.getFileExists(filename, filePath)
    if filename ~= nil and filename ~= "" then
        local absFilename = Utils.getFilename(filename, filePath)
        if absFilename ~= nil and fileExists(absFilename) then
            return true
        end
    end
    return false
end
function THUtils.toBoolean(value, isExtended)
    isExtended = THUtils.validateArg(not isExtended or isExtended == true, "isExtended", isExtended, false)
    if value == true or value == false then
        return value
    elseif type(value) == THValueType.STRING then
        local upperVal = string.upper(value)
        if upperVal == "TRUE" then
            return true
        elseif upperVal == "FALSE" then
            return false
        elseif isExtended then
            if upperVal == "YES" or upperVal == "ON" then
                return true
            elseif upperVal == "NO" or upperVal == "OFF" then
                return false
            end
        end
    elseif isExtended then
        if type(value) == THValueType.NUMBER then
            if value == 1 then
                return true
            elseif value == 0 then
                return false
            end
        end
    end
end
function THUtils.toString(value, allowNil)
    allowNil = THUtils.validateArg(not allowNil or allowNil == true, "allowNil", allowNil, false)
    if value == nil and allowNil then
        return value
    end
    return tostring(value)
end
function THUtils.toNumber(value, isInteger)
    isInteger = THUtils.validateArg(not isInteger or isInteger == true, "isInteger", isInteger, false)
    if value ~= nil then
        value = tonumber(value)
        if value ~= nil then
            if isInteger == true then
                value = math.floor(value)
            end
            return value
        end
    end
end
function THUtils.getIsType(value, reqType)
    local valType = type(value)
    if valType == reqType then
        return true
    end
    if reqType == THValueType.INTEGER then
        if valType == THValueType.NUMBER and math.floor(value) == value then
            return true
        end
    else
        local reqTypeVarType = type(reqType)
        if reqTypeVarType == THValueType.TABLE then
            if valType == THValueType.TABLE and value.isa ~= nil and value:isa(reqType) then
                return true
            end
        elseif reqTypeVarType == THValueType.STRING then
            local typeList = THUtils.splitString(reqType, "|", true)
            if typeList ~= nil then
                for otherType in pairs(typeList) do
                    if otherType == valType then
                        return true
                    end
                    if otherType == THValueType.INTEGER then
                        if valType == THValueType.NUMBER and math.floor(value) == value then
                            return true
                        end
                    end
                end
            end
        else
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "reqType", reqType)
        end
    end
    return false
end
function THUtils.sign(value)
    if THUtils.argIsValid(type(value) == THValueType.NUMBER, "value", value) then
        if value < 0 then
            return -1
        end
    end
    return 1
end
function THUtils.clamp(value, minVal, maxVal)
    if THUtils.argIsValid(type(value) == THValueType.NUMBER, "value", value)
        and THUtils.argIsValid(type(minVal) == THValueType.NUMBER, "minVal", minVal)
        and THUtils.argIsValid(type(maxVal) == THValueType.NUMBER, "maxVal", maxVal)
    then
        if minVal > maxVal then
            local oldMinVal = minVal
            minVal = maxVal
            maxVal = oldMinVal
        end
        if THGameVersion == 22 then
            return MathUtil.clamp(value, minVal, maxVal)
        else
            return math.clamp(value, minVal, maxVal)
        end
    end
    return value
end
function THUtils.floor(value, precision)
    value = THUtils.validateArg(type(value) == THValueType.NUMBER, "value", value, 0)
    if type(precision) ~= "number" or precision < 0 then
        if precision ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "precision", precision)
        end
        precision = 0
    else
        precision = math.floor(precision)
    end
    local factor = 10 ^ precision
    return math.floor(value * factor) / factor
end
function THUtils.ceil(value, precision)
    value = THUtils.validateArg(type(value) == THValueType.NUMBER, "value", value, 0)
    if type(precision) ~= "number" or precision < 0 then
        if precision ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "precision", precision)
        end
        precision = 0
    else
        precision = math.floor(precision)
    end
    local factor = 10 ^ precision
    return math.ceil(value * factor) / factor
end
function THUtils.round(value, precision)
    value = THUtils.validateArg(type(value) == THValueType.NUMBER, "value", value, 0)
    if type(precision) ~= "number" or precision < 0 then
        if precision ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "precision", precision)
        end
        precision = 0
    else
        precision = math.floor(precision)
    end
    local factor = 10 ^ precision
    return math.floor((value * factor) + 0.5) / factor
end
function THUtils.getFactorInRange(val1, val2, testVal, noClamp)
    if THUtils.argIsValid(type(val1) == THValueType.NUMBER, "val1", val1)
        and THUtils.argIsValid(type(val2) == THValueType.NUMBER, "val2", val2)
        and THUtils.argIsValid(type(testVal) == THValueType.NUMBER, "testVal", testVal)
    then
        if val1 == val2 then
            if testVal < val1 then
                return 0
            else
                return 1
            end
        end
        local factor = (testVal - val1) / (val2 - val1)
        if noClamp ~= true then
            if noClamp ~= nil and noClamp ~= false then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "noClamp", noClamp)
            end
            factor = THUtils.clamp(factor, 0, 1)
        end
        return factor
    end
    return 0
end
function THUtils.getValueInRange(val1, val2, alpha, noClamp)
    if THUtils.argIsValid(type(val1) == THValueType.NUMBER, "val1", val1)
        and THUtils.argIsValid(type(val2) == THValueType.NUMBER, "val2", val2)
        and THUtils.argIsValid(type(alpha) == THValueType.NUMBER, "alpha", alpha)
    then
        local newValue = val1 + ((val2 - val1) * alpha)
        if noClamp ~= true then
            if noClamp ~= nil and noClamp ~= false then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "noClamp", noClamp)
            end
            local minVal = math.min(val1, val2)
            local maxVal = math.max(val1, val2)
            newValue = THUtils.clamp(newValue, minVal, maxVal)
        end
        return newValue
    end
    if type(val1) == THValueType.NUMBER then
        return val1
    elseif type(val2) == THValueType.NUMBER then
        return val2
    end
    return 0
end
function THUtils.splitString(str, pattern, isList, toUpper)
    if type(pattern) ~= THValueType.STRING or pattern == "" then
        if pattern ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "pattern", pattern)
        end
        pattern = " "
    end
    toUpper = THUtils.validateArg(not toUpper or toUpper == true, "toUpper", toUpper, false)
    local splitTable = {}
    if str == nil or str == "" then
        return splitTable
    end
    if THUtils.argIsValid(type(str) == THValueType.STRING, "str", str)
        and THUtils.argIsValid(not isList or isList == true, "isList", isList)
    then
        local valToAdd, valLength = nil, string.len(str)
        local strPos = 1
        while true do
            if strPos <= 0 or strPos > valLength then
                break
            end
            local strStart, strEnd = string.find(str, pattern, strPos, true)
            if strStart == nil or strEnd == nil then
                valToAdd = string.sub(str, strPos)
                strPos = valLength + 1
            else
                if strEnd <= 1 then
                    valToAdd = nil
                else
                    valToAdd = string.sub(str, strPos, strEnd - 1)
                end
                strPos = strEnd + 1
            end
            if valToAdd ~= nil and valToAdd ~= "" and valToAdd ~= pattern then
                if toUpper and type(valToAdd) == THValueType.STRING then
                    valToAdd = string.upper(valToAdd)
                end
                if isList == true then
                    splitTable[valToAdd] = true
                else
                    table.insert(splitTable, valToAdd)
                end
            end
        end
    end
    return splitTable
end
function THUtils.properCase(value, noSpace)
    local strValue = tostring(value)
    if THUtils.argIsValid(not noSpace or noSpace == true, "noSpace", noSpace) then
        strValue = string.lower(strValue)
        strValue = string.gsub(strValue, "^%a", string.upper)
        strValue = string.gsub(strValue, "[_ ]%a", string.upper)
        if noSpace then
            strValue = string.gsub(strValue, "[_ ]", "")
        else
            strValue = string.gsub(strValue, "_", "")
        end
    end
    return strValue
end
function THUtils.camelCase(value, noSpace)
    local strValue = tostring(value)
    if THUtils.argIsValid(not noSpace or noSpace == true, "noSpace", noSpace) then
        strValue = string.lower(strValue)
        strValue = string.gsub(strValue, "_%a", string.upper)
        if noSpace then
            strValue = string.gsub(strValue, "[_ ]", "")
        else
            strValue = string.gsub(strValue, "_", "")
        end
    end
    return strValue
end
function THUtils.snakeCase(value, toUpper, noSpace)
    local strValue = tostring(value)
    if THUtils.argIsValid(not toUpper or toUpper == true, "toUpper", toUpper)
        and THUtils.argIsValid(not noSpace or noSpace == true, "noSpace", noSpace)
    then
        strValue = string.gsub(strValue, "(%l)(%u)", "%1_%2")
        if toUpper then
            strValue = string.upper(strValue)
        else
            strValue = string.lower(strValue)
        end
        if noSpace then
            strValue = string.gsub(strValue, " ", "_")
        end
    end
    return strValue
end
function THUtils.isValidName(name)
    if name ~= nil and name ~= "" and type(name) == THValueType.STRING
        and not string.find(name, "[^A-Za-z0-9_]")
    then
        return true
    end
    return false
end
function THUtils.formatPercent(value, precision)
    value = THUtils.validateArg(type(value) == THValueType.NUMBER, "value", value, 0)
    if type(precision) ~= "number" or precision < 0 then
        if precision ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "precision", precision)
        end
        precision = 0
    else
        precision = math.floor(precision)
    end
    local percentValue = THUtils.round(value * 100, precision)
    local percentText = string.format("%0." .. tostring(precision) .. "f%%", percentValue)
    return percentText
end
function THUtils.getValueFromString(strValue)
    local valueVarType = type(strValue)
    if valueVarType == THValueType.NUMBER then
        return strValue
    elseif valueVarType == THValueType.STRING then
        local numValue = THUtils.toNumber(strValue)
        if numValue ~= nil then
            return numValue
        else
            local decimalChar = g_i18n.decimalSeparator
            local thousandsChar = g_i18n.thousandsGroupingChar
            strValue = string.gsub(strValue, thousandsChar, "")
            if strValue ~= nil and strValue ~= "" then
                local numStr = string.match(strValue, "[+-]?%d+[" .. decimalChar .. "]?%d+")
                if numStr == nil or numStr == "" then
                    numStr = string.match(strValue, "[+-]?%d+")
                end
                if numStr ~= nil and numStr ~= "" then
                    numValue = THUtils.toNumber(numStr)
                    if numValue ~= nil then
                        return numValue
                    end
                end
            end
        end
    end
end
function THUtils.clearTable(target)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target) then
        for key in pairs(target) do
            target[key] = nil
        end
    end
    return target
end
function THUtils.sortTable(...)
    return table.sort(...)
end
function THUtils.pack(...)
    return table.pack(...)
end
function THUtils.unpack(target, index, endIndex)
    return table.unpack(target, index, endIndex)
end
function THUtils.safePack(userTable, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)
    if THUtils.argIsValid(type(userTable) == THValueType.TABLE, "userTable", userTable) then
        userTable[1] = a
        userTable[2] = b
        userTable[3] = c
        userTable[4] = d
        userTable[5] = e
        userTable[6] = f
        userTable[7] = g
        userTable[8] = h
        userTable[9] = i
        userTable[10] = j
        userTable[11] = k
        userTable[12] = l
        userTable[13] = m
        userTable[14] = n
        userTable[15] = o
        userTable[16] = p
        userTable[17] = q
        userTable[18] = r
        userTable[19] = s
        userTable[20] = t
        userTable[21] = u
        userTable[22] = v
        userTable[23] = w
        userTable[24] = x
        userTable[25] = y
        userTable[26] = z
        local maxIndex = 0
        for index in pairs(userTable) do
            if type(index) == THValueType.NUMBER
                and index > maxIndex and index <= 26
            then
                maxIndex = index
            end
        end
        userTable.n = maxIndex
        return true
    end
    return false
end
function THUtils.addPackedEntry(target, value, entryIndex)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(value ~= nil, "value", value)
        and THUtils.argIsValid(entryIndex == nil or (THUtils.getIsType(entryIndex, THValueType.INTEGER) and entryIndex > 0), "entryIndex", entryIndex)
    then
        local numEntries = target.n or #target or 0
        if numEntries == 0 then
            table.insert(target, value)
            target.n = 1
        else
            if entryIndex == nil then
                table.insert(target, value)
            else
                entryIndex = THUtils.clamp(entryIndex, 1, numEntries + 1)
                table.insert(target, entryIndex, value)
            end
            target.n = numEntries + 1
        end
        return true
    end
    return false
end
function THUtils.removePackedEntry(target, entryIndex)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(entryIndex == nil or (THUtils.getIsType(entryIndex, THValueType.INTEGER) and entryIndex > 0), "entryIndex", entryIndex)
    then
        local numEntries = target.n or #target or 0
        if numEntries > 0
            and target[entryIndex] ~= nil and numEntries >= entryIndex
        then
            table.remove(target, entryIndex)
            target.n = numEntries - 1
            return true
        end
    end
    return false
end
function THUtils.getTableValue(target, targetPath, pattern, isRaw)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target) then
        isRaw = THUtils.validateArg(not isRaw or isRaw == true, "isRaw", isRaw, false)
        if type(pattern) ~= THValueType.STRING or pattern == "" then
            if pattern ~= nil then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "pattern", pattern)
            end
            pattern = "."
        end
        if targetPath == nil or targetPath == "" then
            return target, true
        end
        if THUtils.argIsValid(type(targetPath) == THValueType.STRING, "targetPath", targetPath) then
            if not string.find(targetPath, pattern, nil, true) then
                if isRaw then
                    local targetValue = rawget(target, targetPath)
                    return targetValue, true
                end
                return target[targetPath], true
            end
            local tableKeys = THUtils.splitString(targetPath, pattern)
            local numEntries = (tableKeys == nil and 0) or #tableKeys
            if numEntries > 0 then
                local finalValue = target
                local function getTableEntry(pTarget, pKey)
                    if pKey ~= nil then
                        if isRaw then
                            return rawget(pTarget, pKey)
                        end
                        return pTarget[pKey]
                    end
                end
                for entryIndex = 1, numEntries do
                    if type(finalValue) ~= THValueType.TABLE then
                        return nil, false
                    end
                    local nextKey = tableKeys[entryIndex]
                    local nextEntry = getTableEntry(finalValue, nextKey)
                    if nextEntry ~= nil then
                        finalValue = nextEntry
                    else
                        local otherKey = THUtils.toBoolean(nextKey)
                        nextEntry = getTableEntry(finalValue, otherKey)
                        if nextEntry ~= nil then
                            finalValue = nextEntry
                        else
                            otherKey = THUtils.toNumber(nextKey, true)
                            nextEntry = getTableEntry(finalValue, otherKey)
                            if nextEntry ~= nil then
                                finalValue = nextEntry
                            else
                                finalValue = nil
                            end
                        end
                    end
                end
                return finalValue, true
            end
        end
    end
    return nil, false
end
function THUtils.copyTable(target, depth, copyMeta)
    local newTable = {}
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target) then
        THUtils.validateArg(not copyMeta or copyMeta == true, "copyMeta", copyMeta, true)
        if type(depth) ~= THValueType.NUMBER or depth < 0 then
            if depth ~= nil then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "depth", depth)
            end
            depth = 0
        else
            depth = math.floor(depth)
        end
        for key, val in pairs(target) do
            if type(val) == THValueType.TABLE and depth > 0 then
                newTable[key] = THUtils.copyTable(val, depth - 1)
            else
                newTable[key] = val
            end
        end
        if copyMeta then
            local currentMt = getmetatable(target)
            if currentMt ~= nil then
                setmetatable(newTable, currentMt)
            end
        end
    end
    return newTable
end
function THUtils.createSubTable(target, tableKey, clear, isRaw)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(tableKey ~= nil, "tableKey", tableKey)
        and THUtils.argIsValid(not clear or clear == true, "clear", clear)
        and THUtils.argIsValid(not isRaw or isRaw == true, "isRaw", isRaw)
    then
        local newTable = nil
        if type(target) == THValueType.STRING then
            local targetName = target
            target = g_thGlobalEnv[targetName]
            if target == nil then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "target", targetName)
                return
            end
        end
        if isRaw == true then
            newTable = rawget(target, tableKey)
        else
            newTable = target[tableKey]
        end
        if newTable == nil then
            newTable = {}
            if isRaw == true then
                rawset(target, tableKey, newTable)
            else
                target[tableKey] = newTable
            end
        elseif clear then
            THUtils.clearTable(newTable)
        end
        return newTable
    end
end
function THUtils.getTableLength(target)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target) then
        local numEntries = 0
        if target ~= nil then
            for _ in pairs(target) do
                numEntries = numEntries + 1
            end
        end
        return numEntries
    end
    return 0
end
function THUtils.getDataTable(target, dataKey)
    local dataTable = nil
    if THUtils.argIsValid(target == nil or type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(type(dataKey) == THValueType.STRING, "dataKey", dataKey)
    then
        dataTable = rawget(target, dataKey)
    end
    return dataTable, target
end
function THUtils.createDataTable(target, dataKey, customMt, ...)
    local dataTable = nil
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(type(dataKey) == THValueType.STRING, "dataKey", dataKey)
        and THUtils.argIsValid(customMt == nil or type(customMt) == THValueType.TABLE, "customMt", customMt)
    then
        dataTable = THUtils.getDataTable(target, dataKey)
        if dataTable == nil then
            dataTable = { parent = target }
            rawset(target, dataKey, dataTable)
        end
        if customMt ~= nil then
            local currentMt = getmetatable(dataTable)
            if currentMt == nil then
                setmetatable(dataTable, customMt)
            elseif currentMt ~= customMt then
                THUtils.errorMsg(true, "Cannot change data metatable")
            end
        end
    end
    return dataTable, target
end
function THUtils.createEnumTable(target)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target) then
        if target.getValues ~= nil then
            THUtils.errorMsg(true, "Enum table already created")
            return
        end
        local keyToValue = {}
        local valueToKey = {}
        for key, value in pairs(target) do
            if keyToValue[key] == nil
                and THUtils.getIsType(key, "string|integer")
                and THUtils.getIsType(value, "integer|number|string|boolean")
            then
                if valueToKey[value] == nil then
                    keyToValue[key] = value
                    valueToKey[value] = key
                else
                    THUtils.errorMsg(true, THMessage.DUPLICATE_ENTRY, "value", value)
                end
            end
        end
        local enumTable = target
        enumTable.getId = function(pValue)
            if pValue ~= nil then
                local vKey = valueToKey[pValue]
                if vKey == nil and type(pValue) == THValueType.STRING then
                    vKey = valueToKey[pValue:upper()]
                end
                return vKey
            end
        end
        enumTable.getValue = function(pKey)
            if pKey ~= nil then
                local value = keyToValue[pKey]
                if value == nil and type(pKey) == THValueType.STRING then
                    value = keyToValue[pKey:upper()]
                end
                return value
            end
        end
        enumTable.getValues = function()
            return keyToValue
        end
    end
end
function THUtils.registerValue(target, key, value)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(key ~= nil, "key", key)
        and THUtils.argIsValid(value ~= nil, "value", value)
    then
        local oldValue = target[key]
        if oldValue == nil then
            target[key] = value
        elseif value ~= oldValue then
            THUtils.errorMsg(true, "Table value %q already exists", key)
        end
    end
end
function THUtils.makeSelfCallback(target, targetFunc)
    if target == nil then
        THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "target", target)
    else
        local callbackFunc = function(...)
            return targetFunc(target, ...)
        end
        return callbackFunc
    end
end
function THUtils.addCallbackToArray(callbackArray, callbackTarget, callbackFunc, ...)
    if THUtils.argIsValid(type(callbackArray) == THValueType.TABLE, "callbackArray", callbackArray)
        and THUtils.argIsValid(type(callbackFunc) == THValueType.FUNCTION, "callbackFunc", callbackFunc)
    then
        local numEntries = #callbackArray
        local callbackData = nil
        local isCallbackFound = false
        if numEntries > 0 then
            for entryIndex = 1, numEntries do
                callbackData = callbackArray[entryIndex]
                if callbackData.func == callbackFunc then
                    isCallbackFound = true
                    break
                end
            end
        end
        if not isCallbackFound then
            callbackData = {
                target = callbackTarget,
                func = callbackFunc,
                args = THUtils.pack(...)
            }
            table.insert(callbackArray, callbackData)
        end
        return true, callbackFunc
    end
    return false
end
function THUtils.raiseArrayCallbacks(callbackArray, ...)
    if THUtils.argIsValid(type(callbackArray) == THValueType.TABLE, "callbackArray", callbackArray) then
        local taskManager = g_asyncTaskManager
        local callbackArgs = THUtils.pack(...)
        for entryIndex = 1, #callbackArray do
            local callbackData = callbackArray[entryIndex]
            local hasCallbackArgs = callbackData.args.n > 0
            local function taskFunc()
                if callbackData.target ~= nil then
                    if hasCallbackArgs then
                        callbackData.func(callbackData.target, THUtils.unpack(callbackData.args), THUtils.unpack(callbackArgs))
                    else
                        callbackData.func(callbackData.target, THUtils.unpack(callbackArgs))
                    end
                else
                    if hasCallbackArgs then
                        callbackData.func(THUtils.unpack(callbackData.args), THUtils.unpack(callbackArgs))
                    else
                        callbackData.func(THUtils.unpack(callbackArgs))
                    end
                end
            end
            if entryIndex == 1 then
                taskManager:addTask(function()
                    THUtils.pcall(taskFunc)
                end)
            else
                taskManager:addSubtask(function()
                    THUtils.pcall(taskFunc)
                end)
            end
        end
    end
end
function THUtils.getFunction(srcTable, funcKey)
    local srcTableType = type(srcTable)
    local realSrcTable = nil
    if srcTable == nil then
        realSrcTable = g_thGlobalEnv
    elseif srcTableType == THValueType.TABLE then
        realSrcTable = srcTable
    else
        realSrcTable = g_thGlobalEnv[srcTable]
    end
    if THUtils.argIsValid(type(realSrcTable) == THValueType.TABLE, "srcTable", srcTable) then
        local srcFunc = realSrcTable[funcKey]
        if srcFunc == nil or type(srcFunc) == THValueType.FUNCTION then
            return srcFunc, realSrcTable
        end
        THUtils.errorMsg(true, THMessage.INVALID_VALUE, "function", funcKey)
    end
end
function THUtils.registerFunction(target, funcName, newFunc)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target)
        and THUtils.argIsValid(type(funcName) == THValueType.STRING, "funcName", funcName)
        and THUtils.argIsValid(type(newFunc) == THValueType.FUNCTION, "newFunc", newFunc)
    then
        local oldFunc = rawget(target, funcName)
        if oldFunc ~= nil then
            THUtils.errorMsg(true, "Function %q already registered", funcName)
            return oldFunc
        end
        local callbackFunc = function(...)
            return newFunc(...)
        end
        rawset(target, funcName, callbackFunc)
        return callbackFunc
    end
end
function THUtils.prependFunction(srcTable, srcFuncName, allowCreate, useSelf, extraArg, tgtFunc)
    local srcFunc, realSrcTable = THUtils.getFunction(srcTable, srcFuncName)
    if srcFunc == nil and allowCreate then
        srcFunc = function(...) end
    end
    if realSrcTable ~= nil
        and THUtils.argIsValid(type(srcFunc) == THValueType.FUNCTION, "srcFuncName", srcFuncName)
        and THUtils.argIsValid(type(tgtFunc) == THValueType.FUNCTION, "tgtFunc", tgtFunc)
        and THUtils.argIsValid(not allowCreate or allowCreate == true, "allowCreate", allowCreate)
        and THUtils.argIsValid(not useSelf or useSelf == true, "useSelf", useSelf)
    then
        local callbackFunc = function(p1, ...)
            if useSelf then
                if extraArg ~= nil then
                    THUtils.pcall(tgtFunc, p1, srcFunc, extraArg, ...)
                else
                    THUtils.pcall(tgtFunc, p1, srcFunc, ...)
                end
            else
                if extraArg ~= nil then
                    THUtils.pcall(tgtFunc, extraArg, srcFunc, p1, ...)
                else
                    THUtils.pcall(tgtFunc, srcFunc, p1, ...)
                end
            end
            return srcFunc(p1, ...)
        end
        rawset(realSrcTable, srcFuncName, callbackFunc)
        return true, callbackFunc, realSrcTable
    end
    return false
end
function THUtils.appendFunction(srcTable, srcFuncName, allowCreate, useSelf, extraArg, tgtFunc)
    local srcFunc, realSrcTable = THUtils.getFunction(srcTable, srcFuncName)
    if srcFunc == nil and allowCreate then
        srcFunc = function(...) end
    end
    if realSrcTable ~= nil
        and THUtils.argIsValid(type(srcFunc) == THValueType.FUNCTION, "srcFuncName", srcFuncName)
        and THUtils.argIsValid(type(tgtFunc) == THValueType.FUNCTION, "tgtFunc", tgtFunc)
        and THUtils.argIsValid(not allowCreate or allowCreate == true, "allowCreate", allowCreate)
        and THUtils.argIsValid(not useSelf or useSelf == true, "useSelf", useSelf)
    then
        local callbackFunc = function(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p12, p13, p14, p15, p16, p17, p18, p19, p20)
            local function appendedFunc(...)
                if useSelf then
                    if extraArg ~= nil then
                        THUtils.pcall(tgtFunc, p1, srcFunc, extraArg, p2, p3, p4, p5, p6, p7, p8, p9, p10, p12, p13, p14, p15, p16, p17, p18, p19, p20)
                    else
                        THUtils.pcall(tgtFunc, p1, srcFunc, p2, p3, p4, p5, p6, p7, p8, p9, p10, p12, p13, p14, p15, p16, p17, p18, p19, p20)
                    end
                else
                    if extraArg ~= nil then
                        THUtils.pcall(tgtFunc, extraArg, srcFunc, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p12, p13, p14, p15, p16, p17, p18, p19, p20)
                    else
                        THUtils.pcall(tgtFunc, srcFunc, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p12, p13, p14, p15, p16, p17, p18, p19, p20)
                    end
                end
                return ...
            end
            return appendedFunc(srcFunc(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p12, p13, p14, p15, p16, p17, p18, p19, p20))
        end
        rawset(realSrcTable, srcFuncName, callbackFunc)
        return true, callbackFunc, realSrcTable
    end
    return false
end
function THUtils.overwriteFunction(srcTable, srcFuncName, allowCreate, useSelf, extraArg, tgtFunc)
    local srcFunc, realSrcTable = THUtils.getFunction(srcTable, srcFuncName)
    if srcFunc == nil and allowCreate then
        srcFunc = function(...) end
    end
    if realSrcTable ~= nil
        and THUtils.argIsValid(type(srcFunc) == THValueType.FUNCTION, "srcFuncName", srcFuncName)
        and THUtils.argIsValid(type(tgtFunc) == THValueType.FUNCTION, "tgtFunc", tgtFunc)
        and THUtils.argIsValid(not allowCreate or allowCreate == true, "allowCreate", allowCreate)
        and THUtils.argIsValid(not useSelf or useSelf == true, "useSelf", useSelf)
    then
        local callbackFunc = function(p1, ...)
            if useSelf then
                if extraArg ~= nil then
                    return tgtFunc(p1, srcFunc, extraArg, ...)
                else
                    return tgtFunc(p1, srcFunc, ...)
                end
            else
                if extraArg ~= nil then
                    return tgtFunc(extraArg, srcFunc, p1, ...)
                else
                    return tgtFunc(srcFunc, p1, ...)
                end
            end
        end
        rawset(realSrcTable, srcFuncName, callbackFunc)
        return true, callbackFunc, realSrcTable
    end
    return false
end
function THUtils.addTask(isProtected, taskFunc)
    if type(isProtected) == THValueType.FUNCTION then
        taskFunc = isProtected
        isProtected = false
    elseif not isProtected or isProtected == true then
        if type(taskFunc) ~= THValueType.FUNCTION then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "taskFunc", taskFunc)
            return
        end
    else
        THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "isProtected", isProtected)
        return
    end
    if isProtected then
        g_asyncTaskManager:addTask(function()
            THUtils.pcall(taskFunc)
        end)
    else
        g_asyncTaskManager:addTask(taskFunc)
    end
end
function THUtils.addSubTask(isProtected, taskFunc)
    if type(isProtected) == THValueType.FUNCTION then
        taskFunc = isProtected
        isProtected = false
    elseif not isProtected or isProtected == true then
        if type(taskFunc) ~= THValueType.FUNCTION then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "taskFunc", taskFunc)
            return
        end
    else
        THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "isProtected", isProtected)
        return
    end
    if isProtected then
        g_asyncTaskManager:addSubtask(function()
            THUtils.pcall(taskFunc)
        end)
    else
        g_asyncTaskManager:addSubtask(taskFunc)
    end
end
function THUtils.getFunctionCache(owner, className, funcName, modEnv)
    owner = owner or THUtils
    if THUtils.argIsValid(type(owner) == THValueType.TABLE, "owner", owner)
        and THUtils.argIsValid(className == nil or type(className) == THValueType.STRING, "className", className)
        and THUtils.argIsValid(type(funcName) == THValueType.STRING, "funcName", funcName)
        and THUtils.argIsValid(modEnv == nil or type(modEnv) == THValueType.TABLE, "modEnv", modEnv)
    then
        local classEnv = modEnv or g_thGlobalEnv
        local funcClass = classEnv
        if className ~= nil then
            funcClass = classEnv[className]
        end
        if THUtils.argIsValid(type(funcClass) == THValueType.TABLE, "className", className) then
            local classFunc = funcClass[funcName]
            if THUtils.argIsValid(type(classFunc) == THValueType.FUNCTION, "funcName", funcName) then
                local cachedFunctions = THUtils.functionCache[owner]
                if cachedFunctions ~= nil then
                    local functionId = (className == nil and string.upper(funcName)) or string.upper(className .. "." .. funcName)
                    return cachedFunctions[functionId]
                end
            end
        end
    end
end
function THUtils.createFunctionCache(owner, className, funcName, modEnv)
    owner = owner or THUtils
    if THUtils.argIsValid(type(owner) == THValueType.TABLE, "owner", owner)
        and THUtils.argIsValid(className == nil or type(className) == THValueType.STRING, "className", className)
        and THUtils.argIsValid(type(funcName) == THValueType.STRING, "funcName", funcName)
        and THUtils.argIsValid(modEnv == nil or type(modEnv) == THValueType.TABLE, "modEnv", modEnv)
    then
        local classEnv = modEnv or g_thGlobalEnv
        local funcClass = classEnv
        if className ~= nil then
            funcClass = classEnv[className]
        end
        if THUtils.argIsValid(type(funcClass) == THValueType.TABLE, "className", className) then
            local classFunc = funcClass[funcName]
            if THUtils.argIsValid(type(classFunc) == THValueType.FUNCTION, "funcName", funcName) then
                local functionId = (className == nil and string.upper(funcName)) or string.upper(className .. "." .. funcName)
                local cachedFunctions = THUtils.functionCache[owner]
                if cachedFunctions == nil then
                    cachedFunctions = {}
                    THUtils.functionCache[owner] = cachedFunctions
                end
                local thFunctionCache = cachedFunctions[functionId]
                if thFunctionCache == nil then
                    thFunctionCache = {
                        id = functionId,
                        owner = owner,
                        className = className,
                        classFunc = classFunc,
                        funcName = funcName,
                        funcClass = funcClass
                    }
                    cachedFunctions[thFunctionCache.id] = thFunctionCache
                end
                return thFunctionCache
            end
        end
    end
end
function THUtils.getIsMasterUser()
    if g_currentMission ~= nil then
        return g_currentMission.isMasterUser == true
    end
    return false
end
function THUtils.getModEnvironment(modName)
    if modName == nil then
        return _G
    end
    if THUtils.argIsValid(type(modName) == THValueType.STRING, "modName", modName) then
        local modEnv = g_thGlobalEnv[modName]
        if modEnv ~= nil and modEnv._G ~= nil then
            return modEnv._G
        end
    end
end
function THUtils.getClassObject(className, modName)
    local classObject = nil
    if modName == nil then
        classObject = THUtils.getTableValue(g_thGlobalEnv, className)
    else
        local modEnv = THUtils.getModEnvironment(modName)
        if modEnv ~= nil then
            classObject = THUtils.getTableValue(modEnv, className)
        end
    end
    return classObject
end
function THUtils.callSuperClass(target, funcName, ...)
    if THUtils.argIsValid(funcName ~= nil, "funcName", funcName) then
        if type(target) == THValueType.TABLE and type(target.superClass) == THValueType.FUNCTION
            and type(target[funcName]) == THValueType.FUNCTION
        then
            local superClass = target:superClass()
            if type(superClass) == THValueType.TABLE
                and type(superClass[funcName]) == THValueType.FUNCTION
            then
                return THUtils.call(superClass[funcName], ...)
            end
            return true
        else
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "target", target)
        end
    end
end
function THUtils.wrapXMLFile(xmlFile, xmlSchema, verbose)
    local xmlFileType = type(xmlFile)
    if THUtils.argIsValid(xmlSchema == nil or type(xmlSchema) == THValueType.TABLE, "xmlSchema", xmlSchema) then
        verbose = THUtils.validateArg(not verbose or verbose == true, "verbose", verbose, true)
        if xmlFileType == THValueType.TABLE and type(xmlFile.handle) == THValueType.NUMBER and xmlFile.handle > 0 then
            if xmlSchema == nil then
                return xmlFile
            end
            return XMLFile.wrap(xmlFile.handle, xmlSchema)
        end
        if xmlFileType == THValueType.NUMBER and xmlFile > 0 then
            return XMLFile.wrap(xmlFile, xmlSchema)
        end
        if verbose then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "xmlFile", xmlFile)
        end
    end
end
function THUtils.getNodeExists(nodeId)
    if nodeId ~= nil then
        return entityExists(nodeId)
    end
    return false
end
function THUtils.getVector2Distance(sx,sz, ex,ez)
    if ex == nil and ez == nil then
        ex = 0
        ez = 0
    end
    if THUtils.argIsValid(type(sx) == THValueType.NUMBER, "sx", sx)
        and THUtils.argIsValid(type(sz) == THValueType.NUMBER, "sz", sz)
        and THUtils.argIsValid(type(ex) == THValueType.NUMBER, "ex", ex)
        and THUtils.argIsValid(type(ez) == THValueType.NUMBER, "ez", ez)
    then
        local distance = math.sqrt(((ex - sx) ^ 2) + ((ez - sz) ^ 2))
        distance = THUtils.round(distance, 6)
        return distance
    end
    return 0
end
function THUtils.getVector3Distance(sx,sy,sz, ex,ey,ez)
    if ex == nil and ey == nil and ez == nil then
        ex = 0
        ey = 0
        ez = 0
    end
    if THUtils.argIsValid(type(sx) == THValueType.NUMBER, "sx", sx)
        and THUtils.argIsValid(type(sy) == THValueType.NUMBER, "sy", sy)
        and THUtils.argIsValid(type(sz) == THValueType.NUMBER, "sz", sz)
        and THUtils.argIsValid(type(ex) == THValueType.NUMBER, "ex", ex)
        and THUtils.argIsValid(type(ey) == THValueType.NUMBER, "ey", ey)
        and THUtils.argIsValid(type(ez) == THValueType.NUMBER, "ez", ez)
    then
        local distance = math.sqrt(((ex - sx) ^ 2) + ((ey - sy) ^ 2) + ((ez - sz) ^ 2))
        distance = THUtils.round(distance, 6)
        return distance
    end
    return 0
end
function THUtils.getVector2AreaByLine(sx, sz, ex, ez, width)
    if THUtils.argIsValid(type(sx) == THValueType.NUMBER, "sx", sx)
        and THUtils.argIsValid(type(sz) == THValueType.NUMBER, "sz", sz)
        and THUtils.argIsValid(type(ex) == THValueType.NUMBER, "ex", ex)
        and THUtils.argIsValid(type(ez) == THValueType.NUMBER, "ez", ez)
    then
        if type(width) ~= "number" or width < 0 then
            if width ~= nil then
                THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "width", width)
            end
            width = 0
        end
        local length = THUtils.getVector2Distance(sx, sz, ex, ez)
        local mx = (sx + ex) / 2
        local mz = (sz + ez) / 2
        if length == 0 then -- single point
            if width == 0 then -- return single point
                return mx, mz, mx, mz, mx, mz
            end
            local halfWidth = width / 2
            return mx - halfWidth, mz - halfWidth, mx + halfWidth, mz - halfWidth, mx - halfWidth, mz + halfWidth
        end
        local factor = 1 + (width / length)
        local dx = ((ex - sx) / 2) * factor
        local dz = ((ez - sz) / 2) * factor
        sx = mx - dx
        sz = mz - dz
        ex = mx + dx
        ez = mz + dz
        local rdx = dz
        local rdz = -dx
        return sx + rdx, sz + rdz, sx - rdx, sz - rdz, ex + rdx, ez + rdz
    end
end
function THUtils.flipVector2Area(sx, sz, wx, wz, hx, hz, flipWidth, flipLength)
    flipWidth = THUtils.validateArg(not flipWidth or flipWidth == true, "flipWidth", flipWidth, false)
    flipLength = THUtils.validateArg(not flipLength or flipLength == true, "flipLength", flipLength, false)
    if flipWidth or flipLength then
        local dwx = wx - sx
        local dwz = wz - sz
        local x4 = hx + dwx
        local z4 = hz + dwz
        if flipWidth then
            if flipLength then
                local oldWX, oldWZ = wx, wz
                sx = x4
                sz = z4
                wx = hx
                wz = hz
                hx = oldWX
                hz = oldWZ
            else
                local oldSX, oldSZ = sx, sz
                sx = wx
                sz = wz
                wx = oldSX
                wz = oldSZ
                hx = x4
                hz = z4
            end
        elseif flipLength then
            local oldSX, oldSZ = sx, sz
            sx = hx
            sz = hz
            wx = x4
            wz = z4
            hx = oldSX
            hz = oldSZ
        end
    end
    return sx, sz, wx, wz, hx, hz
end
function THUtils.getNormalizedValues(uiScale, x, y)
    local normX, normY = 0, 0
    if THUtils.argIsValid(type(uiScale) == THValueType.NUMBER, "uiScale", uiScale) then
        if type(x) == THValueType.NUMBER then
            normX = x * uiScale * g_aspectScaleX / g_referenceScreenWidth
        elseif x ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "x", x)
        end
        if type(y) == THValueType.NUMBER then
            normY = y * uiScale * g_aspectScaleY / g_referenceScreenHeight
        elseif y ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "y", y)
        end
    end
    return normX, normY
end
function THUtils.getElementByProfileName(baseElement, profileName, depth)
    if type(depth) ~= "number" or depth < 0 then
        if depth ~= nil then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "depth", depth)
        end
        depth = 0
    else
        depth = math.floor(depth)
    end
    if THUtils.argIsValid(type(baseElement) == THValueType.TABLE, "baseElement", baseElement) then
        if profileName == nil then
            return
        end
        if profileName == baseElement.profile then
            return baseElement
        end
        local elementList = baseElement.elements
        local numElements = (elementList == nil and 0) or #elementList
        if numElements > 0 then
            for idx = 1, numElements do
                local element = elementList[idx]
                if depth > 0 then
                    local foundElement = THUtils.getElementByProfileName(element, profileName, depth - 1)
                    if foundElement ~= nil then
                        return foundElement
                    end
                elseif profileName == element.profile then
                    return element
                end
            end
        end
    end
end
function THUtils.createNetworkId(target)
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", target) then
        local numNetworkObjects = #THUtils.networkObjects
        local networkIndex = 0
        for objectIndex = 1, numNetworkObjects do
            if THUtils.networkObjects[objectIndex] == target then
                networkIndex = objectIndex
                break
            end
        end
        if networkIndex > 0 then
            local networkId = THUtils.getNetworkId(target)
            if networkId > 0 then
                return networkId
            end
            THUtils.errorMsg(true, "Could not create network id")
        else
            local nextId = numNetworkObjects + 1
            THUtils.networkObjects[nextId] = target
            THUtils.networkIdToObject[nextId] = target
            THUtils.networkObjectToId[target] = nextId
            THUtils.NETWORK_ID_BITS = MathUtil.getNumRequiredBits(nextId)
            return nextId
        end
    end
    return 0
end
function THUtils.getNetworkId(target)
    if target ~= nil then
        local networkId = THUtils.networkObjectToId[target]
        if networkId ~= nil and networkId > 0 then
            if THUtils.networkIdToObject[networkId] == target then
                return networkId
            end
            THUtils.networkObjectToId[target] = 0
            THUtils.errorMsg(true, THMessage.NETWORK_ID_MISMATCH, networkId)
        end
    end
    return 0
end
function THUtils.getTargetByNetworkId(networkId)
    if networkId ~= nil and networkId > 0 then
        local target = THUtils.networkIdToObject[networkId]
        if target ~= nil then
            if THUtils.networkObjectToId[target] ~= networkId then
                THUtils.networkObjectToId[target] = networkId
                THUtils.errorMsg(true, THMessage.NETWORK_ID_MISMATCH, networkId)
            end
            return target
        end
    end
end
_G.THDebugLevel = {
    NORMAL = 1,
    UPDATE = 2,
    LOOP = 3,
    ALL = 4
}
THUtils.createEnumTable(THDebugLevel)
THSetDebugEnabledEvent = {}
local THSetDebugEnabledEvent_mt = Class(THSetDebugEnabledEvent, Event)
THSetDebugLevelEvent = {}
local THSetDebugLevelEvent_mt = Class(THSetDebugLevelEvent, Event)
InitEventClass(THSetDebugEnabledEvent, "THSetDebugEnabledEvent")
InitEventClass(THSetDebugLevelEvent, "THSetDebugLevelEvent")
local g_thDebugFlagIds = {}
local g_thGlobalDebugFlag = false
local g_thCurrentDebugLevel = THDebugLevel.NORMAL
function THUtils.getDebugFlagId(flagId)
    flagId = string.upper(tostring(flagId or ""))
    if flagId == "" or g_thDebugFlagIds[flagId] ~= nil then
        return flagId
    end
    THUtils.errorMsg(nil, "Debug flag %q does not exist", flagId)
end
function THUtils.getDebugFlagIds()
    return g_thDebugFlagIds
end
function THUtils.getDebugLevel()
    return g_thCurrentDebugLevel
end
function THUtils.getDebugLevelById(debugLevelId)
    if debugLevelId ~= nil then
        local idType = type(debugLevelId)
        local debugLevel = nil
        if idType == THValueType.STRING then
            debugLevel = THDebugLevel.getValue(debugLevelId)
            if debugLevel == nil then
                debugLevel = THUtils.toNumber(debugLevelId, true)
                if debugLevel ~= nil then
                    if THDebugLevel.getId(debugLevel) == nil then
                        debugLevel = nil
                    end
                end
            end
        elseif idType == THValueType.NUMBER then
            if THDebugLevel.getId(debugLevelId) ~= nil then
                debugLevel = debugLevelId
            end
        else
            THUtils.errorMsg(nil, THMessage.INVALID_VALUE, "debugLevelId", debugLevelId)
        end
        return debugLevel
    end
end
function THUtils.setDebugLevel(debugLevelId)
    local debugLevel = THUtils.getDebugLevelById(debugLevelId)
    local success = false
    if debugLevel == nil then
        THUtils.errorMsg(nil, THMessage.INVALID_VALUE, "debugLevelId", debugLevelId)
    else
        THUtils.displayMsg("Debug level set to: %s", debugLevelId)
        g_thCurrentDebugLevel = debugLevel
        success = true
    end
    return success, g_thCurrentDebugLevel
end
function THUtils.createDebugFlagId(flagName, isEnabled)
    isEnabled = THUtils.validateArg(not isEnabled or isEnabled == true, "isEnabled", isEnabled, false)
    if THUtils.argIsValid(THUtils.isValidName(flagName), "flagName", flagName) then
        local flagId = string.upper(flagName)
        if g_thDebugFlagIds[flagId] == nil then
            g_thDebugFlagIds[flagId] = isEnabled == true
        end
        return flagId
    end
end
function THUtils.getIsDebugEnabled(flagName, debugLevelId)
    local reqDebugLevel = nil
    if debugLevelId == nil then
        reqDebugLevel = g_thCurrentDebugLevel
    else
        reqDebugLevel = THUtils.getDebugLevelById(debugLevelId)
    end
    local flagId = THUtils.getDebugFlagId(flagName)
    local isDebugEnabled = false
    if flagId ~= nil
        and reqDebugLevel ~= nil and reqDebugLevel <= g_thCurrentDebugLevel
    then
        if flagId == "" then
            isDebugEnabled = g_thGlobalDebugFlag == true
        else
            isDebugEnabled = g_thDebugFlagIds[flagId] == true
        end
    end
    return isDebugEnabled, flagId, reqDebugLevel
end
function THUtils.setDebugEnabled(flagName, isEnabled)
    if type(flagName) == THValueType.BOOLEAN then
        isEnabled = flagName
        flagName = nil
    end
    local flagId = THUtils.getDebugFlagId(flagName)
    if flagId ~= nil then
        isEnabled = THUtils.getNoNil(isEnabled, true)
        if THUtils.argIsValid(not isEnabled or isEnabled == true, "isEnabled", isEnabled) then
            local newIsEnabled = nil
            if flagId == "" then
                g_thGlobalDebugFlag = isEnabled == true
                newIsEnabled = g_thGlobalDebugFlag
            else
                g_thDebugFlagIds[flagId] = isEnabled == true
                newIsEnabled = g_thDebugFlagIds[flagId]
            end
            local enabledText = (newIsEnabled == true and "enabled") or "disabled"
            if flagId == "" then
                THUtils.displayMsg("Debugger is %s", enabledText)
            else
                THUtils.displayMsg("Debugging for %q is %s", flagId, enabledText)
            end
            return true, newIsEnabled, flagId
        end
    end
    return false
end
function THUtils.setDebugAllEnabled(isEnabled)
    isEnabled = THUtils.getNoNil(isEnabled, true)
    if THUtils.argIsValid(not isEnabled or isEnabled == true, "isEnabled", isEnabled) then
        local debugFlagIds = THUtils.getDebugFlagIds()
        if debugFlagIds ~= nil then
            for flagId, curIsEnabled in pairs(debugFlagIds) do
                if curIsEnabled ~= isEnabled then
                    THUtils.setDebugEnabled(flagId, isEnabled)
                end
            end
            return true, isEnabled
        end
    end
    return false
end
function THUtils.debugReset()
    THUtils.setDebugAllEnabled(false)
    THUtils.setDebugLevel(THDebugLevel.NORMAL)
end
function THUtils.printDebugMsg(flagName, debugLevelId, text, ...)
    if THUtils.getIsDebugEnabled(flagName, debugLevelId) then
        THUtils.displayMsg(text, ...)
    end
end
function THUtils.printDebugError(flagName, showStack, text, ...)
    if THUtils.getIsDebugEnabled(flagName) then
        THUtils.errorMsg(showStack, text, ...)
    end
end
function THUtils.printTable(target, depth, ...)
    if g_thDebugTools ~= nil then
        return g_thDebugTools:printTable(target, depth, ...)
    end
    local targetName = target
    if type(target) == THValueType.STRING then
        target = THUtils.getTableValue(g_thGlobalEnv, targetName)
    end
    depth = depth or 0
    if THUtils.argIsValid(type(target) == THValueType.TABLE, "target", targetName)
        and THUtils.argIsValid(THUtils.getIsType(depth, THValueType.INTEGER), "depth", depth)
    then
        THUtils.displayMsg("")
        DebugUtil.printTableRecursively(target, "_", nil, depth)
        THUtils.displayMsg("")
    end
end
THUtils.dumpTable = THUtils.printTable
function THUtils.printTableValues(target, ...)
    if g_thDebugTools ~= nil then
        g_thDebugTools:printTableValues(target, ...)
        return
    end
    if type(target) == THValueType.STRING then
        local targetPath = target
        target = THUtils.getTableValue(g_thGlobalEnv, targetPath)
        if type(target) ~= THValueType.TABLE then
            THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "target", targetPath)
            return
        end
    elseif type(target) ~= THValueType.TABLE then
        THUtils.errorMsg(true, THMessage.ARGUMENT_INVALID, "target", target)
        return
    end
    THUtils.displayMsg("")
    for key, value in pairs(target) do
        local valueType = type(value)
        if valueType ~= THValueType.TABLE
            and valueType ~= THValueType.FUNCTION
        then
            THUtils.displayMsg("[%s]: %s", key, value)
        end
    end
    THUtils.displayMsg("")
end
THUtils.dumpTableValues = THUtils.printTableValues
function THUtils.traceFunction(...)
    if g_thDebugTools ~= nil then
        return g_thDebugTools:traceFunction(...)
    end
end
function THUtils.createDebugConsoleCommands(prefix, target)
    if THUtils.argIsValid(type(prefix) == THValueType.STRING and prefix ~= "", "prefix", prefix)
        and THUtils.argIsValid(target == nil or type(target) == THValueType.TABLE, "target", target)
    then
        local callbackTarget = target
        if target == nil or type(target.consoleCommandSetDebugEnabled) ~= THValueType.FUNCTION then
            callbackTarget = THUtils
        end
        addConsoleCommand(prefix .. ":setDebugEnabled", "Sets debug flagId enabled status", "consoleCommandSetDebugEnabled", callbackTarget, "[flagId] [isEnabled] [sendToServer]")
        callbackTarget = target
        if target == nil or type(target.consoleCommandSetDebugLevel) ~= THValueType.FUNCTION then
            callbackTarget = THUtils
        end
        addConsoleCommand(prefix .. ":setDebugLevel", "Sets debug level", "consoleCommandSetDebugLevel", callbackTarget, "debugLevelId [sendToServer]")
    end
end
function THUtils.consoleCommandSetDebugEnabled(_, flagName, isEnabled, sendToServer)
    THUtils.pcall(function()
        local usageMsg = "Usage: setDebugEnabled [flagId] [isEnabled] [sendToServer]"
        local function showUsageMsg(pShowFlagList)
            printf(usageMsg)
            if pShowFlagList == true then
                printf("")
                printf("Allowed debug flagIds:")
                local debugFlagIds = THUtils.getDebugFlagIds()
                for otherFlagId in pairs(debugFlagIds) do
                    printf("- %s", otherFlagId)
                end
                printf("")
            end
        end
        local flagNameBool = THUtils.toBoolean(flagName)
        local flagId = nil
        if flagNameBool ~= nil then
            flagId = ""
            isEnabled = flagNameBool
            sendToServer = string.upper(tostring(isEnabled))
        else
            if flagName == "?" then
                showUsageMsg(true)
            else
                flagId = string.upper(tostring(flagName))
                isEnabled = string.upper(tostring(isEnabled))
                sendToServer = string.upper(tostring(sendToServer))
                if flagId ~= "ALL" then
                    flagId = THUtils.getDebugFlagId(flagName)
                end
            end
        end
        if flagId == nil then
            showUsageMsg(true)
        else
            if isEnabled == "" or isEnabled == "NIL" or isEnabled == "TRUE" then
                isEnabled = true
            elseif isEnabled == "FALSE" then
                isEnabled = false
            else
                showUsageMsg(false)
                return
            end
            if sendToServer == "" or sendToServer == "NIL" or sendToServer == "FALSE" then
                sendToServer = false
            elseif sendToServer == "TRUE" then
                sendToServer = true
            else
                showUsageMsg(false)
                return
            end
            local success, newIsEnabled = false, false
            if flagId == "ALL" then
                success, newIsEnabled = THUtils.setDebugAllEnabled(isEnabled == true)
            else
                success, newIsEnabled = THUtils.setDebugEnabled(flagId, isEnabled == true)
            end
            if success and g_server == nil
                and sendToServer == true
            then
                THSetDebugEnabledEvent.sendEvent(flagId, newIsEnabled == true)
            end
        end
    end)
end
function THUtils.consoleCommandSetDebugLevel(_, debugLevelId, sendToServer)
    local usageMsg = "Usage: setDebugLevel debugLevel [sendToServer]"
    sendToServer = THUtils.toBoolean(sendToServer)
    local function showUsageMsg(pShowLevelList)
        printf(usageMsg)
        if pShowLevelList == true then
            printf("")
            printf("Allowed debug level ids:")
            local allowedValues = THDebugLevel.getValues()
            for id, level in pairs(allowedValues) do
                printf("[%s]: %s", id, level)
            end
            printf("")
        end
    end
    if debugLevelId == "?" then
        showUsageMsg(true)
        return
    end
    local debugLevel = THUtils.getDebugLevelById(debugLevelId)
    if debugLevel == nil or debugLevel <= 0 then
        showUsageMsg(true)
        return
    end
    sendToServer = tostring(sendToServer):upper()
    if sendToServer == "" or sendToServer == "NIL" or sendToServer == "FALSE" then
        sendToServer = false
    elseif sendToServer == "TRUE" then
        sendToServer = true
    else
        THUtils.errorMsg(nil, THMessage.ARGUMENT_INVALID, "sendToServer", sendToServer)
        showUsageMsg(false)
        return
    end
    local success, newDebugLevel = THUtils.setDebugLevel(debugLevel)
    if success and g_server == nil
        and sendToServer == true
    then
        THSetDebugLevelEvent.sendEvent(newDebugLevel)
    end
end
function THSetDebugEnabledEvent.emptyNew()
    return Event.new(THSetDebugEnabledEvent_mt)
end
function THSetDebugEnabledEvent.new(flagName, isEnabled)
    local self = THSetDebugEnabledEvent.emptyNew()
    self.isEventValid = false
    if THUtils.argIsValid(not isEnabled or isEnabled == true, "isEnabled", isEnabled) then
        local flagId = string.upper(tostring(flagName))
        if flagId ~= "ALL" then
            flagId = THUtils.getDebugFlagId(flagName)
        end
        if flagId ~= nil then
            if g_server ~= nil then
                THUtils.errorMsg(true, THMessage.EVENT_NO_SERVER)
            elseif not THUtils.getIsMasterUser() then
                THUtils.errorMsg(false, THMessage.EVENT_ADMIN_ONLY)
            else
                self.flagId = flagId
                self.isEnabled = THUtils.getNoNil(isEnabled, true)
                self.isEventValid = true
            end
        end
    end
    return self
end
function THSetDebugEnabledEvent.readStream(self, streamId, connection)
    if not connection:getIsServer() then
        if streamReadBool(streamId) then
            self.flagId = streamReadString(streamId)
            self.isEnabled = streamReadBool(streamId)
            self:run(connection)
        end
    else
        THUtils.errorMsg(true, THMessage.EVENT_NO_CLIENT_READ)
    end
end
function THSetDebugEnabledEvent.writeStream(self, streamId, connection)
    if connection:getIsServer() then
        if streamWriteBool(streamId, self.isEventValid) then
            streamWriteString(streamId, self.flagId)
            streamWriteBool(streamId, self.isEnabled)
        end
    else
        THUtils.errorMsg(true, THMessage.EVENT_NO_SERVER_WRITE)
    end
end
function THSetDebugEnabledEvent.run(self, connection)
    if not connection:getIsServer() then
        if self.isEventValid then
            if self.flagId == "ALL" then
                THUtils.setDebugAllEnabled(self.isEnabled)
            else
                THUtils.setDebugEnabled(self.flagId, self.isEnabled)
            end
        end
    end
end
function THSetDebugEnabledEvent.sendEvent(flagName, isEnabled)
    local success = false
    if g_server == nil and g_client ~= nil then
        local newEvent = THSetDebugEnabledEvent.new(flagName, isEnabled)
        if newEvent ~= nil and newEvent.isEventValid then
            g_client:getServerConnection():sendEvent(newEvent)
            success = true
        end
    end
    return success
end
function THSetDebugLevelEvent.emptyNew()
    return Event.new(THSetDebugLevelEvent_mt)
end
function THSetDebugLevelEvent.new(debugLevelId)
    local self = THSetDebugLevelEvent.emptyNew()
    local debugLevel = THUtils.getDebugLevelById(debugLevelId)
    self.isEventValid = false
    if THUtils.argIsValid(debugLevel ~= nil, "debugLevelId", debugLevelId) then
        if g_server ~= nil then
            THUtils.errorMsg(true, THMessage.EVENT_NO_SERVER)
        elseif not THUtils.getIsMasterUser() then
            THUtils.errorMsg(false, THMessage.EVENT_ADMIN_ONLY)
        else
            self.debugLevel = math.floor(debugLevel)
            self.isEventValid = true
        end
    end
    return self
end
function THSetDebugLevelEvent.readStream(self, streamId, connection)
    if not connection:getIsServer() then
        if streamReadBool(streamId) then
            self.debugLevel = streamReadUIntN(streamId, 4)
            self:run(connection)
        end
    else
        THUtils.errorMsg(true, THMessage.EVENT_NO_CLIENT_READ)
    end
end
function THSetDebugLevelEvent.writeStream(self, streamId, connection)
    if connection:getIsServer() then
        if streamWriteBool(streamId, self.isEventValid) then
            streamWriteUIntN(streamId, self.debugLevel, 4)
        end
    else
        THUtils.errorMsg(true, THMessage.EVENT_NO_SERVER_WRITE)
    end
end
function THSetDebugLevelEvent.run(self, connection)
    if not connection:getIsServer() then
        if self.isEventValid then
            THUtils.setDebugLevel(self.debugLevel)
        end
    end
end
function THSetDebugLevelEvent.sendEvent(debugLevelId)
    local success = false
    if g_server == nil and g_client ~= nil then
        local newEvent = THSetDebugLevelEvent.new(debugLevelId)
        if newEvent ~= nil and newEvent.isEventValid then
            g_client:getServerConnection():sendEvent(newEvent)
            success = true
        end
    end
    return success
end
if g_thDebugTools ~= nil then
    THUtils.setDebugEnabled(nil, true)
end