Maelstrom: Split the concept of network node and player so we can potentially have the hosting game have multiple local players.

https://github.com/libsdl-org/Maelstrom/commit/39c1364a76d3dfcde62ef6aee07659ead81d9475

From 39c1364a76d3dfcde62ef6aee07659ead81d9475 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 18 Nov 2011 00:49:06 -0500
Subject: [PATCH] Split the concept of network node and player so we can
 potentially have the hosting game have multiple local players.

---
 game/gameinfo.cpp | 234 +++++++++++++++++++++++++++++++++-------------
 game/gameinfo.h   |  97 ++++++++-----------
 game/lobby.cpp    | 137 +++++++++++++++------------
 game/lobby.h      |   1 -
 game/main.cpp     |   2 +-
 game/protocol.h   |   5 +-
 6 files changed, 293 insertions(+), 183 deletions(-)

diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index 9795e4ee..34c9d420 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -47,22 +47,30 @@ GameInfo::SetSinglePlayer(Uint8 wave, Uint8 lives, Uint8 turbo)
 }
 
 void
-GameInfo::SetMultiplayerHost(Uint32 gameID, Uint8 deathMatch, const char *name)
+GameInfo::SetMultiplayerHost(Uint8 deathMatch, const char *name)
 {
-	this->gameID = gameID;
+	this->gameID = localID;
 	this->seed = GetRandSeed();
 	this->wave = DEFAULT_START_WAVE;
 	this->lives = DEFAULT_START_LIVES;
 	this->turbo = DEFAULT_START_TURBO;
 	this->deathMatch = deathMatch;
-	players[HOST_PLAYER].playerID = gameID;
-	SDL_strlcpy(players[HOST_PLAYER].name, name ? name : "",
-			sizeof(players[HOST_PLAYER].name));
+
+	// We are the host node
+	nodes[HOST_NODE].nodeID = localID;
+
+	// We are the first player
+	GameInfoPlayer *player = GetPlayer(0);
+	player->nodeID = localID;
+	SDL_strlcpy(player->name, name ? name : "", sizeof(player->name));
+	player->controlMask = (CONTROL_KEYBOARD|CONTROL_JOYSTICK1);
 }
 
 void
 GameInfo::CopyFrom(const GameInfo &rhs)
 {
+	int i;
+
 	gameID = rhs.gameID;
 	seed = rhs.seed;
 	wave = rhs.wave;
@@ -70,23 +78,30 @@ GameInfo::CopyFrom(const GameInfo &rhs)
 	turbo = rhs.turbo;
 	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) {
-			players[i].address = rhs.players[i].address;
+	for (i = 0; i < MAX_NODES; ++i) {
+		nodes[i].nodeID = rhs.nodes[i].nodeID;
+		if (nodes[i].address != rhs.nodes[i].address) {
+			nodes[i].address = rhs.nodes[i].address;
 
 			// Reset the ping info
 			InitializePing(i);
 		}
 	}
+
+	for (i = 0; i < MAX_PLAYERS; ++i) {
+		players[i].nodeID = rhs.players[i].nodeID;
+		SDL_strlcpy(players[i].name, rhs.players[i].name,
+			sizeof(players[i].name));
+	}
+
 	UpdateUI();
 }
 
 bool
 GameInfo::ReadFromPacket(DynamicPacket &packet)
 {
+	int i;
+
 	if (!packet.Read(gameID)) {
 		return false;
 	}
@@ -106,14 +121,20 @@ GameInfo::ReadFromPacket(DynamicPacket &packet)
 		return false;
 	}
 
-	for (int i = 0; i < MAX_PLAYERS; ++i) {
-		if (!packet.Read(players[i].playerID)) {
+	for (i = 0; i < MAX_NODES; ++i) {
+		if (!packet.Read(nodes[i].nodeID)) {
 			return false;
 		}
-		if (!packet.Read(players[i].address.host)) {
+		if (!packet.Read(nodes[i].address.host)) {
 			return false;
 		}
-		if (!packet.Read(players[i].address.port)) {
+		if (!packet.Read(nodes[i].address.port)) {
+			return false;
+		}
+	}
+
+	for (i = 0; i < MAX_PLAYERS; ++i) {
+		if (!packet.Read(players[i].nodeID)) {
 			return false;
 		}
 		if (!packet.Read(players[i].name, sizeof(players[i].name))) {
@@ -123,8 +144,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[HOST_PLAYER].address.host) {
-		players[HOST_PLAYER].address = packet.address;
+	if (!nodes[HOST_NODE].address.host) {
+		nodes[HOST_NODE].address = packet.address;
 	}
 
 	return true;
@@ -133,6 +154,8 @@ GameInfo::ReadFromPacket(DynamicPacket &packet)
 void
 GameInfo::WriteToPacket(DynamicPacket &packet)
 {
+	int i;
+
 	packet.Write(gameID);
 	packet.Write(seed);
 	packet.Write(wave);
@@ -140,14 +163,79 @@ GameInfo::WriteToPacket(DynamicPacket &packet)
 	packet.Write(turbo);
 	packet.Write(deathMatch);
 
-	for (int i = 0; i < MAX_PLAYERS; ++i) {
-		packet.Write(players[i].playerID);
-		packet.Write(players[i].address.host);
-		packet.Write(players[i].address.port);
+	for (i = 0; i < MAX_NODES; ++i) {
+		packet.Write(nodes[i].nodeID);
+		packet.Write(nodes[i].address.host);
+		packet.Write(nodes[i].address.port);
+	}
+
+	for (i = 0; i < MAX_PLAYERS; ++i) {
+		packet.Write(players[i].nodeID);
 		packet.Write(players[i].name);
 	}
 }
 
+bool
+GameInfo::HasNode(Uint32 nodeID)
+{
+	for (int i = 0; i < MAX_NODES; ++i) {
+		if (nodes[i].nodeID == nodeID) {
+			return true;
+		}
+	}
+	return false;
+}
+
+bool
+GameInfo::HasNode(const IPaddress &address)
+{
+	for (int i = 0; i < MAX_NODES; ++i) {
+		if (nodes[i].address == address) {
+			return true;
+		}
+	}
+	return false;
+}
+
+void
+GameInfo::RemoveNode(Uint32 nodeID)
+{
+	int i;
+	for (i = 0; i < MAX_NODES; ++i) {
+		if (nodeID == nodes[i].nodeID) {
+			SDL_zero(nodes[i]);
+		}
+	}
+	for (i = 0; i < MAX_PLAYERS; ++i) {
+		if (nodeID == players[i].nodeID) {
+			SDL_zero(players[i]);
+		}
+	}
+}
+
+bool
+GameInfo::IsNetworkNode(int index)
+{
+	if (!nodes[index].nodeID) {
+		return false;
+	}
+	if (nodes[index].nodeID == localID) {
+		return false;
+	}
+	return true;
+}
+
+bool
+GameInfo::IsFull()
+{
+	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		if (!players[i].nodeID) {
+			return false;
+		}
+	}
+	return true;
+}
+
 void
 GameInfo::BindPlayerToUI(int index, UIElement *element)
 {
@@ -195,40 +283,41 @@ GameInfo::UpdateUI(GameInfoPlayer *player)
 		}
 	}
 	if (player->UI.host) {
-		if (player->playerID == localID) {
+		GameInfoNode *node = GetNodeByID(player->nodeID);
+		if (node->nodeID == localID) {
 			//player->UI.host->Show();
 			//player->UI.host->SetText("localhost");
 			player->UI.host->Hide();
-		} else if (player->address.host) {
+		} else if (node->address.host) {
 			player->UI.host->Show();
-			player->UI.host->SetText(SDLNet_ResolveIP(&player->address));
+			player->UI.host->SetText(SDLNet_ResolveIP(&node->address));
 		} else {
 			player->UI.host->Hide();
 		}
 	}
 	if (player->UI.control) {
-		if (player->playerID == localID) {
+		if (player->nodeID == localID) {
 			player->UI.control->SetValue(CONTROL_KEYBOARD);
 		} else {
 			player->UI.control->SetValue(CONTROL_NETWORK);
 		}
 	}
 	if (player->UI.keyboard) {
-		if (player->playerID == localID) {
+		if (player->nodeID == localID) {
 			player->UI.keyboard->Show();
 		} else {
 			player->UI.keyboard->Hide();
 		}
 	}
 	if (player->UI.joystick) {
-		if (player->playerID == localID) {
+		if (player->nodeID == localID) {
 			player->UI.joystick->Show();
 		} else {
 			player->UI.joystick->Hide();
 		}
 	}
 	if (player->UI.network) {
-		if (player->playerID != localID) {
+		if (player->nodeID != localID) {
 			player->UI.network->Show();
 		} else {
 			player->UI.network->Hide();
@@ -237,7 +326,7 @@ GameInfo::UpdateUI(GameInfoPlayer *player)
 	for (int i = 0; i < NUM_PING_STATES; ++i) {
 		UIElement *element = player->UI.ping_states[i];
 		if (element) {
-			if (player->ping.status == i) {
+			if (GetNodeByID(player->nodeID)->ping.status == i) {
 				element->Show();
 			} else {
 				element->Hide();
@@ -249,7 +338,7 @@ GameInfo::UpdateUI(GameInfoPlayer *player)
 void
 GameInfo::InitializePing()
 {
-	for (int i = 0; i < MAX_PLAYERS; ++i) {
+	for (int i = 0; i < MAX_NODES; ++i) {
 		InitializePing(i);
 	}
 }
@@ -257,11 +346,12 @@ GameInfo::InitializePing()
 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;
+	GameInfoNode *node = GetNode(index);
+
+	if (node->nodeID != localID) {
+		node->ping.lastPing = SDL_GetTicks();
+		node->ping.roundTripTime = 0;
+		node->ping.status = PING_GOOD;
 	}
 }
 
@@ -270,25 +360,25 @@ GameInfo::UpdatePingTime(int index, Uint32 timestamp)
 {
 	Uint32 now;
 	Uint32 elapsed;
-	GameInfoPlayer *player;
+	GameInfoNode *node;
 
 	now = SDL_GetTicks();
 	elapsed = (now - timestamp);
 
-	player = GetPlayer(index);
-	player->ping.lastPing = now;
-	if (!player->ping.roundTripTime) {
-		player->ping.roundTripTime = elapsed;
+	node = GetNode(index);
+	node->ping.lastPing = now;
+	if (!node->ping.roundTripTime) {
+		node->ping.roundTripTime = elapsed;
 	} else {
 		// Use a weighted average 2/3 previous value, 1/3 new value
-		player->ping.roundTripTime = (2*player->ping.roundTripTime + 1*elapsed) / 3;
+		node->ping.roundTripTime = (2*node->ping.roundTripTime + 1*elapsed) / 3;
 	}
 }
 
 void
 GameInfo::UpdatePingStatus()
 {
-	for (int i = 0; i < MAX_PLAYERS; ++i) {
+	for (int i = 0; i < MAX_NODES; ++i) {
 		UpdatePingStatus(i);
 	}
 }
@@ -296,47 +386,63 @@ GameInfo::UpdatePingStatus()
 void
 GameInfo::UpdatePingStatus(int index)
 {
-	GameInfoPlayer *player = GetPlayer(index);
+	GameInfoNode *node = GetNode(index);
 
-	if (!IsNetworkPlayer(index)) {
-		player->ping.status = PING_LOCAL;
+	if (!IsNetworkNode(index)) {
+		node->ping.status = PING_LOCAL;
 	} else {
 		Uint32 sinceLastPing;
 
-		sinceLastPing = int(SDL_GetTicks() - player->ping.lastPing);
+		sinceLastPing = int(SDL_GetTicks() - node->ping.lastPing);
 		if (sinceLastPing < 2*PING_INTERVAL) {
-			if (player->ping.roundTripTime <= 2*FRAME_DELAY_MS) {
+			if (node->ping.roundTripTime <= 2*FRAME_DELAY_MS) {
 #ifdef DEBUG_NETWORK
-printf("Game 0x%8.8x: player 0x%8.8x round trip time %d (GOOD)\n",
-	gameID, player->playerID, player->ping.roundTripTime);
+printf("Game 0x%8.8x: node 0x%8.8x round trip time %d (GOOD)\n",
+	gameID, node->nodeID, node->ping.roundTripTime);
 #endif
-				player->ping.status = PING_GOOD;
-			} else if (player->ping.roundTripTime <= 3*FRAME_DELAY_MS) {
+				node->ping.status = PING_GOOD;
+			} else if (node->ping.roundTripTime <= 3*FRAME_DELAY_MS) {
 #ifdef DEBUG_NETWORK
-printf("Game 0x%8.8x: player 0x%8.8x round trip time %d (OKAY)\n",
-	gameID, player->playerID, player->ping.roundTripTime);
+printf("Game 0x%8.8x: node 0x%8.8x round trip time %d (OKAY)\n",
+	gameID, node->nodeID, node->ping.roundTripTime);
 #endif
-				player->ping.status = PING_OKAY;
+				node->ping.status = PING_OKAY;
 			} else {
 #ifdef DEBUG_NETWORK
-printf("Game 0x%8.8x: player 0x%8.8x round trip time %d (BAD)\n",
-	gameID, player->playerID, player->ping.roundTripTime);
+printf("Game 0x%8.8x: node 0x%8.8x round trip time %d (BAD)\n",
+	gameID, node->nodeID, node->ping.roundTripTime);
 #endif
-				player->ping.status = PING_BAD;
+				node->ping.status = PING_BAD;
 			}
 		} else if (sinceLastPing < PING_TIMEOUT) {
 #ifdef DEBUG_NETWORK
-printf("Game 0x%8.8x: player 0x%8.8x since last ping %d (BAD)\n",
-	gameID, player->playerID, sinceLastPing);
+printf("Game 0x%8.8x: node 0x%8.8x since last ping %d (BAD)\n",
+	gameID, node->nodeID, sinceLastPing);
 #endif
-			player->ping.status = PING_BAD;
+			node->ping.status = PING_BAD;
 		} else {
 #ifdef DEBUG_NETWORK
-printf("Game 0x%8.8x: player 0x%8.8x since last ping %d (TIMEDOUT)\n",
-	gameID, player->playerID, sinceLastPing);
+printf("Game 0x%8.8x: node 0x%8.8x since last ping %d (TIMEDOUT)\n",
+	gameID, node->nodeID, sinceLastPing);
 #endif
-			player->ping.status = PING_TIMEDOUT;
+			node->ping.status = PING_TIMEDOUT;
 		}
 	}
-	UpdateUI(player);
+
+	// Update the UI for matching players
+	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		if (players[i].nodeID == node->nodeID) {
+			UpdateUI(&players[i]);
+		}
+	}
+}
+
+PING_STATUS
+GameInfo::GetPingStatus(int index)
+{
+	if (IsNetworkNode(index)) {
+		return nodes[index].ping.status;
+	} else {
+		return PING_LOCAL;
+	}
 }
diff --git a/game/gameinfo.h b/game/gameinfo.h
index 969c8819..8792331b 100644
--- a/game/gameinfo.h
+++ b/game/gameinfo.h
@@ -30,7 +30,7 @@ class UIElement;
 class UIElementCheckbox;
 class UIElementRadioGroup;
 
-enum {
+enum PLAYER_CONTROL {
 	CONTROL_KEYBOARD = 1,
 	CONTROL_JOYSTICK,
 	CONTROL_NETWORK,
@@ -45,11 +45,11 @@ enum PING_STATUS {
 	NUM_PING_STATES
 };
 
-struct GameInfoPlayer
+// This represents a physical machine (host/port combo) on the network
+struct GameInfoNode
 {
-	Uint32 playerID;
+	Uint32 nodeID;
 	IPaddress address;
-	char name[MAX_NAMELEN+1];
 
 	struct {
 		Uint32 lastPing;
@@ -57,6 +57,20 @@ struct GameInfoPlayer
 		PING_STATUS status;
 	} ping;
 
+};
+
+// This represents a player in the game, on a particular network node
+//
+// The hosting node may have any number of players
+// The other nodes may each only have one player, to simplify things
+// like the join/leave/kick process.
+//
+struct GameInfoPlayer
+{
+	Uint32 nodeID;
+	char name[MAX_NAMELEN+1];
+	Uint8 controlMask;
+
 	struct {
 		UIElement *element;
 		UIElementCheckbox *enabled;
@@ -84,10 +98,10 @@ class GameInfo
 
 	void SetSinglePlayer(Uint8 wave, Uint8 lives, Uint8 turbo);
 
-	void SetMultiplayerHost(Uint32 gameID, Uint8 deathMatch, const char *name);
+	void SetMultiplayerHost(Uint8 deathMatch, const char *name);
 
-	void SetLocalID(Uint32 playerID) {
-		localID = playerID;
+	void SetLocalID(Uint32 uniqueID) {
+		localID = uniqueID;
 	}
 
 	void CopyFrom(const GameInfo &rhs);
@@ -95,56 +109,33 @@ class GameInfo
 	bool ReadFromPacket(DynamicPacket &packet);
 	void WriteToPacket(DynamicPacket &packet);
 
-	GameInfoPlayer *GetHost() {
-		return GetPlayer(HOST_PLAYER);
+	GameInfoNode *GetHost() {
+		return GetNode(HOST_NODE);
 	}
-	GameInfoPlayer *GetPlayer(int index) {
-		return &players[index];
+	GameInfoNode *GetNode(int index) {
+		return &nodes[index];
 	}
-	GameInfoPlayer *GetPlayerByID(Uint32 playerID) {
-		for (int i = 0; i < MAX_PLAYERS; ++i) {
-			if (players[i].playerID == playerID) {
-				return &players[i];
-			}
-		}
-		return NULL;
-	}
-
-	bool HasPlayer(Uint32 playerID) {
-		for (int i = 0; i < MAX_PLAYERS; ++i) {
-			if (players[i].playerID == playerID) {
-				return true;
+	GameInfoNode *GetNodeByID(Uint32 nodeID) {
+		for (int i = 0; i < MAX_NODES; ++i) {
+			if (nodeID == nodes[i].nodeID) {
+				return &nodes[i];
 			}
 		}
-		return false;
 	}
-	bool HasPlayer(const IPaddress &address) {
-		for (int i = 0; i < MAX_PLAYERS; ++i) {
-			if (players[i].address == address) {
-				return true;
-			}
-		}
-		return false;
+	GameInfoPlayer *GetPlayer(int index) {
+		return &players[index];
 	}
 
-	bool IsNetworkPlayer(int index) {
-		if (!players[index].playerID) {
-			return false;
-		}
-		if (players[index].playerID == localID) {
-			return false;
-		}
-		return true;
-	}
+	bool HasNode(Uint32 nodeID);
+	bool HasNode(const IPaddress &address);
+	void RemoveNode(Uint32 nodeID);
 
-	bool IsFull() {
-		for (int i = 0; i < MAX_PLAYERS; ++i) {
-			if (!players[i].playerID) {
-				return false;
-			}
-		}
-		return true;
+	bool IsHosting() {
+		return localID == gameID;
 	}
+	bool IsNetworkNode(int index);
+
+	bool IsFull();
 
 	void BindPlayerToUI(int index, UIElement *element);
 	void UpdateUI();
@@ -155,14 +146,7 @@ class GameInfo
 	void UpdatePingTime(int index, Uint32 timestamp);
 	void UpdatePingStatus();
 	void UpdatePingStatus(int index);
-
-	PING_STATUS GetPingStatus(int index) {
-		if (IsNetworkPlayer(index)) {
-			return players[index].ping.status;
-		} else {
-			return PING_LOCAL;
-		}
-	}
+	PING_STATUS GetPingStatus(int index);
 
 public:
 	Uint32 gameID;
@@ -171,6 +155,7 @@ class GameInfo
 	Uint8 lives;
 	Uint8 turbo;
 	Uint8 deathMatch;
+	GameInfoNode nodes[MAX_NODES];
 	GameInfoPlayer players[MAX_PLAYERS];
 
 	Uint32 localID;
diff --git a/game/lobby.cpp b/game/lobby.cpp
index 763e1016..2702e720 100644
--- a/game/lobby.cpp
+++ b/game/lobby.cpp
@@ -46,7 +46,6 @@ LobbyDialogDelegate::LobbyDialogDelegate(UIPanel *panel) :
 	m_game(gGameInfo)
 {
 	m_state = STATE_NONE;
-	m_uniqueID = 0;
 	m_lastPing = 0;
 	m_lastRefresh = 0;
 	m_requestSequence = 1;
@@ -159,11 +158,12 @@ LobbyDialogDelegate::OnHide()
 
 		for (int i = 0; i < MAX_PLAYERS; ++i) {
 			GameInfoPlayer *player = m_game.GetPlayer(i);
-			if (player->playerID) {
-				if (player->playerID == m_game.localID) {
+			if (player->nodeID) {
+				if (player->nodeID == m_game.localID) {
 					AddLocalPlayer(i);
 				} else {
-					AddNetworkPlayer(i, player->address);
+					GameInfoNode *node = m_game.GetNodeByID(player->nodeID);
+					AddNetworkPlayer(i, node->address);
 				}
 			}
 		}
@@ -227,8 +227,11 @@ LobbyDialogDelegate::SetHostOrJoin(void*, int value)
 			return;
 		}
 
-		m_uniqueID = rand();
-		m_game.SetLocalID(m_uniqueID);
+		Uint32 localID = rand();
+		while (localID <= 1) {
+			localID = rand();
+		}
+		m_game.SetLocalID(localID);
 
 		if (value == HOST_GAME) {
 			SetState(STATE_HOSTING);
@@ -285,7 +288,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(HOST_PLAYER, m_gameListElements[i]);
+				m_gameList[i].BindPlayerToUI(0, m_gameListElements[i]);
 			} else {
 				m_gameListElements[i]->Hide();
 			}
@@ -315,7 +318,7 @@ LobbyDialogDelegate::SetState(LOBBY_STATE state)
 	if (state == STATE_NONE) {
 		if (m_state == STATE_HOSTING) {
 			// Notify the players that the game is gone
-			for (int i = 0; i < MAX_PLAYERS; ++i) {
+			for (int i = 0; i < MAX_NODES; ++i) {
 				SendKick(i);
 			}
 		} else if (m_state == STATE_JOINING ||
@@ -324,7 +327,7 @@ LobbyDialogDelegate::SetState(LOBBY_STATE state)
 			SendLeaveRequest();
 		}
 	} else if (state == STATE_HOSTING) {
-		m_game.SetMultiplayerHost(m_uniqueID,
+		m_game.SetMultiplayerHost(
 			prefs->GetNumber(PREFERENCES_DEATHMATCH),
 			prefs->GetString(PREFERENCES_HANDLE));
 	} else if (state == STATE_LISTING) {
@@ -353,8 +356,8 @@ LobbyDialogDelegate::CheckPings()
 		int i = 0;
 		while (i < m_gameList.length()) {
 			GameInfo &game = m_gameList[i];
-			game.UpdatePingStatus(HOST_PLAYER);
-			if (game.GetPingStatus(HOST_PLAYER) == PING_TIMEDOUT) {
+			game.UpdatePingStatus(HOST_NODE);
+			if (game.GetPingStatus(HOST_NODE) == PING_TIMEDOUT) {
 //printf("Game timed out, removing from list\n");
 				m_gameList.remove(game);
 				removed = true;
@@ -367,7 +370,7 @@ LobbyDialogDelegate::CheckPings()
 		}
 	} else if (m_state == STATE_HOSTING) {
 		m_game.UpdatePingStatus();
-		for (int i = 0; i < MAX_PLAYERS; ++i) {
+		for (int i = 0; i < MAX_NODES; ++i) {
 			if (m_game.GetPingStatus(i) == PING_TIMEDOUT) {
 //printf("Player timed out, removing from lobby\n");
 				SendKick(i);
@@ -375,7 +378,7 @@ LobbyDialogDelegate::CheckPings()
 		}
 	} else if (m_state == STATE_JOINED) {
 		m_game.UpdatePingStatus();
-		if (m_game.GetPingStatus(HOST_PLAYER) == PING_TIMEDOUT) {
+		if (m_game.GetPingStatus(HOST_NODE) == PING_TIMEDOUT) {
 //printf("Game timed out, leaving lobbyn");
 			SetState(STATE_LISTING);
 		}
@@ -386,12 +389,12 @@ LobbyDialogDelegate::CheckPings()
 		// 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(m_game.localID);
 		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;
+		for (int i = 0; i < MAX_NODES; ++i) {
+			if (m_game.IsNetworkNode(i)) {
+				m_packet.address = m_game.GetNode(i)->address;
 				
 				SDLNet_UDP_Send(gNetFD, -1, &m_packet);
 			}
@@ -464,7 +467,7 @@ LobbyDialogDelegate::SendJoinRequest()
 {
 	m_packet.StartLobbyMessage(LOBBY_REQUEST_JOIN);
 	m_packet.Write(m_game.gameID);
-	m_packet.Write(m_uniqueID);
+	m_packet.Write(m_game.localID);
 	m_packet.Write(prefs->GetString(PREFERENCES_HANDLE));
 	m_packet.address = m_game.GetHost()->address;
 
@@ -476,7 +479,7 @@ LobbyDialogDelegate::SendLeaveRequest()
 {
 	m_packet.StartLobbyMessage(LOBBY_REQUEST_LEAVE);
 	m_packet.Write(m_game.gameID);
-	m_packet.Write(m_uniqueID);
+	m_packet.Write(m_game.localID);
 	m_packet.address = m_game.GetHost()->address;
 
 	SDLNet_UDP_Send(gNetFD, -1, &m_packet);
@@ -485,22 +488,22 @@ LobbyDialogDelegate::SendLeaveRequest()
 void
 LobbyDialogDelegate::SendKick(int index)
 {
-	GameInfoPlayer *player;
+	GameInfoNode *node;
 
-	if (!m_game.IsNetworkPlayer(index)) {
+	if (!m_game.IsNetworkNode(index)) {
 		return;
 	}
 
-	player = m_game.GetPlayer(index);
+	node = m_game.GetNode(index);
 	m_packet.StartLobbyMessage(LOBBY_KICK);
 	m_packet.Write(m_game.gameID);
-	m_packet.Write(player->playerID);
-	m_packet.address = player->address;
+	m_packet.Write(node->nodeID);
+	m_packet.address = node->address;
 
 	SDLNet_UDP_Send(gNetFD, -1, &m_packet);
 
 	// Now remove them from the game list
-	SDL_zero(*player);
+	m_game.RemoveNode(node->nodeID);
 
 	// Update our own UI
 	UpdateUI();
@@ -558,7 +561,7 @@ LobbyDialogDelegate::ProcessPacket(DynamicPacket &packet)
 			return;
 		}
 
-		if (m_game.IsFull() && !m_game.HasPlayer(packet.address)) {
+		if (m_game.IsFull() && !m_game.HasNode(packet.address)) {
 			return;
 		}
 
@@ -602,7 +605,7 @@ void
 LobbyDialogDelegate::ProcessPing(DynamicPacket &packet)
 {
 	Uint32 gameID;
-	Uint32 playerID;
+	Uint32 nodeID;
 	Uint32 timestamp;
 
 	if (m_state != STATE_HOSTING && m_state != STATE_JOINED) {
@@ -611,7 +614,7 @@ LobbyDialogDelegate::ProcessPing(DynamicPacket &packet)
 	if (!packet.Read(gameID) || gameID != m_game.gameID) {
 		return;
 	}
-	if (!packet.Read(playerID) || !m_game.HasPlayer(playerID)) {
+	if (!packet.Read(nodeID) || !m_game.HasNode(nodeID)) {
 		return;
 	}
 	if (!packet.Read(timestamp)) {
@@ -620,7 +623,7 @@ LobbyDialogDelegate::ProcessPing(DynamicPacket &packet)
 
 	m_reply.StartLobbyMessage(LOBBY_PONG);
 	m_reply.Write(gameID);
-	m_reply.Write(playerID);
+	m_reply.Write(nodeID);
 	m_reply.Write(timestamp);
 	m_reply.address = packet.address;
 
@@ -631,7 +634,7 @@ void
 LobbyDialogDelegate::ProcessPong(DynamicPacket &packet)
 {
 	Uint32 gameID;
-	Uint32 playerID;
+	Uint32 nodeID;
 	Uint32 timestamp;
 
 	if (m_state != STATE_HOSTING && m_state != STATE_JOINED) {
@@ -640,15 +643,15 @@ LobbyDialogDelegate::ProcessPong(DynamicPacket &packet)
 	if (!packet.Read(gameID) || gameID != m_game.gameID) {
 		return;
 	}
-	if (!packet.Read(playerID) || playerID != m_uniqueID) {
+	if (!packet.Read(nodeID) || nodeID != m_game.localID) {
 		return;
 	}
 	if (!packet.Read(timestamp)) {
 		return;
 	}
 
-	for (int i = 0; i < MAX_PLAYERS; ++i) {
-		if (packet.address == m_game.players[i].address) {
+	for (int i = 0; i < MAX_NODES; ++i) {
+		if (packet.address == m_game.GetNode(i)->address) {
 			m_game.UpdatePingTime(i, timestamp);
 		}
 	}
@@ -666,9 +669,13 @@ LobbyDialogDelegate::ProcessNewGame(DynamicPacket &packet)
 	if (!packet.Read(gameID) || gameID != m_game.gameID) {
 		return;
 	}
+	if (m_game.IsHosting()) {
+		// They can't tell us to start!
+		return;
+	}
 
 	// Ooh, ooh, they're starting!
-	if (m_game.HasPlayer(packet.address)) {
+	if (m_game.HasNode(packet.address)) {
 		m_playButton->OnClick();
 	}
 }
@@ -717,13 +724,13 @@ void
 LobbyDialogDelegate::ProcessRequestJoin(DynamicPacket &packet)
 {
 	Uint32 gameID;
-	Uint32 playerID;
+	Uint32 nodeID;
 	char name[MAX_NAMELEN+1];
 
 	if (!packet.Read(gameID) || gameID != m_game.gameID) {
 		return;
 	}
-	if (!packet.Read(playerID)) {
+	if (!packet.Read(nodeID)) {
 		return;
 	}
 	if (!packet.Read(name, sizeof(name))) {
@@ -732,35 +739,45 @@ LobbyDialogDelegate::ProcessRequestJoin(DynamicPacket &packet)
 
 	// Find an empty slot
 	int slot;
-	for (slot = 0; slot < MAX_PLAYERS; ++slot) {
-		if (playerID == m_game.players[slot].playerID) {
-			// We already have this player, ignore it
+	for (slot = 0; slot < MAX_NODES; ++slot) {
+		if (nodeID == m_game.nodes[slot].nodeID) {
+			// We already have this node, ignore it
 			return;
 		}
 	}
-	if (slot == MAX_PLAYERS) {
-		for (slot = 0; slot < MAX_PLAYERS; ++slot) {
-			if (!m_game.players[slot].playerID) {
+	if (slot == MAX_NODES) {
+		for (slot = 0; slot < MAX_NODES; ++slot) {
+			if (!m_game.nodes[slot].nodeID) {
 				break;
 			}
 		}
 	}
+	assert(slot < MAX_NODES);
+
+	GameInfoNode *node = m_game.GetNode(slot);
+	node->nodeID = nodeID;
+	node->address = packet.address;
+	m_game.InitializePing(slot);
+
+	for (slot = 0; slot < MAX_PLAYERS; ++slot) {
+		if (!m_game.players[slot].nodeID) {
+			break;
+		}
+	}
 	assert(slot < MAX_PLAYERS);
 
-	// Fill in the data
 	GameInfoPlayer *player = m_game.GetPlayer(slot);
-	player->playerID = playerID;
-	player->address = packet.address;
+	player->nodeID = nodeID;
 	SDL_strlcpy(player->name, name, sizeof(player->name));
-	m_game.InitializePing(slot);
+	player->controlMask = CONTROL_NETWORK;
 
 	// 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) {
-		if (m_game.IsNetworkPlayer(slot)) {
-			m_reply.address = m_game.players[slot].address;
+	for (slot = 0; slot < MAX_NODES; ++slot) {
+		if (m_game.IsNetworkNode(slot)) {
+			m_reply.address = m_game.nodes[slot].address;
 			SDLNet_UDP_Send(gNetFD, -1, &m_reply);
 		}
 	}
@@ -773,18 +790,20 @@ void
 LobbyDialogDelegate::ProcessRequestLeave(DynamicPacket &packet)
 {
 	Uint32 gameID;
-	Uint32 playerID;
+	Uint32 nodeID;
 
 	if (!packet.Read(gameID) || gameID != m_game.gameID) {
 		return;
 	}
-	if (!packet.Read(playerID) || !m_game.HasPlayer(playerID)) {
+	if (!packet.Read(nodeID) || !m_game.HasNode(nodeID)) {
+		return;
+	}
+	if (nodeID == m_game.localID) {
 		return;
 	}
 
 	// Okay, clear them from the list!
-	GameInfoPlayer *player = m_game.GetPlayerByID(playerID);
-	SDL_zero(*player);
+	m_game.RemoveNode(nodeID);
 
 	// Update our own UI
 	UpdateUI();
@@ -818,8 +837,8 @@ LobbyDialogDelegate::ProcessGameInfo(DynamicPacket &packet)
 			m_gameList.add(game);
 		}
 		if (timestamp) {
-			m_gameList[i].UpdatePingTime(HOST_PLAYER, timestamp);
-			m_gameList[i].UpdatePingStatus(HOST_PLAYER);
+			m_gameList[i].UpdatePingTime(HOST_NODE, timestamp);
+			m_gameList[i].UpdatePingStatus(HOST_NODE);
 		}
 	} else {
 		if (game.gameID != m_game.gameID) {
@@ -830,12 +849,12 @@ LobbyDialogDelegate::ProcessGameInfo(DynamicPacket &packet)
 		m_game.CopyFrom(game);
 
 		if (m_state == STATE_JOINING) {
-			if (m_game.HasPlayer(m_uniqueID)) {
+			if (m_game.HasNode(m_game.localID)) {
 				// We successfully joined the game
 				SetState(STATE_JOINED);
 			}
 		} else {
-			if (!m_game.HasPlayer(m_uniqueID)) {
+			if (!m_game.HasNode(m_game.localID)) {
 				// We were kicked from the game
 				SetState(STATE_LISTING);
 			}
@@ -849,7 +868,7 @@ void
 LobbyDialogDelegate::ProcessKick(DynamicPacket &packet)
 {
 	Uint32 gameID;
-	Uint32 playerID;
+	Uint32 nodeID;
 
 	if (m_state != STATE_JOINING && m_state != STATE_JOINED) {
 		return;
@@ -857,7 +876,7 @@ LobbyDialogDelegate::ProcessKick(DynamicPacket &packet)
 	if (!packet.Read(gameID) || gameID != m_game.gameID) {
 		return;
 	}
-	if (!packet.Read(playerID) || playerID != m_uniqueID) {
+	if (!packet.Read(nodeID) || nodeID != m_game.localID) {
 		return;
 	}
 
diff --git a/game/lobby.h b/game/lobby.h
index 562dac25..d0249c14 100644
--- a/game/lobby.h
+++ b/game/lobby.h
@@ -97,7 +97,6 @@ class LobbyDialogDelegate : public UIDialogDelegate
 		STATE_PLAYING
 	} m_state;
 
-	Uint32 m_uniqueID;
 	Uint32 m_lastPing;
 	Uint32 m_lastRefresh;
 	Uint32 m_requestSequence;
diff --git a/game/main.cpp b/game/main.cpp
index 499fba6b..37ae7c3a 100644
--- a/game/main.cpp
+++ b/game/main.cpp
@@ -67,7 +67,7 @@ static void RunSinglePlayerGame()
 	if (InitNetData(false) < 0) {
 		return;
 	}
-	AddLocalPlayer(HOST_PLAYER);
+	AddLocalPlayer(0);
 	NewGame();
 	HaltNetData();
 }
diff --git a/game/protocol.h b/game/protocol.h
index 23a64698..2b89c80e 100644
--- a/game/protocol.h
+++ b/game/protocol.h
@@ -197,9 +197,10 @@ enum LobbyProtocol {
    array in player.cpp
 */
 #define MAX_PLAYERS	3
+#define MAX_NODES	MAX_PLAYERS
 
-/* The index of the player hosting the game */
-#define HOST_PLAYER	0
+/* The index of the node hosting the game */
+#define HOST_NODE	0
 
 /* If the other side hasn't responded in 3 seconds, we'll drop them */
 #define PING_INTERVAL	1000