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

[vJASS] Desync problem

Status
Not open for further replies.
Hey guys! In one of my maps, there is a periodic event where a sunken city would appear in the ocean. Approaching it triggers a Cthulhu "boss" spawn and a small cinematic. The problem is that triggering this event causes a pretty nasty server split. Can anyone help me find what is causing it?

Here is the function related to the spawning:

JASS:
        static method spawn takes nothing returns nothing
            local real x = GetUnitX(.rlyeh)
            local real y = GetUnitY(.rlyeh)
            local integer i = 0
            local trigger t = null
            
            if .rlyeh == null then
                return
            endif
            
            call .endEvent()
            
            set t = CreateTrigger()
            
            set .darklord = CreateUnit(Player(11), 'cthu', x, y, 0)
            call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, COLOR_RED+"The presence of humans has awoken the mighty Cthulhu from his slumber!")
            call PlayThematicMusic("Sound\\Music\\mp3Music\\Doom.mp3")
            
            call cineMode(true)
            call endCineModeTimed(6)
            
            loop
                exitwhen i > 11
                call UnitShareVision(.darklord, Player(i), true)
                set i = i+1
            endloop

            call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, 1800, 0)
            call SetCameraTargetController(.darklord, 0, 0, false)
            
            call .setTarget()
            
            call TriggerRegisterUnitEvent(t, .darklord, EVENT_UNIT_DEATH)
            call TriggerAddCondition(t, Condition(function thistype.onDeath))
            
            call TimerStart(.cooldown, 10, true, function thistype.onConvertUnit)
            call TimerStart(.tick, 3, true, function thistype.update)
            set t = null
        endmethod

And here is the rest of the script:

JASS:
    struct cthulhu extends array
    
        static trigger spawnTrigger
        static group subjects
        static unit darklord = null
        static unit rlyeh = null
        static region r
        static rect rct
        static timer cooldown
        static timer tick
        static timer expire
        static boolean dead = false
        static real tx
        static real ty
        static integer array oldOwner
        
        static method endEvent takes nothing returns nothing
            call PauseTimer(.tick)
            call PauseTimer(.expire)
            call RegionClearRect(.r, .rct)
            //call DisableTrigger(.spawnTrigger)
            call KillUnit(.rlyeh)
            set .rlyeh = null
        endmethod
        
        static method filterRandomUnit takes nothing returns boolean
            local unit u = GetFilterUnit()
            local integer p = GetPlayerId(GetOwningPlayer(u))
            local boolean flag = p < 11 and IsUnitType(u, UNIT_TYPE_STRUCTURE) == false and GetUnitAbilityLevel(u, 'Avul') == 0 and GetUnitAbilityLevel(u, 'Aloc') == 0 and GetWidgetLife(u) > 0.5
            set u = null
            return flag
        endmethod
        
        static method convertUnit takes unit u returns nothing
            local integer p = GetPlayerId(GetOwningPlayer(u))

            set .oldOwner[GetUnitId(u)] = p
            call SetUnitOwner(u, Player(11), true)
            call GroupAddUnit(.subjects, u)
            call UnitAddAbility(u, 'mcob')
            call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, COLOR_YELLOW+GetUnitName(u)+":|r Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn!")
        endmethod
        
        static method onConvertUnit takes nothing returns nothing
            call GroupEnumUnitsInRange(ENUM_GROUP, GetUnitX(.darklord), GetUnitY(.darklord), 600, Filter(function thistype.filterRandomUnit))
            if FirstOfGroup(ENUM_GROUP) != null then
                call .convertUnit(FirstOfGroup(ENUM_GROUP))
            endif
            call GroupClear(ENUM_GROUP)
        endmethod
        
        static method releaseConvertedUnits takes nothing returns nothing
            local unit u = GetEnumUnit()
            local integer p = .oldOwner[GetUnitId(u)]
            
            call SetUnitOwner(u, Player(p), true)
            call UnitRemoveAbility(u, 'mcob')
            set u = null
        endmethod
        
        static method filterTarget takes nothing returns boolean
            return IsUnitType(GetFilterUnit(), UNIT_TYPE_STRUCTURE) and GetWidgetLife(GetFilterUnit()) > 0.5
        endmethod
        
        static method setTarget takes nothing returns nothing
            local integer i = 0
            local integer p = -1
            local unit target
            
            loop
                exitwhen i > 10
                set i = i+1
                if (GetRandomInt(1, i) == 1 or p == -1) and playerDefeated[i] == false then
                    set p = i
                endif
            endloop
            call GroupEnumUnitsOfPlayer(ENUM_GROUP, Player(p), Filter(function thistype.filterTarget))
            set target = GroupPickRandomUnit(ENUM_GROUP)
            set .tx = GetUnitX(target)
            set .ty = GetUnitY(target)
            set target = null
        endmethod
        
        static method update takes nothing returns nothing
            local real dx = GetUnitX(.darklord) - .tx
            local real dy = GetUnitY(.darklord) - .ty
            
            if (dx*dx)+(dy*dy) < 500*500 then
                call .setTarget()
            endif
            call GroupPointOrder(.subjects, "attack", GetUnitX(.darklord), GetUnitY(.darklord))
            call IssuePointOrder(.darklord, "attack", .tx, .ty)
        endmethod
        
        static method onEventTimeout takes nothing returns nothing
            call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, COLOR_GREEN+"BREAKING NEWS:|r The mysterious city has resubmerged into the ocean - it appears we will not learn its mysteries yet for some time.")
            call StartSound(gg_snd_Warning)
            call .endEvent()
        endmethod
        
        static method onDeath takes nothing returns boolean
            set .dead = true
            set .darklord = null
            call PauseTimer(.cooldown)
            call PauseTimer(.tick)
            call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, COLOR_GREEN+"BREAKING NEWS:|r The mightly Cthulhu has fallen back to the depths! Humanity is once again safe, for now..")
            call StartSound(gg_snd_Rescue)
            call ForGroup(.subjects, function thistype.releaseConvertedUnits)
            call GroupClear(.subjects)
            return false
        endmethod
    
        static method spawn takes nothing returns nothing
            local real x = GetUnitX(.rlyeh)
            local real y = GetUnitY(.rlyeh)
            local integer i = 0
            local trigger t = null
            
            if .rlyeh == null then
                return
            endif
            
            call .endEvent()
            
            set t = CreateTrigger()
            
            set .darklord = CreateUnit(Player(11), 'cthu', x, y, 0)
            call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, COLOR_RED+"The presence of humans has awoken the mighty Cthulhu from his slumber!")
            call PlayThematicMusic("Sound\\Music\\mp3Music\\Doom.mp3")
            
            call cineMode(true)
            call endCineModeTimed(6)
            
            loop
                exitwhen i > 11
                call UnitShareVision(.darklord, Player(i), true)
                set i = i+1
            endloop

            call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, 1800, 0)
            call SetCameraTargetController(.darklord, 0, 0, false)
            
            call .setTarget()
            
            call TriggerRegisterUnitEvent(t, .darklord, EVENT_UNIT_DEATH)
            call TriggerAddCondition(t, Condition(function thistype.onDeath))
            
            call TimerStart(.cooldown, 10, true, function thistype.onConvertUnit)
            call TimerStart(.tick, 3, true, function thistype.update)
            set t = null
        endmethod
        
        static method onEnter takes nothing returns boolean
            local unit target = GetTriggerUnit()
            
            if GetUnitAbilityLevel(target, 'Avul') == 0 and GetUnitAbilityLevel(target, 'Aloc') == 0 then
                call .spawn()
            endif
            
            set target = null
            return false
        endmethod
        
        static method startEvent takes nothing returns nothing
            local real x = 0
            local real y = 0
            local integer roll = GetRandomInt(0, 3)
            
            if roll == 0 then
                set x = GetRandomReal(GetRectMinX(gg_rct_NorthAtlantic), GetRectMaxX(gg_rct_NorthAtlantic))
                set y = GetRandomReal(GetRectMinY(gg_rct_NorthAtlantic), GetRectMaxY(gg_rct_NorthAtlantic))
            elseif roll == 1 then
                set x = GetRandomReal(GetRectMinX(gg_rct_SouthAtlantic), GetRectMaxX(gg_rct_SouthAtlantic))
                set y = GetRandomReal(GetRectMinY(gg_rct_SouthAtlantic), GetRectMaxY(gg_rct_SouthAtlantic))
            elseif roll == 2 then
                set x = GetRandomReal(GetRectMinX(gg_rct_PacificOcean), GetRectMaxX(gg_rct_PacificOcean))
                set y = GetRandomReal(GetRectMinY(gg_rct_PacificOcean), GetRectMaxY(gg_rct_PacificOcean))
            else
                set x = GetRandomReal(GetRectMinX(gg_rct_IndianOcean), GetRectMaxX(gg_rct_IndianOcean))
                set y = GetRandomReal(GetRectMinY(gg_rct_IndianOcean), GetRectMaxY(gg_rct_IndianOcean))
            endif
            
            set .rlyeh = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), 'rlye', x, y, 0)
            call DestroyEffect(AddSpecialEffectTarget("Objects\\Spawnmodels\\Naga\\NagaDeath\\NagaDeath.mdl", .rlyeh, "origin"))
            
            call MoveRectTo(.rct, x, y)
            call RegionAddRect(.r, .rct)
            call EnableTrigger(.spawnTrigger)
            
            call UnitShareVision(.rlyeh, GetLocalPlayer(), true)
            
            call PingMinimap(x, y, 10)
            call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, COLOR_GREEN+"BREAKING NEWS:|r A mysterious city has surfaced in the middle of the ocean. Who knows what secrets it might hide?")
            call StartSound(gg_snd_Warning)
            
            call TimerStart(.expire, 220, false, function thistype.onEventTimeout)
        endmethod
        
        static method onConsiderEvent takes nothing returns nothing
            if .darklord == null and .rlyeh == null and not .dead then
                if GetRandomInt(0, 4) == 1 then
                    call .startEvent()
                endif
            endif
        endmethod
        
        static method onInit takes nothing returns nothing
            local integer i = 0
            
            set .subjects       = CreateGroup()
            set .cooldown       = CreateTimer()
            set .tick           = CreateTimer()
            set .expire         = CreateTimer()
            set .r              = CreateRegion()
            set .rct            = Rect(-600, -600, 600, 600)
            set .spawnTrigger   = CreateTrigger()
            
            call TriggerRegisterEnterRegion(.spawnTrigger, .r, null)
            call TriggerAddCondition(.spawnTrigger, Condition(function thistype.onEnter))
            
        endmethod
    
    endstruct

Finally, here are the functions controling the cinematic functions:

JASS:
    function setGameCamera takes boolean flag returns nothing
        if flag then
            call ResetToGameCamera(1)
            call TimerStart(cameraTimer, 0.04, true, function onLoop)
        else
            call PauseTimer(cameraTimer)
        endif
    endfunction
    
    function cineMode takes boolean flag returns nothing
        call setGameCamera(not flag)
        call EnableUserControl(not flag)
        call EnableOcclusion(not flag)
        call ShowInterface(not flag, 1.5)
        call setGameCamera(not flag)
    endfunction
    
    private function onEndCineMode takes nothing returns nothing
        call ReleaseTimer(GetExpiredTimer())
        call cineMode(false)
    endfunction
    
    function endCineModeTimed takes real duration returns nothing
        call TimerStart(NewTimer(), duration, false, function onEndCineMode)
    endfunction

+rep to anyone who can help me out!
 
The onLoop function simply continously sets the player's camera height to a certain value. It runs both before and after the event, so i am positive that it's not to blame. It does not use "Pan camera as neccessary", if that is what you're asking.

My own theory is that it might have something to do with the way i create a trigger for when cthulhu dies. It could also be something related to the unit itself, as i know some units can cause desync when they spawn (though i'd rather try to find issues with the code before i make any such assumptions).
 
Not to be rude or anything, but local blocks are naturally the first thing i have checked. I never set variables within local block at all anywhere in the code.

About the shared vision, would it be better if i made it into a loop and shaded vision for each player in turn?

EDIT: I just realised, that the shared vision thingy happens when the city spawns, which runs flawlessly ingame. The desync happens when cthulhu spawns, but in the spawn trigger, i apparently went with the looping approach for the shared vision (probably added this at some point to check if this was the issue). It seems like GetLocalPlayer() is not at fault here.
 
Last edited:
call UnitShareVision(.rlyeh, GetLocalPlayer(), true)
... this shouldn't immediately desync, but will definitely be problematic for any vision checks done via the IsUnitVisible or related natives.

So I'd avoid that.

Other than that I can't seem to find anything that could cause a desync. I think you should try narrowing it down by commenting out methods one by one.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,201
... this shouldn't immediately desync, but will definitely be problematic for any vision checks done via the IsUnitVisible or related natives.
It should desync when ever the unit enters active range such that it can be manipulated by other units. Specifically something that needs vision might fail (eg an ability) on one client but not on another. If it is attackable this is especially the case as one client might have it acquired while another client not.

The reason is because the vision state of all players is tracked at all times by all clients. If you make something visible only for the local client player then the visibility states across clients no longer match.

Client 1's view.
Player 1 can see it
Player 2 cannot see it

Client 2's view.
Player 1 cannot see it
Player 2 can see it

If its sight allows a unit of Player 1 to attack another unit then it will desync because Client 1 will allow the unit to attack but on Client 2 the unit cannot attack as it has no vision.

The solution is to loop through all players and share vision separately for each. GetLocalPlayer can only be used as a shortcut for "all players" for UI related calls. Since UI state is not really tracked between clients and humans are the only ones with UI then setting it for each client at once is the same as setting for each player of each client. Display message is the most common one since displaying a message to not an active human slot is pointless (does nothing) then displaying to the client player will display it to all human players (anyone who matters), saving many calls.
 
Hmm, i understand how players having different vision can cause desync, but the GetLocalPlayer() call i used was not inside a local block, so it should be applied to all players anyway. I don't really see how the state would become different for any single player the way i'm using it. But if you say so, i can try changing it and see how that works.
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,201
GetLocalPlayer()
The native evaluates to the client's player. As such it returns a different player for each client when it is evaluated. It can only return active human players since they are the only players with a client.

Most trigger scripts are synchronous, they are executed in parallel by every client. Since the game (and triggers) are deterministic the result is the same state across all clients after execution of some triggers. This means that all clients are synchronized with each other reproducing the same state at roughly the same time (lag).

GetLocalPlayer is one of the natives that not deterministic between clients. Although still executed synchronously its value returned is different for every client. This can be used to run trigger scripts asynchronously (dialog visibility, multiboard visibility, messages etc) since it introduces asynchronous state into the game.

The problem starts if the asynchronous state is used to modify anything very important. For example if you used the result of GetLocalPlayer to create a unit with then all clients would have the unit owner state being different. If one player sends the unit an order or if it gets interacted by another player then the results of the game will differ between the clients. The game is no longer in synchronization ("out of sync"/OOS) so the host server then starts to boot players. The players which are booted are determined by the host and usually is the largest pool of players which are still in synchronization with each other.

Vision is one such state that has to be kept in synchronization across all clients since it is used by many important game mechanics. Units can only attack other units they see. Some orders must see the target point to be used. Buildings can only be placed if the area is revealed and not occupied by a ghost building. A player can only target a unit they see.

As such in most cases you will never want vision to be modified asynchronously such as by GetLocalPlayer since that would more than likely end up in a OOS situation unless extreme care is used.
 
Status
Not open for further replies.
Top