[JASS] A really really ridiculously easy way to sync data / new GetHost command

Level 3
Joined
Aug 19, 2007
Messages
24
Okay, disclaimer: this is a fairly technical discussion. To understand it, you should have:
- A moderate level of understanding of JASS - specifically, knowledge of gamecaches and the definition of "asynchronous" as applied to WC3 and JASS
- A basic level of understanding of the way data is transferred across the internet, specifically basic knowledge of packets and latency

After spending way too much time with WireShark, I finally figured out how some of the sync functions work.

I apologize if this has been posted before - I remember that there was a pretty large sync discussion a long time ago where someone used a SyncSelections system to sync integers. I couldn't find it, nor could I find any useful data regarding syncing so I figured it'd be safe to post.

Anyway, here are my findings.

0. You do not need to use TriggerSyncStart or TriggerSyncReady to sync values. These functions are used for something else (see below).
1. The SyncStoredX (X=Integer, String, etc) functions broadcast the stored value to the host.
2. If the host is activating the function, it will wait the full latency time and send an outgoing sync value subpacket (referred to as 'packet' from here on out, even though it's actually a subpacket) to all clients. This packet doesn't seem to have any effect on the clients whatsoever. I think the reason is due to the player identifier on the packet (that of the host, not the receiving player)
3. If the host receives the client packet before it sends it own packet then it still sends the packet in (2).
4. If the host receives a packet from a client syncing the value before it waits the latency time (including before it even activated the SyncStoredX function) then it "adopts" that value. If it waits the full latency time without receiving a client packet then it "adopts" the host's value. Once the host "adopts" a value it ignores further incoming sync value packets for that same function call (possibly others using the same value, not tested but probable).
4a. I think that the host receiving the client packet before the latency cycle expires (and thus adopting the host's value) is the reason that the old GetHost function fails on occasion - "fast" clients are given priority over the host.
5. The host then sends out another sync packet to each client with the correct value. This packet has the correct player identifier (that of the client).
6. Each client "adopts" the value upon receiving the host's final packet.

What does this mean?
1. You can call SyncStoredX "asynchronously" and it will not cause a desync. In fact, it's guaranteed not to cause a desync because it actually does sync with the host and all clients.
2. Any player that calls SyncStoredX (which you can control using GetLocalPlayer) attempts to broadcast the stored value to every player.
3. SyncStoredX takes a total of 2 latency cycles to complete. The first is the client sending the packet. The second is the host sending the response. This means that syncing is not practical on a fast repeating timescale - for normal battle.net games, the fastest you can expect is 250ms (if you hit the first latency cycle exactly) while typical times will be around 375 ms.
4. Because of (3), synced data is not available immediately. You will have to wait for the time delay mentioned above. You can get around this, see below.

TriggerSyncStart / TriggerSyncReady
0. I haven't tested these functions in as much detail as the others, so my descriptions of packet mechanics may be incorrect. The effect of TriggerSyncReady, however, is perfectly accurate.
1. I have no idea what TriggerSyncStart does. I have tested these two functions together and separately and TriggerSyncStart doesn't seem to do much.
2. As near as I can tell, TriggerSyncReady sends a packet to the host and stops gametime completely (this part is definitely true). The host then sends a response packet and re-enables gametime. The client resets its gametime to that of the response packet (which will be sometime after that of the host).
3. I don't know what happens if you call TriggerSyncReady coupled with GetLocalPlayer, but my guess is that it would only perform the above effects for clients that called the function and wouldn't affect the host calling the function at all.

What does this mean?
1. If you use SyncStoredX, you can use TriggerSyncReady immediately afterwards to force everyone to wait until TriggerSyncReady finishes its job. After that, the stored value from SyncStoredX will be synced.
2. TriggerSleepAction and TriggerSyncReady don't seem to like each other
3. Again, not good for a fast repeating action like an animation.

Usage example:
JASS:
function PlayerBroadcastStoredInteger takes player p, gamecache gc, string missionkey, string key returns nothing
  if GetLocalPlayer() == p then
    call SyncStoredInteger(gc, missionkey, key)
  endif
endfunction
This function will force all players to adopt the value that player p has as a stored integer.

JASS:
function PlayerForceStoredInteger takes player p, gamecache gc, string missionkey, string key returns nothing
  if GetLocalPlayer() == p then
    call SyncStoredInteger(gc, missionkey, key)
  endif
  call TriggerSyncReady()
endfunction
This function will force all players to adopt the value that player p has as a stored integer AND guarantees that everybody has the same data all the time

You can exploit this effect to sync all kinds of asynchronous data. For this next example, I'm going to assume that GetCameraTargetPositionX is asynchronous since it doesn't have a player as an argument and I'm pretty sure I remember people saying that it desyncs. I think this is true, but it might not be.

Please excuse the ridiculous function name.
JASS:
function MyGameCache takes nothing returns gamecache
  //use whatever gamecache-getting function you want here
  if udg_my_gamecache == null then
    call FlushGameCache(InitGameCache("mygamecache"))
    set udg_my_gamecache = InitGameCache("mygamecache")
  endif
  return udg_my_gamecache
endfunction

function GetPlayerCameraTargetPositionX takes player p returns real
  if GetLocalPlayer() == p then
    call StoreReal(MyGameCache(), "cam"+I2S(GetPlayerId(p)), "targetx", GetCameraTargetPositionX())
    call SyncStoredReal(MyGameCache(), "cam"+I2S(GetPlayerId(p)), "targetx")
  endif
  call TriggerSyncReady()
  return GetStoredReal(MyGameCache(), "cam"+I2S(GetPlayerId(p)), "targetx")
endfunction
This function should get a player's camera position with no chance of desync. Note that I don't actually recommend using this function, since you will probably want to get X,Y,Z of camera values and it's much more efficient to only call one TriggerSyncReady per "batch" of data - you could get every player's camera data using a loop with GetLocalPlayer then call TriggerSyncReady at the end of execution to ensure that all the data is synced.

Note that it's possible to use these functions to create a "perfect" GetHost command by using some timers and exploiting the fact that the SyncStored commands are not instantaneous. I found some strange errors with the timers that are probably related to the game's KeepAlive packets - but the errors are easily recognizable and you can just have the function repeat itself until it doesn't detect them (it takes about 5 seconds on average). Here's the entire trigger that I was using for testing.

JASS:
//gamecache udg_PINGCACHE = null
//integer udg_PING = -1
//integer udg_HOSTID = -1
//timer udg_PINGWAIT = CreateTimer()

function GetPingCache takes nothing returns gamecache
    if udg_PINGCACHE == null then
        call FlushGameCache(InitGameCache("pingcache"))
        set udg_PINGCACHE = InitGameCache("pingcache")
    endif
    return udg_PINGCACHE
endfunction

function SetPings takes nothing returns nothing
    local integer i = 0
    local integer lowest = -1
    local integer second
    local integer ping
    loop
      exitwhen i == 12
        //note that these numbers are NOT REAL PING - they are indicative of some combination of latency cycles and actual ping.
        //the important thing is that the host's numbers are either lower than the clients or they are exactly the same (weird error, see below)
        set ping = GetStoredInteger(GetPingCache(), "ping", I2S(i))
        if ping >= 0 and (lowest < 0 or ping < lowest) then
            set udg_HOSTID = i
            set lowest = ping
            set second = -1
        elseif ping == lowest then //because of weird error, see below
            set udg_HOSTID = -1
            set second = i
        endif
      set i = i + 1
    endloop
    if second == -1 then
        call BJDebugMsg("Host found : "+GetPlayerName(Player(udg_HOSTID)))
        call PauseTimer(GetExpiredTimer())
    else
        call ExecuteFunc("FindHost")
    endif
endfunction

function WaitSync takes nothing returns nothing
    if GetStoredBoolean(GetPingCache(), "waitdone", I2S(GetPlayerId(GetLocalPlayer()))) then
        call StoreInteger(GetPingCache(), "ping", I2S(GetPlayerId(GetLocalPlayer())), udg_PING)
        call SyncStoredInteger(GetPingCache(), "ping", I2S(GetPlayerId(GetLocalPlayer())))
        call PauseTimer(GetExpiredTimer())
        call TimerStart(GetExpiredTimer(), 0.3, false, function SetPings) //value of 0.3 is timeout length (more than enough for the host)
    else
        set udg_PING = udg_PING + 1
    endif
endfunction

function FindHost takes nothing returns nothing
    local integer i = 0

    loop
      exitwhen i == 12
        call StoreInteger(GetPingCache(), "ping", I2S(i), -1)
      set i = i + 1
    endloop
    
    call TriggerSyncStart()
    call TriggerSyncReady()
    call TimerStart(udg_PINGWAIT, 0.001, true, function WaitSync)
    
    set udg_PING = 0
    call StoreBoolean(GetPingCache(), "waitdone", I2S(GetPlayerId(GetLocalPlayer())), true)
    call SyncStoredBoolean(GetPingCache(), "waitdone", I2S(GetPlayerId(GetLocalPlayer())))
    call StoreBoolean(GetPingCache(), "waitdone", I2S(GetPlayerId(GetLocalPlayer())), false)
    //check to make sure that this doesn't have an inherent wait time, might be able to circumvent 1st timer
endfunction

function GetHost takes nothing returns player
    if udg_HOSTID >= 0 then
        call BJDebugMsg("The host is "+GetPlayerName(Player(udg_HOSTID)))
        return Player(udg_HOSTID)
    else
        return null
    endif
endfunction

function Trig_New_Host_Stuff_Actions takes nothing returns nothing
    call BJDebugMsg("Attempting to determine host...")
    call FindHost()
endfunction

function InitTrig_New_Host_Stuff takes nothing returns nothing
    local trigger tr = CreateTrigger()
    //call TriggerRegisterTimerEvent(tr, 5, true)
    call TriggerRegisterPlayerEvent(tr, Player(0), EVENT_PLAYER_ARROW_UP_UP)
    call TriggerAddAction(tr, function Trig_New_Host_Stuff_Actions)
endfunction

I was attempting to create an in-game ping command. I came up with some numbers that are probably related to ping, but the client pings were way too high to be ping (300+ms on LAN) so it's probably related to the latency cycles. Note that the numbers given for the host vary a lot on LAN but are very consistent on BNet, probably due to the difference between TCP and UDP. The weird errors I mentioned above actually cause the time between resetting the pingwait boolean and sync to be exactlythe same between host and client, so as I said above it's pretty easy to detect. I'm pretty sure it's related to the keep alive packets (and thus the latency cycles) since I noticed very periodic behavior with the raw numbers.

Anyway, enjoy yourselves.
 
Level 3
Joined
Aug 19, 2007
Messages
24
Packet analysis and rudimentary function testing was done using 1 host and 2 clients, host in IL, clients in NY and montreal (montreal guy has a very mediocre CPU) 3 or 4 times.

Test had each player clear key values 0-11, set a key value equal to their local player ID to 1, then sync the local key value. After 1 second each player output the contents of key values 1-12. All players showed a value of 1 for each player, indicating that the values were synced individually. Syncing all 12 key values instead of just the local value caused the host's key to be 1 and everyone else's to be 0, similar to how the GetHost command is supposed to work.

Additionally, I analyzed packets when only one player synced the value using [ if GetPlayerId(GetLocalPlayer()) == 1 ] using both the host and the clients. In the case of the client, a packet was received by the host containing the client's synced value (I used 4-letter words converted to hex so I could search for them easily) which was then re-sent to each client after waiting the latency period. Both of these packets had the client's player ID attached. In the case of the host, I noticed two outgoing packets to each client; the one that I mentioned "doesn't seem to do anything" which had the host's player ID attached and the one that had the client's player ID attached. No incoming packets containing the data were observed in that case.

The functions mentioned in the other post were tested over LAN and BNet (using 2 computers on a LAN) using 1 host and 1 client with very different hardware maybe 15-20 times, but need further testing with more clients attached.

I should mention that the old GetHost command returned the LAN client computer 100% of the time whereas the "fixed" one returned the LAN client computer 0% of the time. Packet analysis on the host computer showed that the client's incoming packet was appearing before the host's first outgoing packet (occasionally on LAN) or immediately after the host sent out the first outgoing packet (occasionally on LAN and always on BNet). This was using a SyncStoredInteger command without any GetLocalPlayer.

I encourage other people to test it. It's pretty simple to pair GetLocalPlayer with SyncStoredX. I don't really have the time or the friends to do enough rigorous testing right now.
 
Level 8
Joined
Aug 6, 2008
Messages
451
The ability to sync camera data would be more than awesome, but since it is not instant it cant really be used for camera systems or stuff like that.
Damn. :(
 
Top