• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!
  • ✅ The POLL for Hive's Texturing Contest #34 is OPEN! Vote for the TOP 3 SKINS! 🔗Click here to cast your vote!
  • ✅ The POLL for Hive's Techtree Contest #20 is OPEN! Vote for the TOP 3 FACTIONS! 🔗Click here to cast your vote!

Does anybody know a reliable way to detect dispels?

Level 4
Joined
Feb 26, 2022
Messages
13
Title. The basic use-case is to make a type of unit that always recieves unique effects that aren't damage from getting dispelled, even if he doesn't have regular buffs to dispel. Here's the list of things I've tried or at least considered:
  • A common sense, just-works trigger event that fires when a buff is dispelled with built-in functions to get dispeller, dispelee, ability that was used to dispel, and buff that got dispelled?
    • Doesn't exist. I already checked the blizzard.j and common.j files. The string "dispel" appears exactly 7 times accross both of them and no instance of it is related to event registration.
  • Since the effect I want to check is more or less permanent, and the idea is that it will come back 30 seconds after getting dispelled, I thought of this:
    • A "tracker ability" that is hidden and does basically nothing, an "effect active" buff that gets applied via dummy units, and an "effect lockout" effect that also gets applied with dummy units.
    • All units that have this are permenently classified as "summoned" in the OE so they take damage from dispels.
    • An EVENT_PLAYER_UNIT_DAMAGED that has a condition that checks if he does have the tracker ability, and doesn't have the other two.
    • If the condition passes, it means he just got dispelled and therefore the damage source must be from the dispel so we set the damage to zero with event responses, apply the lockout so further damage won't get set to zero, and then do the rest of the script.
    • The problem? It appears from testing that the damage that dispel effects cause to summoned units gets applied before removing the buffs, so it fails because during the check, it still has the "active effect" buff. It will proc on the next instance of damage he takes, but that isn't what I want.
    • This is also kind of awful since we're now doing more checks every single time a unit gets damaged, and even if they fail and it doesn't proceed, it's extra CPU overhead.
  • Check every single unit on the map whenever any spell is cast, filtering out dummy spells and other undesirables by checking the caster unit type (all dummy spells for my project get cast by the same unit).
    • The problem is that this is even more overhead.
    • Imagine a 12 player game with a bunch of curses and slows and inner fires getting cast in a big fight and for each one, every single unit in the map needs to get cast in case it was a dispel.
  • Run the check whenevery any spell is cast as described above, but only the spell's target if it's single target, or people in its radius if it's an AOE.
    • The problem: we can't get a spell's radius.
    • The ability real field ABILITY_RLF_AREA_OF_EFFECT is defined in common.j, however, in the GUI, we are unable to get or set any ability real value except for "Missile Arc ('amac')"
    • And I know from personal experience that if it doesn't let you modify a field in the GUI, it won't work with Lua (or probably JASS), either.
  • Run the check only if the spell being cast is a known because in the base game, there is a limited number of different spells that do dispels and if I add any more, I can add those two.
    • This is problematic because I intend to architect my map in such a way that it is designed to be mergable with other maps/assets. I have a Python script that merges object editor and trigger data.
    • If the conditions say "only proceed if the spell cast was Abolish Magic, Mass Dispel, Purge, (Whatever the Obsidian Destroyer's anti-magic spell is), Dispel Magic (Neutral)(, or one of my custom dispels)", then this becomes an issue the second there are more dispels that I didn't and realistically couldn't account for.
    • Such a solution also kind of just makes me cringe because I consider proper abstraction to be a best practice
  • Periodic global check whether or not each unit has the buff?
    • While probably not as bad as every single attack or spell, it's still a lot of overhead.
    • If I did it only once every ten seconds (or even every one second) then it would probably be fine, but then I run into the issue of now it's not very responsive.
    • If there's a range of 0 and 1 seconds between dispelling and the resultant effects triggering, dependent upon how far it was in between global checks, then this is inconsistent and therefore awful.
  • Periodic check using a timer created for each unit where this will come up?
    • Less overhead than periodic global check, which means we can check more often (such as every 0.1 seconds) because we will be doing it on less units.
    • Better, but basically the same problem, just to a lesser degree and now we have to do cleanup every time a unit dies (extra overhead).
  • Checking out this post: https://www.hiveworkshop.com/threads/how-to-check-if-a-buff-is-dispelled.262487/ where he links to this post: Please! Help with a trigger! where he give us his system.
    • The problem is that it only works for his Effect over time system.
    • "// - EOT_Event_An_EOT_Is_Dispelled - real (event simulator)
      // This event is ran when an eot is dispelled."
    • That means for it to work I have make an event within his ecosystem.
    • Obviously, anything that is user-defined is going to be much more cooperative; if I was sure that the only things doing the dispelling were things I defined in my own scripts, well I wouldn't be posting this.
    • He's got the lines: "//This is the loop for every 0.03 seconds of gametime.
      function EOT_Interval takes nothing returns boolean" in his EOT System script. I've not analyzed the whole thing, but this really makes me think he's just spamming checks periodically, which is what I described above and has the same problem.
    • (Not to disparage Wietlol; his system does seem like I was put together well, I just don't know if it does anything I haven't already thought of for this use-case.)
    • (Also, he wrote it in JASS and I use Lua (or at least I am for this map), so even if it was perfect, it'd still be a lot of refactoring to implement it 1:1 or I'd just have to in-line everything, meaning if there was a simpler way, I'd take that)
  • Looking for native buffs that do something that I might be able to get to call trigger events when dispelled.
    • I can't find any.
    • It really doesn't seem like any spells in vanilla WC3 do something specifically when they expire, certainly not when they're dispelled...

I hope what I have done already consitutes sufficient due dilligence before posting. This might just be an engine limitation I'll just have to deal with or switch to Retera's Java rewrite (if anybody has successfully gotten that to work)...

Anyway, I love you guys. If anybody has a fix I haven't thought of, I would be super grateful. I've tried everything I can think of here.

Thanks in advance <3
 
Last edited:
I might have a somewhat hybrid architecture solution for this "white whale" case that avoids the pitfalls of purely periodic checks while sidestepping the "summoned" classification baggage.

To work around the engine quirk where dispel damage is applied exactly one frame before the buff is removed, let’s try to use Frame-Perfect Interceptor.



First phase ——The Alarm: When a tracked unit takes damage, we don't assume a dispel occurred yet. Instead, we just start a 0.00s timer. Since a 0.00s timer executes on the very next processing frame, we check the buff state upon expiration. If the buff is gone, we know the damage source was a successful dispel.

And we add a somewhat safety net to handle edge cases where a dispel deals zero damage or is fully mitigated, I've added a low-frequency periodic sweep (0.5s) that only iterates through the tracked units.



This should provide instant responsiveness for standard gameplay without the high-frequency overhead of a 0.03s loop.



@Luashine

I’d love to get your eyes on this. It uses a table-based watch group and a next-frame timer to detect the state change. I’m curious about your thoughts on how this handles the Reforged garbage collector, potential desyncs with frame timing, or any general pitfalls. ..(Sorry for the tag, but I've seen your name pop up as the lua authority xd)


Code:
-- Configuration
local DISPEL_BUFF_ID = FourCC('B000')
local COOLDOWN_TIME = 30.0


-- Tables
local DispelWatchGroup = {}  -- [unit] = true/false
local NextFrameQueue = {}    -- Queue for units hit this frame


-- Logic to handle the Dispel effect
function OnUnitDispelled(u)
    if not DispelWatchGroup[u] then return end
  
    print("Dispel detected on: " .. GetUnitName(u))
  
    DispelWatchGroup[u] = false -- Enter Lockout
    local cooldownTimer = CreateTimer()
    TimerStart(cooldownTimer, COOLDOWN_TIME, false, function()
        if GetUnitTypeId(u) ~= 0 then -- Ensure unit still exists
            DispelWatchGroup[u] = true
        end
        PauseTimer(cooldownTimer)
        DestroyTimer(cooldownTimer)
    end)
end


-- Efficient Next-Frame Processor
local frameTimer = CreateTimer()
local function ProcessNextFrame()
    for u, _ in pairs(NextFrameQueue) do
        if GetUnitAbilityLevel(u, DISPEL_BUFF_ID) == 0 then
            OnUnitDispelled(u)
        end
        NextFrameQueue[u] = nil -- Clear the queue
    end
end


-- Fast-Path: Damage Interceptor
local damageTrigger = CreateTrigger()
for i = 0, GetBJMaxPlayerSlots() - 1 do
    TriggerRegisterPlayerUnitEvent(damageTrigger, Player(i), EVENT_PLAYER_UNIT_DAMAGED, nil)
end


TriggerAddCondition(damageTrigger, Condition(function()
    local target = GetTriggerUnit()
  
    -- Only queue the unit if it's tracked and not on cooldown
    if DispelWatchGroup[target] then
        NextFrameQueue[target] = true
        -- Restart/Start the single 0.00s timer to process at the start of next frame
        TimerStart(frameTimer, 0.00, false, ProcessNextFrame)
    end
    return false
end))


-- Safety-Path & Garbage Collection Pruning (0.5s)
TimerStart(CreateTimer(), 0.50, true, function()
    for unit, isTracked in pairs(DispelWatchGroup) do
        -- 1. Check if unit was removed from game/dead to prevent memory leaks
        if GetUnitTypeId(unit) == 0 or IsUnitType(unit, UNIT_TYPE_DEAD) then
            DispelWatchGroup[unit] = nil
        -- 2. Check for zero-damage/custom dispels
        elseif isTracked and GetUnitAbilityLevel(unit, DISPEL_BUFF_ID) == 0 then
            OnUnitDispelled(unit)
        end
    end
end)


function RegisterUnitForDispelWatch(u)
    DispelWatchGroup[u] = true
end

Also, I was wondering can we reduce the ‘noise’ here… for exmpl. reducing the timer spam by adding a flag so it doesn’t trigger on every damage fire.

Code:
if not frameScheduled then
    frameScheduled = true
    TimerStart(frameTimer, 0.00, false, function()
        frameScheduled = false
        ProcessNextFrame()
    end)
end

Some potential optimization for if needed cases:

Code:
 and not IsUnitType(u, UNIT_TYPE_DEAD)

I’m also worried about expire and dispel merging so something like this might do:

Code:
if GameTime < ExpireTime[u] then
    -- dispel
else
    -- expire
end

PS I’m aware that this system is still indirectly dependent on detection to damage…
 
Last edited:
I might have a somewhat hybrid architecture solution for this "white whale" case that avoids the pitfalls of purely periodic checks while sidestepping the "summoned" classification baggage.

To work around the engine quirk where dispel damage is applied exactly one frame before the buff is removed, let’s try to use Frame-Perfect Interceptor.



First phase ——The Alarm: When a tracked unit takes damage, we don't assume a dispel occurred yet. Instead, we just start a 0.00s timer. Since a 0.00s timer executes on the very next processing frame, we check the buff state upon expiration. If the buff is gone, we know the damage source was a successful dispel.

And we add a somewhat safety net to handle edge cases where a dispel deals zero damage or is fully mitigated, I've added a low-frequency periodic sweep (0.5s) that only iterates through the tracked units.



This should provide instant responsiveness for standard gameplay without the high-frequency overhead of a 0.03s loop.



@Luashine

I’d love to get your eyes on this. It uses a table-based watch group and a next-frame timer to detect the state change. I’m curious about your thoughts on how this handles the Reforged garbage collector, potential desyncs with frame timing, or any general pitfalls. ..(Sorry for the tag, but I've seen your name pop up as the lua authority xd)


Code:
-- Configuration
local DISPEL_BUFF_ID = FourCC('B000')
local COOLDOWN_TIME = 30.0


-- Tables
local DispelWatchGroup = {}  -- [unit] = true/false
local NextFrameQueue = {}    -- Queue for units hit this frame


-- Logic to handle the Dispel effect
function OnUnitDispelled(u)
    if not DispelWatchGroup[u] then return end
 
    print("Dispel detected on: " .. GetUnitName(u))
 
    DispelWatchGroup[u] = false -- Enter Lockout
    local cooldownTimer = CreateTimer()
    TimerStart(cooldownTimer, COOLDOWN_TIME, false, function()
        if GetUnitTypeId(u) ~= 0 then -- Ensure unit still exists
            DispelWatchGroup[u] = true
        end
        PauseTimer(cooldownTimer)
        DestroyTimer(cooldownTimer)
    end)
end


-- Efficient Next-Frame Processor
local frameTimer = CreateTimer()
local function ProcessNextFrame()
    for u, _ in pairs(NextFrameQueue) do
        if GetUnitAbilityLevel(u, DISPEL_BUFF_ID) == 0 then
            OnUnitDispelled(u)
        end
        NextFrameQueue[u] = nil -- Clear the queue
    end
end


-- Fast-Path: Damage Interceptor
local damageTrigger = CreateTrigger()
for i = 0, GetBJMaxPlayerSlots() - 1 do
    TriggerRegisterPlayerUnitEvent(damageTrigger, Player(i), EVENT_PLAYER_UNIT_DAMAGED, nil)
end


TriggerAddCondition(damageTrigger, Condition(function()
    local target = GetTriggerUnit()
 
    -- Only queue the unit if it's tracked and not on cooldown
    if DispelWatchGroup[target] then
        NextFrameQueue[target] = true
        -- Restart/Start the single 0.00s timer to process at the start of next frame
        TimerStart(frameTimer, 0.00, false, ProcessNextFrame)
    end
    return false
end))


-- Safety-Path & Garbage Collection Pruning (0.5s)
TimerStart(CreateTimer(), 0.50, true, function()
    for unit, isTracked in pairs(DispelWatchGroup) do
        -- 1. Check if unit was removed from game/dead to prevent memory leaks
        if GetUnitTypeId(unit) == 0 or IsUnitType(unit, UNIT_TYPE_DEAD) then
            DispelWatchGroup[unit] = nil
        -- 2. Check for zero-damage/custom dispels
        elseif isTracked and GetUnitAbilityLevel(unit, DISPEL_BUFF_ID) == 0 then
            OnUnitDispelled(unit)
        end
    end
end)


function RegisterUnitForDispelWatch(u)
    DispelWatchGroup[u] = true
end

Also, I was wondering can we reduce the ‘noise’ here… for exmpl. reducing the timer spam by adding a flag so it doesn’t trigger on every damage fire.

Code:
if not frameScheduled then
    frameScheduled = true
    TimerStart(frameTimer, 0.00, false, function()
        frameScheduled = false
        ProcessNextFrame()
    end)
end

Some potential optimization for if needed cases:

Code:
 and not IsUnitType(u, UNIT_TYPE_DEAD)

I’m also worried about expire and dispel merging so something like this might do:

Code:
if GameTime < ExpireTime[u] then
    -- dispel
else
    -- expire
end

PS I’m aware that this system is still indirectly dependent on detection to damage…
This was great. Thank you. Timers in response to damage events are definitely the way to do it.
I used this function:
JASS:
    local function DispelCheck()
        BJDebugMsg("Checking for dispel...")
        local unit = GetTriggerUnit()
        local DispelTimer = CreateTimer()
        TimerStart(DispelTimer, 0.0, false, function()
            if not UnitHasBuffBJ(unit, DISPEL_TRACKER_CURRENTLY_EMITING_BUFF_ID) then
                BJDebugMsg("HE DOESN'T HAVE THE BUFFS; SETTING EVENT DAMAGE")
                BlzSetEventDamage(0.0)
                BJDebugMsg("EVENT DAMAGE SET SUCCESSFULLY")
            end
        end)
    end
When it triggered from an on-damage event from a dispel, it printed everything. But it stopped at "Checking for dispel..." when he just got beat up by random creeps. That answers the question of the post, so thank you.

but there was a slight issue in that the unit still took damage.
The fix for that I did was to just start by setting event damage to 0 and then if he doesn't have the buff, and then using UnitDamageTarget to re-apply the damage is he does.
There are still a few downsides, such as the fact that now whenever these units take damage it will be considered melee because I don't know if there's a way to get boolean "IsAttackRanged" and also I had to add a secondary "utility lockout" spell that the act of calling UnitDamageTarget doesn't fire again. It also might have other wierd quirks as a result of that and there's still the fact that now all the units have to be permanently classified as summoned, which might cause other strange interactions. But overall, this is still basically the best outcome I could've hoped for, so thanks again, the.Axolotl.

Despite these

This is what I ultimately used (except for a few CTRL-H's to avoid spoilers on the project I'm working on):
JASS:
    local DISPEL_TRACKING_CHECK_INTERVAL = 2.0
    local DISPEL_MULTIPLIER = 0.33
    local DISPEL_LOCKOUT_DURATION = 30.0 -- Needs to last as long as the actual spell.
    local DISPEL_TRACKER_ACTIVE_APPLICATION_ABILITY_ID = FourCC("A04Z") -- to test this I actually just gave him the spell and cast it manually.
    local DISPEL_TRACKER_COOLDOWN_APPLICATION_ABILITY_ID = FourCC("A04Y")
    local DISPEL_UTILITY_LOCKOUT_SPELL_ID = FourCC("A051")
    local DISPEL_UTILITY_LOCKOUT_SPELLBOOK_ID = FourCC("A050")
    local DISPEL_TRACKER_CURRENTLY_EMITING_BUFF_ID = FourCC("B01D")
    local DISPEL_TRACKER_CURRENTLY_DISPELLED_BUFF_ID = FourCC("B01C")


    local TMP_WATER_ELEMENTAL = FourCC("hwat")

    local CHECKED_UNITS_VALUES = {
        [TMP_WATER_ELEMENTAL] = 6.9
        -- ...
    }

    -- Dispel checking.
    local function DispelCheck()
        BJDebugMsg("Checking for dispel...")
        target = GetTriggerUnit()
        local attacker = GetEventDamageSource()
        local damage = GetEventDamage()
        local AttackType = BlzGetEventAttackType()
        local DamageType = BlzGetEventDamageType()
        local WeaponType = BlzGetEventWeaponType()
        BlzSetEventDamage(0.0)
        local unit = GetTriggerUnit()
        local DispelTimer = CreateTimer()
        TimerStart(DispelTimer, 0.0, false, function()
            if UnitHasBuffBJ(unit, DISPEL_TRACKER_CURRENTLY_EMITING_BUFF_ID) then
                UnitAddAbility(target, DISPEL_UTILITY_LOCKOUT_SPELLBOOK_ID)
                UnitDamageTarget(attacker, target, damage, true, false, AttackType, DamageType, WeaponType)
                UnitRemoveAbility(target, DISPEL_UTILITY_LOCKOUT_SPELLBOOK_ID)
                BJDebugMsg("HE HAD THE BUFF SO HE TAKES DAMAGE!")
            else
                BJDebugMsg("DETECTED DISPEL; NOT SETTING THE DAMAGE BUT WE CAN DO OTHER STUFF BELOW")
                -- Would normally add the cooldown effect here.
            end
        end)
    end


    local DispelDamageDetectionTrigger = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(DispelDamageDetectionTrigger, EVENT_PLAYER_UNIT_DAMAGED)
    TriggerAddCondition(DispelDamageDetectionTrigger, Condition(function()
        BJDebugMsg("CHECKING DISPEL TRIGGER CONDITIONS")
        local unit = GetTriggerUnit()
        return CHECKED_UNITS_VALUES[GetUnitTypeId(unit)] ~= nil
        and GetUnitAbilityLevel(unit, DISPEL_UTILITY_LOCKOUT_SPELL_ID) ~= 1
       -- Would normally check the cooldown effect here.
    end))
    TriggerAddAction(DispelDamageDetectionTrigger, DispelCheck)

Currently, I haven't gotten around to actually applying the lockout effect because that will be relatively trivial compared to actually getting it to work.

Anyway, hope that helps somebody else, cheers.
 
Huge thanks! Honestly, seeing the 0.00s timer solve the damage before removal is a win for the books lol; it’s such a clean way to sidestep the engine's frame logic.

Regarding the ranged vs melee issue you hit when reapplying damage...if you're on Reforged, you might be able to use BlzGetEventWeaponType() to differentiate, but your UnitDamageTarget loop-back is a solid fix anyway.

One minor thought for the 'noise'...adding that frameScheduled flag we discussed would definitely keep the timer heap cleaner during heavy combat if we consider scaling. Really glad this worked out, tnx for the follow, back at you! :plol:
 
Periodic check using a timer created for each unit where this will come up?
I think this really is the only sensible clean solution.
and now we have to do cleanup every time a unit dies (extra overhead)
This is a rare event.

I’d love to get your eyes on this.
TimerStart(cooldownTimer, COOLDOWN_TIME, false, function()
if GetUnitTypeId(u) ~= 0 then -- Ensure unit still exists DispelWatchGroup = true end
I don't like how you call game-related stuff in script root (essentially before even config() had a chance to run). Most of the API stuff seems to work, but it is unspecified. Not to mention the GC misbehavior, that nobody understands to this day. I suggest to wrap all game object creation and trigger registration to run later, when the game is completely ready. "TimerStart(CreateTimer(), 0.1, false, function() ... end)" seems to work for singleplayer, though it still creates an object very early on (⚡Lua GC), something I would avoid out of fear for GC-related desyncs.
local function ProcessNextFrame()
for u, _ in pairs(NextFrameQueue) do
next and pairs do have an unspecified order. Lua 5.3 Reference Manual Practically speaking, this is eventually inconsistent aka desync.
TimerStart(CreateTimer(), 0.50, true, function()
for unit, isTracked in pairs(DispelWatchGroup) do
same as above. See: https://www.hiveworkshop.com/threads/syncedtable.332894/
-- Fast-Path: Damage Interceptor local damageTrigger = CreateTrigger()
for i = 0, GetBJMaxPlayerSlots() - 1 do
TriggerRegisterPlayerUnitEvent(damageTrigger, Player(i), EVENT_PLAYER_UNIT_DAMAGED, nil)
end
Same as above^3, doing game world stuff before the game has loaded.
and not IsUnitType(u, UNIT_TYPE_DEAD)
Doc - UnitAlive from common.ai can be used in map scripts without any preparation (Jass needs a native declaration). I can't tell with 100% certainty, but IsUnitType probably uses Unit.IsDead(), soooo it's identical. UnitAlive only takes 1 argument and has got to be nanoseconds faster :p
Lua:
local DispelTimer = CreateTimer()
        TimerStart(DispelTimer, 0.0, false, function()
            if UnitHasBuffBJ(unit, DISPEL_TRACKER_CURRENTLY_EMITING_BUFF_ID) then
                UnitAddAbility(target, DISPEL_UTILITY_LOCKOUT_SPELLBOOK_ID)
                UnitDamageTarget(attacker, target, damage, true, false, AttackType, DamageType, WeaponType)
                UnitRemoveAbility(target, DISPEL_UTILITY_LOCKOUT_SPELLBOOK_ID)
                BJDebugMsg("HE HAD THE BUFF SO HE TAKES DAMAGE!")
            else
                BJDebugMsg("DETECTED DISPEL; NOT SETTING THE DAMAGE BUT WE CAN DO OTHER STUFF BELOW")
                -- Would normally add the cooldown effect here.
            end
        end)
    end
You forgot to destroy the created timer inside
 
Thanks for the deep dive! I genuinely appreciate you taking a look, so it’s great to get the "audit" on this, since I'm peeking a bit in lua world ugh.

You’re 100% right on the initialization and the pairs order. I tend to write R&D snippets in "root" for quick testing, but for a real project, wrapping that in a proper init main is definitely the standard. Same with the iteration; while checking buff levels isn't usually order-dependent for desyncs, using ipairs or a synced array is a habit I should've stuck to even in a snippet.


As for the 0.00s interceptor, I’ve always found it to be such a useful ''bridge'' for these engine quirks. It’s definitely a "style" choice, but I like how it mimics the way legacy systems handle things without needing the heavy lifting of a 0.03s loop. I totally agree that periodic polling is the most robust catch-it-all, but I’m a bit of a geek for event-driven responsiveness when I can get it tbh.


If you have any more thoughts on the GC behavior with deferred-init....especially in Reforged I’m all ears. That’s a rabbit hole I still need to explore. :goblin_boom:
 
heavy lifting of a 0.03s loop. I totally agree that periodic polling is the most robust catch-it-all
The first two quotes were directed only at HydraulicHydra. I like your solution very much and without complete certainty prefer the catch-all periodic as you did, to catch what slips through the cracks. "Robust" as you said :peasant-victory:

Deferred call seems hacky at the surface, but does exactly what it's supposed to. Some places this isn't an option at all. It's cool.

If only I knew (or anyone ;) did) what's going on with GC... Desync due to Lua objects and GC (init phase)
 
I think this really is the only sensible clean solution.

This is a rare event.



I don't like how you call game-related stuff in script root (essentially before even config() had a chance to run). Most of the API stuff seems to work, but it is unspecified. Not to mention the GC misbehavior, that nobody understands to this day. I suggest to wrap all game object creation and trigger registration to run later, when the game is completely ready. "TimerStart(CreateTimer(), 0.1, false, function() ... end)" seems to work for singleplayer, though it still creates an object very early on (⚡Lua GC), something I would avoid out of fear for GC-related desyncs.

next and pairs do have an unspecified order. Lua 5.3 Reference Manual Practically speaking, this is eventually inconsistent aka desync.

same as above. See: https://www.hiveworkshop.com/threads/syncedtable.332894/

Same as above^3, doing game world stuff before the game has loaded.

Doc - UnitAlive from common.ai can be used in map scripts without any preparation (Jass needs a native declaration). I can't tell with 100% certainty, but IsUnitType probably uses Unit.IsDead(), soooo it's identical. UnitAlive only takes 1 argument and has got to be nanoseconds faster :p

You forgot to destroy the created timer inside
You are correct. Thank you. I will fix the timer.
 
Back
Top