/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */

#include <cinttypes>

#include "Game/Game.h"
#include "GameServer.h"

#include "ExternalAI/EngineOutHandler.h"
#include "ExternalAI/SkirmishAIHandler.h"
#include "Game/ClientData.h"
#include "Game/CommandMessage.h"
#include "Game/GameSetup.h"
#include "Game/GlobalUnsynced.h"
#include "Game/SelectedUnitsHandler.h"
#include "Game/ChatMessage.h"
#include "Game/WordCompletion.h"
#include "Game/IVideoCapturing.h"
#include "Game/InMapDraw.h"
#include "Game/Players/Player.h"
#include "Game/Players/PlayerHandler.h"
#include "Game/UI/GameSetupDrawer.h"
#include "Game/UI/MouseHandler.h"
#include "Lua/LuaHandle.h"
#include "Rendering/GlobalRendering.h"
#include "Sim/Misc/GlobalSynced.h"
#include "Sim/Misc/TeamHandler.h"
#include "Sim/Path/IPathManager.h"
#include "Sim/Units/UnitHandler.h"
#include "System/Config/ConfigHandler.h"
#include "System/EventHandler.h"
#include "System/GlobalConfig.h"
#include "System/Log/ILog.h"
#include "System/myMath.h"
#include "Net/Protocol/NetProtocol.h"
#include "System/TimeProfiler.h"
#include "System/LoadSave/DemoRecorder.h"
#include "System/Net/UnpackPacket.h"
#include "System/Sound/ISound.h"

CONFIG(bool, LogClientData).defaultValue(false);

#define LOG_SECTION_NET "Net"
LOG_REGISTER_SECTION_GLOBAL(LOG_SECTION_NET)

static spring::unordered_map<int, unsigned int> localSyncChecksums;


void CGame::AddTraffic(int playerID, int packetCode, int length)
{
	auto it = playerTraffic.find(playerID);
	if (it == playerTraffic.end()) {
		playerTraffic[playerID] = PlayerTrafficInfo();
		it = playerTraffic.find(playerID);
	}

	PlayerTrafficInfo& pti = it->second;
	pti.total += length;

	auto cit = pti.packets.find(packetCode);
	if (cit == pti.packets.end()) {
		pti.packets[packetCode] = length;
	} else {
		cit->second += length;
	}
}

void CGame::SendClientProcUsage()
{
	static spring_time lastProcUsageUpdateTime = spring_gettime();

	if ((spring_gettime() - lastProcUsageUpdateTime).toMilliSecsf() >= 1000.0f) {
		lastProcUsageUpdateTime = spring_gettime();

		if (playing) {
			const float simProcUsage = (profiler.GetPercent("Sim"));
			const float drawProcUsage = (profiler.GetPercent("Draw") / std::max(1.0f, globalRendering->FPS)) * gu->minFPS;
			const float totalProcUsage = simProcUsage + drawProcUsage;

			// take the minimum drawframes into account, too
			clientNet->Send(CBaseNetProtocol::Get().SendCPUUsage(totalProcUsage));
		} else {
			// the CPU-load percentage is undefined prior to SimFrame()
			clientNet->Send(CBaseNetProtocol::Get().SendCPUUsage(0.0f));
		}
	}
}


unsigned int CGame::GetNumQueuedSimFrameMessages(unsigned int maxFrames) const
{
	// read ahead to find number of NETMSG_XXXFRAMES we still have to process
	// this number is effectively a measure of current user network conditions
	std::shared_ptr<const netcode::RawPacket> packet;

	unsigned int numQueuedFrames = 0;
	unsigned int packetPeekIndex = 0;

	while ((packet = clientNet->Peek(packetPeekIndex))) {
		switch (packet->data[0]) {
			case NETMSG_GAME_FRAME_PROGRESS: {
				// this special packet skips queue entirely, so gets processed here
				// it's meant to indicate current game progress for clients fast-forwarding to current point the game
				// NOTE: this event should be unsynced, since its time reference frame is not related to the current
				// progress of the game from the client's point of view
				//
				// send the event to lua call-in
				eventHandler.GameProgress(*(int*)(packet->data + 1));
				// pop it out of the net buffer
				clientNet->DeleteBufferPacketAt(packetPeekIndex);
			} break;

			case NETMSG_NEWFRAME:
			case NETMSG_KEYFRAME:
				if (numQueuedFrames < maxFrames)
					++numQueuedFrames;
			default:
				++packetPeekIndex;
		}
	}

	return numQueuedFrames;
}

void CGame::UpdateNumQueuedSimFrames()
{
	if (gameServer != NULL)
		return;

	static spring_time lastUpdateTime = spring_gettime();

	const spring_time currTime = spring_gettime();
	const spring_time deltaTime = currTime - lastUpdateTime;

	// update consumption-rate faster at higher game speeds
	if (deltaTime.toMilliSecsf() < (500.0f / gs->speedFactor))
		return;

	// NOTE:
	//   unnecessary to scan entire queue *unless* joining a running game
	//   only reason in that case is to handle NETMSG_GAME_FRAME_PROGRESS
	//
	const unsigned int numQueuedFrames = GetNumQueuedSimFrameMessages(-1u);

	if (globalConfig->useNetMessageSmoothingBuffer) {
		if (numQueuedFrames < lastNumQueuedSimFrames) {
			// conservative policy: take minimum of current and previous queue size
			// we *NEVER* want the queue to run completely dry (by not keeping a few
			// messages buffered) because this leads to micro-stutter which is WORSE
			// than trading latency for smoothness (by trailing some extra number of
			// simframes behind the server)
			lastNumQueuedSimFrames = numQueuedFrames;
		} else {
			// trust the past more than the future
			lastNumQueuedSimFrames = mix(lastNumQueuedSimFrames * 1.0f, numQueuedFrames * 1.0f, 0.1f);
		}

		// always stay a bit behind the actual server time
		// at higher speeds we need to keep more distance!
		// (because effect of network jitter is amplified)
		consumeSpeedMult = GAME_SPEED * gs->speedFactor + lastNumQueuedSimFrames - (2 * gs->speedFactor);
	} else {
		// Modified SPRING95 behaviour
		// Aim at staying 2 sim frames behind.
		consumeSpeedMult = GAME_SPEED * gs->speedFactor + (numQueuedFrames/2) - 1;
	}

	// await one sim frame if queue is dry
	if (numQueuedFrames == 0)
		msgProcTimeLeft = -1000.0f * gs->speedFactor;

	lastUpdateTime = currTime;
}

void CGame::UpdateNetMessageProcessingTimeLeft()
{
	// compute new msgProcTimeLeft to "smooth" out SimFrame() calls
	if (gameServer == NULL) {
		const spring_time currentReadNetTime = spring_gettime();
		const spring_time deltaReadNetTime = currentReadNetTime - lastReadNetTime;

		if (skipping) {
			msgProcTimeLeft = 10.0f;
		} else {
			// at <N> Hz we should consume one simframe message every (1000/N) ms
			//
			// <dt> since last call will typically be some small fraction of this
			// so we eat through the queue at a rate proportional to that fraction
			// (the amount of processing time is weighted by dt and also increases
			// when more messages are waiting)
			//
			msgProcTimeLeft += (consumeSpeedMult * deltaReadNetTime.toMilliSecsf());
		}

		lastReadNetTime = currentReadNetTime;
	} else {
		// ensure ClientReadNet returns at least every 15 simframes
		// so CGame can process keyboard input, and render etc.
		msgProcTimeLeft = (GAME_SPEED / float(gu->minFPS) * gs->wantedSpeedFactor) * 1000.0f;
	}
}

float CGame::GetNetMessageProcessingTimeLimit() const
{
	// balance the time spent in simulation & drawing (esp. when reconnecting)
	// use the following algo: i.e. with gu->reconnectSimDrawBalance = 0.2f
	//  -> try to spend minimum 20% of the time in drawing
	//  -> use remaining 80% for reconnecting
	// (maxSimFPS / minDrawFPS) is desired number of simframes per drawframe
	const float maxSimFPS    = (1.0f - gu->reconnectSimDrawBalance) * 1000.0f / std::max(0.01f, gu->avgSimFrameTime);
	const float minDrawFPS   =         gu->reconnectSimDrawBalance  * 1000.0f / std::max(0.01f, gu->avgDrawFrameTime);
	const float simDrawRatio = maxSimFPS / minDrawFPS;

	return Clamp(simDrawRatio * gu->avgSimFrameTime, 5.0f, 1000.0f / gu->minFPS);
}

void CGame::ClientReadNet()
{
	// look ahead so we can adapt consumeSpeedMult to network fluctuations
	UpdateNumQueuedSimFrames();
	UpdateNetMessageProcessingTimeLeft();

	const spring_time msgProcEndTime = spring_gettime() + spring_msecs(GetNetMessageProcessingTimeLimit());

	// really process the messages
	while (true) {
		// smooths simframes across the full second
		if (msgProcTimeLeft <= 0.0f)
			break;
		// balance the time spent in sim & drawing
		if (spring_gettime() > msgProcEndTime)
			break;

		lastNetPacketProcessTime = spring_gettime();

		// get netpacket from the queue
		std::shared_ptr<const netcode::RawPacket> packet = clientNet->GetData(gs->frameNum);

		if (!packet)
			break;

		lastReceivedNetPacketTime = spring_gettime();

		const unsigned char* inbuf = packet->data;
		const unsigned dataLength = packet->length;
		const unsigned char packetCode = inbuf[0];

		switch (packetCode) {
			case NETMSG_QUIT: {
				try {
					netcode::UnpackPacket pckt(packet, 3);
					std::string message;

					pckt >> message;

					LOG("%s", message.c_str());

					GameEnd({});
					AddTraffic(-1, packetCode, dataLength);
					clientNet->Close(true);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_QUIT] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_PLAYERLEFT: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_PLAYERLEFT] invalid player-number %i", __func__, player);
					break;
				}
				playerHandler->PlayerLeft(player, inbuf[2]);
				eventHandler.PlayerRemoved(player, (int) inbuf[2]);

				AddTraffic(player, packetCode, dataLength);
				break;
			}

			case NETMSG_STARTPLAYING: {
				unsigned timeToStart = *(unsigned*)(inbuf+1);
				if (timeToStart > 0) {
					GameSetupDrawer::StartCountdown(timeToStart);
				} else {
					StartPlaying();
				}
				AddTraffic(-1, packetCode, dataLength);
				break;
			}

			case NETMSG_PLAYERSTAT: {
				const unsigned char playerNum = inbuf[1];
				if (!playerHandler->IsValidPlayer(playerNum)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_PLAYERSTAT] invalid player-number %i", __func__, playerNum);
					break;
				}

				CPlayer* player = playerHandler->Player(playerNum);
				player->currentStats = *reinterpret_cast<const PlayerStatistics*>(&inbuf[2]);

				CDemoRecorder* record = clientNet->GetDemoRecorder();
				if (record != NULL) {
					record->SetPlayerStats(playerNum, player->currentStats);
				}
				AddTraffic(playerNum, packetCode, dataLength);
				break;
			}

			case NETMSG_PAUSE: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_PAUSE] invalid player-number %i", __func__, player);
					break;
				}
				gs->paused = !!inbuf[2];
				LOG("%s %s the game",
						playerHandler->Player(player)->name.c_str(),
						(gs->paused ? "paused" : "unpaused"));
				eventHandler.GamePaused(player, gs->paused);
				lastReadNetTime = spring_gettime();
				AddTraffic(player, packetCode, dataLength);
				break;
			}

			case NETMSG_INTERNAL_SPEED: {
				gs->speedFactor = *((float*) &inbuf[1]);
				sound->PitchAdjust(math::sqrt(gs->speedFactor));
				//LOG_L(L_DEBUG, "Internal speed set to %.2f", gs->speedFactor);
				AddTraffic(-1, packetCode, dataLength);
				break;
			}

			case NETMSG_USER_SPEED: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player) && player != SERVER_PLAYER) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_USER_SPEED] invalid player-number %i", __func__, player);
					break;
				}
				const char* pName = (player == SERVER_PLAYER)? "server": playerHandler->Player(player)->name.c_str();

				gs->wantedSpeedFactor = *((float*) &inbuf[2]);
				LOG("Speed set to %.1f [%s]", gs->wantedSpeedFactor, pName);
				AddTraffic(player, packetCode, dataLength);
				break;
			}

			case NETMSG_CPU_USAGE: {
				LOG_L(L_WARNING, "Game clients shouldn't get cpu usage msgs?");
				AddTraffic(-1, packetCode, dataLength);
				break;
			}

			case NETMSG_PLAYERINFO: {
				const unsigned char playerId = inbuf[1];
				if (!playerHandler->IsValidPlayer(playerId)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_PLAYERINFO] invalid player-number %i", __func__, playerId);
					break;
				}
				CPlayer* p  = playerHandler->Player(playerId);
				p->cpuUsage =           *(float*) &inbuf[2];
				p->ping     = *(std::uint32_t*) &inbuf[6];

				AddTraffic(playerId, packetCode, dataLength);
				break;
			}

			case NETMSG_PLAYERNAME: {
				try {
					netcode::UnpackPacket pckt(packet, 2);
					unsigned char playerID;
					pckt >> playerID;
					if (!playerHandler->IsValidPlayer(playerID))
						throw netcode::UnpackPacketException("Invalid player number");

					CPlayer* player = playerHandler->Player(playerID);
					pckt >> player->name;
					player->SetReadyToStart(gameSetup->startPosType != CGameSetup::StartPos_ChooseInGame);
					player->active = true;
					wordCompletion->AddWord(player->name, false, false, false); // required?
					AddTraffic(playerID, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_PLAYERNAME] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_CHAT: {
				try {
					ChatMessage msg(packet);

					HandleChatMsg(msg);
					AddTraffic(msg.fromPlayer, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_CHAT] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_SYSTEMMSG: {
				try {
					netcode::UnpackPacket pckt(packet, 4);
					std::string sysMsg;
					pckt >> sysMsg;
					LOG("%s", sysMsg.c_str());
					AddTraffic(-1, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_SYSTEMMSG] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_STARTPOS: {
				const unsigned char playerID = inbuf[1];
				const unsigned int teamID = inbuf[2];

				if (!playerHandler->IsValidPlayer(playerID) && playerID != SERVER_PLAYER) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_STARTPOS] invalid player-number %i", __func__, playerID);
					break;
				}
				if (!teamHandler->IsValidTeam(teamID)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_STARTPOS] invalid team-number %i", __func__, teamID);
					break;
				}

				const unsigned char rdyState = inbuf[3];
				float3 rawPickPos(*(float*) &inbuf[4], *(float*) &inbuf[8], *(float*) &inbuf[12]);
				float3 clampedPos(rawPickPos);

				CTeam* team = teamHandler->Team(teamID);
				team->ClampStartPosInStartBox(&clampedPos);

				if (eventHandler.AllowStartPosition(playerID, teamID, rdyState, clampedPos, rawPickPos)) {
					team->SetStartPos(clampedPos);

					if (playerID != SERVER_PLAYER) {
						playerHandler->Player(playerID)->SetReadyToStart(rdyState != CPlayer::PLAYER_RDYSTATE_UPDATED);
					}
				}

				AddTraffic(playerID, packetCode, dataLength);
				break;
			}

			case NETMSG_RANDSEED: {
				gsRNG.SetSeed(*((unsigned int*)&inbuf[1]), true);
				AddTraffic(-1, packetCode, dataLength);
				break;
			}

			case NETMSG_GAMEID: {
				const unsigned char* p = &inbuf[1];
				CDemoRecorder* record = clientNet->GetDemoRecorder();
				if (record != NULL) {
					record->SetGameID(p);
				}
				memcpy(gameID, p, sizeof(gameID));
				LOG("GameID: "
						"%02x%02x%02x%02x%02x%02x%02x%02x"
						"%02x%02x%02x%02x%02x%02x%02x%02x",
						p[ 0], p[ 1], p[ 2], p[ 3], p[ 4], p[ 5], p[ 6], p[ 7],
						p[ 8], p[ 9], p[10], p[11], p[12], p[13], p[14], p[15]);
				AddTraffic(-1, packetCode, dataLength);
				eventHandler.GameID(gameID, sizeof(gameID));
				break;
			}

			case NETMSG_PATH_CHECKSUM: {
				const unsigned char playerNum = inbuf[1];
				if (!playerHandler->IsValidPlayer(playerNum)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_PATH_CHECKSUM] invalid player-number %i", __func__, playerNum);
					break;
				}

				const std::uint32_t playerCheckSum = *(std::uint32_t*) &inbuf[2];
				const std::uint32_t localCheckSum = pathManager->GetPathCheckSum();

				const CPlayer* player = playerHandler->Player(playerNum);

				const char* pName = player->name.c_str();
				const char* pType = player->IsSpectator()? "spectator": "player";
				const char* fmtStrs[2] = {
					"[DESYNC WARNING] path-checksum for %s %d (%s) is 0; non-writable PathEstimator-cache?",
					"[DESYNC WARNING] path-checksum %08x for %s %d (%s) does not match local checksum %08x; stale PathEstimator-cache?",
				};

				// XXX maybe use a "Desync" section here?
				if (playerCheckSum == 0) {
					LOG_L(L_WARNING, fmtStrs[0], pType, playerNum, pName);
				} else {
					if (playerCheckSum != localCheckSum) {
						LOG_L(L_WARNING, fmtStrs[1], playerCheckSum, pType, playerNum, pName, localCheckSum);
					}
				}
			} break;

			case NETMSG_KEYFRAME: {
				const int serverFrameNum = *(int*)(inbuf + 1);

				if (gs->frameNum != (serverFrameNum - 1)) {
					LOG_L(L_ERROR, "[Game::%s] keyframe difference: %i (client: %d, server: %d)", __func__, gs->frameNum - (serverFrameNum - 1), gs->frameNum, serverFrameNum);
					break;
				}

				// fall-through and run SimFrame() iff this message really came from the server
				clientNet->Send(CBaseNetProtocol::Get().SendKeyFrame(serverFrameNum));
			}
			case NETMSG_NEWFRAME: {
				msgProcTimeLeft -= 1000.0f;
				lastSimFrameNetPacketTime = spring_gettime();

				SimFrame();

#ifdef SYNCCHECK
				// both NETMSG_SYNCRESPONSE and NETMSG_NEWFRAME are used for ping calculation by server
				ASSERT_SYNCED(gs->frameNum);
				ASSERT_SYNCED(CSyncChecker::GetChecksum());
				clientNet->Send(CBaseNetProtocol::Get().SendSyncResponse(gu->myPlayerNum, gs->frameNum, CSyncChecker::GetChecksum()));

				if (gameServer != NULL && gameServer->GetDemoReader() != NULL) {
					// buffer all checksums, so we can check sync later between demo & local
					localSyncChecksums[gs->frameNum] = CSyncChecker::GetChecksum();
				}

				if ((gs->frameNum & 4095) == 0) {
					// reset checksum every 4096 frames =~ 2.5 minutes
					CSyncChecker::NewFrame();
				}
#endif
				AddTraffic(-1, packetCode, dataLength);

			} break;

			case NETMSG_SYNCRESPONSE: {
#if (defined(SYNCCHECK))
				if (gameServer != nullptr && gameServer->GetDemoReader() != nullptr) {
					// NOTE:
					//   this packet is also sent during live games,
					//   during which we should just ignore it (the
					//   server does sync-checking for us)
					netcode::UnpackPacket pckt(packet, 1);

					unsigned char playerNum; pckt >> playerNum;
					          int  frameNum; pckt >> frameNum;
					unsigned  int  checkSum; pckt >> checkSum;

					const unsigned int ourCheckSum = localSyncChecksums[frameNum];
					const CPlayer* player = playerHandler->Player(playerNum);

					// check if our checksum for this frame matches what
					// player <playerNum> sent to the server at the same
					// frame in the original game (in case of a demo)
					if (playerNum == gu->myPlayerNum) { break; }
					if (checkSum == ourCheckSum) { break; }

					const char* pName = player->name.c_str();
					const char* pType = player->IsSpectator()? "spectator": "player";
					const char* fmtStr = "[DESYNC WARNING] checksum %x from demo %s %d (%s) does not match our checksum %x for frame-number %d";

					LOG_L(L_ERROR, fmtStr, checkSum, pType, playerNum, pName, ourCheckSum, frameNum);
				}
#endif
			} break;


			case NETMSG_COMMAND: {
				try {
					netcode::UnpackPacket pckt(packet, 1);

					unsigned short packetSize; pckt >> packetSize;
					unsigned char playerNum; pckt >> playerNum;
					const unsigned int numParams = (packetSize - 9) / sizeof(float);

					if (!playerHandler->IsValidPlayer(playerNum))
						throw netcode::UnpackPacketException("Invalid player number");

					int cmdID;
					unsigned char cmdOpt;
					pckt >> cmdID;
					pckt >> cmdOpt;

					Command c(cmdID, cmdOpt);
					c.params.reserve(numParams);

					for (int a = 0; a < numParams; ++a) {
						float param; pckt >> param;
						c.PushParam(param);
					}

					selectedUnitsHandler.NetOrder(c, playerNum);
					AddTraffic(playerNum, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_COMMAND] exception \"%s\"", __func__, ex.what());
				}

				break;
			}

			case NETMSG_SELECT: {
				try {
					netcode::UnpackPacket pckt(packet, 1);

					unsigned short packetSize; pckt >> packetSize;
					unsigned char playerNum; pckt >> playerNum;
					const unsigned int numUnitIDs = (packetSize - 4) / sizeof(short int);

					if (!playerHandler->IsValidPlayer(playerNum))
						throw netcode::UnpackPacketException("Invalid player number");

					std::vector<int> selectedUnitIDs;
					selectedUnitIDs.reserve(numUnitIDs);

					for (int a = 0; a < numUnitIDs; ++a) {
						short int unitID; pckt >> unitID;
						const CUnit* unit = unitHandler->GetUnit(unitID);

						if (unit == NULL) {
							// unit was destroyed in simulation (without its ID being recycled)
							// after sending a command but before receiving it back, more likely
							// to happen in high-latency situations
							// LOG_L(L_WARNING, "[NETMSG_SELECT] invalid unitID (%i) from player %i", unitID, playerNum);
							continue;
						}

						// if in godMode, this is always true for any player
						if (playerHandler->Player(playerNum)->CanControlTeam(unit->team)) {
							selectedUnitIDs.push_back(unitID);
						}
					}

					selectedUnitsHandler.NetSelect(selectedUnitIDs, playerNum);
					AddTraffic(playerNum, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_SELECT] exception \"%s\"", __func__, ex.what());
				}

				break;
			}

			case NETMSG_AICOMMAND:
			case NETMSG_AICOMMAND_TRACKED: {
				try {
					netcode::UnpackPacket pckt(packet, 1);
					short int psize;
					pckt >> psize;
					unsigned char player;
					pckt >> player;
					unsigned char aiID;
					pckt >> aiID;

					if (!playerHandler->IsValidPlayer(player))
						throw netcode::UnpackPacketException("Invalid player number");

					short int unitid;
					pckt >> unitid;
					if (unitid < 0 || static_cast<size_t>(unitid) >= unitHandler->MaxUnits())
						throw netcode::UnpackPacketException("Invalid unit ID");

					int cmd_id;
					unsigned char cmd_opt;
					pckt >> cmd_id;
					pckt >> cmd_opt;

					Command c(cmd_id, cmd_opt);
					if (packetCode == NETMSG_AICOMMAND_TRACKED) {
						pckt >> c.aiCommandId;
					}

					// insert the command parameters
					for (int a = 0; a < ((psize - 11) / 4); ++a) {
						float param;
						pckt >> param;
						c.PushParam(param);
					}

					selectedUnitsHandler.AiOrder(unitid, c, player);
					AddTraffic(player, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_AICOMMAND*] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_AICOMMANDS: {
				try {
					netcode::UnpackPacket pckt(packet, 3);
					unsigned char player;
					pckt >> player;

					if (!playerHandler->IsValidPlayer(player))
						throw netcode::UnpackPacketException("Invalid player number");

					unsigned char aiID;
					unsigned char pairwise;
					unsigned int sameCmdID;
					unsigned char sameCmdOpt;
					unsigned short sameCmdParamSize;

					pckt >> aiID;
					pckt >> pairwise;
					pckt >> sameCmdID;
					pckt >> sameCmdOpt;
					pckt >> sameCmdParamSize;
					// parse the unit list
					vector<int> unitIDs;
					short int unitCount;
					pckt >> unitCount;
					for (int u = 0; u < unitCount; u++) {
						short int unitID;
						pckt >> unitID;
						unitIDs.push_back(unitID);
					}
					// parse the command list
					vector<Command> commands;
					short int commandCount;
					pckt >> commandCount;
					for (int c = 0; c < commandCount; c++) {
						int cmd_id;
						unsigned char cmd_opt;
						if (sameCmdID == 0)
							pckt >> cmd_id;
						else
							cmd_id = sameCmdID;
						if (sameCmdOpt == 0xFF)
							pckt >> cmd_opt;
						else
							cmd_opt = sameCmdOpt;

						Command cmd(cmd_id, cmd_opt);
						short int paramCount;
						if (sameCmdParamSize == 0xFFFF)
							pckt >> paramCount;
						else
							paramCount = sameCmdParamSize;
						for (int p = 0; p < paramCount; p++) {
							float param;
							pckt >> param;
							cmd.PushParam(param);
						}
						commands.push_back(cmd);
					}
					// apply the commands
					if (pairwise) {
						for (int x = 0; x < std::min(unitCount, commandCount); ++x) {
							selectedUnitsHandler.AiOrder(unitIDs[x], commands[x], player);
						}
					}
					else {
						for (int c = 0; c < commandCount; c++) {
							for (int u = 0; u < unitCount; u++) {
								selectedUnitsHandler.AiOrder(unitIDs[u], commands[c], player);
							}
						}
					}
					AddTraffic(player, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_AICOMMANDS] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_AISHARE: {
				try {
					netcode::UnpackPacket pckt(packet, 1);

					short int numBytes;
					unsigned char player;
					unsigned char aiID;
					unsigned char srcTeamID;
					unsigned char dstTeamID;
					float metalShare;
					float energyShare;

					pckt >> numBytes;
					pckt >> player;
					if (!playerHandler->IsValidPlayer(player))
						throw netcode::UnpackPacketException("Invalid player number");

					pckt >> aiID;

					// total message length
					const int fixedLen = (1 + sizeof(short) + 3 + (2 * sizeof(float)));
					const int variableLen = numBytes - fixedLen;
					const int numUnitIDs = variableLen / sizeof(short); // each unitID is two bytes

					pckt >> srcTeamID;
					pckt >> dstTeamID;
					pckt >> metalShare;
					pckt >> energyShare;

					CTeam* srcTeam = teamHandler->Team(srcTeamID);
					CTeam* dstTeam = teamHandler->Team(dstTeamID);

					if (metalShare > 0.0f) {
						if (eventHandler.AllowResourceTransfer(srcTeamID, dstTeamID, "m", metalShare)) {
							srcTeam->res.metal                       -= metalShare;
							srcTeam->resSent.metal                   += metalShare;
							srcTeam->GetCurrentStats().metalSent     += metalShare;
							dstTeam->res.metal                       += metalShare;
							dstTeam->resReceived.metal               += metalShare;
							dstTeam->GetCurrentStats().metalReceived += metalShare;
						}
					}
					if (energyShare > 0.0f) {
						if (eventHandler.AllowResourceTransfer(srcTeamID, dstTeamID, "e", energyShare)) {
							srcTeam->res.energy                       -= energyShare;
							srcTeam->resSent.energy                   += energyShare;
							srcTeam->GetCurrentStats().energySent     += energyShare;
							dstTeam->res.energy                       += energyShare;
							dstTeam->resReceived.energy               += energyShare;
							dstTeam->GetCurrentStats().energyReceived += energyShare;
						}
					}

					for (int i = 0, j = fixedLen; i < numUnitIDs; i++, j += sizeof(short)) {
						short int unitID;
						pckt >> unitID;

						CUnit* u = unitHandler->GetUnit(unitID);

						if (u == nullptr)
							throw netcode::UnpackPacketException("Invalid unit ID");

						// ChangeTeam() handles the AllowUnitTransfer() LuaRule
						if (u->team == srcTeamID && !u->beingBuilt) {
							u->ChangeTeam(dstTeamID, CUnit::ChangeGiven);
						}
					}
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_AISHARE] exception \"%s\"", __func__, ex.what());
				}
				break;
			}


			case NETMSG_LOGMSG: {
				try {
					netcode::UnpackPacket unpack(packet, sizeof(uint8_t));

					std::uint16_t packetSize;
					std::uint8_t playerNum;
					std::uint8_t logLevel;
					std::string logString;

					unpack >> packetSize;
					if (packetSize != packet->length)
						throw netcode::UnpackPacketException("invalid size");

					unpack >> playerNum;
					if (!playerHandler->IsValidPlayer(playerNum))
						throw netcode::UnpackPacketException("invalid player number");

					unpack >> logLevel;
					unpack >> logString;

					const CPlayer* player = playerHandler->Player(playerNum);

					const char* fmtStr = "[Game::%s][LOGMSG] sender=\"%s\" string=\"%s\"";
					const char* logStr = logString.c_str();

					switch (logLevel) {
						case LOG_LEVEL_INFO   : { LOG_L(L_INFO   , fmtStr, __func__, player->name.c_str(), logStr); } break;
						case LOG_LEVEL_DEBUG  : { LOG_L(L_DEBUG  , fmtStr, __func__, player->name.c_str(), logStr); } break;
						case LOG_LEVEL_ERROR  : { LOG_L(L_ERROR  , fmtStr, __func__, player->name.c_str(), logStr); } break;
						case LOG_LEVEL_FATAL  : { LOG_L(L_FATAL  , fmtStr, __func__, player->name.c_str(), logStr); } break;
						case LOG_LEVEL_NOTICE : { LOG_L(L_NOTICE , fmtStr, __func__, player->name.c_str(), logStr); } break;
						case LOG_LEVEL_WARNING: { LOG_L(L_WARNING, fmtStr, __func__, player->name.c_str(), logStr); } break;
					}
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_LOGMSG] exception \"%s\"", __func__, ex.what());
				}
			} break;

			case NETMSG_LUAMSG: {
				try {
					netcode::UnpackPacket unpack(packet, sizeof(uint8_t));

					std::uint16_t packetSize;
					std::uint8_t playerNum;
					std::uint16_t script;
					std::uint8_t mode;
					std::vector<std::uint8_t> data;

					unpack >> packetSize;
					if (packetSize != packet->length)
						throw netcode::UnpackPacketException("invalid packet-size");

					unpack >> playerNum;
					if (!playerHandler->IsValidPlayer(playerNum))
						throw netcode::UnpackPacketException("invalid player number");

					data.resize(packetSize - (1 + sizeof(packetSize) + sizeof(playerNum) + sizeof(script) + sizeof(mode)));

					unpack >> script;
					unpack >> mode;
					unpack >> data;

					CLuaHandle::HandleLuaMsg(playerNum, script, mode, data);
					AddTraffic(playerNum, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_LUAMSG] exception \"%s\"", __func__, ex.what());
				}
			} break;


			case NETMSG_SHARE: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_SHARE] invalid player-number %i", __func__, player);
					break;
				}
				const int srcTeamID = playerHandler->Player(player)->team;
				const int dstTeamID = inbuf[2];
				const bool shareUnits = !!inbuf[3];
				CTeam* srcTeam = teamHandler->Team(srcTeamID);
				CTeam* dstTeam = teamHandler->Team(dstTeamID);
				const float metalShare  = Clamp(*(float*)&inbuf[4], 0.0f, (float)srcTeam->res.metal);
				const float energyShare = Clamp(*(float*)&inbuf[8], 0.0f, (float)srcTeam->res.energy);

				if (metalShare > 0.0f) {
					if (eventHandler.AllowResourceTransfer(srcTeamID, dstTeamID, "m", metalShare)) {
						srcTeam->res.metal                       -= metalShare;
						srcTeam->resSent.metal                   += metalShare;
						srcTeam->GetCurrentStats().metalSent     += metalShare;
						dstTeam->res.metal                       += metalShare;
						dstTeam->resReceived.metal               += metalShare;
						dstTeam->GetCurrentStats().metalReceived += metalShare;
					}
				}
				if (energyShare > 0.0f) {
					if (eventHandler.AllowResourceTransfer(srcTeamID, dstTeamID, "e", energyShare)) {
						srcTeam->res.energy                       -= energyShare;
						srcTeam->resSent.energy                   += energyShare;
						srcTeam->GetCurrentStats().energySent     += energyShare;
						dstTeam->res.energy                       += energyShare;
						dstTeam->resReceived.energy               += energyShare;
						dstTeam->GetCurrentStats().energyReceived += energyShare;
					}
				}

				if (shareUnits) {
					vector<int>& netSelUnits = selectedUnitsHandler.netSelected[player];
					vector<int>::const_iterator ui;

					for (ui = netSelUnits.begin(); ui != netSelUnits.end(); ++ui) {
						CUnit* unit = unitHandler->GetUnit(*ui);

						if (unit == nullptr)
							continue;
						if (unit->UnderFirstPersonControl())
							continue;
						// in godmode we can have units selected that are not ours
						if (unit->team != srcTeamID)
							continue;

						if (unit->isDead)
							continue;
						if (unit->IsCrashing())
							continue;

						unit->ChangeTeam(dstTeamID, CUnit::ChangeGiven);
					}

					netSelUnits.clear();
				}
				AddTraffic(player, packetCode, dataLength);
				break;
			}

			case NETMSG_SETSHARE: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_SETSHARE] invalid player-number %i", __func__, player);
					break;
				}
				const unsigned char team = inbuf[2];
				if (!teamHandler->IsValidTeam(team)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_SETSHARE] invalid team-number %i", __func__, team);
					break;
				}

				const float metalShare = *(float*)&inbuf[3];
				const float energyShare = *(float*)&inbuf[7];

				if (eventHandler.AllowResourceLevel(team, "m", metalShare))
					teamHandler->Team(team)->resShare.metal = metalShare;

				if (eventHandler.AllowResourceLevel(team, "e", energyShare))
					teamHandler->Team(team)->resShare.energy = energyShare;

				AddTraffic(player, packetCode, dataLength);
				break;
			}
			case NETMSG_MAPDRAW: {
				int player = inMapDrawer->GotNetMsg(packet);
				if (player >= 0)
					AddTraffic(player, packetCode, dataLength);
				break;
			}
			case NETMSG_TEAM: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_TEAM] invalid player-number %i", __func__, player);
					break;
				}

				const unsigned char action = inbuf[2];
				const int fromTeam = playerHandler->Player(player)->team;

				switch (action) {
					case TEAMMSG_GIVEAWAY: {
						const unsigned char toTeam = inbuf[3];
						const unsigned char giverTeam = inbuf[4];

						if (!teamHandler->IsValidTeam(toTeam) || !teamHandler->IsValidTeam(giverTeam)) {
							LOG_L(L_ERROR, "[Game::%s][TEAMMSG_GIVEWAY] invalid team-numbers {%i,%i}", __func__, toTeam, giverTeam);
							break;
						}

						const int numPlayersInGiverTeam        = playerHandler->ActivePlayersInTeam(giverTeam).size();
						const size_t numTotAIsInGiverTeam      = skirmishAIHandler.GetSkirmishAIsInTeam(giverTeam).size();
						const size_t numControllersInGiverTeam = numPlayersInGiverTeam + numTotAIsInGiverTeam;

						bool giveAwayOk = false;

						if (giverTeam == fromTeam) {
							// player is giving stuff from his own team
							giveAwayOk = true;

							if (numPlayersInGiverTeam == 1) {
								teamHandler->Team(giverTeam)->GiveEverythingTo(toTeam);
							} else {
								playerHandler->Player(player)->StartSpectating();
							}

							selectedUnitsHandler.ClearNetSelect(player);
						} else {
							// player is giving stuff from one of his AI teams
							if (numPlayersInGiverTeam == 0) {
								giveAwayOk = true;
							}
						}
						if (giveAwayOk && (numControllersInGiverTeam == 1)) {
							// team has no controller left now
							teamHandler->Team(giverTeam)->GiveEverythingTo(toTeam);
							teamHandler->Team(giverTeam)->SetLeader(-1);
						}
						CPlayer::UpdateControlledTeams();
						break;
					}
					case TEAMMSG_RESIGN: {
						if (!playing)
							break;

						playerHandler->Player(player)->StartSpectating();

						// update all teams of which the player is leader
						for (size_t t = 0; t < teamHandler->ActiveTeams(); ++t) {
							CTeam* team = teamHandler->Team(t);

							if (team->GetLeader() != player)
								continue;

							const std::vector<int> &teamPlayers = playerHandler->ActivePlayersInTeam(t);
							const std::vector<unsigned char> &teamAIs  = skirmishAIHandler.GetSkirmishAIsInTeam(t);

							if ((teamPlayers.size() + teamAIs.size()) == 0) {
								// no controllers left in team
								//team.active = false;
								team->SetLeader(-1);
							} else if (teamPlayers.empty()) {
								// no human player left in team
								team->SetLeader(skirmishAIHandler.GetSkirmishAI(teamAIs[0])->hostPlayer);
							} else {
								// still human controllers left in team
								team->SetLeader(teamPlayers[0]);
							}
						}
						LOG_L(L_DEBUG, "Player %i (%s) resigned and is now spectating!",
								player,
								playerHandler->Player(player)->name.c_str());
						selectedUnitsHandler.ClearNetSelect(player);
						CPlayer::UpdateControlledTeams();
						break;
					}
					case TEAMMSG_JOIN_TEAM: {
						const unsigned char newTeam = inbuf[3];
						if (!teamHandler->IsValidTeam(newTeam)) {
							LOG_L(L_ERROR, "[Game::%s][TEAMMSG_JOIN_TEAM] invalid team-number %i", __func__, newTeam);
							break;
						}

						teamHandler->Team(newTeam)->AddPlayer(player);
						break;
					}
					case TEAMMSG_TEAM_DIED: {
						// silently drop since we can calculate this ourself, altho it's useful info to store in replays
						break;
					}
					default: {
						LOG_L(L_ERROR, "[Game::%s][NETMSG_TEAM] unknown action %i from player %i", __func__, action, player);
					}
				}
				AddTraffic(player, packetCode, dataLength);
				break;
			}
			case NETMSG_GAMEOVER: {
				const unsigned char player = inbuf[1];
				// silently drop since we can calculate this ourself, altho it's useful info to store in replays
				AddTraffic(player, packetCode, dataLength);
				break;
			}
			case NETMSG_TEAMSTAT: { /* LadderBot (dedicated client) only */ } break;
			case NETMSG_REQUEST_TEAMSTAT: { /* LadderBot (dedicated client) only */ } break;

			case NETMSG_AI_CREATED: {
				try {
					netcode::UnpackPacket pckt(packet, 2);
					unsigned char playerId;
					pckt >> playerId;
					unsigned char skirmishAIId;
					pckt >> skirmishAIId;
					unsigned char aiTeamId;
					pckt >> aiTeamId;
					std::string aiName;
					pckt >> aiName;
					CTeam* tai = teamHandler->Team(aiTeamId);
					const unsigned isLocal = (playerId == gu->myPlayerNum);

					if (isLocal) {
						const SkirmishAIData& aiData = *(skirmishAIHandler.GetLocalSkirmishAIInCreation(aiTeamId));
						if (skirmishAIHandler.IsActiveSkirmishAI(skirmishAIId)) {
							#ifdef DEBUG
								// we will end up here for AIs defined in the start script
								const SkirmishAIData* curAIData = skirmishAIHandler.GetSkirmishAI(skirmishAIId);
								assert((aiData.team == curAIData->team) && (aiData.name == curAIData->name) && (aiData.hostPlayer == curAIData->hostPlayer));
							#endif
						} else {
							// we will end up here for local AIs defined mid-game,
							// eg. with /aicontrol
							const std::string aiName = aiData.name + " "; // aiData would be invalid after the next line
							skirmishAIHandler.AddSkirmishAI(aiData, skirmishAIId);
							wordCompletion->AddWord(aiName, false, false, false);
						}
					} else {
						SkirmishAIData aiData;
						aiData.team       = aiTeamId;
						aiData.name       = aiName;
						aiData.hostPlayer = playerId;
						skirmishAIHandler.AddSkirmishAI(aiData, skirmishAIId);
						wordCompletion->AddWord(aiData.name + " ", false, false, false);
					}

					if (!tai->HasLeader()) {
						tai->SetLeader(playerId);
					}
					CPlayer::UpdateControlledTeams();
					eventHandler.PlayerChanged(playerId);
					if (isLocal) {
						LOG("Skirmish AI being created for team %i ...", aiTeamId);
						eoh->CreateSkirmishAI(skirmishAIId);
					}
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_AI_CREATED] exception \"%s\"", __func__, ex.what());
				}
				break;
			}
			case NETMSG_AI_STATE_CHANGED: {
				const unsigned char playerId     = inbuf[1];
				if (!playerHandler->IsValidPlayer(playerId)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_AI_STATE_CHANGED] invalid player-number %i", __func__, playerId);
					break;
				}

				const unsigned char skirmishAIId  = inbuf[2];
				const ESkirmishAIStatus newState = (ESkirmishAIStatus) inbuf[3];
				SkirmishAIData* aiData           = skirmishAIHandler.GetSkirmishAI(skirmishAIId);
				const ESkirmishAIStatus oldState = aiData->status;
				const unsigned aiTeamId          = aiData->team;
				const bool isLuaAI               = aiData->isLuaAI;
				const unsigned isLocal           = (aiData->hostPlayer == gu->myPlayerNum);
				const size_t numPlayersInAITeam  = playerHandler->ActivePlayersInTeam(aiTeamId).size();
				const size_t numAIsInAITeam      = skirmishAIHandler.GetSkirmishAIsInTeam(aiTeamId).size();
				CTeam* tai                       = teamHandler->Team(aiTeamId);

				aiData->status = newState;

				if (isLocal && !isLuaAI && ((newState == SKIRMAISTATE_DIEING) || (newState == SKIRMAISTATE_RELOADING))) {
					eoh->DestroySkirmishAI(skirmishAIId);
				} else if (newState == SKIRMAISTATE_DEAD) {
					if (oldState == SKIRMAISTATE_RELOADING) {
						if (isLocal) {
							LOG("Skirmish AI \"%s\" being reloaded for team %i ...",
									aiData->name.c_str(), aiTeamId);
							eoh->CreateSkirmishAI(skirmishAIId);
						}
					} else {
						const std::string aiInstanceName = aiData->name;
						wordCompletion->RemoveWord(aiData->name + " ");
						skirmishAIHandler.RemoveSkirmishAI(skirmishAIId);
						aiData = NULL; // not valid anymore after RemoveSkirmishAI()
						// this could be done in the above function as well
						if ((numPlayersInAITeam + numAIsInAITeam) == 1) {
							// team has no controller left now
							tai->SetLeader(-1);
						}
						CPlayer::UpdateControlledTeams();
						eventHandler.PlayerChanged(playerId);
						LOG("Skirmish AI \"%s\" (ID:%i), which controlled team %i is now dead",
								aiInstanceName.c_str(), skirmishAIId, aiTeamId);
					}
				} else if (newState == SKIRMAISTATE_ALIVE) {
					if (isLocal) {
						// short-name and version of the AI is unsynced data
						// -> only available on the AI host
						LOG("Skirmish AI \"%s\" (ID:%i, Short-Name:\"%s\", "
								"Version:\"%s\") took over control of team %i",
								aiData->name.c_str(), skirmishAIId,
								aiData->shortName.c_str(),
								aiData->version.c_str(), aiTeamId);
					} else {
						LOG("Skirmish AI \"%s\" (ID:%i) took over control of team %i",
								aiData->name.c_str(), skirmishAIId, aiTeamId);
					}
				}
				break;
			}
			case NETMSG_ALLIANCE: {
				const unsigned char player = inbuf[1];

				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s] invalid player number %i in NETMSG_ALLIANCE", __func__, player);
					break;
				}

				const bool allied = static_cast<bool>(inbuf[3]);
				const unsigned char whichAllyTeam = inbuf[2];
				const unsigned char fromAllyTeam = teamHandler->AllyTeam(playerHandler->Player(player)->team);

				if (teamHandler->IsValidAllyTeam(whichAllyTeam) && fromAllyTeam != whichAllyTeam) {
					// FIXME NETMSG_ALLIANCE need to reset unit allyTeams
					// FIXME NETMSG_ALLIANCE need a call-in for AIs
					teamHandler->SetAlly(fromAllyTeam, whichAllyTeam, allied);

					// inform the players
					std::ostringstream msg;
					if (fromAllyTeam == gu->myAllyTeam) {
						msg << "Alliance: you have " << (allied ? "allied" : "unallied")
							<< " allyteam " << whichAllyTeam << ".";
					} else if (whichAllyTeam == gu->myAllyTeam) {
						msg << "Alliance: allyteam " << whichAllyTeam << " has "
							<< (allied ? "allied" : "unallied") <<  " with you.";
					} else {
						msg << "Alliance: allyteam " << whichAllyTeam << " has "
							<< (allied ? "allied" : "unallied")
							<<  " with allyteam " << fromAllyTeam << ".";
					}
					LOG("%s", msg.str().c_str());

					// stop attacks against former foe
					if (allied) {
						for (CUnit* u: unitHandler->GetActiveUnits()) {
							if (teamHandler->Ally(u->allyteam, whichAllyTeam)) {
								u->StopAttackingAllyTeam(whichAllyTeam);
							}
						}
					}
					eventHandler.TeamChanged(playerHandler->Player(player)->team);
				} else {
					LOG_L(L_WARNING, "Alliance: Player %i sent out wrong allyTeam index in alliance message", player);
				}
				break;
			}
			case NETMSG_CCOMMAND: {
				try {
					CommandMessage msg(packet);

					ActionReceived(msg.GetAction(), msg.GetPlayerID());
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_CCOMMAND] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_DIRECT_CONTROL: {
				const unsigned char player = inbuf[1];

				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_DIRECT_CONTROL] invalid player-number %i", __func__, player);
					break;
				}

				CPlayer* sender = playerHandler->Player(player);
				if (sender->spectator || !sender->active) {
					break;
				}

				sender->StartControllingUnit();

				AddTraffic(player, packetCode, dataLength);
				break;
			}

			case NETMSG_DC_UPDATE: {
				const unsigned char player = inbuf[1];
				if (!playerHandler->IsValidPlayer(player)) {
					LOG_L(L_ERROR, "Invalid player number (%i) in NETMSG_DC_UPDATE", player);
					break;
				}

				CPlayer* sender = playerHandler->Player(player);
				sender->fpsController.RecvStateUpdate(inbuf);

				AddTraffic(player, packetCode, dataLength);
				break;
			}
			case NETMSG_SETPLAYERNUM:
			case NETMSG_ATTEMPTCONNECT: {
				AddTraffic(-1, packetCode, dataLength);
				break;
			}
			case NETMSG_CREATE_NEWPLAYER: { // server sends this second to let us know about new clients that join midgame
				try {
					netcode::UnpackPacket pckt(packet, 3);
					unsigned char spectator, team, playerNum;
					std::string name;

					// since the >> operator uses dest size to extract data from the packet, we need to use temp variables
					// of the same size of the packet, then convert to dest variable
					pckt >> playerNum;
					pckt >> spectator;
					pckt >> team;
					pckt >> name;

					CPlayer player;
					player.name = name;
					player.spectator = spectator;
					player.team = team;
					player.playerNum = playerNum;

					// add the new player
					playerHandler->AddPlayer(player);
					eventHandler.PlayerAdded(player.playerNum);

					LOG("[Game::%s] added new player %s with number %d to team %d", __func__, name.c_str(), player.playerNum, player.team);

					if (!player.spectator)
						eventHandler.TeamChanged(player.team);

					CDemoRecorder* record = clientNet->GetDemoRecorder();
					if (record != nullptr)
						record->AddNewPlayer(player.name, playerNum);

					AddTraffic(-1, packetCode, dataLength);
				} catch (const netcode::UnpackPacketException& ex) {
					LOG_L(L_ERROR, "[Game::%s][NETMSG_CREATE_NEWPLAYER] exception \"%s\"", __func__, ex.what());
				}
				break;
			}

			case NETMSG_CLIENTDATA: {
				if (!configHandler->GetBool("LogClientData"))
					break;

				const unsigned char fixedSize = sizeof(unsigned char) + sizeof(unsigned short) + sizeof(unsigned char);

				netcode::UnpackPacket pckt(packet, 1);
				unsigned short totalSize;
				unsigned char playerNum;
				pckt >> totalSize;
				pckt >> playerNum;

				if (!playerHandler->IsValidPlayer(playerNum)) {
					LOG_L(L_ERROR, "Invalid player number (%i) in NETMSG_CLIENTDATA", playerNum);
					break;
				}

				std::vector<std::uint8_t> buf(totalSize - fixedSize);
				pckt >> buf;
				std::string clientData = ClientData::GetUncompressed(buf);

				if (clientData.empty()) {
					LOG_L(L_ERROR, "Corrupt Client Data received from player %d", playerNum);
					break;
				}

				CPlayer* sender = playerHandler->Player(playerNum);
				LOG("[Game::%s] Client Data for player %d (%s): \n%s", __func__, playerNum, sender->name.c_str(), clientData.c_str());

				AddTraffic(-1, packetCode, dataLength);
				break;
			}

			// if we received this packet here we are the host player
			// (meaning the message was not processed), so discard it
			case NETMSG_GAME_FRAME_PROGRESS: {
				break;
			}
			default: {
#ifdef SYNCDEBUG
				if (!CSyncDebugger::GetInstance()->ClientReceived(inbuf))
#endif
				{
					LOG_L(L_ERROR, "Unknown net msg received, packet code is %d."
							" A likely cause of this is network instability,"
							" which may happen in a WLAN, for example.",
							packetCode);
				}
				AddTraffic(-1, packetCode, dataLength);
				break;
			}
		}
	}
}
