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

[Solved] Removing phantom units from a unit group

Status
Not open for further replies.
Level 5
Joined
Jul 31, 2020
Messages
103
Hi.

Probably sounds like a hot topic that comes up every now and then. I tried searching for a good answer, but, unfortunately, found nothing that could work in my situation.

Anyway, to explain what I'm talking about. Simply put: removed units. A bit more precisely, units which get removed over time after bone decay runs its course. Now, to the insightful, I don't have to explain what the problem is now, but to people who haven't encountered an issue like this before... Basically, once a unit fully decays, it's removed from the game, but not from unit groups. You might think, running a simple ForGroup or FirstOfGroup loop and hitting them with the old trusty GetUnitTypeId(GetEnumUnit()) == 0 would work. It won't. The reason for it is straightforward: neither loop will actually execute anything on these units since they're removed, therefore even FirstOfGroup would just return null once it finds them inside the group.

The other idea I entertained was hooking into RemoveUnit, and doing my thing there. Won't work either as the game doesn't actually call RemoveUnit from what I've observed when bone decay ends. I can't say how exactly the unit is removed in this case, but unluckily, I can't leverage RemoveUnit in this situation either.

I cannot use a unit indexer. Yes, I do understand -although I haven't seen it because it feels impossible to find one like it- they have something like onUnitDeindex or something similar. I vaguely remember reading this once. Now, if that somehow manages to catch a unit just as it's getting removed, and executes whatever code; that's what I'm looking for. How do we go about doing that?
 
Level 13
Joined
May 10, 2009
Messages
868
Unfortunately, that's a shadow unit breaking your unit group. There's a way of detecting when a unit is removed which is by adding a hidden Immolation or any similar ability to it. Once they are removed from the game, they issue an immediate order - in this case, "unimmolation". Plus, if they are alive when being removed, they'll issue the same order twice. However, the key to distinguish their death from removal is to check if they still have that hidden immolation ability. In case it returns false, that means the unit has just been removed from the game.

That's the method most Unit Indexers out there use to detect unit removal. The downside is, of course, that you'll have to add this hidden immolation to all units.

Example:
JASS:
function Trig_DetectRemoval takes nothing returns nothing
    local unit u = GetTriggerUnit()
    local integer orderid = GetIssuedOrderId()
 
    call BJDebugMsg(GetUnitName(u)+": \""+OrderId2String(orderid)+"\" ("+I2S(orderid)+")")
 
    // DETECT WHEN UNIT IS REMOVED
    // 852178 == unimmolation
    // 'A000' == hidden immolation
    if (orderid == 852178) and (GetUnitAbilityLevel(u, 'A000') == 0) then
        call BJDebugMsg(GetUnitName(u)+" ("+I2S(GetUnitTypeId(u))+") has just been removed from the game.")
    endif
 
    set u = null
endfunction

//===========================================================================
function InitTrig_Detect_Unit_Removal takes nothing returns nothing
    set gg_trg_Detect_Unit_Removal = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Detect_Unit_Removal, EVENT_PLAYER_UNIT_ISSUED_ORDER )
    call TriggerAddAction( gg_trg_Detect_Unit_Removal, function Trig_DetectRemoval)
endfunction

Untitled-2.png
 

Attachments

  • removal.w3x
    18.4 KB · Views: 19

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Why can't you use a unit indexer? It will be very straightforward to solve if you use one.
Instead of using plain old CreateGroup(), you could make make something like CreateGroupEx() that counts your groups and indexes them. Then you register a callback that runs in a unit deindex event that loops through the created groups and call GroupRemoveUnit(group[index], GetDeindexedUnit()) for each group.
 
Level 5
Joined
Jul 31, 2020
Messages
103
Unfortunately, that's a shadow unit breaking your unit group. There's a way of detecting when a unit is removed which is by adding a hidden Immolation or any similar ability to it. Once they are removed from the game, they issue an immediate order - in this case, "unimmolation". Plus, if they are alive when being removed, they'll issue the same order twice. However, the key to distinguish their death from removal is to check if they still have that hidden immolation ability. In case it returns false, that means the unit has just been removed from the game.

That's the method most Unit Indexers out there use to detect unit removal. The downside is, of course, that you'll have to add this hidden immolation to all units.

Example: ...

This is unfortunately not doable in my case, but it's a solution nevertheless, I suppose.

Why can't you use a unit indexer? It will be very straightforward to solve if you use one.
Instead of using plain old CreateGroup(), you could make make something like CreateGroupEx() that counts your groups and indexes them. Then you register a callback that runs in a unit deindex event that loops through the created groups and call GroupRemoveUnit(group[index], GetDeindexedUnit()) for each group.

I'll look into your suggestion a bit later, but it looks reasonable as long as I'm able to implement it without too much bloat.
 

Uncle

Warcraft Moderator
Level 63
Joined
Aug 10, 2018
Messages
6,456
Hi.

Probably sounds like a hot topic that comes up every now and then. I tried searching for a good answer, but, unfortunately, found nothing that could work in my situation.

Anyway, to explain what I'm talking about. Simply put: removed units. A bit more precisely, units which get removed over time after bone decay runs its course. Now, to the insightful, I don't have to explain what the problem is now, but to people who haven't encountered an issue like this before... Basically, once a unit fully decays, it's removed from the game, but not from unit groups. You might think, running a simple ForGroup or FirstOfGroup loop and hitting them with the old trusty GetUnitTypeId(GetEnumUnit()) == 0 would work. It won't. The reason for it is straightforward: neither loop will actually execute anything on these units since they're removed, therefore even FirstOfGroup would just return null once it finds them inside the group.

The other idea I entertained was hooking into RemoveUnit, and doing my thing there. Won't work either as the game doesn't actually call RemoveUnit from what I've observed when bone decay ends. I can't say how exactly the unit is removed in this case, but unluckily, I can't leverage RemoveUnit in this situation either.

I cannot use a unit indexer. Yes, I do understand -although I haven't seen it because it feels impossible to find one like it- they have something like onUnitDeindex or something similar. I vaguely remember reading this once. Now, if that somehow manages to catch a unit just as it's getting removed, and executes whatever code; that's what I'm looking for. How do we go about doing that?
I'm curious as to why you cannot use a unit indexer.

And in regards to removing units from Unit Groups, why aren't you removing them when they initially die?
 
Level 5
Joined
Jul 31, 2020
Messages
103
Why can't you use a unit indexer? It will be very straightforward to solve if you use one.
Instead of using plain old CreateGroup(), you could make make something like CreateGroupEx() that counts your groups and indexes them. Then you register a callback that runs in a unit deindex event that loops through the created groups and call GroupRemoveUnit(group[index], GetDeindexedUnit()) for each group.

I looked into this now, and you're gonna have to clarify because this is a half-sentence to me that currently doesn't really hold any meaning as it repeats my question. It looked reasonable on first glance, but this is not a solution to the question, actually. Or I'm not understanding it. And let's leave out the weird "index and count the groups" part because... just leave it out.
 
And let's leave out the weird "index and count the groups" part because... just leave it out.

It's actually a lot more intuitive than the FirstOfGroup method, once the behavior is understood.

JASS:
local integer i     = 0
local unit enum  = BlzGroupUnitAt(whichGroup, i)
loop
    exitwhen i >= BlzGroupGetSize(whichGroup)
    // Do your stuff here.

    set i = i + 1
    set enum = BlzGroupUnitAt(whichGroup, i)
endloop

It's basically a list which one'll iterate through and process. Analogous to this is some pseudo-code presented below:
Lua:
--[[
    Consider a function iterator() exists, which iterates through the contents of a list.
    Consider a list, which can invoke iterator().
    The contents of the list are {1, 5, 8, 2, 4, 2, 5}.

    When calling iterator(), each element is inspected and processed in the loop.
    So, when iterating through the list, the elements are processed through this order:
    1,
    5,
    8,
    2,
    4,
    2,
    5,

    But what if an element is contextually removed? Let's say 8 is removed.
    So, when iterating through the list, it will look like this:
    1,
    5,
    nil,
    2,
    4,
    2,
    5,
]]
for elem in whichGroup:iterator() do
    -- Do some stuff here.
end

Similarly, the Unit Indexer approach is somewhat complex, but will be satisfying to understand in the end.
Simply put, the Unit Indexer allows one to "detect" when a unit enters the map (unit is created), and when
the unit leaves the map (unit is removed). We can take advantage of this behavior to iterate through
unit groups containing the unit, and removing said unit from those groups.

Lua:
local groups = {}

-- This returns the newly created group
function CreateGroupEx()
    groups[#groups + 1] = CreateGroup()
    return groups[#groups]
end

-- Assume the event that detect
-- removed units is available, called OnUnitDeindex
-- OnUnitDeindex will take a callback function,
-- and supply the removed unit as a parameter
-- to the argument whichunit
OnUnitDeindex(function(whichunit)
    for i = 1, #groups do
        if IsUnitInGroup(whichunit, groups[i]) then
            GroupRemoveUnit(groups[i], whichunit)
        end
    end
    -- At this point, the unit has been removed from
    -- the groups created with CreateGroupEx
end)

Finally, a brute force solution would be to use a temporary group to store all of your units from your
actual group, then clear the actual group and reintroduce the units from the temporary group to the
actual group.

JASS:
globals
    group temp = CreateGroup()
endglobals

// ForGroup excludes "shadow units"
function OnRefreshGroup takes nothing returns nothing
    call GroupAddUnit(temp, GetEnumUnit())
endfunction

// Since temp is technically a constant, there's no need to actually
// nullify the local variable.
function RefreshGroup takes group whichGroup returns nothing
    local group temp2 = temp
    call ForGroup(whichGroup, OnRefreshGroup)
    call GroupClear(whichGroup)
 
    set temp = whichGroup
    call ForGroup(temp, OnRefreshGroup)
    call GroupClear(temp)
    set temp = temp2
endfunction
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
I looked into this now, and you're gonna have to clarify because this is a half-sentence to me that currently doesn't really hold any meaning as it repeats my question. It looked reasonable on first glance, but this is not a solution to the question, actually. Or I'm not understanding it. And let's leave out the weird "index and count the groups" part because... just leave it out.
I meant something like this

JASS:
library GhostFreeGroup /*


    */uses /*

    */Table     /*
    */UnitDex   /*


    *///! novjass

    |-----|
    | API |
    |-----|
    /*

    */function NewGroup takes nothing returns group/*
        - Use this instead of CreateGroup() to create a group that
        automatically removes "ghost units"

    */function ReleaseGroup takes group g returns nothing/*
        - Use this instead of DestroyGroup() for groups created by NewGroup(),
        well any group actually


    *///! endnovjass

    /*============================================================================*/

    private struct Group

        readonly group group
        readonly thistype next
        readonly thistype prev

        readonly static Table table

        static method create takes nothing returns thistype
            local thistype node = allocate()
            set node.group = CreateGroup()
            set table[GetHandleId(node.group)] = node

            set node.next = 0
            set node.prev = thistype(0).prev
            set node.prev.next = node
            set thistype(0).prev = node

            return node
        endmethod

        method destroy takes nothing returns nothing
            set this.next.prev = this.prev
            set this.prev.next = this.next

            call table.remove(GetHandleId(this.group))
            call DestroyGroup(this.group)
            set this.group = null
            call this.deallocate()
        endmethod

        private static method onUnitDeindex takes nothing returns boolean
            local thistype node = thistype(0).next
            loop
                exitwhen node == 0
                call GroupRemoveUnit(node.group, GetIndexedUnit())
                set node = node.next
            endloop
            return false
        endmethod

        private static method onInit takes nothing returns nothing
            set table = Table.create()
            call OnUnitDeindex(function thistype.onUnitDeindex)
        endmethod

    endstruct

    function NewGroup takes nothing returns group
        return Group.create().group
    endfunction

    function ReleaseGroup takes group g returns nothing
        local Group this = Group.table[GetHandleId(g)]
        if this != 0 then
            call this.destroy()
        else
            call DestroyGroup(g)
        endif
    endfunction


endlibrary


Or you could use BlzGroupUnitAt() instead like others suggested.

EDIT:
If you are using Lua, check MyPad's post above, since it has better alternatives.
 
Last edited:
Level 5
Joined
Jul 31, 2020
Messages
103
Alright, I finally understand what you meant by "index groups" as that was the part which seemed incomprehensible at first. So I can now find removed units, but still cannot remove them from the group though. I'm checking if the units are indeed found, and yeah, they are. This is progress. But GroupRemoveUnit returns false on them; they do not get removed. I used the BlzGroupUnitAt() approach as it seems quite lucrative, and it's a pity I didn't even know of its existence until now.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
You can filter a unit group to clean it of removed from game units. Pick all units in the unit group, and if picked unit is not equal to no unit then add it to a clean unit group. After this is all done the original unit group can be cleaned or destroyed with the new unit group taking its place. In worst place a temporary unit group could be used for the results that is then added back to the cleared first unit group to avoid having to update references to that group.
 
Level 5
Joined
Jul 31, 2020
Messages
103
You can filter a unit group to clean it of removed from game units. Pick all units in the unit group, and if picked unit is not equal to no unit then add it to a clean unit group. After this is all done the original unit group can be cleaned or destroyed with the new unit group taking its place. In worst place a temporary unit group could be used for the results that is then added back to the cleared first unit group to avoid having to update references to that group.

This was already posted by MyPad as a brute force solution, and yes, I get the concept. However, it's wasteful (pretty much why it's brute force), and they have been claiming units can just be removed which I mentioned doesn't seem to be the case as RemoveUnit returns false, and they remain in the group. If there's no way to avoid creating a temporary group, then so be it, I suppose.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
With Lua one could in theory use the garbage collector to eventually purge these units from the group before references to them are lost using some relationship tables with weak keys. However being Lua one could also just use a table as a list instead of a unit group.
 
Level 5
Joined
Jul 31, 2020
Messages
103
@Dr Super Good One more thing. You mentioned updating references, and in connection to this is one additional nuance with JASS I'm not familiar with (I don't know how it works in the background). As I said with the brute force solution, I understand the concept, and implemented the variant of it which is the most wasteful with a temporary group being created, the original copied into it, the temporary processed, and then only the existing units copied back to the cleared out original. This approach would make sense in most languages in this situation since my original group is global while the temporary one is local, therefore it would be most unwise to set the reference of the original to a local group which gets freed once we fall out of scope.

So my question is exactly that; would this be the case with JASS too, considering the proper way of freeing groups is to manually call DestroyGroup(), then set the variable to null to avoid an additional reference leak? Or is it a correct assumption that since these groups would normally leak, this can be leveraged, and a local group can just be fed to a global variable with a simple reference update?

(I already marked the thread as resolved, thanks to everyone who helped me. I might eventually make the jump to Lua from JASS if it seems like the hip thing to do nowadays.)
 
Level 13
Joined
May 10, 2009
Messages
868
and they have been claiming units can just be removed which I mentioned doesn't seem to be the case as RemoveUnit returns false, and they remain in the group
That's why you need the UnitIndexer (onDeIndex event). Attempting to remove units from a group after they're long gone won't work. Following AGD's suggestion should do the job easily.

A simple example which uses first of group as enumeration was attached below. Do note that udg_Group isn't destroyed nor cleared; removed units are being removed from it.
 

Attachments

  • Group Handling.w3x
    20.8 KB · Views: 17
Status
Not open for further replies.
Top