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

Lightning spell does desync sometimes

Level 10
Joined
May 24, 2016
Messages
367
The ability is based on Pocket Factory. After casting, a Factory (nfac) is created (this is by defauth). The code waits for the projectile travel time, then finds the spawned Factory and stores its handle.


A periodic timer (0.03 s) is then started. Every tick it:


  • checks whether both the caster and the Factory are still alive;
  • if the caster is within the Factory's range, it creates (or updates) a lightning effect between the Factory and the caster;
  • if the caster leaves the range, it destroys the lightning;
  • if the caster enters the range again, it recreates the lightning;
  • while the lightning is active, it periodically restores mana to the caster;
  • when either the caster or the Factory dies, it destroys the lightning, visual effects, sound, flushes the hashtable, and stops the timer.

The issue: the desync does not occur when the lightning is created for the first time. It only happens after the lightning has been destroyed and then recreated.


Reproduction steps:


  1. Cast the ability and create the Factory.
  2. The lightning is created and works correctly.
  3. Move outside the Factory's range so the lightning is destroyed.
  4. Move back into range so the lightning is recreated.
  5. The game desyncs immediately after the lightning is recreated.

The first AddLightningEx() always works correctly. The desync appears to be related specifically to the DestroyLightning() → AddLightningEx() reconnection cycle, or to code executed during that transition.
JASS:
//Spell registration. Nothing strange here
// L_point, L_point2 are global locs that are declared before 
if spellid == 'ANsy' then
        set x = GetSpellTargetX()
    set y = GetSpellTargetY()

    set cx = GetUnitX(c)
    set cy = GetUnitY(c)

set dx = x - cx
set dy = y - cy

set L_point = GetUnitLoc(c)
set L_point2 = Location(x,y)

set dz = GetLocationZ(L_point2) - GetLocationZ(L_point)

set dist = SquareRoot(dx*dx + dy*dy + dz*dz)

call RemoveLocation(L_point)
call RemoveLocation(L_point2)

    set t = CreateTimer()
    set idT = GetHandleId(t)

    call SaveUnitHandle(Hash,idT,0,c)
    call SaveReal(Hash,idT,1,x)
    call SaveReal(Hash,idT,2,y)
    call SaveInteger(Hash,idT,7171,GetUnitAbilityLevel(c,'ANsy'))

    call TimerStart(t,dist/1000.+0.5,false,function MechaGenerator_L)

JASS:
function MechaGenerator_Destroy takes timer t returns nothing
    local integer idT = GetHandleId(t)

    local unit gen = LoadUnitHandle(Hash,idT,3)
    local lightning l = LoadLightningHandle(Hash,idT,66)
    local effect ef = LoadEffectHandle(Hash,idT,67)
    local sound s = LoadSoundHandle(Hash,idT,73)

    if gen != null and not IsUnitDead(gen) then
        call KillUnit(gen)
    endif

    if l != null then
        call DestroyLightning(l)
    endif

    if ef != null then
        call DestroyEffect(ef)
    endif

    if s != null then
        call StopSound(s,true,false)
        call KillSoundWhenDone(s)
    endif

    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t)

    set gen = null
    set l = null
    set ef = null
    set s = null
    set t = null
endfunction
    
    function MechaGenerator_FindGenerator takes unit caster, real x, real y, integer lvl returns unit
    local group g = CreateGroup()
    local unit u
    local unit result = null

    call GroupEnumUnitsInRange(g, x, y, 600., null)

    loop
        set u = FirstOfGroup(g)
        exitwhen u == null

        call GroupRemoveUnit(g, u)

        if GetUnitTypeId(u) == 'nfac' and GetOwningPlayer(u) == GetOwningPlayer(caster) and not IsUnitDead(u) then

            if lvl > 1 then
                call UnitAddAbility(u,'A04C')

                if lvl == 2 then
                    call SetUnitAbilityLevel(u,'A04C',3)
                else
                    call SetUnitAbilityLevel(u,'A04C',5)
                endif

                call SetUnitScale(u,1.2,1.2,1.2)
                call UnitRemoveAbility(u,'A04C')
            endif

            set result = u
            exitwhen true
        endif
    endloop

    call DestroyGroup(g)

    set g = null
    set u = null

    return result
endfunction


function MechaGenerator_L2 takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local integer idT = GetHandleId(t)

    local unit c = LoadUnitHandle(Hash,idT,0)
    local unit gen = LoadUnitHandle(Hash,idT,3)

    local lightning l = LoadLightningHandle(Hash,idT,66)
    local effect ef = LoadEffectHandle(Hash,idT,67)
    local sound s = LoadSoundHandle(Hash,idT,73)

    local real x = LoadReal(Hash,idT,1)
    local real y = LoadReal(Hash,idT,2)

    local real cx
    local real cy
    local real gx
    local real gy

    local real dist
    local real maxDistance = 600.

    local integer lvl = LoadInteger(Hash,idT,7171)

    local real manaRestore = 0.6

   // local location L_point
    //local location L_point2

    if lvl < 1 then
        set lvl = 1
    endif

    if lvl == 1 then
        set maxDistance = 650.
        set manaRestore = 0.6
    elseif lvl == 2 then
        set maxDistance = 850.
        set manaRestore = 0.85
    else
        set maxDistance = 1050.
        set manaRestore = 1.1
    endif

    // Кастер умер
    if c == null or IsUnitDead(c) then
        call MechaGenerator_Destroy(t)
        return
    endif

    // Первый тик — ищем генератор
    if gen == null then

        set gen = MechaGenerator_FindGenerator(c,x,y,lvl)

        if gen == null then
            call MechaGenerator_Destroy(t)
            return
        endif

        call SaveUnitHandle(Hash,idT,3,gen)

        // Запускаем периодический режим
        call TimerStart(t,0.03,true,function MechaGenerator_L2)

    endif

    // Генератор умер
    if IsUnitDead(gen) then
        call MechaGenerator_Destroy(t)
        return
    endif

    set cx = GetUnitX(c)
    set cy = GetUnitY(c)

    set gx = GetUnitX(gen)
    set gy = GetUnitY(gen)

    set dist = SquareRoot((gx-cx)*(gx-cx)+(gy-cy)*(gy-cy))
        // Кастер вышел из радиуса действия генератора
    if dist > maxDistance then

        if l != null then

            call DestroyLightning(l)
            call SaveLightningHandle(Hash,idT,66,null)

            if ef != null then
                call DestroyEffect(ef)
                call SaveEffectHandle(Hash,idT,67,null)
            endif

            if s != null then
                call StopSound(s,true,false)
                call KillSoundWhenDone(s)
                call SaveSoundHandle(Hash,idT,73,null)
            endif

        endif

    else

        // Создаем молнию первый раз
        if l == null then

            set L_point = GetUnitLoc(c)
            set L_point2 = GetUnitLoc(gen)

            set l = AddLightningEx("CLPB",true,GetLocationX(L_point),GetLocationY(L_point),GetLocationZ(L_point)+50.,GetLocationX(L_point2),GetLocationY(L_point2),GetLocationZ(L_point2)+50.)

            call RemoveLocation(L_point)
            call RemoveLocation(L_point2)

            call SetLightningColor(l,100./255.,60./255.,255./255.,1.)

            call SaveLightningHandle(Hash,idT,66,l)

            call DestroyEffect(AddSpecialEffectTarget("war3mapImported\\GearChangeBlue.mdx",c,"chest"))

            set ef = AddSpecialEffectTarget("war3mapImported\\RefreshingSurge.mdx",c,"chest")
            call SaveEffectHandle(Hash,idT,67,ef)

            set s = CreateSound("Abilities\\Spells\\Human\\CloudOfFog\\CloudOfFogLoop1.wav",true,true,true,10,10,"DefaultEAXON")
            call SetSoundVolume(s,60)
            call AttachSoundToUnit(s,c)
            call StartSound(s)

            call SaveSoundHandle(Hash,idT,73,s)

        else

            // Просто двигаем существующую молнию

            set L_point = GetUnitLoc(c)
            set L_point2 = GetUnitLoc(gen)

            call MoveLightningEx(l,true,GetLocationX(L_point),GetLocationY(L_point),GetLocationZ(L_point)+50.,GetLocationX(L_point2),GetLocationY(L_point2),GetLocationZ(L_point2)+50.)

            call RemoveLocation(L_point)
            call RemoveLocation(L_point2)

        endif

        // Восполняем ману
        call SetUnitState(c,UNIT_STATE_MANA,GetUnitState(c,UNIT_STATE_MANA)+manaRestore)

    endif
    
        set c = null
    set gen = null

    set l = null
    set ef = null
    set s = null

    set L_point = null
    set L_point2 = null

    set t = null
endfunction

SOLVED: Gear Change Effect the model is broken and causes desyncs.
 
Code:
call TimerStart(t,dist/1000.+0.5,false,function MechaGenerator_L)
In this context the value dist is based on a value computed using GetLocationZ which is not network safe (return result is not deterministic between clients). As such it will desync when dist has a different value between clients.
Code:
set dz = GetLocationZ(L_point2) - GetLocationZ(L_point)
set dist = SquareRoot(dx*dx + dy*dy + dz*dz)
To get around this all timing, physics and collision projectile logic must use just X and Y plane coordinates. Z should only be used for visual effects such as to position the lightning correctly or to arc projectiles sensibly.

As far as I am aware Warcraft III does this internally for all abilities, effectively being a 2D game with Z faked just for graphics.
 
Last edited:
Code:
call TimerStart(t,dist/1000.+0.5,false,function MechaGenerator_L)
In this context the value dist is based on a value computed using GetLocationZ which is not network safe (return result is not deterministic between clients). As such it will desync when dist has a different value between clients.
Code:
set dz = GetLocationZ(L_point2) - GetLocationZ(L_point)
set dist = SquareRoot(dx*dx + dy*dy + dz*dz)
To get around this all timing, physics and collision projectile logic must use just X and Y plane coordinates. Z should only be used for visual effects such as to position the lightning correctly or to arc projectiles sensibly.

As far as I am aware Warcraft III does this internally for all abilities, effectively being a 2D game with Z faked just for graphics.
still desyncs after fixing that. The player who cast that spell gets desynced

Creating lighning on a new unit (old ones created before do not cause desync) do that. Im still testing why
 
I tried reworking the spell, so Im sure the case of desync is right. Tried debugging the handle of units, knowing there's no difference beetwen clients.

I tried turning off systems like unit indexer and many other

And i never found the reason why it happens. I give up I guess. I turned off the lightning.
 
I recommend posting your full final code (code after the GetLocationZ fix) in case there are any other problems that someone might be able to spot. I admit I did not check beyond the GetLocationZ part as that was the first and most obvious desync hazard.
I lost it somewhere when got mad and decided to remove lightning at all (I leaved no code under //)


It does not even matter anymore what do I do with the lightning, since using it on a unit that was created before works fine. But simply creating a unit (I used hpea default peasant to make sure unit itself does not make harm) and creating a lightning (without or with a delay) does desyncs.

Moreover I could not even understand why the caster of the ability and only him gets desynced, while other players remain in the game lobby.

You can check the code I left on my first message. I believe it was okay anyway and nothing strange there.
 
You can check the code I left on my first message. I believe it was okay anyway and nothing strange there.
First snippet is invalid code as it is not a full function. It also references a function MechaGenerator_L which you did not provide. You provided MechaGenerator_L2 which I am guessing is what it was referencing in your actual code?

MechaGenerator_Destroy does not clear the used hashtable indices of the timer resulting in a leak. This can also potentially feed bad data into future calls if a timer happens to recycle a previous index.
 
First snippet is invalid code as it is not a full function. It also references a function MechaGenerator_L which you did not provide. You provided MechaGenerator_L2 which I am guessing is what it was referencing in your actual code?

MechaGenerator_Destroy does not clear the used hashtable indices of the timer resulting in a leak. This can also potentially feed bad data into future calls if a timer happens to recycle a previous index.
About MechaGenerator_L. That's a simple mistype since I was reworking the spell and forgot to edit it here on hiveworkshop. But the code itself is 100% the one Ive been using, MechaGenerator_L should be MechaGenerator_L2

MechaGenerator_Destroy might be problematic thx for pointing out. But since caster gets desynced before spell is over I dont think that was a problem itself.
 
There was a case of desync when assigning a new player color in an async way (inside GetLocalPlayer()). The root cause was determined to be the async load of a new texture:
0. Inside GetLocalPlayer block
1. Assign a player color that has not been loaded yet in the current game
2. Game loads the player color (a separate texture)
3. This creates a new handle or something
4. Desync

Are you sure it's not the same with this model?
 
There was a case of desync when assigning a new player color in an async way (inside GetLocalPlayer()). The root cause was determined to be the async load of a new texture:
0. Inside GetLocalPlayer block
1. Assign a player color that has not been loaded yet in the current game
2. Game loads the player color (a separate texture)
3. This creates a new handle or something
4. Desync

Are you sure it's not the same with this model?
That's a valid point, and I considered that possibility. However, in my case the lightning is created synchronously for all players, not inside a GetLocalPlayer() block. Also, the lightning works perfectly when attached to a unit that existed since map initialization (I used ally hero stored in HERO[1]). The desync only occurs when one endpoint is a newly created unit. I remember beetwen many of my test there were a few times when lightning DID WORK on a new unit, but it was when I completely disabled 90% (there is not much, but u can see mana restoration, special effects and ound effects) of the function except for add lightning and move lightning.
After that I thought I figured out the reason, and tried disabling the effects and sound effect - nah desync got returned. So I left confused and nothing more.

But lightning HERO[1] worked fine all that time.


So while it could still be related to lazy resource loading, the evidence currently points more toward an interaction between lightning and dynamically created units rather than asynchronous model loading.
 
Back
Top