Lua-Infused GUI + automatic memory leak handling

This bundle is marked as high quality. It exceeds standards and is highly desirable.
Modernizing the Trigger Editor for a brighter future for GUI.
Description from old thread:
This resource, among many things, converts locations, groups, rects, forces and even the GUI versions of Hashtables into Lua tables, making it so that you never have to remove them (as they are automatically garbage collected). Hashtables in GUI are also no longer bound to 255 instances, so you can create them as often as you want. As a major plus, Lua tables use significantly less memory and fewer CPU resources than the WarCraft 3 natives.

My aim is to remove as many penalties as possible from GUI and to incentivize mapmakers to switch their underlying scripts to Lua due to ease of use/better quality of life.

Here I offer a fixed up version with the following changes:
  • asserts on arguments so DebugUtils can more effectively tell you what's wrong
  • StringHashBJ and GetHandleIdBJ returns 0 if the argument is falsy, otherwise returns the argument itself
  • fixed Hashtable API overrides to support niche Hashtable mechanic of being able to store integer, real, string, boolean and a handle simultaneously on same key pair
  • explicit boolean return for following natives: IsUnitInGroup, IsUnitGroupEmptyBJ, BlzForceHasPlayer, IsPlayerInForce, IsUnitInForce
  • GroupPickRandomUnit will no longer return 0 if group is empty
  • swapped FlushChildHashtableBJ arguments to match the Blizzard.j signature
  • type override to return 'userdata' for FakeLocation, FakeRect, FakeGroup, FakeForce and FakeHashtable
  • added Debug.beginFile/endFile
  • added EmmyLua annotations
  • stored the 4 timers defined in Lua root by Blizzard.j so that their references never get lost and the objects never get collected by GC which ultimately causes desyncs
  • WC3 Native Math API replaced with Lua's math API
  • SubStringBJ replaced with string.sub
  • Force/Group loops being able to remove their respective elements from within their loop
Installation:
  1. Get the following scripts at the top of your trigger editor in following order:
    1. DebugUtils (Optional)
    2. IngameConsole (Optional)
    3. Total Initialization
    4. LuaInfusedGUI
    5. everything else
  2. Copy the "Unit Remove Event (LIGUI)" (Aurm) Ability from object editor into your map
    1. or create your own by basing it off of footman's Defend ability, just modify the _REMOVE_ABIL constant below
Lua:
if Debug then Debug.beginFile "LuaInfusedGUI" end
--[[
    Lua-Infused GUI with automatic memory leak resolution: Modernizing the experience for a better future for users of the Trigger Editor.

    Credits:
        Bribe, Tasyen, Dr Super Good, HerlySQR, Antares, Marcielos

    Installation:
        1. Get the following scripts at the top of your trigger editor in following order:
         - DebugUtils (Optional)
         - IngameConsole (Optional)
         - Total Initialization
         - LuaInfusedGUI
         - everything else
        2. Copy the "Unit Remove Event (LIGUI)" (Aurm) Ability from object editor into your map
         - or create your own by basing it off of footman's Defend ability, just modify the _REMOVE_ABIL constant below

    Transforming rects, locations, groups, forces and BJ hashtable wrappers into Lua tables, which are automatically garbage collected.

    Provides RegisterAnyPlayerUnitEvent to cut down on handle count and simplify syntax for Lua users while benefitting GUI.

    Provides GUI.enumUnitsInRect/InRange/Selected/etc. which replaces the first parameter with a function (which takes a unit), for immediate action without needing a separate group variable.

    Provides GUI.loopArray for safe iteration over a __jarray

    Update: 24 May 2026 by InsanityAI & Marcielos
    Changes:
        - Groups now auto-remove units that were removed from the game
        - Added GUI.RegisterUnitRemovedEventListener and GUI.DeregisterUnitRemovedEventListener
        - Added GUI.forForce and fixed Force and Group being able to remove units/players within their respective loops

    Update: 30 Mar 2026 by Macielos
    Changes:
        - Removed overrides for UnitRemoveBuffBJ, TimerDialogDisplayBJ, LeaderboardDisplayBJ as they do not have identical argument order with their corresponding native

    Update: 16 Mar 2026 by InsanityAI
    Changes:
        - Fixed asserts early exiting functions in case _THROW_ERROR_ON_INVALID_ARG is set to true and the arg condition is valid

    Update: 15 Mar 2026 by InsanityAI
    Changes:
        - Added _THROW_ERROR_ON_INVALID_ARG & _PRINT_WARNING_ON_INVALID_ARG flags that modify how assert works within this system
        - Location and Rect overrides now return non-nil values even if error is disabled but no valid argument was provided

    Update: 01 Mar 2026 by Marcielos & InsanityAI
    Changes:
        - Fixed GroupClear, GroupAddUnit, GroupAddGroup and GroupRemoveGroup overrides
        - Fixed Hashtable API where argument order was wrong
        - Overridden CreateMinimapIconAtLoc and ExecuteFunc

    Update: 02 Feb 2026 by Insanity_AI
    Changes:
        - FakedType property is now a string
        - replaced _G with _ENV for a (negligible) speed boost
        - additional asserts for hashtable API
        - Hashtable API now replaces the natives instead of BJs
        - GroupRemoveUnit now no longer breaks the FakeGroup (thanks Antares & Macielos)
        - fixed SetHeroStat
        - added some String & Math API overrides (check the bottom of the script for the list)
        - modified GroupXOrder overrides to use group natives in order to retain speed and formation of units when ordered as a group (thanks Macielos)
        - swapped order of overrides: group <-> location, so that group overrides happen first

    Update: 30 Sep 2025 by Insanity_AI
    Changes:
        - asserts on arguments so DebugUtils can more effectively tell you what's wrong
        - StringHashBJ and GetHandleIdBJ returns 0 if the argument is falsy, otherwise returns the argument itself
        - fixed Hashtable API overrides to support niche Hashtable mechanic of being able to store integer, real, string, boolean and a handle simultaneously on same key pair
        - explicit boolean return for following natives: IsUnitInGroup, IsUnitGroupEmptyBJ, BlzForceHasPlayer, IsPlayerInForce, IsUnitInForce
        - GroupPickRandomUnit will no longer return 0 if group is empty
        - swapped FlushChildHashtableBJ arguments to match the Blizzard.j signature
        - type override to return 'userdata' for FakeLocation, FakeRect, FakeGroup, FakeForce and FakeHashtable
        - added Debug.beginFile/endFile
        - added EmmyLua annotations
        - stored the 4 timers defined in Lua root by Blizzard.j so that their references never get lost and the objects never get collected by GC which ultimately causes desyncs
        - WC3 Native Math API replaced with Lua's math API
        - SubStringBJ replaced with string.sub

    Requires:
        https://github.com/BribeFromTheHive/Lua-Core/blob/main/Total_Initialization.lua

    Uses optionally:
        https://github.com/BribeFromTheHive/Lua-Core/blob/main/Global_Variable_Remapper.lua
--]]
GUI = {}
do
    --Configurables
    local _THROW_ERROR_ON_INVALID_ARG   = true          -- set to true if you want LIGUI to throw errors when incorrect arguments are sent to overriden functions
    local _PRINT_WARNING_ON_INVALID_ARG = true           -- set to true if you want warnings by LIGUI when incorrect arguments are sent to overriden functions
    local _USE_GLOBAL_REMAP             = false          -- set to true if you want GUI to have extended functionality such as "udg_HashTableArray" (which gives GUI an infinite supply of shared hashtables)
    local _REMOVE_ABIL                  = FourCC('Aurm') -- a copy of Defend ability that is used to detect when exactly does a unit get removed.

    --Define common variables to be utilized throughout the script.
    local unpack                        = table.unpack
    local assert                        = assert
    -- Used to check if function should exit early due to invalid arguments, instead of executing its internal logic
    local check                         = (function() ---@type fun(condition:boolean, msg: string): shouldEarlyExit: boolean
    if _THROW_ERROR_ON_INVALID_ARG then
        return function(condition, msg)
            return not assert(condition, msg)
        end
    elseif _PRINT_WARNING_ON_INVALID_ARG then
        return function(condition, msg)
            if not condition then
                if Debug then
                    Debug.errorHandler("LIGUI: " .. msg, 3)
                else
                    print("|cFFFF0000LIGUI: " .. msg)
                end
            end
            return not condition
        end
    else
        return function(condition)
            return not condition
        end
    end
    end)()

    ---@class FakedType
    ---@field __faketype string

    do
        local oldType = type
        --[[ Type extender - if object being checked is a table, check if it's one of the replacements for userdata --]]
        ---@param obj unknown
        ---@return string typeName
        function type(obj)
            local thisType = oldType(obj)
            if thisType == 'table' and obj.__faketype and oldType(obj.__faketype) == 'string' then
                return obj --[[@as FakedType]].__faketype
            end
            return thisType
        end
    end

    --[[-----------------------------------------------------------------------------------------
        __jarray expander by Bribe

        This snippet will ensure that objects used as indices in udg_ arrays will be automatically
        cleaned up when the garbage collector runs, and tries to re-use metatables whenever possible.
        -------------------------------------------------------------------------------------------]]
    do
        local mts = {}
        local weakKeys = { __mode = "k" } --ensures tables with non-nilled objects as keys will be garbage collected.

        ---Re-define __jarray.
        ---@param default? any
        ---@param tab? table
        ---@return table
        function __jarray(default, tab)
            local mt
            if default then
                mts[default] = mts[default] or {
                    __index = function()
                        return default
                    end,
                    __mode = "k"
                }
                mt = mts[default]
            else
                mt = weakKeys
            end
            return setmetatable(tab or {}, mt)
        end

        --have to do a wide search for all arrays in the variable editor. The WarCraft 3 _ENV table is HUGE,
        --and without editing the war3map.lua file manually, it is not possible to rewrite it in advance.
        for k, v in pairs(_ENV) do
            if type(v) == "table" and string.sub(k, 1, 4) == "udg_" then
                __jarray(v[0], v)
            end
        end
        ---Add this safe iterator function for jarrays.
        ---@param whichTable table
        ---@param func fun(index:integer, value:any)
        function GUI.loopArray(whichTable, func)
            for i = rawget(whichTable, 0) ~= nil and 0 or 1, #whichTable do
                func(i, rawget(whichTable, i))
            end
        end
    end
    --[=============[
      • HASHTABLES •
    --]=============]
    --[[ GUI hashtable converter by Tasyen and Bribe

        Converts GUI hashtables API into Lua Tables, overwrites StringHashBJ and GetHandleIdBJ to permit
        typecasting, bypasses the 256 hashtable limit by avoiding hashtables, provides the variable
        "HashTableArray", which automatically creates hashtables for you as needed (so you don't have to
        initialize them each time). ]]
    do
        ---@param s string
        ---@return string s
        function StringHashBJ(s)
            return s or 0
        end

        ---@generic T
        ---@param id T
        ---@return T id
        function GetHandleIdBJ(id)
            return id or 0
        end

        ---@alias FakeHashtableBucket<T> {[unknown]: {[unknown]: T}}
        ---@class FakeHashtable: FakedType
        ---@field boolean FakeHashtableBucket<boolean>
        ---@field integer FakeHashtableBucket<integer>
        ---@field real FakeHashtableBucket<real>
        ---@field string FakeHashtableBucket<string>
        ---@field handle FakeHashtableBucket<handle>

        ---@param whichHashTable FakeHashtable
        ---@param type 'boolean'|'integer'|'real'|'string'|'handle'
        ---@param parentKey unknown
        ---@return unknown
        local function load(whichHashTable, type, parentKey)
            local typedTable = whichHashTable[type]
            if not typedTable then
                typedTable = {}
                whichHashTable[type] = typedTable
            end
            local index = typedTable[parentKey]
            if not index then
                index = __jarray()
                typedTable[parentKey] = index
            end
            return index
        end
        if _USE_GLOBAL_REMAP then
            OnInit(function(import)
                local remap = import "GlobalRemapArray"
                local hashes = __jarray()
                remap("udg_HashTableArray", function(index)
                    return load(hashes, 'handle', index)
                end)
            end)
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@return boolean shouldEarlyExit
        local function checkHashtableArgs(whichHashTable, parentKey, childKey)
            return check(whichHashTable ~= nil, 'whichHashTable cannot be nil') or
                    check(parentKey ~= nil, 'parentKey cannot be nil') or
                    check(childKey ~= nil, 'childKey cannot be nil')
        end

        ---@return FakeHashtable
        function InitHashtable()
            return { __faketype = "userdata" }
        end

        ---@param value unknown?
        ---@param childKey unknown
        ---@param parentKey unknown
        ---@param whichHashTable FakeHashtable
        ---@param type 'boolean'|'integer'|'real'|'string'|'handle'
        local function saveInto(whichHashTable, type, parentKey, childKey, value)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return end
            load(whichHashTable, type, parentKey)[childKey] = value
        end

        ---@generic T
        ---@param type 'boolean'|'integer'|'real'|'string'|'handle'
        ---@return fun(whichHashTable: FakeHashtable, parentKey: unknown, childKey: unknown, value: T)
        local function createSaveIntoTyped(type)
            check(type ~= nil, 'type cannot be nil')
            ---@generic T
            ---@param whichHashTable FakeHashtable
            ---@param parentKey unknown
            ---@param childKey unknown
            ---@param value T
            return function(whichHashTable, parentKey, childKey, value)
                return saveInto(whichHashTable, type, parentKey, childKey, value)
            end
        end

        SaveInteger = createSaveIntoTyped('integer') ---@type fun(whichHashtable: FakeHashtable, parentKey: unknown, childKey: unknown, value: integer)
        SaveReal = createSaveIntoTyped('real') ---@type fun(whichHashtable: FakeHashtable, parentKey: unknown, childKey: unknown, value: number)
        SaveBoolean = createSaveIntoTyped('boolean') ---@type fun(whichHashtable: FakeHashtable, parentKey: unknown, childKey: unknown, value: boolean)
        SaveStr = createSaveIntoTyped('string') ---@type fun(whichHashtable: FakeHashtable, parentKey: unknown, childKey: unknown, value: string)
        local saveHandle = createSaveIntoTyped('handle')

        ---@param whichHashTable FakeHashtable
        ---@param type 'boolean'|'integer'|'real'|'string'|'handle'
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@param default unknown|nil
        ---@return unknown|nil
        local function loadFrom(whichHashTable, type, parentKey, childKey, default)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return default end
            local val = load(whichHashTable, type, parentKey)[childKey]
            return val ~= nil and val or default
        end

        ---@param type 'boolean'|'integer'|'real'|'string'|'handle'|nil
        ---@param default unknown
        ---@return fun(whichHashTable: FakeHashtable, parentKey: unknown, childKey: unknown): unknown|nil
        local function createDefault(type, default)
            return function(whichHashTable, parentKey, childKey)
                return loadFrom(whichHashTable, type or 'handle', parentKey, childKey, default)
            end
        end
        LoadInteger = createDefault('integer', 0) ---@type fun(whichHashTable: FakeHashtable, parentKey: unknown, childKey: unknown): integer
        LoadReal = createDefault('real', 0) ---@type fun(whichHashTable: FakeHashtable, parentKey: unknown, childKey: unknown): number
        LoadBoolean = createDefault('boolean', false) ---@type fun(whichHashTable: FakeHashtable, parentKey: unknown, childKey: unknown): boolean
        LoadStr = createDefault('string', '') ---@type fun(whichHashTable: FakeHashtable, parentKey: unknown, childKey: unknown): string
        local loadHandle = createDefault('handle', nil)

        do
            local sub = string.sub
            for key in pairs(_ENV) do
                if sub(key, -6) == "Handle" then
                    local str = sub(key, 1, 4)
                    if str == "Save" then
                        _ENV[key] = saveHandle
                    elseif str == "Load" then
                        _ENV[key] = loadHandle
                    end
                end
            end
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@return boolean
        function HaveSavedBoolean(whichHashTable, parentKey, childKey)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return false end
            return load(whichHashTable, parentKey, 'boolean')[childKey] ~= nil
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@return boolean
        function HaveSavedInteger(whichHashTable, parentKey, childKey)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return false end
            return load(whichHashTable, parentKey, 'integer')[childKey] ~= nil
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@return boolean
        function HaveSavedReal(whichHashTable, parentKey, childKey)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return false end
            return load(whichHashTable, parentKey, 'real')[childKey] ~= nil
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@return boolean
        function HaveSavedString(whichHashTable, parentKey, childKey)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return false end
            return load(whichHashTable, parentKey, 'string')[childKey] ~= nil
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        ---@param childKey unknown
        ---@return boolean
        function HaveSavedHandle(whichHashTable, parentKey, childKey)
            if checkHashtableArgs(whichHashTable, parentKey, childKey) then return false end
            return load(whichHashTable, parentKey, 'handle')[childKey] ~= nil
        end

        ---@param whichHashTable FakeHashtable
        function FlushParentHashtable(whichHashTable)
            if check(whichHashTable ~= nil, 'whichHashTable cannot be nil') then return end
            whichHashTable.boolean = nil
            whichHashTable.integer = nil
            whichHashTable.real = nil
            whichHashTable.string = nil
            whichHashTable.handle = nil
        end

        ---@param whichHashTable FakeHashtable
        ---@param parentKey unknown
        function FlushChildHashtable(whichHashTable, parentKey)
            if check(whichHashTable ~= nil, 'whichHashTable cannot be nil') then return end
            if check(parentKey ~= nil, 'parentKey cannot be nil') then return end
            if whichHashTable.boolean then whichHashTable.boolean[parentKey] = nil end
            if whichHashTable.integer then whichHashTable.integer[parentKey] = nil end
            if whichHashTable.real then whichHashTable.real[parentKey] = nil end
            if whichHashTable.string then whichHashTable.string[parentKey] = nil end
            if whichHashTable.handle then whichHashTable.handle[parentKey] = nil end
        end
    end
    --[=============================[
      • GROUPS (UNIT GROUPS IN GUI) •
    --]=============================]
    local unitRemovedEvent ---@type fun(unit: unit)
    do
        local mainGroup = CreateGroup()
        local issueGroup = CreateGroup() --[[@as group]]
        DestroyGroup(bj_suspendDecayFleshGroup --[[@as group]])
        DestroyGroup(bj_suspendDecayBoneGroup --[[@as group]])
        DestroyGroup = DoNothing

        ---@class FakeGroup: FakedType, group
        ---@field [integer] unit
        ---@field indexOf {[unit]: integer}

        local oldGroupClear = GroupClear --[[@as fun(group: group)]]
        local oldGroupAddUnit = GroupAddUnit --[[@as fun(group: group, unit: unit)]]

        local groupDBRegisterUnitInGroup, groupDBDeregisterUnitFromGroup, groupDBDeregisterGroup, groupDBDeregisterUnit, groupDBDeregisterGroupSimple
        do
            local weakKeyMt = { __mode = 'k' }

            local groupDB = {
                unitsInGroups = {} --[[@as table<unit, table<FakeGroup, true>>]],
                groups = setmetatable({}, weakKeyMt) --[[@as table<FakeGroup, true> ]],
            }

            ---@param group FakeGroup
            ---@param unit unit
            groupDBRegisterUnitInGroup = function(group, unit)
                local relevantGroups = groupDB.unitsInGroups[unit]
                if not relevantGroups then
                    relevantGroups = setmetatable({}, weakKeyMt) --[[@as table<FakeGroup, true>]]
                    groupDB.unitsInGroups[unit] = relevantGroups
                end
                relevantGroups[group] = true
                groupDB.groups[group] = true

                local pos = #group + 1
                group.indexOf[unit] = pos
                group[pos] = unit
            end

            ---@param group FakeGroup
            ---@param unit unit
            groupDBDeregisterUnitFromGroup = function(group, unit)
                local pos = group.indexOf[unit]
                if pos == nil then return end
                groupDB.unitsInGroups[unit][group] = nil

                -- remove unit from group
                local size = #group
                if pos ~= size then
                    local replUnit = group[size]
                    group[pos] = replUnit
                    group.indexOf[replUnit] = pos
                end
                group[size] = nil
                group.indexOf[unit] = nil
            end

            ---@param unit unit
            groupDBDeregisterUnit = function(unit)
                local relevantGroups = groupDB.unitsInGroups[unit]
                if not relevantGroups then return end
                for group, _ in pairs(relevantGroups) do
                    groupDBDeregisterUnitFromGroup(group, unit)
                    if #group == 0 then
                        groupDB.groups[group] = nil
                    end
                end
                groupDB.unitsInGroups[unit] = nil
            end
            unitRemovedEvent = groupDBDeregisterUnit

            ---@param group FakeGroup
            groupDBDeregisterGroup = function(group)
                if not groupDB.groups[group] then return end
                for i = #group, 1, -1 do
                    groupDBDeregisterUnitFromGroup(group, group[i])
                end

                groupDB.groups[group] = nil
            end

            groupDBDeregisterGroupSimple = function(group)
                groupDB.groups[group] = nil
            end
        end

        ---@return FakeGroup
        function CreateGroup()
            return { indexOf = {}, __faketype = "userdata" }
        end

        bj_lastCreatedGroup = CreateGroup()
        bj_suspendDecayFleshGroup = CreateGroup()
        bj_suspendDecayBoneGroup = CreateGroup()

        ---@param group FakeGroup
        ---@param unit unit
        function GroupAddUnit(group, unit)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(unit ~= nil, 'unit cannot be nil') then return end

            if group.indexOf[unit] then return end
            groupDBRegisterUnitInGroup(group, unit)
        end

        ---@param group FakeGroup
        ---@param unit unit
        function GroupRemoveUnit(group, unit)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(unit ~= nil, 'unit cannot be nil') then return end
            groupDBDeregisterUnitFromGroup(group, unit)
            if #group == 0 then groupDBDeregisterGroupSimple(group) end
        end

        ---@param group FakeGroup
        function GroupClear(group)
            if check(group ~= nil, 'group cannot be nil') then return end
            groupDBDeregisterGroup(group)
        end

        ---@param unit unit
        ---@param group FakeGroup
        ---@return boolean
        function IsUnitInGroup(unit, group)
            if check(unit ~= nil, 'unit cannot be nil') then return false end
            if check(group ~= nil, 'group cannot be nil') then return false end
            return group.indexOf[unit] and true or false
        end

        ---@param group FakeGroup
        ---@return unit|nil
        function FirstOfGroup(group)
            if check(group ~= nil, 'group cannot be nil') then return end
            return group[1]
        end

        local enumUnit
        ---@return unit enumUnit
        function GetEnumUnit()
            return enumUnit
        end

        ---@param group FakeGroup
        ---@param code fun(u: unit)
        function GUI.forGroup(group, code)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(code ~= nil, 'code cannot be nil') then return end
            local i = 1
            local unit
            while i <= #group do
                unit = group[i]
                code(unit)
                if group.indexOf[unit] then
                    i = i + 1
                end
            end
        end

        ---@param group FakeGroup
        ---@param code fun(u)
        function ForGroup(group, code)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(code ~= nil, 'code cannot be nil') then return end
            local old = enumUnit
            GUI.forGroup(group, function(unit)
                enumUnit = unit
                code()
            end)
            enumUnit = old
        end

        do
            local oldUnitAt = BlzGroupUnitAt

            ---@param group FakeGroup
            ---@param index integer
            ---@return unit|nil
            function BlzGroupUnitAt(group, index)
                if check(group ~= nil, 'group cannot be nil') then return nil end
                if check(index ~= nil, 'index cannot be nil') then return nil end
                return group[index + 1]
            end

            local oldGetSize = BlzGroupGetSize

            ---@param code fun(u: unit)
            local function groupAction(code)
                for i = 0, oldGetSize(mainGroup) - 1 do
                    code(oldUnitAt(mainGroup, i) --[[@as unit should be fine]])
                end
            end
            for _, name in ipairs({
                "OfType",
                "OfPlayer",
                "OfTypeCounted",
                "InRect",
                "InRectCounted",
                "InRange",
                "InRangeOfLoc",
                "InRangeCounted",
                "InRangeOfLocCounted",
                "Selected"
            }) do
                local varStr = "GroupEnumUnits" .. name
                local old = _ENV[varStr]

                ---@param group FakeGroup
                ---@param ... unknown
                _ENV[varStr] = function(group, ...)
                    if group then
                        old(mainGroup, ...)
                        GroupClear(group)
                        groupAction(function(unit)
                            GroupAddUnit(group, unit)
                        end)
                    end
                end
                --Provide API for Lua users who just want to efficiently run code, without caring about the group itself.
                ---@param code fun(group: FakeGroup, ...: unknown)
                ---@param ... unknown
                GUI["enumUnits" .. name] = function(code, ...)
                    if check(code ~= nil, 'code cannot be nil') then return end
                    old(mainGroup, ...)
                    groupAction(code)
                end
            end
        end

        for _, name in ipairs {
            "GroupImmediateOrder",
            "GroupImmediateOrderById",
            "GroupPointOrder",
            "GroupPointOrderById",
            "GroupTargetOrder",
            "GroupTargetOrderById"
        } do
            local old = _ENV[name]
            ---@param group FakeGroup
            ---@param ... unknown
            ---@return boolean
            _ENV[name] = function(group, ...)
                if check(group ~= nil, ' group cannot be nil') then return false end
                oldGroupClear(issueGroup)
                for _, unit in ipairs(group) do
                    oldGroupAddUnit(issueGroup, unit)
                end
                return old(issueGroup, ...)
            end
        end

        ---@param group FakeGroup
        ---@return integer
        function BlzGroupGetSize(group)
            if check(group ~= nil, 'group cannot be nil') then return 0 end
            return #group
        end

        ---@param group FakeGroup
        ---@param add FakeGroup
        function GroupAddGroup(add, group)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(add ~= nil, 'add cannot be nil') then return end
            GUI.forGroup(add, function(unit)
                GroupAddUnit(group, unit)
            end)
        end

        ---@param group FakeGroup
        ---@param remove FakeGroup
        function GroupRemoveGroup(remove, group)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(remove ~= nil, 'remove cannot be nil') then return end
            GUI.forGroup(remove, function(unit)
                GroupRemoveUnit(group, unit)
            end)
        end

        ---@param group FakeGroup
        ---@return unit|nil
        function GroupPickRandomUnit(group)
            if check(group ~= nil, 'group cannot be nil') then return nil end
            return group[1] and group[GetRandomInt(1, #group)]
        end

        ---@param group FakeGroup
        ---@return boolean
        function IsUnitGroupEmptyBJ(group)
            if check(group ~= nil, 'group cannot be nil') then return true end -- if it's a nil group, I'm sure the appropriate logic is to say it's empty?
            return not group[1]
        end

        ForGroupBJ = ForGroup
        CountUnitsInGroup = BlzGroupGetSize
        BlzGroupAddGroupFast = GroupAddGroup
        BlzGroupRemoveGroupFast = GroupRemoveGroup
        GroupPickRandomUnitEnum = nil
        CountUnitsInGroupEnum = nil
        GroupAddGroupEnum = nil
        GroupRemoveGroupEnum = nil
    end

    --[===========================[
      • LOCATIONS (POINTS IN GUI) •
    --]===========================]
    do
        ---@class FakeLocation: FakedType
        ---@field [1] number x
        ---@field [2] number y

        local oldLocation = Location
        local location

        ---@param x number
        ---@param y number
        ---@return FakeLocation
        function Location(x, y)
            if check(x ~= nil, 'x cannot be nil') then return { 0.00, 0.00, __faketype = 'userdata' } end
            if check(y ~= nil, 'y cannot be nil') then return { 0.00, 0.00, __faketype = 'userdata' } end
            return { x, y, __faketype = "userdata" }
        end

        do
            local oldRemove = RemoveLocation
            local oldGetX   = GetLocationX
            local oldGetY   = GetLocationY
            local oldRally  = GetUnitRallyPoint

            ---@param unit unit
            ---@return FakeLocation?
            function GetUnitRallyPoint(unit)
                if check(unit ~= nil, 'unit cannot be nil') then return nil end -- no unit, no rally
                local removeThis = oldRally(unit)                               --Actually needs to create a location for a brief moment, as there is no GetUnitRallyX/Y
                if removeThis == nil then return nil end                        -- in case there's no rally
                local loc = Location(oldGetX(removeThis), oldGetY(removeThis))
                oldRemove(removeThis)
                return loc
            end
        end

        RemoveLocation = DoNothing ---@type fun(location: FakeLocation)

        do
            local oldMoveLoc = MoveLocation
            local oldGetZ = GetLocationZ

            ---@param x number
            ---@param y number
            ---@return number z
            function GUI.getCoordZ(x, y)
                function GUI.getCoordZ(x, y)
                    if check(x ~= nil, 'x cannot be nil') then return 0 end
                    if check(y ~= nil, 'y cannot be nil') then return 0 end
                    oldMoveLoc(location, x, y)
                    return oldGetZ(location)
                end

                location = oldLocation(x, y)
                return GUI.getCoordZ(x, y)
            end
        end

        ---@param loc FakeLocation
        ---@return number x
        function GetLocationX(loc)
            if check(loc ~= nil, 'loc cannot be nil') then return 0 end
            return loc[1]
        end

        ---@param loc FakeLocation
        ---@return number y
        function GetLocationY(loc)
            if check(loc ~= nil, 'loc cannot be nil') then return 0 end
            return loc[2]
        end

        ---@param loc FakeLocation
        ---@return number z
        function GetLocationZ(loc)
            if check(loc ~= nil, 'loc cannot be nil') then return 0 end
            return GUI.getCoordZ(loc[1], loc[2])
        end

        ---@param loc FakeLocation
        ---@param x number
        ---@param y number
        function MoveLocation(loc, x, y)
            if check(loc ~= nil, 'loc cannot be nil') then return end
            loc[1] = x
            loc[2] = y
        end

        ---@param varName string
        ---@param suffix string|nil
        local function fakeCreate(varName, suffix)
            local getX = _ENV[varName .. "X"]
            local getY = _ENV[varName .. "Y"]
            _ENV[varName .. (suffix or "Loc")] = function(obj) return Location(getX(obj), getY(obj)) end
        end
        fakeCreate("GetUnit")
        fakeCreate("GetOrderPoint")
        fakeCreate("GetSpellTarget")
        fakeCreate("CameraSetupGetDestPosition")
        fakeCreate("GetCameraTargetPosition")
        fakeCreate("GetCameraEyePosition")
        fakeCreate("BlzGetTriggerPlayerMouse", "Position")
        fakeCreate("GetStartLocation")

        ---@param effect effect
        ---@param loc FakeLocation
        function BlzSetSpecialEffectPositionLoc(effect, loc)
            if check(effect ~= nil, 'effect cannot be nil') then return end
            if check(loc ~= nil, 'loc cannot be nil') then return end
            local x, y = loc[1], loc[2]
            BlzSetSpecialEffectPosition(effect, x, y, GUI.getCoordZ(x, y))
        end

        ---@param oldVarName string
        ---@param newVarName string
        ---@param index integer needed to determine which of the parameters calls for a location.
        local function hook(oldVarName, newVarName, index)
            local new = _ENV[newVarName]
            local func

            local errorMsgIndex1 = 'Function ' .. oldVarName .. '\'s argument #1 - location cannot be nil!'
            local errorMsgIndex2 = 'Function ' .. oldVarName .. '\'s argument #2 - location cannot be nil!'
            local errorMsgIndex3 = 'Function ' .. oldVarName .. '\'s argument #3 - location cannot be nil!'

            if index == 1 then
                func = function(loc, ...)
                    if check(loc ~= nil, errorMsgIndex1) then return new(0, 0, ...) end
                    return new(loc[1], loc[2], ...)
                end
            elseif index == 2 then
                func = function(a, loc, ...)
                    if check(loc ~= nil, errorMsgIndex2) then return new(a, 0, 0, ...) end
                    return new(a, loc[1], loc[2], ...)
                end
            else --index==3
                func = function(a, b, loc, ...)
                    if check(loc ~= nil, errorMsgIndex3) then return new(a, b, 0, 0, ...) end
                    return new(a, b, loc[1], loc[2], ...)
                end
            end
            _ENV[oldVarName] = func
        end
        hook("IsLocationInRegion", "IsPointInRegion", 2)
        hook("IsUnitInRangeLoc", "IsUnitInRangeXY", 2)
        hook("IssuePointOrderLoc", "IssuePointOrder", 3)
        IssuePointOrderLocBJ = IssuePointOrderLoc
        hook("IssuePointOrderByIdLoc", "IssuePointOrderById", 3)
        hook("IsLocationVisibleToPlayer", "IsVisibleToPlayer", 1)
        hook("IsLocationFoggedToPlayer", "IsFoggedToPlayer", 1)
        hook("IsLocationMaskedToPlayer", "IsMaskedToPlayer", 1)
        hook("CreateFogModifierRadiusLoc", "CreateFogModifierRadius", 3)
        hook("AddSpecialEffectLoc", "AddSpecialEffect", 2)
        hook("AddSpellEffectLoc", "AddSpellEffect", 3)
        hook("AddSpellEffectByIdLoc", "AddSpellEffectById", 3)
        hook("SetBlightLoc", "SetBlight", 2)
        hook("DefineStartLocationLoc", "DefineStartLocation", 2)
        hook("GroupEnumUnitsInRangeOfLoc", "GroupEnumUnitsInRange", 2)
        hook("GroupEnumUnitsInRangeOfLocCounted", "GroupEnumUnitsInRangeCounted", 2)
        hook("GroupPointOrderLoc", "GroupPointOrder", 3)
        GroupPointOrderLocBJ = GroupPointOrderLoc
        hook("GroupPointOrderByIdLoc", "GroupPointOrderById", 3)
        hook("MoveRectToLoc", "MoveRectTo", 2)
        hook("RegionAddCellAtLoc", "RegionAddCell", 2)
        hook("RegionClearCellAtLoc", "RegionClearCell", 2)
        hook("CreateUnitAtLoc", "CreateUnit", 3)
        hook("CreateUnitAtLocByName", "CreateUnitByName", 3)
        hook("SetUnitPositionLoc", "SetUnitPosition", 2)
        hook("ReviveHeroLoc", "ReviveHero", 2)
        hook("SetFogStateRadiusLoc", "SetFogStateRadius", 3)
        hook('CreateMinimapIconAtLoc', 'CreateMinimapIcon', 1)

        ---@param min FakeLocation
        ---@param max FakeLocation
        ---@return FakeRect newRect
        function RectFromLoc(min, max)
            if check(min ~= nil, 'min cannot be nil') then return nil end
            if check(max ~= nil, 'max cannot be nil') then return nil end
            return Rect(min[1], min[2], max[1], max[2]) --[[@as FakeRect]]
        end

        ---@param whichRect FakeRect
        ---@param min FakeLocation
        ---@param max FakeLocation
        function SetRectFromLoc(whichRect, min, max)
            if check(min ~= nil, 'min cannot be nil') then return end
            if check(max ~= nil, 'max cannot be nil') then return end
            SetRect(whichRect, min[1], min[2], max[1], max[2])
        end
    end

    --[========================[
      • RECTS (REGIONS IN GUI) •
    --]========================]
    do
        ---@class FakeRect: FakedType
        ---@field [1] number minX
        ---@field [2] number minY
        ---@field [3] number maxX
        ---@field [4] number maxY

        local oldRect, rect = Rect, nil
        ---@param minX number
        ---@param minY number
        ---@param maxX number
        ---@param maxY number
        ---@return FakeRect
        function Rect(minX, minY, maxX, maxY)
            if check(minX ~= nil, 'minX cannot be nil') then return { 0, 0, 0, 0, __faketype = "userdata" } end
            if check(minY ~= nil, 'minY cannot be nil') then return { 0, 0, 0, 0, __faketype = "userdata" } end
            if check(maxX ~= nil, 'maxX cannot be nil') then return { 0, 0, 0, 0, __faketype = "userdata" } end
            if check(maxY ~= nil, 'maxY cannot be nil') then return { 0, 0, 0, 0, __faketype = "userdata" } end
            return { minX, minY, maxX, maxY, __faketype = "userdata" }
        end

        local oldSetRect = SetRect
        ---@param rect FakeRect
        ---@param minX number
        ---@param minY number
        ---@param maxX number
        ---@param maxY number
        function SetRect(rect, minX, minY, maxX, maxY)
            if check(rect ~= nil, 'rect cannot be nil') then return end
            if check(minX ~= nil, 'minX cannot be nil') then return end
            if check(minY ~= nil, 'minY cannot be nil') then return end
            if check(maxX ~= nil, 'maxX cannot be nil') then return end
            if check(maxY ~= nil, 'maxY cannot be nil') then return end
            rect[1] = minX
            rect[2] = minY
            rect[3] = maxX
            rect[4] = maxY
        end

        do
            local oldWorld = GetWorldBounds
            local getMinX = GetRectMinX
            local getMinY = GetRectMinY
            local getMaxX = GetRectMaxX
            local getMaxY = GetRectMaxY
            local remover = RemoveRect
            RemoveRect = DoNothing
            local newWorld

            ---@return FakeRect
            function GetWorldBounds()
                if not newWorld then
                    local w = oldWorld() --[[@as rect]]
                    newWorld = Rect(getMinX(w), getMinY(w), getMaxX(w), getMaxY(w))
                    remover(w)
                end
                return Rect(unpack(newWorld))
            end

            GetEntireMapRect = GetWorldBounds
        end

        ---@param rect FakeRect
        ---@return number
        function GetRectMinX(rect)
            if check(rect ~= nil, 'rect cannot be nil') then return 0 end
            return rect[1]
        end

        ---@param rect FakeRect
        ---@return number
        function GetRectMinY(rect)
            if check(rect ~= nil, 'rect cannot be nil') then return 0 end
            return rect[2]
        end

        ---@param rect FakeRect
        ---@return number
        function GetRectMaxX(rect)
            if check(rect ~= nil, 'rect cannot be nil') then return 0 end
            return rect[3]
        end

        ---@param rect FakeRect
        ---@return number
        function GetRectMaxY(rect)
            if check(rect ~= nil, 'rect cannot be nil') then return 0 end
            return rect[4]
        end

        ---@param rect FakeRect
        ---@return number
        function GetRectCenterX(rect)
            if check(rect ~= nil, 'rect cannot be nil') then return 0 end
            return (rect[1] + rect[3]) / 2
        end

        ---@param rect FakeRect
        ---@return number
        function GetRectCenterY(rect)
            if check(rect ~= nil, 'rect cannot be nil') then return 0 end
            return (rect[2] + rect[4]) / 2
        end

        ---@param rect FakeRect
        ---@param x number
        ---@param y number
        function MoveRectTo(rect, x, y)
            if check(rect ~= nil, 'rect cannot be nil') then return end
            if check(x ~= nil, 'x cannot be nil') then return end
            if check(y ~= nil, 'y cannot be nil') then return end
            x = x - GetRectCenterX(rect)
            y = y - GetRectCenterY(rect)
            SetRect(rect, rect[1] + x, rect[2] + y, rect[3] + x, rect[4] + y)
        end

        ---@param varName string
        ---@param index integer needed to determine which of the parameters calls for a rect.
        local function hook(varName, index)
            local old = _ENV[varName]
            local func

            local errorMsgIndex1 = 'Function ' .. varName .. '\'s argument #1 - rect cannot be nil!'
            local errorMsgIndex2 = 'Function ' .. varName .. '\'s argument #2 - rect cannot be nil!'
            local errorMsgIndex3 = 'Function ' .. varName .. '\'s argument #3 - rect cannot be nil!'
            if index == 1 then
                func = function(rct, ...)
                    if check(rct ~= nil, errorMsgIndex1) then
                        oldSetRect(rect --[[@as rect]], 0, 0, 0, 0)
                    else
                        oldSetRect(rect --[[@as rect]], unpack(rct))
                    end
                    return old(rect, ...)
                end
            elseif index == 2 then
                func = function(a, rct, ...)
                    if check(rct ~= nil, errorMsgIndex2) then
                        oldSetRect(rect --[[@as rect]], 0, 0, 0, 0)
                    else
                        oldSetRect(rect --[[@as rect]], unpack(rct))
                    end
                    return old(a, rect, ...)
                end
            else --index==3
                func = function(a, b, rct, ...)
                    if check(rct ~= nil, errorMsgIndex3) then
                        oldSetRect(rect --[[@as rect]], 0, 0, 0, 0)
                    else
                        oldSetRect(rect --[[@as rect]], unpack(rct))
                    end
                    return old(a, b, rect, ...)
                end
            end

            ---@param ... unknown
            _ENV[varName] = function(...)
                if not rect then rect = oldRect(0, 0, 32, 32) end
                _ENV[varName] = func
                return func(...)
            end
        end
        hook("EnumDestructablesInRect", 1)
        hook("EnumItemsInRect", 1)
        hook("AddWeatherEffect", 1)
        hook("SetDoodadAnimationRect", 1)
        hook("GroupEnumUnitsInRect", 2)
        hook("GroupEnumUnitsInRectCounted", 2)
        hook("RegionAddRect", 2)
        hook("RegionClearRect", 2)
        hook("SetBlightRect", 2)
        hook("SetFogStateRect", 3)
        hook("CreateFogModifierRect", 3)
    end
    --[===============================[
      • FORCES (PLAYER GROUPS IN GUI) •
    --]===============================]
    do
        ---@class FakeForce: FakedType
        ---@field [integer] player
        ---@field indexOf {[player]: integer}

        local oldForce, mainForce = CreateForce, nil
        local function initForce()
            initForce = DoNothing
            mainForce = oldForce()
        end

        ---@return FakeForce
        function CreateForce()
            return { indexOf = {}, __faketype = "userdata" }
        end

        DestroyForce = DoNothing ---@type fun(force: FakeForce)
        local oldClear = ForceClear

        ---@param force FakeForce
        function ForceClear(force)
            if check(force ~= nil, 'force cannot be nil') then return end
            for i, val in ipairs(force) do
                force.indexOf[val] = nil
                force[i] = nil
            end
        end

        do
            local oldCripple = CripplePlayer
            local oldAdd = ForceAddPlayer

            ---@param player player
            ---@param force FakeForce
            ---@param flag boolean
            function GUI.cripplePlayer(player, force, flag)
                function GUI.cripplePlayer(player, force, flag)
                    for _, val in ipairs(force) do
                        oldAdd(mainForce --[[@ as force]], val)
                    end
                    oldCripple(player, mainForce --[[@ as force]], flag)
                    oldClear(mainForce --[[@ as force]])
                end

                initForce()
                GUI.cripplePlayer(player, force, flag)
            end

            ---@param player player
            ---@param force FakeForce
            ---@param flag boolean
            function CripplePlayer(player, force, flag)
                if check(player ~= nil, 'player cannot be nil') then return end
                if check(force ~= nil, 'force cannot be nil') then return end
                GUI.cripplePlayer(player, force, flag)
            end
        end

        ---@param force FakeForce
        ---@param player player
        function ForceAddPlayer(force, player)
            if check(force ~= nil, 'force cannot be nil') then return end
            if check(player ~= nil, 'player cannot be nil') then return end
            if force.indexOf[player] then return end

            local pos = #force + 1
            force.indexOf[player] = pos
            force[pos] = player
        end

        ---@param force FakeForce
        ---@param player player
        function ForceRemovePlayer(force, player)
            if check(force ~= nil, 'force cannot be nil') then return end
            if check(player ~= nil, 'player cannot be nil') then return end
            local pos = force.indexOf[player]
            if pos == nil then return end

            force.indexOf[player] = nil
            local top = #force
            if pos ~= top then
                force[pos] = force[top]
                force.indexOf[force[top]] = pos
            end
            force[top] = nil
        end

        ---@param force FakeForce
        ---@param player player
        ---@return boolean
        function BlzForceHasPlayer(force, player)
            if check(force ~= nil, 'force cannot be nil') then return false end
            if check(player ~= nil, 'player cannot be nil') then return false end
            return force.indexOf[player] and true or false
        end

        ---@param player player
        ---@param force FakeForce
        ---@return boolean
        function IsPlayerInForce(player, force)
            if check(player ~= nil, 'player cannot be nil') then return false end
            if check(force ~= nil, 'force cannot be nil') then return false end
            return force.indexOf[player] and true or false
        end

        ---@param unit unit
        ---@param force FakeForce
        ---@return boolean
        function IsUnitInForce(unit, force)
            if check(unit ~= nil, 'unit cannot be nil') then return false end
            if check(force ~= nil, 'force cannot be nil') then return false end
            return force.indexOf[GetOwningPlayer(unit)] and true or false
        end

        local enumPlayer
        local oldForForce = ForForce
        local oldEnumPlayer = GetEnumPlayer

        ---@param force FakeForce
        ---@param code fun(p: player)
        function GUI.forForce(force, code)
            if check(force ~= nil, 'force cannot be nil') then return end
            if check(code ~= nil, 'code cannot be nil') then return end
            local i = 1
            local player
            while i <= #force do
                player = force[i]
                code(player)
                if force.indexOf[player] then
                    i = i + 1
                end
            end
        end

        ---@return player
        function GetEnumPlayer()
            return enumPlayer
        end

        ---@param force FakeForce
        ---@param code function
        function ForForce(force, code)
            if check(force ~= nil, 'force cannot be nil') then return end
            if check(code ~= nil, 'code cannot be nil') then return end
            local old = enumPlayer
            GUI.forForce(force, function(player)
                enumPlayer = player
                code()
            end)
            enumPlayer = old
        end

        ---@param force FakeForce
        local function funnelEnum(force)
            if check(force ~= nil, 'force cannot be nil') then return end
            ForceClear(force)
            oldForForce(mainForce, function()
                ForceAddPlayer(force, oldEnumPlayer())
            end)
            oldClear(mainForce --[[@as force]])
        end
        ---@param varStr string
        local function hookEnum(varStr)
            local old = _ENV[varStr]
            local deferred
            function deferred(force, ...)
                function deferred(force, ...)
                    old(mainForce, ...)
                    funnelEnum(force)
                end

                initForce()
                _ENV[varStr](force, ...)
            end

            _ENV[varStr] = function(force, ...)
                if check(force ~= nil, 'force cannot be nil') then return end
                deferred(force, ...)
            end
        end
        hookEnum("ForceEnumPlayers")
        hookEnum("ForceEnumPlayersCounted")
        hookEnum("ForceEnumAllies")
        hookEnum("ForceEnumEnemies")
        ---@param force FakeForce
        ---@return integer
        function CountPlayersInForceBJ(force)
            if check(force ~= nil, 'force cannot be nil') then return 0 end
            return #force
        end

        CountPlayersInForceEnum = nil

        ---@param player player
        ---@return FakeForce
        function GetForceOfPlayer(player)
            if check(player ~= nil, 'player cannot be nil') then return nil end
            --No longer leaks. There was no reason to dynamically create forces to begin with.
            return bj_FORCE_PLAYER[GetPlayerId(player)]
        end
    end

    -- section on Blizzard.j desyncable objects
    do
        local desyncCausingTimer1 = bj_queuedExecTimeoutTimer
        local desyncCausingTimer2 = bj_delayedSuspendDecayTimer
        local desyncCausingTimer3 = bj_volumeGroupsTimer
        local desyncCausingTimer4 = bj_lastStartedTimer
        function GUI.__constantly_loaded()
            -- some nonsense lines to make sure this function "always" needs the relevant upvalues
            if desyncCausingTimer1 then return true end
            if desyncCausingTimer2 then return true end
            if desyncCausingTimer3 then return true end
            if desyncCausingTimer4 then return true end
        end
    end

    --Blizzard forgot to add this, but still enabled it for GUI. Therefore, I've extracted and simplified the code from DebugIdInteger2IdString
    ---@param value integer
    ---@return string
    function BlzFourCC2S(value)
        if value == nil then return "" end
        local result = ""
        for _ = 1, 4 do
            result = string.char(value % 256) .. result
            value = value // 256
        end
        return result
    end

    ---@param trig trigger
    ---@param r FakeRect
    function TriggerRegisterDestDeathInRegionEvent(trig, r)
        if check(trig ~= nil, 'trigger cannot be nil') then return end
        if check(r ~= nil, 'rect cannot be nil') then return end
        --Removes the limit on the number of destructables that can be registered.
        EnumDestructablesInRect(r, nil, function() TriggerRegisterDeathEvent(trig, GetEnumDestructable()) end)
    end

    IsUnitAliveBJ = UnitAlive --use the reliable native instead of the life checks

    ---@param u unit
    ---@return boolean
    function IsUnitDeadBJ(u)
        return not UnitAlive(u)
    end

    ---@param whichUnit unit
    ---@param propWindow number
    function SetUnitPropWindowBJ(whichUnit, propWindow)
        --Allows the Prop Window to be set to zero to allow unit movement to be suspended.
        SetUnitPropWindow(whichUnit, math.rad(propWindow))
    end

    if _USE_GLOBAL_REMAP then
        OnInit(function(import)
            import "GlobalRemap"
            GlobalRemap("udg_INFINITE_LOOP", function() return -1 end) --a readonly variable for infinite looping in GUI.
        end)
    end

    do
        local cache = __jarray()

        ---@param whichTrig trigger
        ---@return function
        function GUI.wrapTrigger(whichTrig)
            if check(whichTrig ~= nil, 'whichTrig cannot be nil') then return nil end
            local func = cache[whichTrig]
            if not func then
                func = function()
                    if IsTriggerEnabled(whichTrig) and TriggerEvaluate(whichTrig) then
                        TriggerExecute(whichTrig)
                    end
                end
                cache[whichTrig] = func
            end
            return func
        end
    end
    do
        --[[---------------------------------------------------------------------------------------------
            RegisterAnyPlayerUnitEvent by Bribe

            RegisterAnyPlayerUnitEvent cuts down on handle count for alread-registered events, plus has
            the benefit for Lua users to just use function calls.

            Adds a third parameter to the RegisterAnyPlayerUnitEvent function: "skip". If true, disables
            the specified event, while allowing a single function to run discretely. It also allows (if
            Global Variable Remapper is included) GUI to un-register a playerunitevent by setting
            udg_RemoveAnyUnitEvent to the trigger they wish to remove.

            The "return" value of RegisterAnyPlayerUnitEvent calls the "remove" method. The API, therefore,
            has been reduced to just this one function (in addition to the bj override).
        -----------------------------------------------------------------------------------------------]]
        local fStack, tStack, oldBJ = {}, {},
        TriggerRegisterAnyUnitEventBJ ---@type {[eventid]: function[]}, {[eventid]: trigger[]}

        ---@param event playerunitevent
        ---@param userFunc function
        ---@param skip boolean?
        function RegisterAnyPlayerUnitEvent(event, userFunc, skip)
            if check(event ~= nil, 'event cannot be nil') then return end
            if check(userFunc ~= nil, 'userFunc cannot be nil') then return end
            if skip then
                local t = tStack[event]
                if t and IsTriggerEnabled(t) then
                    DisableTrigger(t)
                    userFunc()
                    EnableTrigger(t)
                else
                    userFunc()
                end
            else
                local funcs, insertAt = fStack[event], 1
                if funcs then
                    insertAt = #funcs + 1
                    if insertAt == 1 then EnableTrigger(tStack[event]) end
                else
                    local t = CreateTrigger()
                    oldBJ(t, event)
                    tStack[event], funcs = t, {}
                    fStack[event] = funcs
                    TriggerAddCondition(t, Filter(function()
                        for _, func in ipairs(funcs) do func() end
                    end))
                end
                funcs[insertAt] = userFunc
                return function()
                    local total = #funcs
                    for i = 1, total do
                        if funcs[i] == userFunc then
                            if total == 1 then
                                DisableTrigger(tStack[event]) --no more events are registered, disable the event (for now).
                            elseif total > i then
                                funcs[i] = funcs[total]
                            end                --pop just the top index down to this vacant slot so we don't have to down-shift the entire stack.
                            funcs[total] = nil --remove the top entry.
                            return true
                        end
                    end
                end
            end
        end

        local trigFuncs
        ---@param trig trigger
        ---@param event playerunitevent
        ---@return function|nil
        function TriggerRegisterAnyUnitEventBJ(trig, event)
            if check(trig ~= nil, 'trig cannot be nil') then return nil end
            if check(event ~= nil, 'event cannot be nil') then return nil end
            local removeFunc = RegisterAnyPlayerUnitEvent(event, GUI.wrapTrigger(trig))
            if _USE_GLOBAL_REMAP then
                if not trigFuncs then
                    trigFuncs = __jarray()
                    GlobalRemap("udg_RemoveAnyUnitEvent", nil, function(t)
                        if trigFuncs[t] then
                            trigFuncs[t]()
                            trigFuncs[t] = nil
                        end
                    end)
                end
                trigFuncs[trig] = removeFunc
            end
            return removeFunc
        end
    end

    -- Modify to allow requests for negative hero stats, as per request from Tasyen.
    ---@param whichHero unit
    ---@param whichStat integer
    ---@param value integer
    function SetHeroStat(whichHero, whichStat, value)
        if check(whichStat ~= nil, 'whichStat cannot be nil') then return end
        if (whichStat == bj_HEROSTAT_STR) then
            SetHeroStr(whichHero, value, true)
        elseif (whichStat == bj_HEROSTAT_AGI) then
            SetHeroAgi(whichHero, value, true)
        elseif (whichStat == bj_HEROSTAT_INT) then
            SetHeroInt(whichHero, value, true)
        end
    end

    -- ExecuteFunc native is useless in Lua, so let's replace it:
    ---@param funcName string
    function ExecuteFunc(funcName)
        local func = _ENV[funcName]
        if func == nil then
            check(false, 'Function by the name ' .. funcName .. ' is not found!')
        else
            func()
        end
    end

    --The next part of the code is purely optional, as it is intended to optimize rather than add new functionality
    CommentString                        = nil
    RegisterDestDeathInRegionEnum        = nil

    --This next list comes from HerlySQR, and its purpose is to eliminate useless wrapper functions (only where the parameters aligned):
    StringIdentity                       = GetLocalizedString
    TriggerRegisterTimerExpireEventBJ    = TriggerRegisterTimerExpireEvent
    TriggerRegisterDialogEventBJ         = TriggerRegisterDialogEvent
    TriggerRegisterUpgradeCommandEventBJ = TriggerRegisterUpgradeCommandEvent
    RemoveWeatherEffectBJ                = RemoveWeatherEffect
    DestroyLightningBJ                   = DestroyLightning
    GetLightningColorABJ                 = GetLightningColorA
    GetLightningColorRBJ                 = GetLightningColorR
    GetLightningColorGBJ                 = GetLightningColorG
    GetLightningColorBBJ                 = GetLightningColorB
    SetLightningColorBJ                  = SetLightningColor
    GetAbilityEffectBJ                   = GetAbilityEffectById
    GetAbilitySoundBJ                    = GetAbilitySoundById
    ResetTerrainFogBJ                    = ResetTerrainFog
    SetSoundDistanceCutoffBJ             = SetSoundDistanceCutoff
    SetSoundPitchBJ                      = SetSoundPitch
    AttachSoundToUnitBJ                  = AttachSoundToUnit
    KillSoundWhenDoneBJ                  = KillSoundWhenDone
    PlayThematicMusicBJ                  = PlayThematicMusic
    EndThematicMusicBJ                   = EndThematicMusic
    StopMusicBJ                          = StopMusic
    ResumeMusicBJ                        = ResumeMusic
    VolumeGroupResetImmediateBJ          = VolumeGroupReset
    WaitForSoundBJ                       = TriggerWaitForSound
    ClearMapMusicBJ                      = ClearMapMusic
    DestroyEffectBJ                      = DestroyEffect
    GetItemLifeBJ                        = GetWidgetLife -- This was just to type casting
    SetItemLifeBJ                        = SetWidgetLife -- This was just to type casting
    GetLearnedSkillBJ                    = GetLearnedSkill
    UnitDropItemPointBJ                  = UnitDropItemPoint
    UnitDropItemTargetBJ                 = UnitDropItemTarget
    UnitUseItemDestructable              = UnitUseItemTarget -- This was just to type casting
    UnitInventorySizeBJ                  = UnitInventorySize
    SetItemInvulnerableBJ                = SetItemInvulnerable
    SetItemDropOnDeathBJ                 = SetItemDropOnDeath
    SetItemDroppableBJ                   = SetItemDroppable
    SetItemPlayerBJ                      = SetItemPlayer
    ChooseRandomItemBJ                   = ChooseRandomItem
    ChooseRandomNPBuildingBJ             = ChooseRandomNPBuilding
    ChooseRandomCreepBJ                  = ChooseRandomCreep
    String2UnitIdBJ                      = UnitId -- I think they just wanted a better name
    GetIssuedOrderIdBJ                   = GetIssuedOrderId
    GetKillingUnitBJ                     = GetKillingUnit
    IsUnitHiddenBJ                       = IsUnitHidden
    IssueTrainOrderByIdBJ                = IssueImmediateOrderById -- I think they just wanted a better name
    IssueUpgradeOrderByIdBJ              = IssueImmediateOrderById -- I think they just wanted a better name
    GetAttackedUnitBJ                    = GetTriggerUnit          -- I think they just wanted a better name
    SetUnitFlyHeightBJ                   = SetUnitFlyHeight
    SetUnitTurnSpeedBJ                   = SetUnitTurnSpeed
    GetUnitDefaultPropWindowBJ           = GetUnitDefaultPropWindow
    SetUnitBlendTimeBJ                   = SetUnitBlendTime
    SetUnitAcquireRangeBJ                = SetUnitAcquireRange
    UnitSetCanSleepBJ                    = UnitAddSleep
    UnitCanSleepBJ                       = UnitCanSleep
    UnitWakeUpBJ                         = UnitWakeUp
    UnitIsSleepingBJ                     = UnitIsSleeping
    IsUnitPausedBJ                       = IsUnitPaused
    SetUnitExplodedBJ                    = SetUnitExploded
    GetTransportUnitBJ                   = GetTransportUnit
    GetLoadedUnitBJ                      = GetLoadedUnit
    IsUnitInTransportBJ                  = IsUnitInTransport
    IsUnitLoadedBJ                       = IsUnitLoaded
    IsUnitIllusionBJ                     = IsUnitIllusion
    SetDestructableInvulnerableBJ        = SetDestructableInvulnerable
    IsDestructableInvulnerableBJ         = IsDestructableInvulnerable
    SetDestructableMaxLifeBJ             = SetDestructableMaxLife
    WaygateIsActiveBJ                    = WaygateIsActive
    QueueUnitAnimationBJ                 = QueueUnitAnimation
    SetDestructableAnimationBJ           = SetDestructableAnimation
    QueueDestructableAnimationBJ         = QueueDestructableAnimation
    DialogSetMessageBJ                   = DialogSetMessage
    DialogClearBJ                        = DialogClear
    GetClickedButtonBJ                   = GetClickedButton
    GetClickedDialogBJ                   = GetClickedDialog
    DestroyQuestBJ                       = DestroyQuest
    QuestSetTitleBJ                      = QuestSetTitle
    QuestSetDescriptionBJ                = QuestSetDescription
    QuestSetCompletedBJ                  = QuestSetCompleted
    QuestSetFailedBJ                     = QuestSetFailed
    QuestSetDiscoveredBJ                 = QuestSetDiscovered
    QuestItemSetDescriptionBJ            = QuestItemSetDescription
    QuestItemSetCompletedBJ              = QuestItemSetCompleted
    DestroyDefeatConditionBJ             = DestroyDefeatCondition
    DefeatConditionSetDescriptionBJ      = DefeatConditionSetDescription
    FlashQuestDialogButtonBJ             = FlashQuestDialogButton
    DestroyTimerBJ                       = DestroyTimer
    DestroyTimerDialogBJ                 = DestroyTimerDialog
    TimerDialogSetTitleBJ                = TimerDialogSetTitle
    TimerDialogSetSpeedBJ                = TimerDialogSetSpeed
    LeaderboardSetStyleBJ                = LeaderboardSetStyle
    LeaderboardGetItemCountBJ            = LeaderboardGetItemCount
    LeaderboardHasPlayerItemBJ           = LeaderboardHasPlayerItem
    DestroyLeaderboardBJ                 = DestroyLeaderboard
    LeaderboardSortItemsByPlayerBJ       = LeaderboardSortItemsByPlayer
    LeaderboardSortItemsByLabelBJ        = LeaderboardSortItemsByLabel
    PlayerGetLeaderboardBJ               = PlayerGetLeaderboard
    DestroyMultiboardBJ                  = DestroyMultiboard
    SetTextTagPosUnitBJ                  = SetTextTagPosUnit
    SetTextTagSuspendedBJ                = SetTextTagSuspended
    SetTextTagPermanentBJ                = SetTextTagPermanent
    SetTextTagAgeBJ                      = SetTextTagAge
    SetTextTagLifespanBJ                 = SetTextTagLifespan
    SetTextTagFadepointBJ                = SetTextTagFadepoint
    DestroyTextTagBJ                     = DestroyTextTag
    ForceCinematicSubtitlesBJ            = ForceCinematicSubtitles
    DisplayCineFilterBJ                  = DisplayCineFilter
    SaveGameCacheBJ                      = SaveGameCache
    FlushGameCacheBJ                     = FlushGameCache
    SaveGameCheckPointBJ                 = SaveGameCheckpoint
    LoadGameBJ                           = LoadGame
    RenameSaveDirectoryBJ                = RenameSaveDirectory
    RemoveSaveDirectoryBJ                = RemoveSaveDirectory
    CopySaveGameBJ                       = CopySaveGame
    IssueTargetOrderBJ                   = IssueTargetOrder
    IssueTargetDestructableOrder         = IssueTargetOrder -- This was just to type casting
    IssueTargetItemOrder                 = IssueTargetOrder -- This was just to type casting
    IssueImmediateOrderBJ                = IssueImmediateOrder
    GroupTargetOrderBJ                   = GroupTargetOrder
    GroupImmediateOrderBJ                = GroupImmediateOrder
    GroupTrainOrderByIdBJ                = GroupImmediateOrderById
    GroupTargetDestructableOrder         = GroupTargetOrder       -- This was just to type casting
    GroupTargetItemOrder                 = GroupTargetOrder       -- This was just to type casting
    GetDyingDestructable                 = GetTriggerDestructable -- I think they just wanted a better name
    GetAbilityName                       = GetObjectName          -- I think they just wanted a better name

    -- List of math overrides, provided by Antares & Insanity_AI
    CosBJ                                = function(degrees) return math.cos(degrees * bj_DEGTORAD) end ---@type fun(degrees: number): number
    SinBJ                                = function(degrees) return math.sin(degrees * bj_DEGTORAD) end ---@type fun(degrees: number): number
    TanBJ                                = function(degrees) return math.tan(degrees * bj_DEGTORAD) end ---@type fun(degrees: number): number
    AsinBJ                               = function(ratio) return math.asin(ratio) * bj_RADTODEG end ---@type fun(ratio: number): number
    AcosBJ                               = function(ratio) return math.acos(ratio) * bj_RADTODEG end ---@type fun(ratio: number): number

    -- Native Atans are faster than math.atan, surprisingly
    -- AtanBJ                               = function(ratio) return math.atan(ratio) * bj_RADTODEG end ---@type fun(ratio: number): number
    -- Atan2BJ                              = function(y, x) return math.atan(y, x) * bj_RADTODEG end ---@type fun(x: number, y: number): number

    Cos                                  = math.cos
    Sin                                  = math.sin
    Tan                                  = math.tan
    Acos                                 = math.acos
    Asin                                 = math.asin
    Pow                                  = function(base, exponent) return base ^ exponent end ---@type fun(base: number, exponent: number): number
    SquareRoot                           = math.sqrt
    Deg2Rad                              = function(degrees) return degrees * bj_DEGTORAD end ---@type fun(degrees: number): number
    Rad2Deg                              = function(radians) return radians * bj_RADTODEG end ---@type fun(radians: number): number

    SubStringBJ                          = string.sub
    SubString                            = function(source, start, _end) return string.sub(source, start + 1, _end) end ---@type fun(source: string, start: integer, _end: integer): string
    StringLength                         = string.len
    StringCase                           = function(source, upper)
        if upper then
            return string.upper(source)
        else
            return
            string.lower(source)
        end
    end ---@type fun(source: string, upper: boolean): string

    --[=======================[
      • UNIT REMOVAL DETECTOR •
    --]=======================]
    do
        local UNDEFEND_ORDER_ID = 852056
        local allUnits = {} ---@type table<unit, boolean>

        ---@alias UnitRemovalEventListener fun(removedUnit: unit)

        ---@class EventListenerMap
        ---@field [integer] UnitRemovalEventListener
        ---@field [UnitRemovalEventListener] integer
        ---@field n integer
        local eventListeners = { n = 0 }

        local function indexUnit(unit)
            if not allUnits[unit] then
                allUnits[unit] = true
                UnitAddAbility(unit, _REMOVE_ABIL)
                UnitMakeAbilityPermanent(unit, true, _REMOVE_ABIL)
            end -- else - the unit was dead, but has re-entered the map (e.g. unloaded from meat wagon)
        end

        OnInit.main(function()
            local enterTrigger = CreateTrigger()
            TriggerRegisterEnterRectSimple(enterTrigger, GetWorldBounds() --[[@as rect]]) -- returns FakeRect but due to all overrides, the BJ will be able to process it
            TriggerAddAction(enterTrigger, function()
                local unit = GetTriggerUnit()
                indexUnit(unit)
            end)

            local deindexTrigger = CreateTrigger()
            TriggerRegisterAnyUnitEventBJ(deindexTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
            TriggerAddAction(deindexTrigger, function()
                local unit = GetTriggerUnit()
                if GetIssuedOrderId() == UNDEFEND_ORDER_ID and not UnitAlive(unit) and allUnits[unit] and GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then
                    allUnits[unit] = nil
                    for _, listener in ipairs(eventListeners) do
                        -- todo: wrap it in a coroutine so that TSA/yields don't pause this entire thing (after coroutine recycler is added)
                        pcall(listener --[[@as UnitRemovalEventListener]], unit)
                    end
                    unitRemovedEvent(unit)
                end
            end)

            local playerCountMax = GetBJMaxPlayerSlots() - 1 -- 24 + 4 neutrals
            for j = 0, playerCountMax do
                SetPlayerAbilityAvailable(Player(j), _REMOVE_ABIL, false)
            end
        end)

        ---@param listener fun(removedUnit: unit)
        function GUI.RegisterUnitRemovedEventListener(listener)
            if check(listener ~= nil, 'listener cannot be nil') then return end
            if eventListeners[listener] then return end

            eventListeners.n = eventListeners.n + 1
            eventListeners[listener] = eventListeners.n
            eventListeners[eventListeners.n] = listener
        end

        ---@param listener fun(removedUnit: unit)
        function GUI.DeregisterUnitRemovedEventListener(listener)
            if check(listener ~= nil, 'listener cannot be nil') then return end
            if eventListeners[listener] then return end

            eventListeners[eventListeners[listener]] = eventListeners[eventListeners.n]
            eventListeners[eventListeners.n] = nil
            eventListeners.n = eventListeners.n - 1
            eventListeners[listener] = nil
        end
    end
end
if Debug then Debug.endFile() end
Contents

Lua-Infused GUI (Map)

Reviews
Antares
Game-changer for GUI maps moving forward! Any eventual remaining issues can be fixed in post. High Quality
The pic is funny :xxd:

What I want to know is why all the __faketype stuff and why all the asserts? I was coding with no safety wheels in place on purpose. Otherwise looks like a healthy quality of life improvement!

I didn't do the hashtable tolerance for different types on purpose because I view it as a terrible practice and wanted to discourage its usage.
 
I didn't do the hashtable tolerance for different types on purpose because I view it as a terrible practice and wanted to discourage its usage.
If GUI users were worried about bad practices, they wouldn't be using GUI. :peasant-cool:

How does a GUI user find out that this is not encouraged or supported? Someone will convert his map from JASS to Lua because he's been told that Lua is better, he's gonna install Lua-Infused GUI and it will break his map, and he won't have the expertise to figure out why.
 
Last edited:
That's a good reason! I would say I was being more idealistic and opinionated, proposing a world where people WANT to use GUI, and could benefit from it, but you're right that people who are being lazy (the majority) will have issues with stuff randomly breaking.

But what's up with stuff like __faketype?
 
Originally I wanted to use __name for the type function override, but I thought that maybe that isn't a good idea if a user has other systems that define that property for logging purposes, but still rely on type to return 'table' in such cases, so I opted for using an uncommon/custom field instead.

As for why it is __faketype.__name....

Now that I think about it, there's no reason why I decided to give it a table instead of outright defining it 'userdata', perhaps I should remove that extra lookup.
 
I started testing this resource today and must say it look promising as hell.

I found a bug though - in GroupRemoveUnit you don't update group[pos], so removed unit is not swapped in the array if it's not the last - below's my fix:

Lua:
function GroupRemoveUnit(group, unit)
assert(group ~= nil, 'group cannot be nil')
assert(unit ~= nil, 'unit cannot be nil')
local indexOf = group.indexOf
if indexOf == nil then return end
    local pos = indexOf[unit]
if pos == nil then return end
    local size = #group
    if pos ~= size then
        indexOf[group[size]] = pos
        group[pos] = group[size]          -- <== ADD THIS LINE
end
    group[size] = nil
    indexOf[unit] = nil
    if groups then
        groups[unit][group] = nil
    end
end

====

EDIT: Found another one: for functions GroupAddGroup and GroupRemoveGroup arguments are swapped.

====

EDIT: Okay, this one is tricky - for group created via CreateGroup():

This works:
GroupPointOrder(g, 'move', 0, 0)

But these throw error below:
GroupPointOrderLocBJ(g, "move", Location(0, 0))
GroupPointOrderLoc(g, "move", Location(0, 0))

1767123545808.png


Same for GroupPointOrderByIdLoc. Funny enough, GroupImmediateOrder and GroupTargetOrder work. Could be a combination of group and location in one method, by I don't know. Weird, but, I found a workaround - replacing this particular hook with manual override:

GroupPointOrderLoc = function(g, order, l)
return GroupPointOrder(g, order, l[1], l[2])
end

Maybe it's related to GroupPointOrder being overriden again below, but I can't see it right now.

====

EDIT: some more suggestions:

1. The forementioned group functions:

Lua:
for _, name in ipairs {"ImmediateOrder",
"ImmediateOrderById",
"PointOrder",
"PointOrderById",
"TargetOrder",
"TargetOrderById"
} do
local new = _G["Issue" .. name]

---@param group FakeGroup
---@param ... unknown
_G["Group" .. name] = function(group, ...)
for i = 1, #group do
new(group[i], ...)
end
end
end

now change units' behaviour - they no longer keep speed or formation. I'd rather implement them by calling native blizz functions with reuseable local "native" group.

2. When overriding functions with common prefix like above, I'd write full function names rather than concatenate the prefix. It would make it much easier to find places where particular functions are overriden.

====

I hope you'll find some time to further work on this library soon, as it has incredible potential for saving dev's effort and making triggers and scripts much more concise. I will probably use it at some point, for now it needs some more testing. I think including it to an existing large project like I just did with mine is a simple way to catch all those edge cases.
 

Attachments

  • 1767123458664.png
    1767123458664.png
    90.9 KB · Views: 36
Last edited:
I'm coming back to Warcraft after like a decade. Been a lot of stuff happening that I missed. I've got a list of basic practices I wrote down long ago, just so my GUI triggers at least didn't leak enough memory to make maps stutter like their girlfriend's really disapproving dad just walked in.

So, to make sure I've got this perfectly clear: if I add the Lua Infused GUI system, which of these rules are no longer necessary?

1. Remove Unit Groups either with "Custom script: set bj_wantDestroyGroup = true" before use, or Custom script: call DestroyGroup(udg_GroupVar) after
2. Remove Player Groups after use with "set PlayerGroupVariable = whatever," then "Custom script: call DestroyForce(udg_PlayerGroupVariable)"
3. Remove location points after use with "set LocationVariable = somewhere," then "Custom script: call RemoveLocation(udg_LocationVariable)"
4. Remove Special Effects after use with "Special Effect - Destroy (Last created special effect)"
5. Remove Sounds after use with "Sound - Destroy (Last played sound)"
6. Remove destroyed Destructibles with "Destructible - Remove <Destructible> Object"
7. Remove destroyed Lightning effects with "Lightning - Destroy <Lightning>"
8. Never use Thunder Clap, War Stomp, Shockwave, Earthquake, or Volcano because of the terrain deformation
9. Never use "Pick All Units Of Type"
10. Never use "Wait (game time)"
 
Hi, welcome back :)

1. Remove Unit Groups either with "Custom script: set bj_wantDestroyGroup = true" before use, or Custom script: call DestroyGroup(udg_GroupVar) after
2. Remove Player Groups after use with "set PlayerGroupVariable = whatever," then "Custom script: call DestroyForce(udg_PlayerGroupVariable)"
3. Remove location points after use with "set LocationVariable = somewhere," then "Custom script: call RemoveLocation(udg_LocationVariable)"
These 3 are no longer required.

On the following rules, Lua Infused GUI has no effect, but:

4. Remove Special Effects after use with "Special Effect - Destroy (Last created special effect)"
If the effect is supposed to be permanently visible, you do not want to destroy it.

5. Remove Sounds after use with "Sound - Destroy (Last played sound)"
You don't destroy a sound if it's been made via Sound Editor.

6. Remove destroyed Destructibles with "Destructible - Remove <Destructible> Object"
This is called only if you want a destructible straight up removed from the game, similar with units' Remove <Unit>.

7. Remove destroyed Lightning effects with "Lightning - Destroy <Lightning>"
Same thing with effects, you likely don't want this destroyed if it's supposed to be permanent lightning effect.

8. Never use Thunder Clap, War Stomp, Shockwave, Earthquake, or Volcano because of the terrain deformation
This is only an issue if you're using GetLocationZ, BlzGetUnitZ or BlzGetLocalUnitZ.

9. Never use "Pick All Units Of Type"
Not sure why this is a rule?

10. Never use "Wait (game time)"
Only usable with cinematics, but otherwise, you're correct with this rule.
 
I may not be very impartial here because I was making some bugfixes to this library ;). But now, I can confidently say should be mature and stable enough to be used in both new maps and to "upgrade" existing maps to handle all the leaks automatically.

Another cool (optional, but I see no reason not to use it) feature is validation of function parameters - it throws errors when you pass nulls to many native functions. Thus it can detect non-working stuff in your maps you'd never notice otherwise. For that reason you should thoroughly test your map after enabling this library, but it's definitely worth the effort.
 
I found a new issue, maybe not a bug, but a serious unwanted consequence. Looks like killed or removed units, as well as units with timed life expired, are never removed from any groups. I guess you'd have to either entirely replace units with some LUA objects, or use a unit indexer to detect their deaths and removals. Ehhhh...

I found this one. It'll probably do the job, although the number of dependencies will be growing significantly...
 
I wonder if weak-value table for a group would suffice. 🤔
But then we'd need to hook to __gc metamethod to also have the unit removed in the indexOf table as well, and I don't know how safe is it to fiddle with that.

EDIT: also, does the unit indexer's trick even work for units that get removed completely, I'm under the impression their deindex runs on unit death? (which also is tricky if a dead unit that gets revived before its decay duration runs out)

EDIT 2: ah, edits :D
 
Last edited:
Looks like it does some fancy magic with detecting reincarnation/revival.

I've never used a unit indexer, and to be honest, I already have so many libs in my campaign (both mine and others') that I'm a bit scared of adding big new dependencies, especially if they do hacks like adding dummy abilities that may collide with my stuff.

Damn, I'm making Warcraft 3 campaigns for fun, but now I began to do so much coding in them that I feel like at work :P.
 
Okay, so I did some tests:
  • the __gc route is a no go, I didn't manage to get it to run, either userdata cannot have __gc modified or its metatable or smth
  • weak key/value table also a no go, I assume it's due to DebugUtils, but simply having a system that could interfere with such unit removal detection is not good anyway, so I scrapped that.
  • Bribe's UnitEvent method works and I've made a small snippet out of it:

Lua:
if Debug then Debug.beginFile "UnitRemovalDetector" end
OnInit.main("UnitRemovalDetector", function(require)
    local _REMOVE_ABIL = FourCC('A000')
    local allUnits = {} ---@type table<unit, boolean>

    function UnitRemovedEvent(unit)
        print("Unit", unit, "was removed!")
    end

    OnInit.trig(function(require)
        local enterTrigger = CreateTrigger()
        TriggerRegisterEnterRectSimple(enterTrigger, GetWorldBounds())
        TriggerAddAction(enterTrigger, function()
            local unit = GetTriggerUnit()
            print("Entering unit", unit)
            if not allUnits[unit] then
                allUnits[unit] = true
                UnitAddAbility(unit, _REMOVE_ABIL)
                UnitMakeAbilityPermanent(unit, true, _REMOVE_ABIL)
            -- else - the unit was dead, but has re-entered the map (e.g. unloaded from meat wagon)
            end
        end)

        local deindexTrigger = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ(deindexTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        TriggerAddAction(deindexTrigger, function()
            local unit = GetTriggerUnit()
            if allUnits[unit] then
                if GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then
                    allUnits[unit] = nil
                    UnitRemovedEvent(unit)
                end
            end
        end)
        ForForce(GetPlayersAll(), function() -- should get replaced with a player id loop as GetPlayersAll only populates players that are defined, not all (+ no neutrals either)
            SetPlayerAbilityAvailable(GetEnumPlayer(), _REMOVE_ABIL, false)
        end)
    end)
end)
if Debug then Debug.endFile() end

The removal ability in question is a copy of Footman's Defend ability. So, it seems that by units simply having that ability the game will trigger a order event with orderId = 0 upon unit removal.
Said ability doesn't need to get activated, and gets set as unavailable for player so they don't appear on units' command cards

With this fix, LIGUI will then require a copy of defend ability in maps, which I think is a lot better option when it comes to dependencies than the UnitEvent itself requires.
 
Sounds like hell of a hack, but well, modding this game is built on hacks, so worth a try.

I tested your snippet, I think I made it work, but I had a couple of issues with it.

I used my trigger for printing every order and I didn't catch an order with orderId = 0 after a unit's decayed, but I caught an undefend order (852056) - maybe that's what you want?

One problem is, for units with actual defend it is triggered right after death. But there shouldn't be a lot of such units (except in Footman Frenzy :P), so maybe for them just create a timer on death, and check every 1s: if unit alive (was resurrected) - stop and do nothing, if GetUnitTypeId() == 0 -> remove unit?

Also, you'd need to distinguish it from normal undefend. I added an extra condition to check if a unit's dead.

if GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then - what's this line for? Shouldn't it always be 0 as the ability's unavailable?

Also, the enter trigger doesn't work for preplaced units, you need to go through them too at map start.

Here's a version that worked for me. Now just to clean it up and probably use this units/groups structure in your script to remove a unit from all its groups. And hopefully, that'll be the end of this issue.

Lua:
if Debug then Debug.beginFile "UnitRemovalDetector" end
OnInit.main("UnitRemovalDetector", function(require)
    local _REMOVE_ABIL = FourCC('A6CC')
    local allUnits = {} ---@type table<unit, boolean>

    function UnitRemovedEvent(unit)
        print("Unit", unit, "was removed!")
    end

    OnInit.trig(function(require)
        local enterTrigger = CreateTrigger()
        TriggerRegisterEnterRectSimple(enterTrigger, GetWorldBounds())
        TriggerAddAction(enterTrigger, function()
            local unit = GetTriggerUnit()
            if not allUnits[unit] then
                allUnits[unit] = true
                UnitAddAbility(unit, _REMOVE_ABIL)
                UnitMakeAbilityPermanent(unit, true, _REMOVE_ABIL)
                -- else - the unit was dead, but has re-entered the map (e.g. unloaded from meat wagon)
            end
        end)

        local deindexTrigger = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ(deindexTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        TriggerAddCondition(deindexTrigger, Condition(function()
            return GetIssuedOrderId() == 852056 and IsUnitDeadBJ(GetTriggerUnit())
        end))
        TriggerAddAction(deindexTrigger, function()
            local unit = GetTriggerUnit()
            if allUnits[unit] then
                if GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then
                    allUnits[unit] = nil
                    UnitRemovedEvent(unit)
                end
            end
        end)

        for j = 0, bj_MAX_PLAYERS do
            SetPlayerAbilityAvailable(Player(j), _REMOVE_ABIL, false)
            local g = CreateGroup()
            GroupEnumUnitsOfPlayer(g, Player(j), nil)
            ForGroup(g, function()
                if not allUnits[GetEnumUnit()] then
                    allUnits[GetEnumUnit()] = true
                end
            end)
            DestroyGroup(g)
        end
    end)
end)
if Debug then Debug.endFile() end
 
Last edited:
I used my trigger for printing every order and I didn't catch an order with orderId = 0 after a unit's decayed, but I caught an undefend order (852056) - maybe that's what you want?
Strange, the game was definitively priting me id=0 Nevermind, tested again and you're right.

if GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then - what's this line for? Shouldn't it always be 0 as the ability's unavailable?
apparently not, I'm guessing the game decides to force deactivate defend ability upon ability removal, probably why the order event happens. Otherwise the game would run the UnitRemovedEvent
on the first order the unit receives.

Also, the enter trigger doesn't work for preplaced units, you need to go through them too at map start.
And here I thought if I create a trigger before units start preplacing that the game would still process those events, guess I thought wrong :D
Actually, OnInit.trig is too late, I had a peek at generated map script and preplaced units generate before triggers, blizzard.j and globals are initialized, so OnInit.main should be the appropriate stage where you register the enter trigger. But then that means we can't use the for each player loop, so I guess this is the superior option. (because someone can remove a preplaced unit before the trigger registration, and that would make the system think the unit still exists (although, we could hook onto RemoveUnit and listen there))
 
Last edited:
apparently not, I'm guessing the game decides to force deactivate defend ability upon ability removal, probably why the order event happens. Otherwise the game would run the UnitRemovedEvent
on the first order the unit receives.
But I think it would happen without this condition I added:

Lua:
TriggerAddCondition(deindexTrigger, Condition(function()
            return GetIssuedOrderId() == 852056 and IsUnitDeadBJ(GetTriggerUnit())
        end))

In game, when I call for a living unit via lua console:
GetUnitAbilityLevel(MainSelected(), FourCC('A6CC'))
I get 0.

So I'd stick with my condition. But you'd also need to hook on RemoveUnit() function, as in that case it may be alive during removal.

And here I thought if I create a trigger before units start preplacing that the game would still process those events, guess I thought wrong :D
Actually, OnInit.trig is too late, I had a peek at generated map script and preplaced units generate before triggers, blizzard.j and globals are initialized, so OnInit.main should be the appropriate stage where you register the enter trigger. But then that means we can't use the for each player loop, so I guess this is the superior option. (because someone can remove a preplaced unit before the trigger registration, and that would make the system think the unit still exists (although, we could hook onto RemoveUnit and listen there))
I once did something like below when I needed to track all initial heroes (preplaced AND loaded from game cache) and I worried that looping through all units at map start could be expensive. But I see in the detection removal script that looping also does the job fine.

Lua:
    local originalCreateUnit = CreateUnit
    CreateUnit = function(...)
        return track(originalCreateUnit(...))
    end

    local originalCreateUnitByName = CreateUnitByName
    CreateUnitByName = function(...)
        return track(originalCreateUnitByName(...))
    end

    local originalCreateUnitAtLoc = CreateUnitAtLoc
    CreateUnitAtLoc = function(...)
        return track(originalCreateUnitAtLoc(...))
    end

    local originalCreateUnitAtLocByName = CreateUnitAtLocByName
    CreateUnitAtLocByName = function(...)
        return track(originalCreateUnitAtLocByName(...))
    end

    local originalBlzCreateUnitWithSkin = BlzCreateUnitWithSkin
    BlzCreateUnitWithSkin = function(...)
        return track(originalBlzCreateUnitWithSkin(...))
    end

    local originalRestoreUnit = RestoreUnit
    RestoreUnit = function(...)
        return track(originalRestoreUnit(...))
    end
 
I've been sick for a week so I kinda put that plan on pause, I do however have a theoretical fix that I haven't gotten to testing yet. If you want, you can use/test that for the time being.
One thing I remember is me needing to pick a proper unique ability id for that remove ability that has the least chance of clashing with something pre-existing, so that the users don't have to change it, in hopes of making this more plug&play (as much as possible)
 
Okay, I had to make some changes for this version to work:
  • I placed unit removal in OnInit.trig(function()) - otherwise on game start it crashes the map trying to create a trigger. It unfortunately means dependency on TotalInitialization or some other init lib that must be placed before LIGUI. I can't see TotalInitialization calling any custom Blizz functions you override, and it's pretty much a standard for every LUA user, so that dependency is probably fine
  • in the new version you create bj_lastCreatedGroup as a "blizz" unit group, before overriding CreateGroup(). This caused some problems when this variable was used in blizz functions, like CreateNUnitsAtLoc, so I had to revert it. I created a separate group to be used as mainGroup.
  • I changed the ability id for undefend copy ability, you can change it back.

Changed version here, it's working so far in my maps, but see if I didn't break anything on your end.
 
You could have left local mainGroup = bj_lastCreatedGroup, since bj_lastCreatedGroup is initialized by Blizzard.j, but other than that, LGTM 👍

Edit: Ah, about TotalInit, I thought I had entire library wrapped in OnInit.main, that's my bad, try changing that OnInit.trig to OnInit.main, otherwise it's not gonna capture preplaced units
 
You could have left local mainGroup = bj_lastCreatedGroup, since bj_lastCreatedGroup is initialized by Blizzard.j, but other than that, LGTM 👍
To me the less dependencies between blizz and non-blizz stuff, the less chance something will break. But of course, I just needed bj_lastCreatedGroup to be LUA group, not blizz group.

I did some more testing, for a while I had a problem with units with actual defend - for them undefend order triggered too early, just after unit death, so triggers with conditions "dying unit is in group" stopped working (as they were checked after a unit was deindexed). But I think it was due to conflict with yet another unit indexer by chopinsky I forgot I had :P.

I also noticed that for units with defend ability this undefend order is actually called twice, so this condition you had earlier may actually be needed to catch only the latter order:
GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0

I pushed the latest version.
 
Another day, another issue with groups from me. Not exactly a bug, but still a different behaviour from native Blizz code.

When you iterate a Blizz group, you can safely remove the iterated unit from this group. In some languages std libs don't allow that, but it's quite common case in map triggers, so I think LIGUI should support that:
for each unit in group
if some condition then
remove enum unit from group
end
end

In current implementation this will break as removed unit will be swapped with the last one, so the swapped unit will be skipped and the last one will be nil.

Here's a fix proposal from me:

Lua:
        function GUI.forGroup(group, code)
            if check(group ~= nil, 'group cannot be nil') then return end
            if check(code ~= nil, 'code cannot be nil') then return end
            local i = 1
            local unit
            while i <= #group do
                unit = group[i]
                code(unit)
                if group.indexOf[unit] then
                    i = i + 1
                end
            end
        end

BTW, LIGUI made me start doing one of the last things lack of which separated my modding hobby from work - writing integration tests. I even wrote a tiny test framework to be run on my test map :P.
 
Back
Top