| 01-24-2010, 04:15 PM | #1 |
Summary Some maps, such as tower defenses, are not sensitive to latency. If a player lags for five or ten or even twenty seconds it's better for them to lose the time than hold everyone else and wait at the lag screen [deal with your own lag!]. Host bots allow raising the lag ceiling this high, but doing so effectively hides if a player is actually lagging or not. This system detects when a player is falling behind, and displays a timer dialog showing how far behind they are (in game seconds). The system also implements a -wait command which, if the hosting bot supports it, will jumpstart the lag screen instead of waiting until the lag ceiling. The map can trigger this automatically at critical points if it wants. Effectively, this system lowers the cost of raising the lag ceiling. If your map uses it then a host bot can detect it and raise the lag ceiling higher than normal, which makes the game much less annoying if one player is lagging. Mechanics Every player keeps track of the elapsed game time and, twice per second, stores and syncs it in a game cache value (different for each player; they don't overwrite each other). When a player starts to lag, their synced values will take longer to reach the host than the syncs of the other players. A lagger's synced game time will fall behind, and the system just checks for that happening. JASS://///////////////////////////////////////////////////////////////////////// /// Asynchronous Lag Library /// v1.00 /// Strilanc /////////////////////////////////////////////////////////////////////////// /// This library is meant to complement using a host bot. Host bots tend to have higher /// limits on the length of time a player can lag before the game is held by the lag screen. /// This is generally a good thing, but it hides the fact that a player is lagging. /// /// This library detects players lagging and displays them using timer dialogs. The host bot /// does not need to support that aspect of the library. /// /// This library also provides a -wait command, which sends a signal the bot can catch in /// order to trigger the lag screen. The host bot must support this signal in order for the /// command to work. /////////////////////////////////////////////////////////////////////////// /// Notes: /// - Requires vJass in order to compile. /// - Just paste the code into the map and the library will do its thing. /// - You may call SignalBotToShowLagScreen() to force lag screens [if anyone is lagging] at critical points in the map. /// - This library uses SyncStoredInteger, which has been known to interact badly with TriggerSleepAction. You /// should use PolledWait or timers instead (GUI people: use "Wait (Game-Time)" instead of "Wait"). TriggerSleepAction /// has tons of problems anyways, so you should already not be using it. /////////////////////////////////////////////////////////////////////////// library HostBotAsyncLag initializer init globals private constant string WAIT_COMMAND = "-wait" private constant integer TICK_PERIOD = 500 //ms private constant integer MAX_DELAY = 2000 //ms private gamecache gc private timer array timers private timerdialog array dialogs private integer gameTime = 0 private string array colorCodes private constant string FILENAME = "HostBot.AsyncLag" private constant string TICK_KEY = "tick" private constant string WAIT_KEY = "wait" endglobals ///Signals any supporting host bot to hold the game until all lagging players catch up function SignalBotToShowLagScreen takes nothing returns nothing call SyncStoredInteger(gc, WAIT_KEY, WAIT_KEY) endfunction ///Determines the latest sync-received time from the player with the given id private function GetPlayerSyncReceivedTime takes integer id returns integer return GetStoredInteger(gc, TICK_KEY, I2S(id)) endfunction ///Advances and sync-sends the local time, without desyncing the local game cache private function AdvanceLocalTime takes integer dt returns nothing local integer id = GetPlayerId(GetLocalPlayer()) local integer syncedTime = GetPlayerSyncReceivedTime(id) set gameTime = gameTime + dt //send local time call StoreInteger(gc, TICK_KEY, I2S(id), gameTime) call SyncStoredInteger(gc, TICK_KEY, I2S(id)) //restore synced local time [to avoid desyncing cache values between players] call StoreInteger(gc, TICK_KEY, I2S(id), syncedTime) endfunction ///Determines the latest sync-received time from any player. [This value is the same for all players] private function ComputeSharedTime takes nothing returns integer local integer t = 0 local integer id = 0 loop exitwhen id >= 12 set t = IMaxBJ(t, GetPlayerSyncReceivedTime(id)) set id = id + 1 endloop return t endfunction ///Advances the local time and identifies any laggers, without desyncing the game private function OnTick takes nothing returns nothing //display any lagging players local integer dt local integer sharedTime = ComputeSharedTime() local integer i = 0 loop exitwhen i >= 12 call TimerDialogDisplay(dialogs[i], false) if GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(Player(i)) == MAP_CONTROL_USER then set dt = sharedTime - GetPlayerSyncReceivedTime(i) if dt > MAX_DELAY then //player is lagging call TimerDialogSetTitle(dialogs[i], "("+WAIT_COMMAND+") |cFF"+colorCodes[i]+GetPlayerName(Player(i))+"|r lag") call TimerStart(timers[i], dt/1000.0, false, null) call PauseTimer(timers[i]) call TimerDialogDisplay(dialogs[i], true) endif endif set i = i + 1 endloop call AdvanceLocalTime(TICK_PERIOD) endfunction private function init takes nothing returns nothing local trigger t local integer i //init ticker set t = CreateTrigger() call TriggerAddAction(t, function OnTick) call TriggerRegisterTimerEvent(t, TICK_PERIOD/1000.0, true) //init lag screen signalling command set t = CreateTrigger() call TriggerAddAction(t, function SignalBotToShowLagScreen) set i = 0 loop exitwhen i >= 12 call TriggerRegisterPlayerChatEvent(t, Player(i), WAIT_COMMAND, true) set i = i + 1 endloop //preload timers and dialogs set i = 0 loop exitwhen i >= 12 set timers[i] = CreateTimer() set dialogs[i] = CreateTimerDialog(timers[i]) set i = i + 1 endloop //init game cache call FlushGameCache(InitGameCache(FILENAME)) set gc = InitGameCache(FILENAME) call StoreInteger(gc, WAIT_KEY, WAIT_KEY, 0) //init color codes set colorCodes[0] = "FF0303" //red set colorCodes[1] = "0042FF" //blue set colorCodes[2] = "1CE6B9" //teal set colorCodes[3] = "540081" //purple set colorCodes[4] = "FFFC01" //yellow set colorCodes[5] = "FEBA0E" //orange set colorCodes[6] = "20C000" //green set colorCodes[7] = "FF00FF" //pink set colorCodes[8] = "808080" //grey set colorCodes[9] = "0080FF" //light blue set colorCodes[10] = "008000" //dark green set colorCodes[11] = "800000" //brown set t = null endfunction endlibrary |
| 01-24-2010, 08:04 PM | #2 |
Looks nice. One question though, if this is used when no bot is present, are there any side-effects? If so, is there a way to detect if a bot is present? That way, the script can be disabled for that hosting. |
| 01-25-2010, 02:35 AM | #3 |
Under a normal wc3 host, you might see the timer dialog for a fraction of a second before the lag screen pops up. Nothing serious at all. |
| 04-16-2010, 04:00 PM | #4 |
This seems to be more relocated to the duties of debugging than actually playing with all of the overhead of timers and timerdialogs. All of the timer dialogs seem to be a real hassle and would intrude upon lots of maps. Could you not just set a series of 12 global variables (not in an array, but a list, like DELAY_PLAYER_RED or something) and then give the user access to those globals to do with as necessary? I think that would be a better solution. |
| 04-16-2010, 05:08 PM | #5 |
Hmmm, yeah that could work. It is, essentially, a way to measure delay in real time. |
| 04-19-2010, 03:37 PM | #6 |
Exactly, but it should be as un-intrusive as possible for the actual map itself, while still giving the player that option. |
| 07-06-2010, 06:05 PM | #7 |
I finally got around to working on this. Basically I'm splitting the system into two pieces: - A 'LatencyMeasure' library, which just provides a GetPlayerLatency function and the machinations to make it work. - The Async Lag library, which has the timer dialog / host bot command part of it. |
| 07-20-2010, 07:58 PM | #8 |
I have a candidate update but I'm not sure if I'm satisfied with it. There's an awful lot of variance in the measurement, because of how it works. I'm going to have to think of ways to improve it. JASS://///////////////////////////////////////////////////////////////////////// /// Latency Measure Library /// Version: v1.01 /// Author: Strilanc /// Summary: Provides the ability to measure players' latency (command delay). /////////////////////////////////////////////////////////////////////////// /// == Functions == /// GetPlayerLatency(player) returns a smoothed estimate of a player's latency. /// GetPlayerRawLatency(player) returns a jumpy measurement of a player's current latency. /// /// == How does it work? == /// The library works by sending out periodic per-player game cache syncs containing the game /// time. This causes the time in game cache to lag behind the true game time based on their delay. /// So players' latencies are measured as the difference between their cached time and the current time. /// /// == Potential Pitfalls == /// - Requires vJass in order to compile. /// - The latency is measured in game seconds, not real seconds. The main side effect of this is that /// the measured latencies will never go higher than the time it takes for the lag screen to show up. /// - Game cache syncs can prematurely end TriggerSleepAction calls. Do not use TriggerSleepAction /// if you are using this library. Use PolledWait or timers instead (GUI people: use "Wait (Game-Time)" /// instead of "Wait"). TriggerSleepAction has tons of problems anyways, so you should already not be using it. /// - The system causes small amounts of network traffic. /// - The system trusts clients to send accurate game times. A malicious client could manipulate their own measurement, /// or even the measurements of other players. So *DO NOT* use this system for important effects like booting laggers. /////////////////////////////////////////////////////////////////////////// library LatencyMeasure initializer init globals private constant real TICK_PERIOD = 0.5 //seconds private constant real ESTIMATE_RETAIN_FACTOR = Pow(0.75, TICK_PERIOD) //factor per tick private gamecache gc private real array latencies private timer clock = CreateTimer() private constant string FILENAME = "Latency.gc" private constant string TICK_KEY = "tick" endglobals ///Returns the current volatile measurement of a player's latency function GetPlayerRawLatency takes player p returns real return TimerGetElapsed(clock) - GetStoredReal(gc, TICK_KEY, I2S(GetPlayerId(p))) endfunction ///Returns a smoothed estimate of a player's latency function GetPlayerLatency takes player p returns real return latencies[GetPlayerId(p)] endfunction ///Synchronously stores a local real in game cache. ///The value is set after the sync action bounces off the host. private function SyncStoreLocalReal takes gamecache gc, string missionKey, string key, real value returns nothing local real oldValue = GetStoredReal(gc, missionKey, key) call StoreReal(gc, missionKey, key, value) call SyncStoredReal(gc, missionKey, key) call StoreReal(gc, missionKey, key, oldValue) endfunction ///Periodically sync-stores the local time and updates latency estimates private function OnTick takes nothing returns nothing ///Adjusts latency estimates towards the current measured latencies. local integer i = 0 loop exitwhen i >= 12 set latencies[i] = latencies[i] * ESTIMATE_RETAIN_FACTOR set latencies[i] = latencies[i] + GetPlayerRawLatency(Player(i)) * (1 - ESTIMATE_RETAIN_FACTOR) set i = i + 1 endloop //sync-store the game time separately for each player call SyncStoreLocalReal(gc, TICK_KEY, I2S(GetPlayerId(GetLocalPlayer())), TimerGetElapsed(clock)) endfunction private function init takes nothing returns nothing local trigger t = CreateTrigger() call TriggerAddAction(t, function OnTick) call TriggerRegisterTimerEvent(t, TICK_PERIOD, true) call FlushGameCache(InitGameCache(FILENAME)) set gc = InitGameCache(FILENAME) call TimerStart(clock, 999999999, false, null) set t = null endfunction endlibrary |
| 07-21-2010, 08:17 PM | #9 |
nitpicker: JASS:
set latencies[i] = latencies[i] * ESTIMATE_RETAIN_FACTOR + GetPlayerRawLatency(Player(i)) * (1 - ESTIMATE_RETAIN_FACTOR)
JASS:
set latencies[i] = latencies[i] * ESTIMATE_RETAIN_FACTOR
set latencies[i] = latencies[i] + GetPlayerRawLatency(Player(i)) * (1 - ESTIMATE_RETAIN_FACTOR)
|
| 07-22-2010, 12:41 PM | #10 | |
Quote:
Seriously? I just don't like putting a huge constant name on the same line twice. |
