• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Automatic Memory Leak Fixer

Status
Not open for further replies.

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
This is part 1 of my overall initiative to do automatic memory leak cleanup for GUI. Part 2 will be unit groups.

I have not tested every single aspect of the API, but I've attached a demo map which shows that the location API works successfully behind the scenes.

One thing that needs stating is that Hashtables themselves do not store Locations. I have a GUI hashtable wrapper which (hypothetically) solves that problem, which you can find in hidden tags below. I haven't tested the hashtable stuff at all, so none of this is ready to be submitted to the code section yet.

Lua:
--Replace all locations with Lua Tables, allowing for automatic garbage collection by not even using locations to begin with.

do
    local oldLoc, oldMoveLoc = Location, MoveLocation
    local oldGetX=GetLocationX
    local oldGetY=GetLocationY
    local oldGetZ=GetLocationZ
    local oldRally = GetUnitRallyPoint
    local oldRemove = RemoveLocation
   
OnMainInit(function()
    local location = oldLoc(0,0) --one single location to govern the cases where a location needs to be referenced.

    --First, the easy stuff:
    RemoveLocation = DoNothing
    function Location(x,y)
        return {x,y}
    end
    function MoveLocation(loc, x, y)
        loc[1]=x
        loc[2]=y
    end
    function GetLocationX(loc) return loc[1] end
    function GetLocationY(loc) return loc[2] end
    function GetLocationZ(loc)
        oldMoveLoc(location, loc[1], loc[2])
        return oldGetZ(loc)
    end
    function GetUnitLoc(unit)
        return {GetUnitX(unit), GetUnitY(unit)}
    end
    local function fakeCreate(varName, suffix)
        local getX=_G[varName.."X"]
        local getY=_G[varName.."Y"]
        _G[varName..(suffix or "Loc")]=function() return {getX(), getY()} end
    end
    fakeCreate("GetOrderPoint")
    fakeCreate("GetSpellTarget")
    fakeCreate("CameraSetupGetDestPosition")
    fakeCreate("GetCameraTargetPosition")
    fakeCreate("GetCameraEyePosition")
    fakeCreate("BlzGetTriggerPlayerMouse", "Position")
    fakeCreate("GetStartLocation")

    --Actually needs to create a location for a brief moment, as there is no GetUnitRallyX/Y
    function GetUnitRallyPoint(unit)
        local removeThis = oldRally(unit)
        local loc = {oldGetX(removeThis), oldGetY(removeThis)}
        oldRemove(removeThis)
        return loc
    end
    do local old = BlzSetSpecialEffectPositionLoc
        BlzSetSpecialEffectPositionLoc = function(effect, loc)
            oldMoveLoc(location, loc[1], loc[2])
            old(effect, location)
        end
    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 = _G[newVarName]
        local func
        if index==1 then
            func=function(loc, ...)
                return new(loc[1], loc[2], ...)
            end
        elseif index==2 then
            func=function(a, loc, ...)
                return new(a, loc[1], loc[2], ...)
            end
        else--index==3
            func=function(a, b, loc, ...)
                return new(a, b, loc[1], loc[2], ...)
            end
        end
        _G[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)
   
    ---@param min table location
    ---@param max table location
    ---@return rect newRect
    RectFromLoc = function(min, max)
        return Rect(min[1], min[2], max[1], max[2])
    end
    ---@param whichRect rect
    ---@param min table location
    ---@param max table location
    SetRectFromLoc = function(whichRect, min, max)
        SetRect(whichRect, min[1], min[2], max[1], max[2])
    end
end)
end


Lua:
do --[[
    GUI hashtable converter version 1.1 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).
    Be sure to include the GUI Enhancer Collection to ensure that the object references don't create
    memory leaks over time: https://github.com/BribeFromTheHive/Lua-Core/blob/main/GUI%20Enhancer%20Collection.lua
]]
    --GUI typecasting yo.
    function StringHashBJ(s) return s end
    function GetHandleIdBJ(id) return id end
   
    if OnLibraryInit then OnLibraryInit("GlobalRemapArray", function()
        local hashes = {}
        GlobalRemapArray("udg_HashTableArray", function(index)
            local val = hashes[index]
            if not val then
                val = HashTable.create()
                hashes[index] = val
            end
            return val
        end)
    end) end
   
    function InitHashtableBJ() return HashTable.create() end
   
    local function saveInto(value, childKey, parentKey, whichHashTable)
        whichHashTable[parentKey][childKey] = value
    end
   
    local function loadFrom(childKey, parentKey, whichHashTable, default)
        local val = whichHashTable[parentKey][childKey]
        if default and val == nil then
            return default
        end
        return val
    end
   
    SaveIntegerBJ = saveInto
    SaveRealBJ = saveInto
    SaveBooleanBJ = saveInto
    SaveStringBJ = saveInto
   
    local loadNumber = function(childKey, parentKey, whichHashTable) return loadFrom(childKey, parentKey, whichHashTable, 0) end
   
    LoadIntegerBJ = loadNumber
    LoadRealBJ = loadNumber
    LoadBooleanBJ = function(childKey, parentKey, whichHashTable) return loadFrom(childKey, parentKey, whichHashTable, false) end
    LoadStringBJ = function(childKey, parentKey, whichHashTable) return loadFrom(childKey, parentKey, whichHashTable, "") end
   
    local sub = string.sub
   
    for key in pairs(_G) do
        if sub(key, -8)=="HandleBJ" then
            local str=sub(key, 1,4)
            if str=="Save" then     _G[key] = saveInto
            elseif str=="Load" then _G[key] = loadFrom end
        end
    end
   
    function HaveSavedValue(childKey, _, parentKey, whichHashTable)
        return whichHashTable[parentKey][childKey] ~= nil
    end
   
    FlushParentHashtableBJ = HashTable.flush
    function FlushChildHashtableBJ(whichHashTable, parentKey)
        whichHashTable[parentKey].flush()
    end
end
 

Attachments

  • Location Leak Test.zip
    91.6 KB · Views: 25

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Okay, I've finally cracked it! And this is getting to be monolithic. I may end up combining a bunch of my GUI stuff to make this whole thing less painful to import, as it needs 6 separate (but similar) systems to run.

Convert Unit Groups to tables:
Lua:
do
    local mainGroup
    local oldClear=GroupClear
    local oldAdd=GroupAddUnit
    local oldFirst=FirstOfGroup
    local oldRemove=GroupRemoveUnit

OnMainInit(function()
    mainGroup = CreateGroup()
    CreateGroup=function() return {contains={}, list={}} end
    DestroyGroup=DoNothing
    function GroupClear(group)
        for i,val in ipairs(group.list) do
            group.contains[val]=nil
            group.list[i]=nil
        end
    end
    function GroupAddUnit(group, unit)
        if not group.contains[unit] then
            local pos = #group.list+1
            group.contains[unit]=pos
            group.list[pos]=unit
        end
    end
    function GroupRemoveUnit(group, unit)
        local pos = group.contains[unit]
        if pos then
            table.remove(group.list, pos)
            group.contains[unit]=nil
        end
    end
    function IsUnitInGroup(unit, group)
        return group.contains[unit]
    end
    function FirstOfGroup(group)
        return group.list[1]
    end
    function BlzGroupUnitAt(group, index)
        return group.list[index]
    end

    local enumUnit
    GetEnumUnit=function() return enumUnit end

    local function loopGroup(group, code)
        for _,unit in ipairs(group.list) do
            code(unit)
        end
    end

    ForGroup = function(group, code)
        local old = enumUnit
        loopGroup(group, function(unit)
            enumUnit=unit
            code()
        end)
        enumUnit=old
    end

    --The final embers of the once-acclaimed FirstOfGroup loop.
    local function funnelEnum(group)
        GroupClear(group)
        local unit = oldFirst(mainGroup)
        while unit do
            oldRemove(mainGroup, unit)
            GroupAddUnit(group, unit)
            unit = oldFirst(mainGroup)
        end
    end

    local function hookEnum(varStr)
        local old=_G[varStr]
        _G[varStr]=function(group, ...)
            old(mainGroup, ...)
            funnelEnum(group)
        end
    end
    hookEnum("GroupEnumUnitsOfType")
    hookEnum("GroupEnumUnitsOfPlayer")
    hookEnum("GroupEnumUnitsOfTypeCounted")
    hookEnum("GroupEnumUnitsInRect")
    hookEnum("GroupEnumUnitsInRectCounted")
    hookEnum("GroupEnumUnitsInRange")
    hookEnum("GroupEnumUnitsInRangeOfLoc")
    hookEnum("GroupEnumUnitsInRangeCounted")
    hookEnum("GroupEnumUnitsInRangeOfLocCounted")
    hookEnum("GroupEnumUnitsSelected")
    
    local function hookOrder(varName)
        local new = _G["Issue"..varName]
        _G["Group"..varName]=function(group, ...)
            for _,unit in ipairs(group.list) do
                new(unit, ...)
            end
        end
    end
    hookOrder("ImmediateOrder")
    hookOrder("ImmediateOrderById")
    hookOrder("PointOrder")
    hookOrder("PointOrderById")
    hookOrder("TargetOrder")
    hookOrder("TargetOrderById")

    BlzGroupGetSize=function(group) return #group.list end
    
    function GroupAddGroup(group, add)
        loopGroup(add, function(unit)
            GroupAddUnit(group, unit)
        end)
    end
    function GroupRemoveGroup(group, remove)
        loopGroup(remove, function(unit)
            GroupRemoveUnit(group, unit)
        end)
    end

    GroupPickRandomUnit=function(group)
        return group.list[GetRandomInt(1,#group.list)]
    end
    IsUnitGroupEmptyBJ=function(group)
        return #group.list == 0
    end
    
    ForGroupBJ=ForGroup
    CountUnitsInGroup=BlzGroupGetSize
    BlzGroupAddGroupFast=GroupAddGroup
    BlzGroupRemoveGroupFast=GroupRemoveGroup
    GroupPickRandomUnitEnum=nil
    CountUnitsInGroupEnum=nil
    GroupAddGroupEnum=nil
    GroupRemoveGroupEnum=nil
end)
end

Player Groups:
Lua:
do
    local mainForce
    local oldClear=ForceClear
    local oldAdd=ForceAddPlayer

OnMainInit(function()
    mainForce = CreateForce()
    CreateForce=function() return {contains={}, list={}} end
    DestroyForce=DoNothing
    function ForceClear(force)
        for i,val in ipairs(force.list) do
            force.contains[val]=nil
            force.list[i]=nil
        end
    end
    function ForceAddPlayer(force, player)
        if not force.contains[player] then
            local pos = #force.list+1
            force.contains[player]=pos
            force.list[pos]=player
        end
    end
    function ForceRemovePlayer(force, player)
        local pos = force.contains[player]
        if pos then
            table.remove(force.list, pos)
            force.contains[player]=nil
        end
    end
    function BlzForceHasPlayer(force, player)
        return force.contains[player]
    end
    function IsPlayerInForce(player, force)
        return force.contains[player]
    end
    function IsUnitInForce(unit, force)
        return force.contains[GetOwningPlayer(unit)]
    end
    local oldCripple = CripplePlayer
    CripplePlayer = function(player,force,flag)
        for _,val in ipairs(force.list) do
            oldAdd(mainForce, val)
        end
        oldCripple(player, mainForce, flag)
        oldClear(mainForce)
    end

    local enumPlayer
    local oldForForce = ForForce
    local oldEnumPlayer = GetEnumPlayer
    GetEnumPlayer=function() return enumPlayer end

    ForForce = function(force, code)
        local old = enumPlayer
        for _,player in ipairs(force.list) do
            enumPlayer=player
            code()
        end
        enumPlayer=old
    end

    local function funnelEnum(force)
        ForceClear(force)
        oldForForce(mainForce, function()
            ForceAddPlayer(force, oldEnumPlayer)
        end)
        oldClear(mainForce)
    end
    local function hookEnum(varStr)
        local old=_G[varStr]
        _G[varStr]=function(force, ...)
            old(mainForce, ...)
            funnelEnum(force)
        end
    end
    hookEnum("ForceEnumPlayers")
    hookEnum("ForceEnumPlayersCounted")
    hookEnum("ForceEnumAllies")
    hookEnum("ForceEnumEnemies")
    CountPlayersInForceBJ=function(force) return #force.list end
    CountPlayersInForceEnum=nil
end)
end

Rects (regions in GUI):
Lua:
--Replace all Rects with Lua Tables, allowing for automatic garbage collection. One rect is used internally to support the required API.
do
    local oldWorld = GetWorldBounds
    local newWorld
    local setter = SetRect
    local remover = RemoveRect
    local getMinX=GetRectMinX
    local getMinY=GetRectMinY
    local getMaxX=GetRectMaxX
    local getMaxY=GetRectMaxY
    local unpack = table.unpack

OnMainInit(function()

    local rect = Rect(0,0,32,32)
    RemoveRect=DoNothing
    function GetRectMinX(r) return r[1] end
    function GetRectMinY(r) return r[2] end
    function GetRectMaxX(r) return r[3] end
    function GetRectMaxY(r) return r[4] end
    function Rect(...) return {...} end
    
    function SetRect(r, mix, miy, max, may)
        r[1]=mix
        r[2]=miy
        r[3]=max
        r[4]=may
    end

    function GetWorldBounds()
        if not newWorld then
            local w = oldWorld()
            newWorld = {getMinX(w),getMinY(w),getMaxX(w),getMaxY(w)}
            remover(w)
        end
        return {unpack(newWorld)}
    end
    
    function GetRectCenterX(r) return (r[1] + r[3])/2 end
    function GetRectCenterY(r) return (r[2] + r[4])/2 end

    function MoveRectTo(r, x, y)
        x = x - GetRectCenterX(r)
        y = y - GetRectCenterY(r)
        SetRect(r, r[1]+x, r[2]+y, r[3]+x, r[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 = _G[varName]
        local func
        if index==1 then
            func=function(rct, ...)
                setter(rect, unpack(rct))
                return old(rect, ...)
            end
        elseif index==2 then
            func=function(a, rct, ...)
                setter(rect, unpack(rct))
                return old(a, rect, ...)
            end
        else--index==3
            func=function(a, b, rct, ...)
                setter(rect, unpack(rct))
                return old(a, b, rect, ...)
            end
        end
        _G[varName] = func
    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)
end

Locations:
Lua:
--Replace all locations with Lua Tables, allowing for automatic garbage collection by not even using locations to begin with.

do
    local oldLoc, oldMoveLoc = Location, MoveLocation
    local oldGetX=GetLocationX
    local oldGetY=GetLocationY
    local oldGetZ=GetLocationZ
    local oldRally = GetUnitRallyPoint
    local oldRemove = RemoveLocation
    
OnMainInit(function()
    local location = oldLoc(0,0) --one single location to govern the cases where a location needs to be referenced.

    --First, the easy stuff:
    RemoveLocation = DoNothing
    function Location(x,y)
        return {x,y}
    end
    function MoveLocation(loc, x, y)
        loc[1]=x
        loc[2]=y
    end
    function GetLocationX(loc) return loc[1] end
    function GetLocationY(loc) return loc[2] end
    function GetLocationZ(loc)
        oldMoveLoc(location, loc[1], loc[2])
        return oldGetZ(loc)
    end
    function GetUnitLoc(unit)
        return {GetUnitX(unit), GetUnitY(unit)}
    end
    local function fakeCreate(varName, suffix)
        local getX=_G[varName.."X"]
        local getY=_G[varName.."Y"]
        _G[varName..(suffix or "Loc")]=function() return {getX(), getY()} end
    end
    fakeCreate("GetOrderPoint")
    fakeCreate("GetSpellTarget")
    fakeCreate("CameraSetupGetDestPosition")
    fakeCreate("GetCameraTargetPosition")
    fakeCreate("GetCameraEyePosition")
    fakeCreate("BlzGetTriggerPlayerMouse", "Position")
    fakeCreate("GetStartLocation")

    --Actually needs to create a location for a brief moment, as there is no GetUnitRallyX/Y
    function GetUnitRallyPoint(unit)
        local removeThis = oldRally(unit)
        local loc = {oldGetX(removeThis), oldGetY(removeThis)}
        oldRemove(removeThis)
        return loc
    end
    do local old = BlzSetSpecialEffectPositionLoc
        BlzSetSpecialEffectPositionLoc = function(effect, loc)
            oldMoveLoc(location, loc[1], loc[2])
            old(effect, location)
        end
    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 = _G[newVarName]
        local func
        if index==1 then
            func=function(loc, ...)
                return new(loc[1], loc[2], ...)
            end
        elseif index==2 then
            func=function(a, loc, ...)
                return new(a, loc[1], loc[2], ...)
            end
        else--index==3
            func=function(a, b, loc, ...)
                return new(a, b, loc[1], loc[2], ...)
            end
        end
        _G[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)
    
    ---@param min table location
    ---@param max table location
    ---@return rect newRect
    RectFromLoc = function(min, max)
        return Rect(min[1], min[2], max[1], max[2])
    end
    ---@param whichRect rect
    ---@param min table location
    ---@param max table location
    SetRectFromLoc = function(whichRect, min, max)
        SetRect(whichRect, min[1], min[2], max[1], max[2])
    end
end)
end

Enable GUI hashtables to work with Tables:
Lua:
do --[[
    GUI hashtable converter version 1.2 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).
    Be sure to include the GUI Enhancer Collection to ensure that the object references don't create
    memory leaks over time: https://github.com/BribeFromTheHive/Lua-Core/blob/main/GUI%20Enhancer%20Collection.lua
]]
    --GUI typecasting yo.
    function StringHashBJ(s) return s end
    function GetHandleIdBJ(id) return id end
    local function load(whichHashTable,parentKey)
        local index = whichHashTable[parentKey]
        if not index then
            index={}
            whichHashTable[parentKey]=index
        end
        return index
    end
    if OnLibraryInit then
        OnLibraryInit("GlobalRemapArray", function()
            local hashes = {}
            GlobalRemapArray("udg_HashTableArray", function(index)
                return load(hashes, index)
            end)
        end)
    end
    
    function InitHashtableBJ() return {} end
    
    local function saveInto(value, childKey, parentKey, whichHashTable)
        load(whichHashTable, parentKey)[childKey] = value
    end
    
    local function loadFrom(childKey, parentKey, whichHashTable, default)
        local val = load(whichHashTable, parentKey)[childKey]
        if default and val == nil then
            return default
        end
        return val
    end
    
    SaveIntegerBJ = saveInto
    SaveRealBJ = saveInto
    SaveBooleanBJ = saveInto
    SaveStringBJ = saveInto
    
    local function createDefault(default)
        return function(childKey, parentKey, whichHashTable) return loadFrom(childKey, parentKey, whichHashTable, default) end
    end
    
    local loadNumber = createDefault(0)
    LoadIntegerBJ = loadNumber
    LoadRealBJ = loadNumber
    LoadBooleanBJ = createDefault(false)
    LoadStringBJ = createDefault("")
    
    do
        local sub = string.sub
        for key in pairs(_G) do
            if sub(key, -8)=="HandleBJ" then
                local str=sub(key, 1,4)
                if str=="Save" then     _G[key] = saveInto
                elseif str=="Load" then _G[key] = loadFrom end
            end
        end
    end
    
    function HaveSavedValue(childKey, _, parentKey, whichHashTable)
        return load(whichHashTable, parentKey)[childKey] ~= nil
    end
    
    FlushParentHashtableBJ = function(whichHashTable)
        for key in pairs(whichHashTable) do
            whichHashTable[key]=nil
        end
    end
    function FlushChildHashtableBJ(parentKey, whichHashTable)
        whichHashTable[parentKey]=nil
    end
end

GUI Fixer Collection:
Lua:
--The GUI Enhancer Collection: a HiveWorkshop community effort to improve the Trigger Editor experience.
--Updated: 25 Sep 2022

GetForceOfPlayer=function(player)
    --No longer leaks. There was no reason to dynamically create forces to begin with.
    return bj_FORCE_PLAYER[GetPlayerId(player)]
end

--Blizzard forgot to add this, but still enabled it for GUI. Therefore, I've extracted the code from ASCII/DebugIdInteger2IdString
function BlzFourCC2S(value)
    local result = ""
    for _=1,4 do
        result = string.char(value %% 256) .. result --crazy that World Editor needs double percentage symbols
        value = value // 256
    end
    return result
end

function TriggerRegisterDestDeathInRegionEvent(trig, r)
    --Removes the limit on the number of destructables that can be registered.
    EnumDestructablesInRect(r, nil, function() TriggerRegisterDeathEvent(trig, GetEnumDestructable()) end)
end
function IsUnitDeadBJ(u)  return not UnitAlive(u) end
function IsUnitAliveBJ(u) return     UnitAlive(u) end --use the reliable native instead of the life checks

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 OnLibraryInit then OnLibraryInit("GlobalRemap", function()
    GlobalRemap("udg_INFINITE_LOOP", function() return -1 end) --a readonly variable for infinite looping in GUI.
    
end) end

do
    local cache={}
    function Trig2Func(whichTrig)
        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
    --[[-----------------------------------------------------------------------------------------
    __jarray expander 1.2 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.
    -------------------------------------------------------------------------------------------]]
    local mts,cleaner = {},{__mode="k"}
    --without the cleaner variable, tables with non-nilled values pointing to dynamic objects will never be garbage collected.

    ---Re-define __jarray.
    ---@param default? any
    ---@param tab? table
    ---@return table
    __jarray=function(default, tab)
        local mt
        if default then
            mt=mts[default]
            if not mt then
                mt={__index=function() return default end, __mode="k"}
                mts[default]=mt
            end
        else
            mt=cleaner
        end
        return setmetatable(tab or {}, mt)
    end
    --have to do a wide search for all arrays in the variable editor. The WarCraft 3 _G 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(_G) 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 LoopJArray(whichTable, func)
        for i=rawget(whichTable, 0)~=nil and 0 or 1, #whichTable do
            func(i, rawget(whichTable, i))
        end
    end
end

do
--[[---------------------------------------------------------------------------------------------
  
    RegisterAnyPlayerUnitEvent v1.2.0.0 by Bribe
    
    RegisterAnyPlayerUnitEvent cuts down on handle count for alread-registered events, plus has
    this 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
    
    function RegisterAnyPlayerUnitEvent(event, userFunc, skip)
        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
    function TriggerRegisterAnyUnitEventBJ(trig, event)
        local removeFunc=RegisterAnyPlayerUnitEvent(event, Trig2Func(trig))
        if GlobalRemap then
            if not trigFuncs then
                trigFuncs={}
                --requires https://github.com/BribeFromTheHive/Lua-Core/blob/main/Global%20Variable%20Remapper.lua
                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)
    (whichStat==bj_HEROSTAT_STR and SetHeroStr or whichStat==bj_HEROSTAT_AGI and SetHeroAgi or SetHeroInt)(whichHero, value, true)
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

--The next addition comes from HerlySQR:
StringIdentity                          = GetLocalizedString
GetEntireMapRect                        = GetWorldBounds
GetHandleIdBJ                           = GetHandleId
StringHashBJ                            = StringHash
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
UnitRemoveBuffBJ                        = UnitRemoveAbility -- The buffs are abilities
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
GroupTrainOrderByIdBJ                   = GroupImmediateOrderById -- 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
TimerDialogDisplayBJ                    = TimerDialogDisplay
LeaderboardSetStyleBJ                   = LeaderboardSetStyle
LeaderboardGetItemCountBJ               = LeaderboardGetItemCount
LeaderboardHasPlayerItemBJ              = LeaderboardHasPlayerItem
DestroyLeaderboardBJ                    = DestroyLeaderboard
LeaderboardDisplayBJ                    = LeaderboardDisplay
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
FlushParentHashtableBJ                  = FlushParentHashtable
SaveGameCheckPointBJ                    = SaveGameCheckpoint
LoadGameBJ                              = LoadGame
RenameSaveDirectoryBJ                   = RenameSaveDirectory
RemoveSaveDirectoryBJ                   = RemoveSaveDirectory
CopySaveGameBJ                          = CopySaveGame
IssueTargetOrderBJ                      = IssueTargetOrder
IssuePointOrderLocBJ                    = IssuePointOrderLoc
IssueTargetDestructableOrder            = IssueTargetOrder -- This was just to type casting
IssueTargetItemOrder                    = IssueTargetOrder -- This was just to type casting
IssueImmediateOrderBJ                   = IssueImmediateOrder
GroupTargetOrderBJ                      = GroupTargetOrder
GroupPointOrderLocBJ                    = GroupPointOrderLoc
GroupImmediateOrderBJ                   = GroupImmediateOrder
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

-- end of GUI Enhancer Collection
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Perfect! Thank you very much Bribe. Rest assured that this kind of effort will be very useful. Even though we know it's much more effective for Singleplayer only... this without a doubt, it already solves the problems of half the community. Keep innovating!
This should not have issues with multiplayer, unlike what had been reported in some cases with Dr. SuperGood's resource. This one is just using Lua tables, which means that even if the garbage collector runs asynchronously, there is no net traffic created by this code at that time.

Of course, it should be tested (as should anything), but "using Lua" doesn't cause desyncs. That's all these resource are: just transforming GUI framework to be "typecasting" using Lua's dynamic objects instead, which are way more modern than the WarCraft 3 game engine itself (oddly enough).
 
Level 4
Joined
May 19, 2020
Messages
319
This should not have issues with multiplayer, unlike what had been reported in some cases with Dr. SuperGood's resource. This one is just using Lua tables, which means that even if the garbage collector runs asynchronously, there is no net traffic created by this code at that time.

Of course, it should be tested (as should anything), but "using Lua" doesn't cause desyncs. That's all these resource are: just transforming GUI framework to be "typecasting" using Lua's dynamic objects instead, which are way more modern than the WarCraft 3 game engine itself (oddly enough).
Interesting to know, because from everything I've read, this collector would cause conflicts in undesirable removals, but it seems that the LUA is a triumphant reality for the current Wc3 modders. Lol
Very daring to counter (positively) a system of Dr. SuperGood's and with such effectiveness... Jokes aside, congrats to job in the LUA!

Now a doubt. I'm illiterate outside of the GUI and I'm a curious one, terrible at vJASS... What I imagine here may not have any scope for such possibilities... But in your experience, I would very much like to know if the LUA system is so in-depth and powerful like that, if it would be able to modify "Raw codes" of the limited Wc3 mechanics, even for singleplayer custom maps?
In carrying out solutions to internal number issues fully inherent to the game engine, such as these situations:
Situation A:
Situation B:
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I'm not sure about your attacking unit thing, but unit collision sizes and attack ranges will be the same whether the map is JASS or Lua.

Regarding "one-upping" DSG's resource, keep in mind that he wrote that 3 years ago, and it's really Blizzard's fault that there are any problems with it (rather than his).
 
Level 4
Joined
May 19, 2020
Messages
319
Yes I understand. Certainly in the work of @Dr Super Good (my congrats) there are no negligences ... in fact, it was LUA himself who became more efficient in dealing with Blizzard's problems.

I know it's the same reading of the default data between any of the systems, but I thought the LUA code would have a possibility to access this kind of mechanical change more easily.
Many data, in the OE it is possible to change it, but mechanically it is restricted. Well then, I realized that in the same way that there is a minimum Collision, which is not the value "1", it must be something close to "8" , there is also a minimum Attack Range, which would be something around the value "32" and in the Movement- Group Separation, the minimum standard distance is still much higher, something around 128. The limit of these indexes that I was looking to overcome. lol
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,196
This one is just using Lua tables, which means that even if the garbage collector runs asynchronously, there is no net traffic created by this code at that time.
The issue was not net traffic, but rather that the checksums did not match up so even if the game could remain in sync, it caused a desync. Of course running garbage collection at different times would cause objects to recycling handle IDs at different times on different clients which would result in a proper desync but that was what the people at Blizzard were meant to have solved...

Let us not forget the plan to make a native garbage collector. But that also never happened...
 
Level 19
Joined
Jan 3, 2022
Messages
320
This is a great idea. I hooked all those create/destroy functions before to see if anything explicitly leaked, but didn't realize it's possible to avoid touching game's data structures altogether. Now I wonder if the lag is related to hashtables in the map I'm working on.
@maddeem Do you want to present your table stuff here?

I'd not recommend replacing the BJ functions, in case they ever add new internal bj_ variables there. On the other hand, if they update anything, that'd break this library because it's so invasive. It really must be kept up-to-date at all times with blizzard.j and common.j
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
This is a great idea. I hooked all those create/destroy functions before to see if anything explicitly leaked, but didn't realize it's possible to avoid touching game's data structures altogether. Now I wonder if the lag is related to hashtables in the map I'm working on.
@maddeem Do you want to present your table stuff here?

I'd not recommend replacing the BJ functions, in case they ever add new internal bj_ variables there. On the other hand, if they update anything, that'd break this library because it's so invasive. It really must be kept up-to-date at all times with blizzard.j and common.j
Yeah, I would HOPE it will need maintaining over the years ahead, but preferably the game is updated at one point in the future that this thing becomes wholly obsolete. Only time will tell.

I know @maddeem is big on the whole "table recycling" thing, but I would need hard evidence of a real world scenario where it would be preferable. From my understanding, their tests were dealing with hundreds of thousands of Lua objects being created every 0.01 seconds. Bear in mind that no one ever used repeating 0.01 second timers in any actual system. The common one is 0.03125 (Timer32) and the second most common was 0.025 (XE).

Honestly, the fact that Lua tables instead of WarCraft 3 locations are using such a massively tiny amount of resources tells me that there is absolutely bizarre stuff happening within the game engine that clogs up loads of memory for no apparent reason.
 
Table recycling is probably more trouble than it's worth. You can't pairs iterate to free the table keys because if those keys contain war3 handled objects it will desync. That means you have to create a metatable and an entry and lookup table per table that isn't an array.

It is also more error prone and slower. I only continue with the practice because it's an absolute requirement if you are doing path finding algorithms. You just create far too many tables and memory can very quickly get out of control.
 
Level 9
Joined
Jul 20, 2018
Messages
176
One additional note about unit groups. According to my tests, if unit is removed or decayed (same thing), ForGroup does not iterate on it and IsUnitInGroup returns false. So, in old patches (before introduction of BlzGroupGetSize) it was considered that non-existing units are removed from groups automatically (actually they are not), and someone could rely on this. I did not notice this "feature" here.

To implement this behavior next functions can be used inside ForGroup and IsUnitInGroup:
Lua:
function UnitExists takes unit u returns boolean
    return GetUnitTypeId(u) != 0
endfunction

function UnitDoesNotExist takes unit u returns boolean
    return GetUnitTypeId(u) == 0
endfunction

Their behavior is consistent with behavior of natives ForGroup and IsUnitInGroup.

I suggest to add a flag to support this feature, if someone need it.
 

Attachments

  • AutomatedUnitRemovalFromGroup.w3x
    11.1 KB · Views: 5
  • AlivenessTest.w3x
    9.3 KB · Views: 4

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Hi, sorry I missed this yesterday. I did have this in an earlier draft of ForGroup (though not IsUnitInGroup) and decided against it for most cases. What I intend to do is instead add an option that checks if UnitEvent exists and then modify the code so that the groups automatically expel it on Unit removal. @Eikonium 's [Lua] - Set/Group Datastructure already implements this behavior, so the concept is sound. Only issue is that his doesn't override the original natives as it is only geared towards Lua coders.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Status
Not open for further replies.
Top