• 🏆 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!

Creep spawn system lags after a while when spawn a unit

Status
Not open for further replies.
Level 24
Joined
Jun 26, 2020
Messages
1,852
Hello, I made a creep spawn system, it worked fine, but for some reason from a certain point when creates the first creep, eventually the game lags a lot, and I don't know why, I can't just erase things until I don't get the problem, because almost everything is important to the system works, I just wanna know if you can see something I can't.
This is the function that creates the creep:
Lua:
---@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

---@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
And this is the timer callback:
Lua:
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 = 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
    end
    PlayersInRegion:clear()
end
And this is the entire system:
Lua:
OnLibraryInit({name = "CreepSpawn", "Timed", "LinkedList", "Set", "AbilityUtils", "Vec2", "SyncedTable"}, function ()
    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 integer
    ---@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

    ---@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[]
    ---@return RegionData
    local function Create(re, types, inDay, inNight, minLevel, maxLevel)
        local x, y = GetRectCenterX(re), GetRectCenterY(re)
        local this = { ---@type RegionData
            rectID = GetHandleId(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 = 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
        end
        PlayersInRegion:clear()
    end

    OnMapInit(function ()
        Timed.call(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

            Timed.echo(INTERVAL, Update)

            -- 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)
    end)

    OnTrigInit(function ()
        local function killedOrCapturedfunction(_, target)
            target.captured = true
        end
        Digimon.capturedEvent(killedOrCapturedfunction)
        Digimon.killEvent(killedOrCapturedfunction)

        Digimon.postDamageEvent(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)

end)
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
I tried to deep print the value regionData, using this function from the @Eikonium's Debug Utils:
Lua:
--***************deep table.print**************
---Returns a string showing all pairs included in the specified table.  E.g. {"a", 5, {7}} will result in '{(1, a), (2, 5), (3, {(1, 7)})}'.
---For any non-table object, returns the existing tostring(anyObject).
---Optional param recursionDepth: Defines, on which depth level of nested tables the elements should be included in the string. Setting this to nil will display elements on any depth. Setting this to 0 will just return tostring(table). 1 will show pairs within the table. 2 will show pairs within tables within the table etc.
---table.toString is not multiplayer synced.
---@param anyObject table | any
---@param recursionDepth integer --optional
---@return string
function table.tostring(anyObject, recursionDepth)
    recursionDepth = recursionDepth or -1
    local result = tostring(anyObject)
    if recursionDepth ~= 0 and type(anyObject) == 'table' then
        local elementArray = {}
        for k,v in pairs(anyObject) do
            table.insert(elementArray, '(' .. tostring(k) .. ', ' .. table.tostring(v, recursionDepth -1) .. ')')
        end
        result = '{' .. table.concat(elementArray, ', ') .. '}'
    end
    return result
end

---Displays all pairs of the specified table on screen. E.g. {"a", 5, {7}} will display as '{(1, a), (2, 5), (3, {(1, 7)})}'.
---Second parameter recursionDepth defines, in what depth elements of tables should be printed. Tables below the specified depth will not show their elements, but the usual "table: <hash>" instead. Setting this to nil displays elements to infinite depth.
---@param anyObject any
---@param recursionDepth integer | nil --optional
function table.print(anyObject, recursionDepth)
    print(table.tostring(anyObject, recursionDepth))
end
But it throws me stack overflow error, what can I do?
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
After testing, it seems that this is the function that caused me lag (there are 2 usages in the timer callback):
Lua:
---@param x number
---@param y number
---@param radius number
---@param callback fun(u:unit)
---@param includeLocust? boolean
function ForUnitsInRange(x, y, radius, callback, includeLocust)
    local be = Filter(function ()
        local u = GetFilterUnit()
        if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
            callback(u)
        end
    end)
    GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, be)
    DestroyBoolExpr(be)
end
I was using this function from several months ago and I didn't get this problem, I think this happens now because I increased the number of creep spawns, so what is wrong with this function? is the fact I'm creating and destroying boolexpr?

Edit: It seems that yes, I replaced the function with this one and I don't get lag:
Lua:
---@param x number
---@param y number
---@param radius number
---@param callback fun(u:unit)
---@param includeLocust? boolean
function ForUnitsInRange(x, y, radius, callback, includeLocust)
    GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, nil)
    ForGroup(ENUM_GROUP, function ()
        local u = GetEnumUnit()
        if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
            callback(u)
        end
    end)
    GroupClear(ENUM_GROUP)
end
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
479
Try declaring the boolexpr only once during loading screen. It's the same function every time, no need to recreate it so often.

Lua:
do
    local be ---@type boolexpr

    --define function used for boolexpr
    local cond =function ()
        local u = GetFilterUnit()
        if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
            callback(u)
        end
    end

    OnMapInit(function() be = Filter(cond) end) --create boolexpr once during loading screen (not in Lua root, it's a Wc3 object)

    function ForUnitsInRange(x, y, radius, callback, includeLocust)
        GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, be) --use same boolexpr every time
    end
end

Btw., GroupEnumUnitsInRange doesn't include locust units, so you don't need the boolexpr anyway.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Try declaring the boolexpr only once during loading screen. It's the same function every time, no need to recreate it so often.

Lua:
do
    local be ---@type boolexpr

    --define function used for boolexpr
    local cond =function ()
        local u = GetFilterUnit()
        if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
            callback(u)
        end
    end

    OnMapInit(function() be = Filter(cond) end) --create boolexpr once during loading screen (not in Lua root, it's a Wc3 object)

    function ForUnitsInRange(x, y, radius, callback, includeLocust)
        GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, be) --use same boolexpr every time
    end
end
No, because the local parameter callback in your example is used outside the function, but it could be possible doing something like that to only use 1 boolexpr, but it has sereval steps that I preffer my solution.
Btw., GroupEnumUnitsInRange doesn't include locust units,
Really? I'm not sure.
so you don't need the boolexpr anyway.
I will need it if I will iterate with GroupEnumUnitsInRange, because the parameter should be a boolexpr.
 
Level 20
Joined
Jul 10, 2009
Messages
479
I haven't taken a closer look at your function honestly, just wanted to show you how to logically separate the boolexpr.

If you want to keep the callback within the boolexpr, you can create one boolexpr per callback.

Lua:
do
    ...
    local boolexprs = {} ---@type table<function,boolexpr>

    function ForUnitsInRange(x, y, radius, callback, includeLocust)
        --save new boolexpr in case it doesn't yet exist
        boolexprs[callback] = boolexprs[callback] or Filter(function ()
            local u = GetFilterUnit()
            if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
                callback(u)
            end
        end)
        --use GroupEnumUnitsInRange with the same boolexpr as always
        GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, boolexprs[callback]) --use same boolexpr every time
    end
end

Using ForGroup also works, you have already found that solution.

Really? I'm not sure.
Then go test it. I'm also unsure every time and need to google or test. ;)

I will need it if I will iterate with GroupEnumUnitsInRange, because the parameter should be a boolexpr.
You don't need to pass a boolexpr. GroupEnumUnitsInRange(group, x, y, radius, nil) works perfectly fine.
Even if you had to pass a boolexpr, you could have simply created a trivial one during loading screen that always returns true and keep using that.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
@Eikonium yeah, I just founded the solution, by the way I don't think this could work:
If you want to keep the callback within the boolexpr, you can create one boolexpr per callback.

Lua:
do
    ...
    local boolexprs = {} ---@type table<function,boolexpr>

    function ForUnitsInRange(x, y, radius, callback, includeLocust)
        --save new boolexpr in case it doesn't yet exist
        boolexprs[callback] = boolexprs[callback] or Filter(function ()
            local u = GetFilterUnit()
            if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
                callback(u)
            end
        end)
        --use GroupEnumUnitsInRange with the same boolexpr as always
        GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, boolexprs[callback]) --use same boolexpr every time
    end
end
Because (correct me if I'm wrong), in my system:
Lua:
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 = GetHeroLevel(u)
            end
        end)
I'm passing a new created function everytime I call the function Update and iterate over the list All so I wouldn't recycle boolexpr doing that, because all of them would be different.
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Thinking well, my previous solution won't work with nested ForUnitsInRange, so I made this, inspired on the Wurst Standard Library:
Lua:
local ENUM_GROUP = CreateGroup()
local LOCUST_ID = FourCC('Aloc')

local callbacks = {} ---@type fun(u: unit)[]
local filter = Filter(function () callbacks[#callbacks](GetFilterUnit())  end)

---@param x number
---@param y number
---@param radius number
---@param callback fun(u:unit)
---@param includeLocust? boolean
function ForUnitsInRange(x, y, radius, callback, includeLocust)
    table.insert(callbacks, function (u)
        if not includeLocust or GetUnitAbilityLevel(u, LOCUST_ID) > 0 then
            callback(u)
        end
    end)
    GroupEnumUnitsInRange(ENUM_GROUP, x, y, radius, filter)
    table.remove(callbacks)
end

---@param whichPlayer player
---@param callback fun(u:unit)
function ForUnitsOfPlayer(whichPlayer, callback)
    table.insert(callbacks, callback)
    GroupEnumUnitsOfPlayer(ENUM_GROUP, whichPlayer, filter)
    table.remove(callbacks)
end
 
Status
Not open for further replies.
Top