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:
This function will force all players to adopt the value that player p has as a stored integer.
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.
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.
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.
- 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
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
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
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.