Maelstrom: Implemented ping system to catch players that dropped out and give a network health indicator.

https://github.com/libsdl-org/Maelstrom/commit/9b4a7d3c7279cf59f2e1228114f9e36a934954ff

From 9b4a7d3c7279cf59f2e1228114f9e36a934954ff Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 6 Nov 2011 18:04:26 -0500
Subject: [PATCH] Implemented ping system to catch players that dropped out and
 give a network health indicator.

---
 Images/ping1.bmp      | Bin 0 -> 2742 bytes
 Images/ping2.bmp      | Bin 0 -> 2742 bytes
 Images/ping3.bmp      | Bin 0 -> 2742 bytes
 UI/lobby.xml          | 102 ++++++++++++++++++-------
 netlogic/gameinfo.cpp | 111 ++++++++++++++++++++++++---
 netlogic/gameinfo.h   |  47 +++++++++++-
 netlogic/lobby.cpp    | 173 +++++++++++++++++++++++++++++++++++++++---
 netlogic/lobby.h      |   4 +
 netlogic/netplay.cpp  |   2 +-
 netlogic/protocol.h   |  15 +++-
 10 files changed, 399 insertions(+), 55 deletions(-)
 create mode 100644 Images/ping1.bmp
 create mode 100644 Images/ping2.bmp
 create mode 100644 Images/ping3.bmp

diff --git a/Images/ping1.bmp b/Images/ping1.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..b9c84ffd70e2968381980e326b82876e7a44346b
GIT binary patch
literal 2742
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
Y<<}q7KdNr49T~<d90b;WrY#5iAD~)a+W-In

literal 0
HcmV?d00001

diff --git a/Images/ping2.bmp b/Images/ping2.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..40a242832f5972a8661e13a87e3762e3cf076efa
GIT binary patch
literal 2742
zcmbW1dr*{B6u`-u`lGU@f2t`f8&d&+fkq%hK!b{5hAFhrkV$05;SoCEB2XZ&l*a__
zlz_{76`_C(WD}MJ7iSk2me+2;E)EOAhy-KMx+@Qrh3(w$>~~prNt^C`-}%lx_nhB3
z_w{`g(Db|w)7(JX!n+yXP4GTr^EyZyn=}~jcnM#|0Agiz^{J0xWd$Ci(WqN|Ff=y4
z3YyV~#;3-bI~e{w7XS|>q&{cA^94uuXSX_K6qaBL^Y9gD<^+rG>gu|DtUQo&YUAF(
z?T2IE3yyxv|A5Uq_8A2v#gl**d}a}dKQ^p{Sr*OI%&T8U?9937UDEtveuKk_tT+9F
zHafU7e3ScHAZvJx!(i8~A0TxdJ$9)@!B=_{+Xjxc{IRE0xa|mM!&c|X*}qLw02VFG
zLTAdx%>qdqwE*G9@R$qt*OcojUf)<&$M9#hGP~%s4R&si01B(hPMn4yXU)#6JQQzU
zKX%aqDZC5nV?@dy<s(V$L%#fLZy!3oFXmgY69^kfb&SF;nHT$@opw<HH{jgS-uEpN
z`c$;-E)l*FneK6v1E&-o@Bs%?2vrbh)wSj$9Qe>4EgPCuw+clviE0EbC<z=<<ij+y
zusTlA0#1xf<$c2=L7DlF1Idi;YIO(Cmd|Q+pyL!knxn95ni`YQA_W90)x(>Q#Jgnj
zKRH`DI6g_UG%M60l`Zj-k$_zO^j~wxHx&<`b?ax30lGm5(7Y3J-cLL4n#FhL*17!D
z@NtnKtft++tSO|vE3&Eogh-hvR^^F0!4sIIVD<qB>TC70#{ewL1_Sczx`(#J<+*2H
z_<&pM!Ta5lFZQ`4*<U9+e5E_`YF}KdGFGUV&@I9#U8dzh@%Z3GXqPPjikbX#TC3Bo
zFh4S>=C)fF-yy5giznDy(dJhr`J(p5*Nr_P4e})M;N68KT(pThQ*+vT5J46h$rgO3
z0C3ja8h_a<!11f_#f1foeP}S0O8P%7xV)#l)#rkEe~mPv?z(t%3PyneQmq*c;_`#C
z&+GMiG@7TNn+`*|`Gr?J{o(p=k4Ux+iI(;%z)0Kt{QNz=p5{e+b(1KpsFtU^T{18k
z$}NX$f#-y*KeOg^Q@~%lu=Ju|@U~EneN>7=Z04r0#Lypj@<BCCVXidFdQX;ApA^V)
z<)iuC8eZ>cv2p?&C=e8!2;M}{VN|LbdF?=yZRDvP$1)r_**g-r&fk}LW|e){C@Ja~
zsFDv>$OdzSonaM%l-9mD@eur%vO9+Jdo%?-8tu|UQa^ympNVWVG*S{uW_O@BIy37W
zmGo|SvQz9Cr}$i#)I#^%YS(iOyJ3H;rTgk+2Y>5|XjYIT+M-BqA4->~@;iqgEE^ye
z;FLzlgw3R2G!QroP}2A{_TQv#Iey0Nbm^|#Dv$hnuQHME#rDsCkp^Fu9ToJ(TpJ1(
zbjJxguPSenXd^i}l!gkGM1hyd1rn|Rgup2+ZWQg}oOVtxcFwDDKG*2VYx1dR4g6UW
ze5oy}qF$s>>+jElC!T){C%IS-)j=UTdFg!C)S2mNxpJVjyI<JV*WN#%P~V)Jon<0S
zKOa*NeAsAf1^uZsts3!X7m$uIis_zO0nrQjB%fJ>A?#5f+Fd^f>xY4CZ;fbf$I&N9
z2(uVPi?y9%*B!Ee{EsfpOYZ_rL=|hS^$_JyXF5X|0yz0aFn>0hh<aKp|AmkH2V0N8
Fe*iMDTs;5)

literal 0
HcmV?d00001

diff --git a/Images/ping3.bmp b/Images/ping3.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..7ecfd82523886bb8c488ceb68966680609aaf1fa
GIT binary patch
literal 2742
zcmc&!T}YEr7+!*2Bz7sGDT1OI5>0dyr*5|SgCL?46CDa(Imqx2S&YoQkuJOt!i*$V
z-nAmZa7Jb^(|$}_ITI91(lCaFmRZg@d(V0Io$vT<sdpXD_xhaYJn!?q=j{7DTR3=B
zMu!68NqA!5IRVcR*>Q+6SsUy>KZXw_AnfdT51by)4$R%%-Szc9PUk1D7vL_E2j=nu
zkLRDYz_7CNa!5!@Xs9wc_;h>wO)SBu?~uVeNS5A!{@8hO@uje^;)H~TxVZCCQRif`
zQ&?hOLy14R1geLK0Gc7!GC%({JiIm~<&IiyOHc1lO6rJ=EDH-$!WLCxAmTuSoe2CH
z76F)8trLlfH!3RLm`v~M>fUKIL-FxVK|yDhmcG*x=!_97@R+pCCToB(;4n6}AeY~3
zZJp`qS?K7PEh-vMOl%7ZQn=k-7^sAUvlIX@IG)@*PIjL>10~ez2le%iuC6(=*{Rb#
ziH^QdQgRV^YC>`af6)iGm?0GqnPh?te3qAg>U2X|?QllMP(;KPm8uEeQkXOgn#C*z
z0xz)*K}<Ag4*oFQZja6OvAcWRZl6O2Bh{lbL>!RIhVMiYb9z6d^U2A%va)M?4GHgZ
zt(TYIT3uZS3~gXDG;7awaotMaA4qgKW@BPn<Z?@HZr}9u602gfeJ~gvwY1C?7Ft$T
zzM((sSU3?9l3zr8m%vS}zM7nTJvG&$R9fZo{+yhFnwqBt1%n2|OQUhBvC&~N&2)CY
zz#~8voaoPCa$Q0X<dwriL6+6(jE(J7DsQKy-Bqa`W@e6PwRXLJyt?|e(Ky-M>}Y6s
zySTVX8}(&DF17MCk4rRX$43ApfYH&<3WX&psV^&QAUk_FKi^(jI#yLRVKBTho2Ol_
z-=F~#J{La<p^(@u@Pdq-0x*`?+Vb@FPUYp@*JvJVwIh1{Xl?DYk&!PT02!LITm%rY
zXNWKvDfp9AmcG4B4gzCiV`FXY$L8iQT;eN78vz4fZv+VxmY_=?iHSlKQ=m!7B?ubw
zSMU}&Rzjryfl$e9;<<dpK0Yjw0nQ{a^jH3Of7Sp1e!v0vC1QUP8Gj!1{1)KD49wz{
zybsR<W?}{;0{^AI@<4x<f(OF790`yNtc4gOuS9{r3^eo;fd3WzCYl9^OvLsi&d39a
zJbpD0=~qm?AmpixgUs%*!1v?g_ly2Q6>=6@Nc3Wr7+`zI@DaA)I+-WB@sXZI{7=q%
P@)%3_Ig{Q52ZH|s2vxwF

literal 0
HcmV?d00001

diff --git a/UI/lobby.xml b/UI/lobby.xml
index 35787853..e2745581 100644
--- a/UI/lobby.xml
+++ b/UI/lobby.xml
@@ -25,7 +25,7 @@
 
 		<Area name="gamelist">
 			<Elements>
-				<DialogLabel name="ping_label" text="Ping">
+				<DialogLabel name="ping_label">
 					<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" x="398" y="46"/>
 				</DialogLabel>
 
@@ -42,9 +42,15 @@
 						<DialogLabel name="host" text="bwg101.corp.pacbell.net">
 							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="join" x="120"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="32ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="12"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="12"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 
@@ -61,9 +67,15 @@
 						<DialogLabel name="host" text="bwg101.corp.pacbell.net">
 							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="join" x="120"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="32ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="42"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="42"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 
@@ -80,9 +92,15 @@
 						<DialogLabel name="host" text="bwg101.corp.pacbell.net">
 							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="join" x="120"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="32ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="72"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="72"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 
@@ -99,9 +117,15 @@
 						<DialogLabel name="host" text="bwg101.corp.pacbell.net">
 							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="join" x="120"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="32ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="102"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="102"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 
@@ -118,17 +142,23 @@
 						<DialogLabel name="host" text="bwg101.corp.pacbell.net">
 							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="join" x="120"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="32ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="132"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="132"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 			</Elements>
 		</Area>
 		<Area name="gameinfo">
 			<Elements>
-				<DialogLabel name="ping_label" text="Ping">
-					<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" x="398" y="46"/>
+				<DialogLabel name="ping_label">
+					<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" x="398" y="50"/>
 				</DialogLabel>
 
 				<Area name="player1">
@@ -157,9 +187,15 @@
 						<DialogLabel name="host" text="bwg101.corp.pacbell.net">
 							<Anchor anchorFrom="TOPLEFT" anchorTo="BOTTOMLEFT" anchor="name" y="2"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="32ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="12"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="12"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 
@@ -189,9 +225,15 @@
 						<DialogLabel name="host" text="192.168.0.101">
 							<Anchor anchorFrom="TOPLEFT" anchorTo="BOTTOMLEFT" anchor="name" y="2"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="77ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="58"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="58"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 
@@ -221,9 +263,15 @@
 						<DialogLabel name="host" text="192.168.0.101">
 							<Anchor anchorFrom="TOPLEFT" anchorTo="BOTTOMLEFT" anchor="name" y="2"/>
 						</DialogLabel>
-						<DialogLabel name="ping" text="18ms">
-							<Anchor anchorFrom="TOP" anchorTo="BOTTOM" anchor="ping_label" y="104"/>
-						</DialogLabel>
+						<Image name="ping1" image="Images/ping1.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping_label" y="104"/>
+						</Image>
+						<Image name="ping2" image="Images/ping2.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
+						<Image name="ping3" image="Images/ping3.bmp">
+							<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ping1"/>
+						</Image>
 					</Elements>
 				</Area>
 			</Elements>
diff --git a/netlogic/gameinfo.cpp b/netlogic/gameinfo.cpp
index bcb873de..48c23ca4 100644
--- a/netlogic/gameinfo.cpp
+++ b/netlogic/gameinfo.cpp
@@ -37,8 +37,9 @@ void
 GameInfo::SetHostInfo(Uint32 gameID, const char *name)
 {
 	this->gameID = gameID;
-	players[0].playerID = gameID;
-	SDL_strlcpy(players[0].name, name, sizeof(players[0].name));
+	players[HOST_PLAYER].playerID = gameID;
+	SDL_strlcpy(players[HOST_PLAYER].name, name,
+			sizeof(players[HOST_PLAYER].name));
 }
 
 void
@@ -48,10 +49,15 @@ GameInfo::CopyFrom(const GameInfo &rhs)
 	deathMatch = rhs.deathMatch;
 
 	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		players[i].playerID = rhs.players[i].playerID;
+		SDL_strlcpy(players[i].name, rhs.players[i].name,
+			sizeof(players[i].name));
 		if (players[i].address != rhs.players[i].address) {
-			// FIXME: reset the ping info
+			players[i].address = rhs.players[i].address;
+
+			// Reset the ping info
+			InitializePing(i);
 		}
-		players[i] = rhs.players[i];
 	}
 	UpdateUI();
 }
@@ -83,8 +89,8 @@ GameInfo::ReadFromPacket(DynamicPacket &packet)
 
 	// We want to get the public address of the server
 	// If we already have one, we assume that's the fastest interface
-	if (!players[0].address.host) {
-		players[0].address = packet.address;
+	if (!players[HOST_PLAYER].address.host) {
+		players[HOST_PLAYER].address = packet.address;
 	}
 
 	return true;
@@ -107,17 +113,27 @@ GameInfo::WriteToPacket(DynamicPacket &packet)
 void
 GameInfo::BindPlayerToUI(int index, UIElement *element)
 {
+	char name[32];
 	GameInfoPlayer *player = &players[index];
 
+	if (player->UI.element == element) {
+		return;
+	}
+
+	player->UI.element = element;
 	player->UI.enabled = element->GetElement<UIElementCheckbox>("enabled");
 	player->UI.name = element->GetElement<UIElement>("name");
 	player->UI.host = element->GetElement<UIElement>("host");
-	player->UI.ping = element->GetElement<UIElement>("ping");
 	player->UI.control = element->GetElement<UIElementRadioGroup>("control");
 	player->UI.keyboard = element->GetElement<UIElement>("keyboard");
 	player->UI.joystick = element->GetElement<UIElement>("joystick");
 	player->UI.network = element->GetElement<UIElement>("network");
 
+	for (int i = 0; i < NUM_PING_STATES; ++i) {
+		SDL_snprintf(name, sizeof(name), "ping%d", i);
+		player->UI.ping_states[i] = element->GetElement<UIElement>(name);
+	}
+
 	UpdateUI(player);
 }
 
@@ -152,10 +168,6 @@ GameInfo::UpdateUI(GameInfoPlayer *player)
 			player->UI.host->Hide();
 		}
 	}
-	if (player->UI.ping) {
-		// FIXME: not yet implemented
-		player->UI.ping->Hide();
-	}
 	if (player->UI.control) {
 		if (player->playerID == localID) {
 			player->UI.control->SetValue(CONTROL_KEYBOARD);
@@ -184,4 +196,81 @@ GameInfo::UpdateUI(GameInfoPlayer *player)
 			player->UI.network->Hide();
 		}
 	}
+	for (int i = 0; i < NUM_PING_STATES; ++i) {
+		UIElement *element = player->UI.ping_states[i];
+		if (element) {
+			if (player->ping.status == i) {
+				element->Show();
+			} else {
+				element->Hide();
+			}
+		}
+	}
+}
+
+void
+GameInfo::UpdatePingTime(int index, Uint32 timestamp)
+{
+	Uint32 now;
+	Uint32 elapsed;
+	GameInfoPlayer *player;
+
+	now = SDL_GetTicks();
+	elapsed = (now - timestamp);
+
+	player = GetPlayer(index);
+	player->ping.lastPing = now;
+	player->ping.roundTripTime = elapsed;
+}
+
+void
+GameInfo::InitializePing(int index)
+{
+	if (IsNetworkPlayer(index)) {
+		GameInfoPlayer *player = GetPlayer(index);
+		player->ping.lastPing = SDL_GetTicks();
+		player->ping.roundTripTime = 0;
+		player->ping.status = PING_GOOD;
+	}
+}
+
+void
+GameInfo::UpdatePingStatus()
+{
+	Uint32 now;
+	Uint32 sinceLastPing;
+
+	now = SDL_GetTicks();
+	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		GameInfoPlayer *player = GetPlayer(i);
+		if (!IsNetworkPlayer(i)) {
+			player->ping.status = PING_LOCAL;
+			continue;
+		}
+
+		sinceLastPing = int(now - player->ping.lastPing);
+		if (sinceLastPing < PING_INTERVAL) {
+			if (player->ping.roundTripTime <= 48) {
+//printf("Game 0x%8.8x: player 0x%8.8x round trip time %d (GOOD)\n", gameID, player->playerID, player->ping.roundTripTime);
+				player->ping.status = PING_GOOD;
+			} else if (player->ping.roundTripTime <= 64) {
+//printf("Game 0x%8.8x: player 0x%8.8x round trip time %d (OKAY)\n", gameID, player->playerID, player->ping.roundTripTime);
+				player->ping.status = PING_OKAY;
+			} else {
+//printf("Game 0x%8.8x: player 0x%8.8x round trip time %d (BAD)\n", gameID, player->playerID, player->ping.roundTripTime);
+				player->ping.status = PING_BAD;
+			}
+		} else if (sinceLastPing < 2*PING_INTERVAL) {
+//printf("Game 0x%8.8x: player 0x%8.8x since last ping %d (OKAY)\n", gameID, player->playerID, sinceLastPing);
+			player->ping.status = PING_OKAY;
+		} else if (sinceLastPing < PING_TIMEOUT) {
+//printf("Game 0x%8.8x: player 0x%8.8x since last ping %d (BAD)\n", gameID, player->playerID, sinceLastPing);
+			player->ping.status = PING_BAD;
+		} else {
+//printf("Game 0x%8.8x: player 0x%8.8x since last ping %d (TIMEDOUT)\n", gameID, player->playerID, sinceLastPing);
+			player->ping.status = PING_TIMEDOUT;
+		}
+
+		UpdateUI(player);
+	}
 }
diff --git a/netlogic/gameinfo.h b/netlogic/gameinfo.h
index da505a4e..ec00323e 100644
--- a/netlogic/gameinfo.h
+++ b/netlogic/gameinfo.h
@@ -36,6 +36,15 @@ enum {
 	CONTROL_NETWORK,
 };
 
+enum PING_STATUS {
+	PING_LOCAL,
+	PING_GOOD,
+	PING_OKAY,
+	PING_BAD,
+	PING_TIMEDOUT,
+	NUM_PING_STATES
+};
+
 struct GameInfoPlayer
 {
 	Uint32 playerID;
@@ -43,14 +52,21 @@ struct GameInfoPlayer
 	char name[MAX_NAMELEN+1];
 
 	struct {
+		Uint32 lastPing;
+		Uint32 roundTripTime;
+		PING_STATUS status;
+	} ping;
+
+	struct {
+		UIElement *element;
 		UIElementCheckbox *enabled;
 		UIElement *name;
 		UIElement *host;
-		UIElement *ping;
 		UIElementRadioGroup *control;
 		UIElement *keyboard;
 		UIElement *joystick;
 		UIElement *network;
+		UIElement *ping_states[NUM_PING_STATES];
 	} UI;
 };
 
@@ -78,7 +94,7 @@ class GameInfo
 	void WriteToPacket(DynamicPacket &packet);
 
 	GameInfoPlayer *GetHost() {
-		return GetPlayer(0);
+		return GetPlayer(HOST_PLAYER);
 	}
 	GameInfoPlayer *GetPlayer(int index) {
 		return &players[index];
@@ -109,6 +125,16 @@ class GameInfo
 		return false;
 	}
 
+	bool IsNetworkPlayer(int index) {
+		if (!players[index].playerID) {
+			return false;
+		}
+		if (players[index].playerID == localID) {
+			return false;
+		}
+		return true;
+	}
+
 	bool IsFull() {
 		for (int i = 0; i < MAX_PLAYERS; ++i) {
 			if (!players[i].playerID) {
@@ -122,6 +148,23 @@ class GameInfo
 	void UpdateUI();
 	void UpdateUI(GameInfoPlayer *player);
 
+	void InitializePing() {
+		for (int i = 0; i < MAX_PLAYERS; ++i) {
+			InitializePing(i);
+		}
+	}
+	void InitializePing(int index);
+	void UpdatePingTime(int index, Uint32 timestamp);
+	void UpdatePingStatus();
+
+	PING_STATUS GetPingStatus(int index) {
+		if (IsNetworkPlayer(index)) {
+			return players[index].ping.status;
+		} else {
+			return PING_LOCAL;
+		}
+	}
+
 public:
 	Uint32 gameID;
 	Uint8 deathMatch;
diff --git a/netlogic/lobby.cpp b/netlogic/lobby.cpp
index 959b5e3d..ac6818a9 100644
--- a/netlogic/lobby.cpp
+++ b/netlogic/lobby.cpp
@@ -35,7 +35,6 @@
 // Update the game list every 3 seconds
 //#define GLOBAL_SERVER_HOST	"obelix.dreamhost.com"
 #define GLOBAL_SERVER_HOST	"localhost"
-#define GLOBAL_CHECK_INTERVAL	3000
 
 
 LobbyDialogDelegate::LobbyDialogDelegate(UIPanel *panel) :
@@ -43,6 +42,7 @@ LobbyDialogDelegate::LobbyDialogDelegate(UIPanel *panel) :
 {
 	m_state = STATE_NONE;
 	m_uniqueID = 0;
+	m_lastPing = 0;
 	m_lastRefresh = 0;
 	m_requestSequence = 1;
 }
@@ -174,7 +174,7 @@ LobbyDialogDelegate::OnTick()
 
 	Uint32 now = SDL_GetTicks();
 	if (!m_lastRefresh ||
-	    (now - m_lastRefresh) > GLOBAL_CHECK_INTERVAL) {
+	    (now - m_lastRefresh) > PING_INTERVAL) {
 		if (m_state == STATE_HOSTING) {
 			AdvertiseGame();
 		} else if (m_state == STATE_LISTING) {
@@ -193,6 +193,12 @@ LobbyDialogDelegate::OnTick()
 		ProcessPacket(m_packet);
 		m_packet.Reset();
 	}
+
+	// Do this after processing packets in case a pong was pending
+	if (!m_lastPing || (now - m_lastPing) > PING_INTERVAL) {
+		CheckPings();
+		m_lastPing = now;
+	}
 }
 
 void
@@ -261,7 +267,7 @@ LobbyDialogDelegate::UpdateUI()
 		for (int i = 0; (unsigned)i < SDL_arraysize(m_gameListElements); ++i) {
 			if (i < m_gameList.length()) {
 				m_gameListElements[i]->Show();
-				m_gameList[i].BindPlayerToUI(0, m_gameListElements[i]);
+				m_gameList[i].BindPlayerToUI(HOST_PLAYER, m_gameListElements[i]);
 			} else {
 				m_gameListElements[i]->Hide();
 			}
@@ -310,11 +316,68 @@ LobbyDialogDelegate::SetState(LOBBY_STATE state)
 	// Update the UI for the new state
 	UpdateUI();
 
+	m_lastPing = 0;
+
 	// Send any packet requests immediately
 	// Comment this out to simulate initial packet loss
 	m_lastRefresh = 0;
 }
 
+void
+LobbyDialogDelegate::CheckPings()
+{
+	// Check for ping timeouts
+	if (m_state == STATE_LISTING) {
+		bool removed = false;
+		int i = 0;
+		while (i < m_gameList.length()) {
+			GameInfo &game = m_gameList[i];
+			game.UpdatePingStatus();
+			if (game.GetPingStatus(HOST_PLAYER) == PING_TIMEDOUT) {
+//printf("Game timed out, removing from list\n");
+				m_gameList.remove(game);
+				removed = true;
+			} else {
+				++i;
+			}
+		}
+		if (removed) {
+			UpdateUI();
+		}
+	} else if (m_state == STATE_HOSTING) {
+		m_game.UpdatePingStatus();
+		for (int i = 0; i < MAX_PLAYERS; ++i) {
+			if (m_game.GetPingStatus(i) == PING_TIMEDOUT) {
+//printf("Player timed out, removing from lobby\n");
+				SendKick(i);
+			}
+		}
+	} else if (m_state == STATE_JOINED) {
+		m_game.UpdatePingStatus();
+		if (m_game.GetPingStatus(HOST_PLAYER) == PING_TIMEDOUT) {
+//printf("Game timed out, leaving lobbyn");
+			SetState(STATE_LISTING);
+		}
+	}
+
+	if (m_state == STATE_HOSTING || m_state == STATE_JOINED) {
+
+		// Send pings to everyone who is still here
+		m_packet.StartLobbyMessage(LOBBY_PING);
+		m_packet.Write(m_game.gameID);
+		m_packet.Write(m_uniqueID);
+		m_packet.Write(SDL_GetTicks());
+
+		for (int i = 0; i < MAX_PLAYERS; ++i) {
+			if (m_game.IsNetworkPlayer(i)) {
+				m_packet.address = m_game.GetPlayer(i)->address;
+				
+				SDLNet_UDP_Send(gNetFD, -1, &m_packet);
+			}
+		}
+	}
+}
+
 void
 LobbyDialogDelegate::AdvertiseGame()
 {
@@ -350,6 +413,7 @@ LobbyDialogDelegate::GetGameList()
 
 	// Get game info for local games
 	m_packet.StartLobbyMessage(LOBBY_REQUEST_GAME_INFO);
+	m_packet.Write(SDL_GetTicks());
 	m_packet.address.host = INADDR_BROADCAST;
 	m_packet.address.port = SDL_SwapBE16(NETPLAY_PORT);
 	SDLNet_UDP_Send(gNetFD, -1, &m_packet);
@@ -359,6 +423,7 @@ void
 LobbyDialogDelegate::GetGameInfo()
 {
 	m_packet.StartLobbyMessage(LOBBY_REQUEST_GAME_INFO);
+	m_packet.Write(SDL_GetTicks());
 	m_packet.address = m_game.GetHost()->address;
 	SDLNet_UDP_Send(gNetFD, -1, &m_packet);
 }
@@ -367,6 +432,7 @@ void
 LobbyDialogDelegate::JoinGame(GameInfo &game)
 {
 	m_game.CopyFrom(game);
+	m_game.InitializePing();
 	SetState(STATE_JOINING);
 }
 
@@ -398,17 +464,23 @@ LobbyDialogDelegate::SendKick(int index)
 {
 	GameInfoPlayer *player;
 
-	player = m_game.GetPlayer(index);
-	if (!player->playerID || player->playerID == m_uniqueID) {
+	if (!m_game.IsNetworkPlayer(index)) {
 		return;
 	}
 
+	player = m_game.GetPlayer(index);
 	m_packet.StartLobbyMessage(LOBBY_KICK);
 	m_packet.Write(m_game.gameID);
 	m_packet.Write(player->playerID);
 	m_packet.address = player->address;
 
 	SDLNet_UDP_Send(gNetFD, -1, &m_packet);
+
+	// Now remove them from the game list
+	SDL_zero(*player);
+
+	// Update our own UI
+	UpdateUI();
 }
 
 void
@@ -468,7 +540,9 @@ LobbyDialogDelegate::ProcessPacket(DynamicPacket &packet)
 		}
 
 		if (cmd == LOBBY_PING) {
-			//ProcessPing(packet);
+			ProcessPing(packet);
+		} else if (cmd == LOBBY_PONG) {
+			ProcessPong(packet);
 		} else if (cmd == LOBBY_REQUEST_GAME_INFO) {
 			ProcessRequestGameInfo(packet);
 		} else if (cmd == LOBBY_REQUEST_JOIN) {
@@ -491,8 +565,9 @@ LobbyDialogDelegate::ProcessPacket(DynamicPacket &packet)
 
 	// These packets we handle in all the join states
 	if (cmd == LOBBY_PING) {
-		// Somebody thinks we're still in a game lobby
-		//RejectPing(packet);
+		ProcessPing(packet);
+	} else if (cmd == LOBBY_PONG) {
+		ProcessPong(packet);
 	} else if (cmd == LOBBY_GAME_INFO) {
 		ProcessGameInfo(packet);
 	} else if (cmd == LOBBY_KICK) {
@@ -500,6 +575,62 @@ LobbyDialogDelegate::ProcessPacket(DynamicPacket &packet)
 	}
 }
 
+void
+LobbyDialogDelegate::ProcessPing(DynamicPacket &packet)
+{
+	Uint32 gameID;
+	Uint32 playerID;
+	Uint32 timestamp;
+
+	if (m_state != STATE_HOSTING && m_state != STATE_JOINED) {
+		return;
+	}
+	if (!packet.Read(gameID) || gameID != m_game.gameID) {
+		return;
+	}
+	if (!packet.Read(playerID) || !m_game.HasPlayer(playerID)) {
+		return;
+	}
+	if (!packet.Read(timestamp)) {
+		return;
+	}
+
+	m_reply.StartLobbyMessage(LOBBY_PONG);
+	m_reply.Write(gameID);
+	m_reply.Write(playerID);
+	m_reply.Write(timestamp);
+	m_reply.address = packet.address;
+
+	SDLNet_UDP_Send(gNetFD, -1, &m_reply);
+}
+
+void
+LobbyDialogDelegate::ProcessPong(DynamicPacket &packet)
+{
+	Uint32 gameID;
+	Uint32 playerID;
+	Uint32 timestamp;
+
+	if (m_state != STATE_HOSTING && m_state != STATE_JOINED) {
+		return;
+	}
+	if (!packet.Read(gameID) || gameID != m_game.gameID) {
+		return;
+	}
+	if (!packet.Read(playerID) || playerID != m_uniqueID) {
+		return;
+	}
+	if (!packet.Read(timestamp)) {
+		return;
+	}
+
+	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		if (packet.address == m_game.players[i].address) {
+			m_game.UpdatePingTime(i, timestamp);
+		}
+	}
+}
+
 void
 LobbyDialogDelegate::ProcessNewGame(DynamicPacket &packet)
 {
@@ -535,7 +666,14 @@ LobbyDialogDelegate::ProcessAnnouncePlayer(DynamicPacket &packet)
 void
 LobbyDialogDelegate::ProcessRequestGameInfo(DynamicPacket &packet)
 {
+	Uint32 timestamp;
+
+	if (!packet.Read(timestamp)) {
+		return;
+	}
+
 	m_reply.StartLobbyMessage(LOBBY_GAME_INFO);
+	m_reply.Write(timestamp);
 	m_game.WriteToPacket(m_reply);
 	m_reply.address = packet.address;
 
@@ -581,15 +719,15 @@ LobbyDialogDelegate::ProcessRequestJoin(DynamicPacket &packet)
 	player->playerID = playerID;
 	player->address = packet.address;
 	SDL_strlcpy(player->name, name, sizeof(player->name));
+	m_game.InitializePing(slot);
 
 	// Let everybody know!
 	m_reply.StartLobbyMessage(LOBBY_GAME_INFO);
+	m_reply.Write((Uint32)0);
 	m_game.WriteToPacket(m_reply);
 	for (slot = 0; slot < MAX_PLAYERS; ++slot) {
-		GameInfoPlayer *player = m_game.GetPlayer(slot);
-		Uint32 playerID = player->playerID;
-		if (playerID && playerID != m_uniqueID) {
-			m_reply.address = player->address;
+		if (m_game.IsNetworkPlayer(slot)) {
+			m_reply.address = m_game.players[slot].address;
 			SDLNet_UDP_Send(gNetFD, -1, &m_reply);
 		}
 	}
@@ -622,8 +760,13 @@ LobbyDialogDelegate::ProcessRequestLeave(DynamicPacket &packet)
 void
 LobbyDialogDelegate::ProcessGameInfo(DynamicPacket &packet)
 {
+	Uint32 timestamp;
 	GameInfo game;
 
+	if (!packet.Read(timestamp)) {
+		return;
+	}
+
 	if (!game.ReadFromPacket(packet)) {
 		return;
 	}
@@ -638,8 +781,13 @@ LobbyDialogDelegate::ProcessGameInfo(DynamicPacket &packet)
 			}
 		}
 		if (i == m_gameList.length()) {
+			game.InitializePing();
 			m_gameList.add(game);
 		}
+		if (timestamp) {
+			m_gameList[i].UpdatePingTime(HOST_PLAYER, timestamp);
+			m_gameList[i].UpdatePingStatus();
+		}
 	} else {
 		if (game.gameID != m_game.gameID) {
 			// Probably an old packet...
@@ -691,6 +839,7 @@ LobbyDialogDelegate::ProcessGameServerList(DynamicPacket &packet)
 
 	// Request game information from the servers
 	m_reply.StartLobbyMessage(LOBBY_REQUEST_GAME_INFO);
+	m_reply.Write(SDL_GetTicks());
 
 	if (!packet.Read(serverCount)) {
 		return;
diff --git a/netlogic/lobby.h b/netlogic/lobby.h
index 877bc346..9e6e3d62 100644
--- a/netlogic/lobby.h
+++ b/netlogic/lobby.h
@@ -57,6 +57,7 @@ class LobbyDialogDelegate : public UIDialogDelegate
 
 	void UpdateUI();
 
+	void CheckPings();
 	void AdvertiseGame();
 	void RemoveGame();
 	void GetGameList();
@@ -71,6 +72,8 @@ class LobbyDialogDelegate : public UIDialogDelegate
 	void PackAddresses(DynamicPacket &packet);
 
 	void ProcessPacket(DynamicPacket &packet);
+	void ProcessPing(DynamicPacket &packet);
+	void ProcessPong(DynamicPacket &packet);
 	void ProcessNewGame(DynamicPacket &packet);
 	void ProcessAnnouncePlayer(DynamicPacket &packet);
 	void ProcessRequestGameInfo(DynamicPacket &packet);
@@ -94,6 +97,7 @@ class LobbyDialogDelegate : public UIDialogDelegate
 	} m_state;
 
 	Uint32 m_uniqueID;
+	Uint32 m_lastPing;
 	Uint32 m_lastRefresh;
 	Uint32 m_requestSequence;
 
diff --git a/netlogic/netplay.cpp b/netlogic/netplay.cpp
index 6af5016e..b4a63107 100644
--- a/netlogic/netplay.cpp
+++ b/netlogic/netplay.cpp
@@ -172,7 +172,7 @@ int CheckPlayers(void)
 	}
 	/* Add ourselves if needed */
 	if ( gNumPlayers == 0 ) {
-		AddLocalPlayer(0);
+		AddLocalPlayer(HOST_PLAYER);
 		gNumPlayers = 1;
 		FoundUs = 1;
 	}
diff --git a/netlogic/protocol.h b/netlogic/protocol.h
index 40185b26..08654dab 100644
--- a/netlogic/protocol.h
+++ b/netlogic/protocol.h
@@ -111,7 +111,7 @@ enum LobbyProtocol {
 
 		Uint32 gameID
 		Uint32 playerID
-		Uint32 sequence
+		Uint32 timestamp
 	 */
 
 	LOBBY_PONG,
@@ -119,16 +119,19 @@ enum LobbyProtocol {
 
 		Uint32 gameID
 		Uint32 playerID
-		Uint32 sequence
+		Uint32 timestamp
 	 */
 
 	LOBBY_REQUEST_GAME_INFO,
 	/* Sent by the joining game to get info for the game list
+
+		Uint32 timestamp
 	 */
 
 	LOBBY_GAME_INFO,
 	/* Sent by the hosting game, if there are slots open
 
+		Uint32 timestamp
 		Uint32 gameID
 		Uint8 deathMatch;
 		Uint32 player1_uniqueID;
@@ -195,6 +198,14 @@ enum LobbyProtocol {
 */
 #define MAX_PLAYERS	3
 
+/* The index of the player hosting the game */
+#define HOST_PLAYER	0
+
+/* If the other side hasn't responded in 3 seconds, we'll drop them */
+#define PING_INTERVAL	1000
+#define

(Patch may be truncated, please check the link at the top of this post.)