• 🏆 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] How can I increase the performance of this system?

Status
Not open for further replies.
Level 24
Joined
Jun 26, 2020
Messages
1,852
Hello, I was creating a map with a guy, and we were testing, I made this system for spawning creeps, but the map lags a lot and it get worse, but just for me and not for his or another person, I know the problem is this system because the memory starts too increase a lot when is running, and I know it doesn't have memory leaks because the memory used decreases eventually (the function Update is the main part):
Lua:
OnInit(function ()
    Require "Timed"
    Require "LinkedList"
    Require "Set"
    Require "AbilityUtils"
    Require "Vec2"
    Require "SyncedTable"
    Require "Digimon Capture"

    local CREEPS_PER_PLAYER     ---@type integer
    local CREEPS_PER_REGION     ---@type integer
    local LIFE_SPAN             ---@type number
    local LIFE_REDUCED          ---@type number
    local DELAY_SPAWN           ---@type number
    local DELAY_NORMAL          ---@type number
    local DELAY_DEATH           ---@type number
    local RANGE_LEVEL_1         ---@type number
    local RANGE_LEVEL_2         ---@type number
    local RANGE_RETURN          ---@type number
    local RANGE_IN_HOME         ---@type number
    local NEIGHBOURHOOD         ---@type number
    local INTERVAL              ---@type number
    local CHANCE_UNCOMMON       ---@type integer
    local CHANCE_RARE           ---@type integer
    local CHANCE_LEGENDARY      ---@type integer

    ---@class Creep : Digimon
    ---@field remaining number
    ---@field captured boolean
    ---@field reduced boolean
    ---@field returning boolean
    ---@field spawnpoint Vec2
    ---@field rd RegionData

    ---@class RegionData
    ---@field rectID table
    ---@field spawnpoint Vec2
    ---@field types unitpool
    ---@field inDay boolean
    ---@field inNight boolean
    ---@field minLevel integer
    ---@field maxLevel integer
    ---@field inregion boolean
    ---@field delay number
    ---@field waitToSpawn number
    ---@field creeps Creep[]
    ---@field neighbourhood Set
    ---@field sameRegion Set
    ---@field checked boolean

    ---@param pool unitpool
    ---@param pos Vec2
    ---@return Creep
    local function CreateCreep(pool, pos)
        local creep = Digimon.add(PlaceRandomUnit(pool, Digimon.NEUTRAL, pos.x, pos.y, bj_UNIT_FACING)) ---@type Creep

        creep.captured = false
        creep.reduced = false
        creep.returning = false
        creep.remaining = LIFE_SPAN
        creep.spawnpoint = pos

        return creep
    end

    -- The system

    local All = LinkedList.create()

    ---@param types Creep[]
    ---@return unitpool
    local function GenerateCreepPool(types)
        local pool = CreateUnitPool()

        local commons = {}
        local uncommons = {}
        local rares = {}
        local legendaries = {}

        for i = 1, #types do
            local id = types[i]
            local rarity = Digimon.getRarity(id)

            if rarity == Rarity.COMMON then
                table.insert(commons, id)
            elseif rarity == Rarity.UNCOMMON then
                table.insert(uncommons, id)
            elseif rarity == Rarity.RARE then
                table.insert(rares, id)
            elseif rarity == Rarity.LEGENDARY then
                table.insert(legendaries, id)
            end
        end

        local chanceCommon = 1
        if #legendaries > 0 then
            chanceCommon = chanceCommon - CHANCE_LEGENDARY
            for _, v in ipairs(legendaries) do
                UnitPoolAddUnitType(pool, v, CHANCE_LEGENDARY/#legendaries)
            end
        end
        if #rares > 0 then
            chanceCommon = chanceCommon - CHANCE_RARE
            for _, v in ipairs(rares) do
                UnitPoolAddUnitType(pool, v, CHANCE_RARE/#rares)
            end
        end
        if #uncommons > 0 then
            chanceCommon = chanceCommon - CHANCE_UNCOMMON
            for _, v in ipairs(uncommons) do
                UnitPoolAddUnitType(pool, v, CHANCE_UNCOMMON/#uncommons)
            end
        end
        for _, v in ipairs(commons) do
            UnitPoolAddUnitType(pool, v, chanceCommon/#commons)
        end

        return pool
    end

    ---@param re rect
    ---@param types integer[]
    ---@param inDay boolean
    ---@param inNight boolean
    ---@param minLevel integer
    ---@param maxLevel integer
    ---@return RegionData
    local function Create(re, types, inDay, inNight, minLevel, maxLevel)
        local x, y = GetRectCenterX(re), GetRectCenterY(re)
        local this = { ---@type RegionData
            rectID = re,
            spawnpoint = Vec2.new(x, y),
            types = GenerateCreepPool(types),
            inDay = inDay,
            inNight = inNight,
            minLevel = minLevel,
            maxLevel = maxLevel,

            inregion = false,
            delay = 0.,
            waitToSpawn = 0.,
            creeps = {},
            neighbourhood = Set.create(),
            sameRegion = Set.create()
        }

        All:insert(this)

        for node in All:loop() do
            local r = node.value ---@type RegionData
            if DistanceBetweenCoords(x, y, r.spawnpoint.x, r.spawnpoint.y) <= NEIGHBOURHOOD then
                this.neighbourhood:addSingle(r)
                r.neighbourhood:addSingle(this)
                if this.rectID == r.rectID then
                    this.sameRegion:addSingle(r)
                    r.sameRegion:addSingle(this)
                end
            end
        end

        return this
    end

    local list = nil ---@type RegionData[]

    ---Returns a random neighbour that didn't reach its limit and is not in cooldown, if there is not, then return nil
    ---@param r RegionData
    ---@param quantity integer
    ---@return RegionData
    local function GetFreeNeighbour(r, quantity)
        list = {}

        for n in r.neighbourhood:elements() do
            if #n.creeps < quantity and n.waitToSpawn <= 0. and n.delay <= 0
                and ((n.inDay and GetTimeOfDay() >= bj_TOD_DAWN and GetTimeOfDay() < bj_TOD_DUSK)
                or (n.inNight and (GetTimeOfDay() < bj_TOD_DAWN or GetTimeOfDay() >= bj_TOD_DUSK))) then

                table.insert(list, n)
            end
        end

        if #list > 0 then
            return list[math.random(#list)]
        end
    end

    ---Returns a random integer between min and max
    ---but has more chance to get a closer integer to lvl
    ---@param lvl integer
    ---@param min integer
    ---@param max integer
    ---@return integer
    local function GetProccessedLevel(lvl, min, max)
        if min >= max then
            return min
        end

        local weights = {}
        local maxWeight = 0

        for x = min, max do
            local weight = 1/(1+(x-lvl)^2)
            maxWeight = maxWeight + weight
            weights[x] = maxWeight
        end

        local r = maxWeight * math.random()
        local l = min
        for x = min, max-1 do
            if r > weights[x] then
                l = l + 1
            else
                break
            end
        end

        return l
    end

    local PlayersInRegion = Set.create()

    local function Update()
        for node in All:loop() do
            local regionData = node.value ---@type RegionData
            -- Check if the unit nearby the spawn region belongs to a player
            regionData.inregion = false
            local lvl = 1
            ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_1, function (u)
                if GetPlayerController(GetOwningPlayer(u)) == MAP_CONTROL_USER then
                    regionData.someoneClose = true
                    regionData.inregion = true
                    PlayersInRegion:addSingle(GetOwningPlayer(u))
                    lvl = math.max(lvl, GetHeroLevel(u))
                end
            end)
            -- Control the creep or the spawn
            if regionData.inregion then
                regionData.delay = regionData.delay - INTERVAL
                regionData.waitToSpawn = regionData.waitToSpawn - INTERVAL
                if regionData.delay <= 0. then
                    -- Spawn per neighbourhood instead per region
                    local r = GetFreeNeighbour(regionData, math.min(CREEPS_PER_REGION, CREEPS_PER_PLAYER * PlayersInRegion:size())) -- If don't have neighbours, then just use the same region
                    if r then
                        local creep = CreateCreep(r.types, r.spawnpoint)
                        creep:setLevel(GetProccessedLevel(lvl, r.minLevel, r.maxLevel))
                        creep.rd = regionData
                        for r2 in r.sameRegion:elements() do
                            table.insert(r2.creeps, creep)
                        end
                        -- They share the same delay
                        for n in regionData.neighbourhood:elements() do
                            n.waitToSpawn = math.max(DELAY_SPAWN, n.waitToSpawn)
                        end
                    end
                end
            else
                -- Check if a unit is still nearby the spawn region
                regionData.someoneClose = false
                ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_2, function (u)
                    if not regionData.someoneClose and GetPlayerController(GetOwningPlayer(u)) == MAP_CONTROL_USER then
                        regionData.someoneClose = true
                    end
                end)
                for _, creep in ipairs(regionData.creeps) do
                    if creep.rd == regionData then
                        creep.remaining = creep.remaining - INTERVAL

                        --If there is no nearby unit in the RANGE_LEVEL_2 then reduce once the duration
                        if not regionData.someoneClose and not creep.reduced then
                            creep.remaining = creep.remaining - LIFE_REDUCED
                            creep.reduced = true
                        end
                    end
                end
                regionData.delay = math.max(regionData.delay, DELAY_NORMAL)
            end

            for i = #regionData.creeps, 1, -1 do
                local creep = regionData.creeps[i] ---@type Creep
                if creep.rd == regionData then
                    local distance = creep.spawnpoint:dist(creep:getPos())
                    if distance > RANGE_RETURN then
                        creep:issueOrder(Orders.move, creep.spawnpoint.x, creep.spawnpoint.y)
                        creep.returning = true
                    end
                    if distance <= RANGE_IN_HOME then
                        creep.returning = false
                    end
                    if creep.captured or creep.remaining <= 0. then
                        if creep.remaining <= 0. then
                            regionData.delay = DELAY_NORMAL
                            creep:destroy()
                        elseif creep.captured  then
                            regionData.delay = DELAY_DEATH
                        end
                        for r2 in regionData.sameRegion:elements() do
                            table.remove(r2.creeps, i)
                        end
                    end
                end
            end
            PlayersInRegion:clear()
        end
    end

    OnInit.trig(function ()
        TriggerExecute(gg_trg_Creep_Spawn_System_Config)

        CREEPS_PER_PLAYER = udg_CREEPS_PER_PLAYER
        CREEPS_PER_REGION = udg_CREEPS_PER_REGION
        LIFE_SPAN = udg_LIFE_SPAN
        LIFE_REDUCED = udg_LIFE_REDUCED
        DELAY_SPAWN = udg_DELAY_SPAWN
        DELAY_NORMAL = udg_DELAY_NORMAL
        DELAY_DEATH = udg_DELAY_DEATH
        RANGE_LEVEL_1 = udg_RANGE_LEVEL_1
        RANGE_LEVEL_2 = udg_RANGE_LEVEL_2
        RANGE_RETURN = udg_RANGE_RETURN
        RANGE_IN_HOME = udg_RANGE_IN_HOME
        INTERVAL = udg_SPAWN_INTERVAL
        NEIGHBOURHOOD = udg_NEIGHBOURHOOD
        CHANCE_UNCOMMON = udg_CHANCE_UNCOMMON
        CHANCE_RARE = udg_CHANCE_RARE
        CHANCE_LEGENDARY = udg_CHANCE_LEGENDARY

        -- Clear
        udg_CREEPS_PER_PLAYER = nil
        udg_CREEPS_PER_REGION = nil
        udg_LIFE_SPAN = nil
        udg_LIFE_REDUCED = nil
        udg_DELAY_SPAWN = nil
        udg_DELAY_NORMAL = nil
        udg_DELAY_DEATH = nil
        udg_RANGE_LEVEL_1 = nil
        udg_RANGE_LEVEL_2 = nil
        udg_RANGE_RETURN = nil
        udg_RANGE_IN_HOME = nil
        udg_SPAWN_INTERVAL = nil
        udg_NEIGHBOURHOOD = nil
        udg_CHANCE_UNCOMMON = nil
        udg_CHANCE_RARE = nil
        udg_CHANCE_LEGENDARY = nil

        TriggerClearActions(gg_trg_Creep_Spawn_System_Config)
        DestroyTrigger(gg_trg_Creep_Spawn_System_Config)
        gg_trg_Creep_Spawn_System_Config = nil
    end)

    OnInit.final(function ()
        Timed.echo(INTERVAL, Update)
    end)

    local function killedOrCapturedfunction(info)
        info.target.captured = true
    end
    Digimon.capturedEvent:register(killedOrCapturedfunction)
    Digimon.killEvent:register(killedOrCapturedfunction)

    Digimon.postDamageEvent:register(function (info)
        local creep = info.target ---@type Creep
        if creep.returning then
            creep:issueOrder(Orders.attack, creep.spawnpoint.x, creep.spawnpoint.y)
        end
    end)

    -- For GUI
    udg_CreepSpawnCreate = CreateTrigger()
    TriggerAddAction(udg_CreepSpawnCreate, function ()
        Create(
            udg_CreepSpawnRegion,
            udg_CreepSpawnTypes,
            udg_CreepSpawnInDay,
            udg_CreepSpawnInNight,
            udg_CreepSpawnMinLevel,
            udg_CreepSpawnMaxLevel)
        udg_CreepSpawnRegion = nil
        udg_CreepSpawnTypes = __jarray(0)
        udg_CreepSpawnInDay = true
        udg_CreepSpawnInNight = true
        udg_CreepSpawnMinLevel = 1
        udg_CreepSpawnMaxLevel = 1
    end)
end)
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
What's the value of udg_SPAWN_INTERVAL? How come it gets set to nil?
Is a variable that I set in GUI, and then stored in a local variable in the system:
1671744990010.png

Lua:
INTERVAL = udg_SPAWN_INTERVAL
Is All a huge table?
Is a Linked List, and is big as I want, because I have to add one per each region I wanna spawn creeps, I think until know I have at least 270 elements.
Maybe setting local regionData on a fast timer for a huge table is filling up memory before a garbage collection cycle
I don't get you.
 
Level 19
Joined
Jan 3, 2022
Messages
320
I don't get you.
What he says is that you overload the garbage collector by creating too much garbage on heap. Garbage includes: new/resized tables, new functions, very rapid string manipulation, (not sure) handles (aka userdata in Lua).

If you say Update is the main function then you have to look there:
ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_1, function (u) -- new function

ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_2, function (u) -- again
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
What he says is that you overload the garbage collector by creating too much garbage on heap. Garbage includes: new/resized tables, new functions, very rapid string manipulation, (not sure) handles (aka userdata in Lua).

If you say Update is the main function then you have to look there:
ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_1, function (u) -- new function

ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_2, function (u) -- again
So I have to increase the update interval?
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Ok I did this:

Lua:
local regionData, lvl
local function check(u)
    if GetPlayerController(GetOwningPlayer(u)) == MAP_CONTROL_USER then
        regionData.someoneClose = true
        regionData.inregion = true
        PlayersInRegion:addSingle(GetOwningPlayer(u))
        lvl = math.max(lvl, GetHeroLevel(u))
    end
end

local function Update()
    for node in All:loop() do
        regionData = node.value ---@type RegionData
        -- Check if the unit nearby the spawn region belongs to a player
        regionData.inregion = false
        lvl = 1
        ForUnitsInRange(regionData.spawnpoint.x, regionData.spawnpoint.y, RANGE_LEVEL_1, check)
Before kinda easily the memory reached 10MB and now with a bit more effort reaches 8MB, what else I can do?
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Now I have to add a system that make a unit of certain unit-type be able to do something only in day or night, and the ideas I'm thinking are not efficient, basically is checking with the game time of day event for every unit of those types.
 
Level 19
Joined
Jan 3, 2022
Messages
320
I don't think this solved the problem, because the memory is still increasing even if I do nothing in the game and never decrease, this method only slow it a bit, what else can I do?
Nobody can tell you, because this damn GC is a blackbox. Is it a conventional handle leak /or/ a case where the script generates too much garbage for the GC to clean up? Nobody knows. To stress my intent: this is the reason I started documenting the jassdoc, I have both a desync (Lua-only) and a leak issue (Jass+Lua). At least then I'd be able to find places with leaking handles. All I can do in your case is to inspect the entire trigger/map code. Do I have time to do this? I-I-i don't know...
and the ideas I'm thinking are not efficient, basically is checking with the game time of day event for every unit of those types.
The classic case of data structures & algorithms. The first step is to track all these units in a custom table, instead of creating a new group for each iteration. The second step is to use a smart algorithm to execute as little code as possible.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,199
In worst case you can recycle tables rather than creating new ones. Have 1 global table to act as a bin for created tables and use a global function that first polls the bin if a free table is available, and only if not does it create a new one. Similar to location and group objects, tables are passed to a "free" function when no longer useful which inserts them into the global table to be recycled. The global table can have a maximum capacity past which tables are left to be claimed by the GC. All dynamic functions instead become global function calls that are passed state tables.

By reducing the amount of tables needing to be cleaned by the garbage collector, the garbage collector should be able to keep up. This sort of programming is commonly used in game engines since garbage collection is generally avoided due to causing frame time consistency issues.
 
Level 19
Joined
Jan 3, 2022
Messages
320
Sounds like a handle leak (... type of situation). The Lua side can handle large memory on heap without lagging, but if you see the game process to slowly leak memory with the performance rapidly getting worse - it's something in the game's internals.
What kinda numbers does GetHandleId return for objects? You don't have to check each one, just get an overview.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Some of my systems create handles: I use heros and I recycle them, also have a dummy caster system and a system the spawn items (also I use the @chopinski missiles system, but not sure that it has something to do), would they have something wrong?

Lua:
OnInit("HeroRecycler", function ()
    Require "LinkedList"
    Require "Timed"
    Require "AddHook"
    Require.optional "WorldBounds"

    -- System based on UnitRecycler https://www.hiveworkshop.com/threads/286701/
    -- but with heros

    -- CONFIGURATION SECTION

    -- The owner of the stocked/recycled units
    local OWNER = Player(PLAYER_NEUTRAL_PASSIVE)
    -- Determines if dead units will be automatically recycled
    -- after a delay designated by the "constant function
    -- DeathTime below"
    local AUTO_RECYCLE_DEAD = false

    --  The delay before dead units will be automatically recycled in case when AUTO_RECYCLE_DEAD == true
    local DeathTime = nil ---@type function
    if AUTO_RECYCLE_DEAD then
        function DeathTime(u)
            --[[if <condition> then
                return someValue
            elseif <condition> then
                return someValue
            endif]]
            return 8.00
        end
    end

    -- Filters units allowed for recycling
    local function UnitTypeFilter(u)
        return IsUnitType(u, UNIT_TYPE_HERO) and not IsUnitIllusion(u) and not IsUnitType(u, UNIT_TYPE_SUMMONED)
    end

    local originalScale = __jarray(1) ---@type number[]

    -- When recycling a unit back to the stock, these resets will be applied to the
    -- unit. You can add more actions to this or you can delete this module if you
    -- don't need it.
    local function HeroRecyclerResets(u)
        SetHeroXP(u, 0, false)
        SetUnitScale(u, BlzGetUnitRealField(u, UNIT_RF_SCALING_VALUE), 0., 0.)
        SetUnitVertexColor(u, 255, 255, 255, 255)
        SetUnitFlyHeight(u, GetUnitDefaultFlyHeight(u), 0)
    end

    -- END OF CONFIGURATION

    --[[ == Do not do changes below this line if you're not so sure on what you're doing == ]]--

    HeroRecycler = true

    -- Hide recycled units at the top of the map beyond reach of the camera
    local unitCampX = 0.
    local unitCampY = 0.

    if WorldBounds then
        unitCampY = WorldBounds.maxY
    else
        local bounds = GetWorldBounds()
        unitCampY = GetRectMaxY(bounds)
        RemoveRect(bounds)
    end

    local List = {} ---@type table<integer, unit[]>

    local function RecycleHeroInternal(u)
        local list = List[GetUnitTypeId(u)] or {}
        List[GetUnitTypeId(u)] = list

        table.insert(list, u)
        if UnitAlive(u) then
            SetUnitPosition(u, unitCampX, unitCampY)
        else
            ReviveHero(u, unitCampX, unitCampY, false)
        end
        for i = 0, 5 do
            if UnitItemInSlot(u, i) then
                RemoveItem(UnitItemInSlot(u, i))
            end
        end
        PauseUnit(u, true)
        ShowUnitHide(u)
        SetUnitOwner(u, OWNER, true)
        SetWidgetLife(u, GetUnitState(u, UNIT_STATE_MAX_LIFE))
        SetUnitState(u, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MAX_MANA))
        HeroRecyclerResets(u)
    end

    ---Stores the hero to use it later and return if the process was successful
    ---you can add a delay
    ---@param u unit
    ---@param delay? number
    ---@return boolean
    function RecycleHero(u, delay)
        if u and UnitTypeFilter(u) then
            if delay then
                Timed.call(delay, function () RecycleHeroInternal(u) end)
            else
                RecycleHeroInternal(u)
            end
            return true
        end
        return false
    end

    ---Returns a stored hero, if there is no a stored hero, it creates one
    ---@param owner player
    ---@param id integer
    ---@param x number
    ---@param y number
    ---@param angle number
    ---@return unit
    function GetRecycledHero(owner, id, x, y, angle)
        if IsHeroUnitId(id) then
            local list = List[id] or {}
            List[id] = list

            local u = table.remove(list)
            if not u then
                u = CreateUnit(owner, id, x, y, angle)
                originalScale[id] = BlzGetUnitRealField(u, UNIT_RF_SCALING_VALUE)
            else
                SetUnitOwner(u, owner, true)
                SetUnitPosition(u, x, y)
                BlzSetUnitFacingEx(u, angle)
                PauseUnit(u, false)
                ShowUnitShow(u)
            end
            return u
        end
        return nil
    end

    ---Pre-place a hero to use them and return if the process was successful
    ---@param id integer
    ---@return boolean added
    function HeroAddToStock(id)
        if IsHeroUnitId(id) then
            local u = CreateUnit(OWNER, id, 0.00, 0.00, 0)
            if u and UnitTypeFilter(u) then
                RecycleHero(u)
                return true
            end
        end
        return false
    end

    if AUTO_RECYCLE_DEAD then
        local t = CreateTrigger()
        for i = 0, PLAYER_NEUTRAL_PASSIVE do
            TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_DEATH, nil)
        end
        TriggerAddAction(t, function ()
            local u = GetTriggerUnit()
            if UnitTypeFilter(u) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) then
                RecycleHero(u, DeathTime(u))
            end
        end)
    end
end)
Lua:
OnInit("DummyCaster", function ()
    Require "WorldBounds"
    Require "Timed"

    -- System based on MUI DummyCaster

    -- Import the dummy from the object editor
    local DummyID = FourCC('n000')

    -- WARNING: Do not touch anything below this line!

    -- Default 3 values you may use, pick one as desired
    ---@enum CastType
    CastType = {
        IMMEDIATE = 0,
        POINT = 1,
        TARGET = 2
    }

    local Dummies = {}
    local Abilities = __jarray(0)

    local function GetDummy(player, x, y, angle)
        local dummy = table.remove(Dummies)
        if not dummy then
            dummy = CreateUnit(player, DummyID, x, y, angle)
        else
            ShowUnitShow(dummy)
            SetUnitOwner(dummy, player, false)
            SetUnitPosition(dummy, x, y)
            BlzSetUnitFacingEx(dummy, angle)
        end
        return dummy
    end

    local function RefreshDummy(dummy)
        ShowUnitHide(dummy)
        SetUnitPosition(dummy, WorldBounds.maxX, WorldBounds.maxY)
        UnitRemoveAbility(dummy, Abilities[dummy])
        Abilities[dummy] = nil
        table.insert(Dummies, dummy)
    end

    ---Casts a spell from a dummy caster, returns if the spell was successfully casted
    ---@param owner player
    ---@param x number
    ---@param y number
    ---@param abilId integer
    ---@param orderId integer
    ---@param level integer
    ---@param castType CastType
    ---@param tx? number | unit
    ---@param ty? number
    ---@return boolean
    function DummyCast(owner, x, y, abilId, orderId, level, castType, tx, ty)
        local angle = 0
        if castType == CastType.IMMEDIATE then
            if tx then
                error("Too much arguments", 2)
            end
        elseif castType == CastType.POINT then
            if not tx or not ty then
                error("You didn't set a target point", 2)
            elseif not type(tx) == "number" or not type(ty) == "number" then
                error("Invalid target", 2)
            end
            angle = math.atan(ty - y, tx - x)
        elseif castType == CastType.TARGET then
            if Wc3Type(tx) ~= "unit" then
                error("Invalid target", 2)
            end
            angle = math.atan(GetUnitX(tx) - y, GetUnitX(tx) - x)
        else
            error("Invalid target-type", 2)
        end
        local dummy = GetDummy(owner, x, y, angle)
        UnitAddAbility(dummy, abilId)
        SetUnitAbilityLevel(dummy, abilId, level)
        Abilities[dummy] = abilId
        local success = false
        if castType == CastType.IMMEDIATE then
            success = IssueImmediateOrderById(dummy, orderId)
        elseif castType == CastType.POINT then
            success = IssuePointOrderById(dummy, orderId, tx, ty)
        elseif castType == CastType.TARGET then
            success = IssueTargetOrderById(dummy, orderId, tx)
        end
        if not success then
            Timed.call(1., function ()
                RefreshDummy(dummy)
            end)
        end
        return success
    end

    local t = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_FINISH)
    TriggerAddAction(t, function ()
        if GetUnitTypeId(GetSpellAbilityUnit()) == DummyID then
            local u = GetSpellAbilityUnit()
            Timed.call(1., function ()
                RefreshDummy(u)
            end)
        end
    end)
end)
Lua:
OnInit(function ()
    Require "LinkedList"

    local INTERVAL = 15.

    local All = LinkedList.create()
    local Reference = {}

    local function Create(rects, types, maxItems)
        local new = {
            rects = rects,
            types = types,
            maxItems = maxItems,
            count = 0
        }

        All:insert(new)

        return new
    end

    local function Update()
        for node in All:loop() do
            local itemSpawn = node.value
            for _, where in ipairs(itemSpawn.rects) do
                -- Only create an item if didn't surpassed their max
                if itemSpawn.count < itemSpawn.maxItems then
                    -- Create an item in a random position of the rect
                    local m = CreateItem(itemSpawn.types[math.random(#itemSpawn.types)], GetRandomReal(GetRectMinX(where), GetRectMaxX(where)), GetRandomReal(GetRectMinY(where), GetRectMaxY(where)))
                    Reference[m] = itemSpawn
                    itemSpawn.count = itemSpawn.count + 1
                end
            end
        end
    end

    -- Discount the item when is picked
    local t = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_PICKUP_ITEM)
    TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_STACK_ITEM)
    TriggerAddAction(t, function ()
        local m
        if GetTriggerEventId() == EVENT_PLAYER_UNIT_PICKUP_ITEM then
            m = GetManipulatedItem()
        else
            m = BlzGetStackingItemSource()
        end
        local this = Reference[m]
        if this then
            Reference[m] = nil
            this.count = this.count - 1
        end
    end)
    -- Start update
    Timed.call(function ()
        Timed.echo(Update, INTERVAL)
    end)

    -- For GUI
    udg_ItemSpawnCreate = CreateTrigger()
    TriggerAddAction(udg_ItemSpawnCreate, function ()
        Create(udg_ItemSpawnRegions, udg_ItemSpawnTypes, udg_ItemSpawnMaxItems)
        udg_ItemSpawnRegions = {}
        udg_ItemSpawnTypes = {}
        udg_ItemSpawnMaxItems = 0
    end)
end)
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Try avoiding the jarray objects in case those are not garbage collected properly.

Each native object also creates a Lua wraper object that must be garbage collected. I wonder if it is possible to create enough native objects to overload the garbage collector.
I'm also using the @Bribe's Lua-Infused GUI, that I think handles that:

Lua:
--[[-----------------------------------------------------------------------------------------
__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.
-------------------------------------------------------------------------------------------]]
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
__jarray=function(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 _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 GUI.loopArray(whichTable, func)
    for i=rawget(whichTable, 0)~=nil and 0 or 1, #whichTable do
        func(i, rawget(whichTable, i))
    end
end
 
Status
Not open for further replies.
Top