/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com.                */
#include "gamecontext.h"

#include "entities/character.h"
#include "gamemodes/ddnet.h"
#include "gamemodes/mod.h"
#include "player.h"
#include "score.h"
#include "teeinfo.h"

#include <antibot/antibot_data.h>

#include <base/aio.h>
#include <base/dbg.h>
#include <base/fs.h>
#include <base/io.h>
#include <base/logger.h>
#include <base/math.h>
#include <base/mem.h>
#include <base/secure.h>
#include <base/str.h>
#include <base/time.h>

#include <engine/console.h>
#include <engine/engine.h>
#include <engine/map.h>
#include <engine/server/server.h>
#include <engine/shared/config.h>
#include <engine/shared/datafile.h>
#include <engine/shared/json.h>
#include <engine/shared/linereader.h>
#include <engine/shared/memheap.h>
#include <engine/shared/protocol.h>
#include <engine/shared/protocolglue.h>
#include <engine/storage.h>

#include <generated/protocol.h>
#include <generated/protocol7.h>
#include <generated/protocolglue.h>

#include <game/collision.h>
#include <game/gamecore.h>
#include <game/mapitems.h>
#include <game/version.h>

#include <vector>

// Not thread-safe!
class CClientChatLogger : public ILogger
{
	CGameContext *m_pGameServer;
	int m_ClientId;
	ILogger *m_pOuterLogger;

public:
	CClientChatLogger(CGameContext *pGameServer, int ClientId, ILogger *pOuterLogger) :
		m_pGameServer(pGameServer),
		m_ClientId(ClientId),
		m_pOuterLogger(pOuterLogger)
	{
	}
	void Log(const CLogMessage *pMessage) override;
};

void CClientChatLogger::Log(const CLogMessage *pMessage)
{
	if(str_comp(pMessage->m_aSystem, "chatresp") == 0)
	{
		if(m_Filter.Filters(pMessage))
		{
			return;
		}
		m_pGameServer->SendChatTarget(m_ClientId, pMessage->Message());
	}
	else
	{
		m_pOuterLogger->Log(pMessage);
	}
}

CGameContext::CGameContext(bool Resetting) :
	m_Mutes("mutes"),
	m_VoteMutes("votemutes")
{
	m_Resetting = false;
	m_pServer = nullptr;

	for(auto &pPlayer : m_apPlayers)
		pPlayer = nullptr;

	mem_zero(&m_aLastPlayerInput, sizeof(m_aLastPlayerInput));
	std::fill(std::begin(m_aPlayerHasInput), std::end(m_aPlayerHasInput), false);

	m_pController = nullptr;

	m_pVoteOptionFirst = nullptr;
	m_pVoteOptionLast = nullptr;
	m_LastMapVote = 0;

	m_SqlRandomMapResult = nullptr;

	m_pLoadMapInfoResult = nullptr;
	m_aMapInfoMessage[0] = '\0';

	m_pScore = nullptr;

	m_VoteCreator = -1;
	m_VoteType = VOTE_TYPE_UNKNOWN;
	m_VoteCloseTime = 0;
	m_VoteUpdate = false;
	m_VotePos = 0;
	m_aVoteDescription[0] = '\0';
	m_aSixupVoteDescription[0] = '\0';
	m_aVoteCommand[0] = '\0';
	m_aVoteReason[0] = '\0';
	m_NumVoteOptions = 0;
	m_VoteEnforce = VOTE_ENFORCE_UNKNOWN;

	m_LatestLog = 0;
	mem_zero(&m_aLogs, sizeof(m_aLogs));

	str_copy(m_aVersionString, GAME_VERSION);
	if(GIT_SHORTREV_HASH != nullptr)
	{
		str_append(m_aVersionString, " ");
		str_append(m_aVersionString, GIT_SHORTREV_HASH);
	}

	if(!Resetting)
	{
		m_pMap = CreateMap();

		for(auto &pSavedTee : m_apSavedTees)
			pSavedTee = nullptr;

		for(auto &pSavedTeam : m_apSavedTeams)
			pSavedTeam = nullptr;

		std::fill(std::begin(m_aTeamMapping), std::end(m_aTeamMapping), -1);

		m_NonEmptySince = 0;
		m_pVoteOptionHeap = new CHeap();
	}

	m_aDeleteTempfile[0] = 0;
	m_TeeHistorianActive = false;
}

CGameContext::~CGameContext()
{
	for(auto &pPlayer : m_apPlayers)
		delete pPlayer;

	if(!m_Resetting)
	{
		m_pMap->Unload();
		m_pMap = nullptr;

		for(auto &pSavedTee : m_apSavedTees)
			delete pSavedTee;

		for(auto &pSavedTeam : m_apSavedTeams)
			delete pSavedTeam;

		delete m_pVoteOptionHeap;
	}

	delete m_pScore;
	m_pScore = nullptr;
}

void CGameContext::Clear()
{
	CHeap *pVoteOptionHeap = m_pVoteOptionHeap;
	CVoteOptionServer *pVoteOptionFirst = m_pVoteOptionFirst;
	CVoteOptionServer *pVoteOptionLast = m_pVoteOptionLast;
	int NumVoteOptions = m_NumVoteOptions;
	CTuningParams Tuning = m_aTuningList[0];
	CMutes Mutes = m_Mutes;
	CMutes VoteMutes = m_VoteMutes;
	std::unique_ptr<IMap> pMap;
	std::swap(pMap, m_pMap);

	m_Resetting = true;
	this->~CGameContext();
	new(this) CGameContext(true);

	m_pVoteOptionHeap = pVoteOptionHeap;
	m_pVoteOptionFirst = pVoteOptionFirst;
	m_pVoteOptionLast = pVoteOptionLast;
	m_NumVoteOptions = NumVoteOptions;
	m_aTuningList[0] = Tuning;
	m_Mutes = Mutes;
	m_VoteMutes = VoteMutes;
	std::swap(pMap, m_pMap);
}

void CGameContext::TeeHistorianWrite(const void *pData, int DataSize, void *pUser)
{
	CGameContext *pSelf = (CGameContext *)pUser;
	aio_write(pSelf->m_pTeeHistorianFile, pData, DataSize);
}

std::optional<std::vector<int>> CGameContext::ClientsForVictim(int ClientId, const char *pVictim, void *pUser)
{
	CGameContext *pSelf = (CGameContext *)pUser;
	std::vector<int> vClientIds;

	if(!str_comp(pVictim, "me"))
	{
		if(ClientId < 0)
			return std::nullopt;
		vClientIds.emplace_back(ClientId);
	}
	else if(!str_comp(pVictim, "all"))
	{
		const int MaxClients = pSelf->Server()->MaxClients();
		for(int i = 0; i < MaxClients; i++)
		{
			if(!pSelf->Server()->ClientIngame(i))
				continue;

			vClientIds.emplace_back(i);
		}
	}
	else
	{
		return std::nullopt;
	}

	return std::make_optional(std::move(vClientIds));
}

void CGameContext::CommandCallback(int ClientId, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser)
{
	CGameContext *pSelf = (CGameContext *)pUser;
	if(pSelf->m_TeeHistorianActive)
	{
		pSelf->m_TeeHistorian.RecordConsoleCommand(ClientId, FlagMask, pCmd, pResult);
	}
}

CNetObj_PlayerInput CGameContext::GetLastPlayerInput(int ClientId) const
{
	dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "invalid ClientId");
	return m_aLastPlayerInput[ClientId];
}

CCharacter *CGameContext::GetPlayerChar(int ClientId)
{
	if(ClientId < 0 || ClientId >= MAX_CLIENTS || !m_apPlayers[ClientId])
		return nullptr;
	return m_apPlayers[ClientId]->GetCharacter();
}

const CCharacter *CGameContext::GetPlayerChar(int ClientId) const
{
	if(ClientId < 0 || ClientId >= MAX_CLIENTS || !m_apPlayers[ClientId])
		return nullptr;
	return m_apPlayers[ClientId]->GetCharacter();
}

const CPlayer *CGameContext::FindPlayerByName(const char *pName) const
{
	std::optional<int> ClientId = FindClientIdByName(pName);
	if(!ClientId.has_value())
		return nullptr;
	return m_apPlayers[ClientId.value()];
}

CPlayer *CGameContext::FindPlayerByName(const char *pName)
{
	std::optional<int> ClientId = FindClientIdByName(pName);
	if(!ClientId.has_value())
		return nullptr;
	return m_apPlayers[ClientId.value()];
}

std::optional<int> CGameContext::FindClientIdByName(const char *pName) const
{
	if(!pName)
		return std::nullopt;

	for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++)
	{
		if(!Server()->ClientIngame(ClientId))
			continue;
		if(str_comp(pName, Server()->ClientName(ClientId)))
			continue;

		return ClientId;
	}
	return std::nullopt;
}

bool CGameContext::EmulateBug(int Bug) const
{
	return m_MapBugs.Contains(Bug);
}

void CGameContext::FillAntibot(CAntibotRoundData *pData)
{
	if(!pData->m_Map.m_pTiles)
	{
		Collision()->FillAntibot(&pData->m_Map);
	}
	pData->m_Tick = Server()->Tick();
	mem_zero(pData->m_aCharacters, sizeof(pData->m_aCharacters));
	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		CAntibotCharacterData *pChar = &pData->m_aCharacters[i];
		for(auto &LatestInput : pChar->m_aLatestInputs)
		{
			LatestInput.m_Direction = 0;
			LatestInput.m_TargetX = -1;
			LatestInput.m_TargetY = -1;
			LatestInput.m_Jump = -1;
			LatestInput.m_Fire = -1;
			LatestInput.m_Hook = -1;
			LatestInput.m_PlayerFlags = -1;
			LatestInput.m_WantedWeapon = -1;
			LatestInput.m_NextWeapon = -1;
			LatestInput.m_PrevWeapon = -1;
		}
		pChar->m_Alive = false;
		pChar->m_Pause = false;
		pChar->m_Team = -1;

		pChar->m_Pos = vec2(-1, -1);
		pChar->m_Vel = vec2(0, 0);
		pChar->m_Angle = -1;
		pChar->m_HookedPlayer = -1;
		pChar->m_SpawnTick = -1;
		pChar->m_WeaponChangeTick = -1;

		if(m_apPlayers[i])
		{
			str_copy(pChar->m_aName, Server()->ClientName(i));
			CCharacter *pGameChar = m_apPlayers[i]->GetCharacter();
			pChar->m_Alive = (bool)pGameChar;
			pChar->m_Pause = m_apPlayers[i]->IsPaused();
			pChar->m_Team = m_apPlayers[i]->GetTeam();
			if(pGameChar)
			{
				pGameChar->FillAntibot(pChar);
			}
		}
	}
}

void CGameContext::CreateDamageInd(vec2 Pos, float Angle, int Amount, CClientMask Mask)
{
	float a = 3 * pi / 2 + Angle;
	float s = a - pi / 3;
	float e = a + pi / 3;
	for(int i = 0; i < Amount; i++)
	{
		float f = mix(s, e, (i + 1) / (float)(Amount + 1));
		CNetEvent_DamageInd *pEvent = m_Events.Create<CNetEvent_DamageInd>(Mask);
		if(pEvent)
		{
			pEvent->m_X = (int)Pos.x;
			pEvent->m_Y = (int)Pos.y;
			pEvent->m_Angle = (int)(f * 256.0f);
		}
	}
}

void CGameContext::CreateHammerHit(vec2 Pos, CClientMask Mask)
{
	CNetEvent_HammerHit *pEvent = m_Events.Create<CNetEvent_HammerHit>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
	}
}

void CGameContext::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, int ActivatedTeam, CClientMask Mask)
{
	// create the event
	CNetEvent_Explosion *pEvent = m_Events.Create<CNetEvent_Explosion>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
	}

	// deal damage
	CEntity *apEnts[MAX_CLIENTS];
	float Radius = 135.0f;
	float InnerRadius = 48.0f;
	int Num = m_World.FindEntities(Pos, Radius, apEnts, MAX_CLIENTS, CGameWorld::ENTTYPE_CHARACTER);
	CClientMask TeamMask = CClientMask().set();
	for(int i = 0; i < Num; i++)
	{
		auto *pChr = static_cast<CCharacter *>(apEnts[i]);
		vec2 Diff = pChr->m_Pos - Pos;
		vec2 ForceDir(0, 1);
		float l = length(Diff);
		if(l)
			ForceDir = normalize(Diff);
		l = 1 - std::clamp((l - InnerRadius) / (Radius - InnerRadius), 0.0f, 1.0f);
		float Strength;
		if(Owner == -1 || !m_apPlayers[Owner] || !m_apPlayers[Owner]->m_TuneZone)
			Strength = GlobalTuning()->m_ExplosionStrength;
		else
			Strength = TuningList()[m_apPlayers[Owner]->m_TuneZone].m_ExplosionStrength;

		float Dmg = Strength * l;
		if(!(int)Dmg)
			continue;

		if((GetPlayerChar(Owner) ? !GetPlayerChar(Owner)->GrenadeHitDisabled() : g_Config.m_SvHit) || NoDamage || Owner == pChr->GetPlayer()->GetCid())
		{
			if(Owner != -1 && pChr->IsAlive() && !pChr->CanCollide(Owner))
				continue;
			if(Owner == -1 && ActivatedTeam != -1 && pChr->IsAlive() && pChr->Team() != ActivatedTeam)
				continue;

			// Explode at most once per team
			int PlayerTeam = pChr->Team();
			if((GetPlayerChar(Owner) ? GetPlayerChar(Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit) || NoDamage)
			{
				if(PlayerTeam == TEAM_SUPER)
					continue;
				if(!TeamMask.test(PlayerTeam))
					continue;
				TeamMask.reset(PlayerTeam);
			}

			pChr->TakeDamage(ForceDir * Dmg * 2, (int)Dmg, Owner, Weapon);
		}
	}
}

void CGameContext::CreatePlayerSpawn(vec2 Pos, CClientMask Mask)
{
	CNetEvent_Spawn *pEvent = m_Events.Create<CNetEvent_Spawn>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
	}
}

void CGameContext::CreateDeath(vec2 Pos, int ClientId, CClientMask Mask)
{
	CNetEvent_Death *pEvent = m_Events.Create<CNetEvent_Death>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
		pEvent->m_ClientId = ClientId;
	}
}

void CGameContext::CreateBirthdayEffect(vec2 Pos, CClientMask Mask)
{
	CNetEvent_Birthday *pEvent = m_Events.Create<CNetEvent_Birthday>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
	}
}

void CGameContext::CreateFinishEffect(vec2 Pos, CClientMask Mask)
{
	CNetEvent_Finish *pEvent = m_Events.Create<CNetEvent_Finish>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
	}
}

void CGameContext::CreateSound(vec2 Pos, int Sound, CClientMask Mask)
{
	if(Sound < 0)
		return;

	// create a sound
	CNetEvent_SoundWorld *pEvent = m_Events.Create<CNetEvent_SoundWorld>(Mask);
	if(pEvent)
	{
		pEvent->m_X = (int)Pos.x;
		pEvent->m_Y = (int)Pos.y;
		pEvent->m_SoundId = Sound;
	}
}

void CGameContext::CreateSoundGlobal(int Sound, int Target) const
{
	if(Sound < 0)
		return;

	CNetMsg_Sv_SoundGlobal Msg;
	Msg.m_SoundId = Sound;
	if(Target == -2)
		Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, -1);
	else
	{
		int Flag = MSGFLAG_VITAL;
		if(Target != -1)
			Flag |= MSGFLAG_NORECORD;
		Server()->SendPackMsg(&Msg, Flag, Target);
	}
}

void CGameContext::SnapSwitchers(int SnappingClient)
{
	if(Switchers().empty())
		return;

	CPlayer *pPlayer = SnappingClient != SERVER_DEMO_CLIENT ? m_apPlayers[SnappingClient] : nullptr;
	int Team = pPlayer && pPlayer->GetCharacter() ? pPlayer->GetCharacter()->Team() : 0;

	if(pPlayer && (pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->SpectatorId() != SPEC_FREEVIEW && m_apPlayers[pPlayer->SpectatorId()] && m_apPlayers[pPlayer->SpectatorId()]->GetCharacter())
		Team = m_apPlayers[pPlayer->SpectatorId()]->GetCharacter()->Team();

	if(Team == TEAM_SUPER)
		return;

	int SentTeam = Team;
	if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
		SentTeam = 0;
	else if(SnappingClient != SERVER_DEMO_CLIENT)
		SentTeam = m_pController->Teams().TeamForClient(SentTeam, SnappingClient);

	CNetObj_SwitchState SwitchState = {};

	SwitchState.m_HighestSwitchNumber = std::clamp((int)Switchers().size() - 1, 0, 255);
	std::fill(std::begin(SwitchState.m_aStatus), std::end(SwitchState.m_aStatus), 0);

	std::vector<std::pair<int, int>> vEndTicks; // <EndTick, SwitchNumber>

	for(int i = 0; i <= SwitchState.m_HighestSwitchNumber; i++)
	{
		int Status = (int)Switchers()[i].m_aStatus[Team];
		SwitchState.m_aStatus[i / 32] |= (Status << (i % 32));

		int EndTick = Switchers()[i].m_aEndTick[Team];
		if(EndTick > 0 && EndTick < Server()->Tick() + 3 * Server()->TickSpeed() && Switchers()[i].m_aLastUpdateTick[Team] < Server()->Tick())
		{
			// only keep track of EndTicks that have less than three second left and are not currently being updated by a player being present on a switch tile, to limit how often these are sent
			vEndTicks.emplace_back(Switchers()[i].m_aEndTick[Team], i);
		}
	}

	// send the endtick of switchers that are about to toggle back (up to four, prioritizing those with the earliest endticks)
	std::fill(std::begin(SwitchState.m_aSwitchNumbers), std::end(SwitchState.m_aSwitchNumbers), 0);
	std::fill(std::begin(SwitchState.m_aEndTicks), std::end(SwitchState.m_aEndTicks), 0);

	std::sort(vEndTicks.begin(), vEndTicks.end());
	const size_t NumTimedSwitchers = std::min(vEndTicks.size(), std::size(SwitchState.m_aEndTicks));

	for(size_t i = 0; i < NumTimedSwitchers; i++)
	{
		SwitchState.m_aSwitchNumbers[i] = vEndTicks[i].second;
		SwitchState.m_aEndTicks[i] = vEndTicks[i].first;
	}

	Server()->SnapNewItem(SentTeam, SwitchState);
}

void CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapId, const vec2 &To, const vec2 &From, int StartTick, int Owner, int LaserType, int Subtype, int SwitchNumber) const
{
	if(Context.GetClientVersion() >= VERSION_DDNET_MULTI_LASER)
	{
		CNetObj_DDNetLaser Laser = {};
		Laser.m_ToX = (int)To.x;
		Laser.m_ToY = (int)To.y;
		Laser.m_FromX = (int)From.x;
		Laser.m_FromY = (int)From.y;
		Laser.m_StartTick = StartTick;
		Laser.m_Owner = Owner;
		Laser.m_Type = LaserType;
		Laser.m_Subtype = Subtype;
		Laser.m_SwitchNumber = SwitchNumber;
		Laser.m_Flags = 0;
		if(!Server()->Translate(Laser.m_Owner, Context.ClientId()))
			Laser.m_Owner = -1;
		Server()->SnapNewItem(SnapId, Laser);
	}
	else
	{
		CNetObj_Laser Laser = {};
		Laser.m_X = (int)To.x;
		Laser.m_Y = (int)To.y;
		Laser.m_FromX = (int)From.x;
		Laser.m_FromY = (int)From.y;
		Laser.m_StartTick = StartTick;
		Server()->SnapNewItem(SnapId, Laser);
	}
}

void CGameContext::SnapPickup(const CSnapContext &Context, int SnapId, const vec2 &Pos, int Type, int SubType, int SwitchNumber, int Flags) const
{
	if(Context.IsSixup())
	{
		protocol7::CNetObj_Pickup Pickup = {};
		Pickup.m_X = (int)Pos.x;
		Pickup.m_Y = (int)Pos.y;
		Pickup.m_Type = PickupType_SixToSeven(Type, SubType);
		Server()->SnapNewItem(SnapId, Pickup);
	}
	else if(Context.GetClientVersion() >= VERSION_DDNET_ENTITY_NETOBJS)
	{
		CNetObj_DDNetPickup Pickup = {};
		Pickup.m_X = (int)Pos.x;
		Pickup.m_Y = (int)Pos.y;
		Pickup.m_Type = Type;
		Pickup.m_Subtype = SubType;
		Pickup.m_SwitchNumber = SwitchNumber;
		Pickup.m_Flags = Flags;
		Server()->SnapNewItem(SnapId, Pickup);
	}
	else
	{
		CNetObj_Pickup Pickup = {};

		Pickup.m_X = (int)Pos.x;
		Pickup.m_Y = (int)Pos.y;

		Pickup.m_Type = Type;
		if(Context.GetClientVersion() < VERSION_DDNET_WEAPON_SHIELDS)
		{
			if(Type >= POWERUP_ARMOR_SHOTGUN && Type <= POWERUP_ARMOR_LASER)
			{
				Pickup.m_Type = POWERUP_ARMOR;
			}
		}
		Pickup.m_Subtype = SubType;

		Server()->SnapNewItem(SnapId, Pickup);
	}
}

void CGameContext::CallVote(int ClientId, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc)
{
	// check if a vote is already running
	if(m_VoteCloseTime)
		return;

	int64_t Now = Server()->Tick();
	CPlayer *pPlayer = m_apPlayers[ClientId];

	if(!pPlayer)
		return;

	SendChat(-1, TEAM_ALL, pChatmsg, -1, FLAG_SIX);
	if(!pSixupDesc)
		pSixupDesc = pDesc;

	m_VoteCreator = ClientId;
	StartVote(pDesc, pCmd, pReason, pSixupDesc);
	pPlayer->m_Vote = 1;
	pPlayer->m_VotePos = m_VotePos = 1;
	pPlayer->m_LastVoteCall = Now;

	CNetMsg_Sv_YourVote Msg = {pPlayer->m_Vote};
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CGameContext::SendChatTarget(int To, const char *pText, int VersionFlags) const
{
	CNetMsg_Sv_Chat Msg;
	Msg.m_Team = 0;
	Msg.m_ClientId = -1;
	Msg.m_pMessage = pText;

	if(g_Config.m_SvDemoChat)
		Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);

	if(To == -1)
	{
		for(int i = 0; i < Server()->MaxClients(); i++)
		{
			if(!((Server()->IsSixup(i) && (VersionFlags & FLAG_SIXUP)) ||
				   (!Server()->IsSixup(i) && (VersionFlags & FLAG_SIX))))
				continue;

			Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
		}
	}
	else
	{
		if(!((Server()->IsSixup(To) && (VersionFlags & FLAG_SIXUP)) ||
			   (!Server()->IsSixup(To) && (VersionFlags & FLAG_SIX))))
			return;

		Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, To);
	}
}

void CGameContext::SendChatTeam(int Team, const char *pText) const
{
	for(int i = 0; i < MAX_CLIENTS; i++)
		if(m_apPlayers[i] != nullptr && GetDDRaceTeam(i) == Team)
			SendChatTarget(i, pText);
}

void CGameContext::SendChat(int ChatterClientId, int Team, const char *pText, int SpamProtectionClientId, int VersionFlags)
{
	dbg_assert(ChatterClientId >= -1 && ChatterClientId < MAX_CLIENTS, "ChatterClientId invalid: %d", ChatterClientId);

	if(SpamProtectionClientId >= 0 && SpamProtectionClientId < MAX_CLIENTS)
		if(ProcessSpamProtection(SpamProtectionClientId))
			return;

	char aText[MAX_CHAT_LENGTH];
	str_copy(aText, pText);
	const char *pTeamString = Team == TEAM_ALL ? "chat" : "teamchat";
	if(ChatterClientId == -1)
	{
		log_info(pTeamString, "*** %s", aText);
	}
	else
	{
		log_info(pTeamString, "%d:%d:%s: %s", ChatterClientId, Team, Server()->ClientName(ChatterClientId), aText);
	}

	if(Team == TEAM_ALL)
	{
		CNetMsg_Sv_Chat Msg;
		Msg.m_Team = 0;
		Msg.m_ClientId = ChatterClientId;
		Msg.m_pMessage = aText;

		// pack one for the recording only
		if(g_Config.m_SvDemoChat)
			Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);

		// send to the clients
		for(int i = 0; i < Server()->MaxClients(); i++)
		{
			if(!m_apPlayers[i])
				continue;
			bool Send = (Server()->IsSixup(i) && (VersionFlags & FLAG_SIXUP)) ||
				    (!Server()->IsSixup(i) && (VersionFlags & FLAG_SIX));

			if(!m_apPlayers[i]->m_DND && Send)
				Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
		}

		char aBuf[sizeof(aText) + 8];
		str_format(aBuf, sizeof(aBuf), "Chat: %s", aText);
		LogEvent(aBuf, ChatterClientId);
	}
	else
	{
		CTeamsCore *pTeams = &m_pController->Teams().m_Core;
		CNetMsg_Sv_Chat Msg;
		Msg.m_Team = 1;
		Msg.m_ClientId = ChatterClientId;
		Msg.m_pMessage = aText;

		// pack one for the recording only
		if(g_Config.m_SvDemoChat)
			Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, SERVER_DEMO_CLIENT);

		// send to the clients
		for(int i = 0; i < Server()->MaxClients(); i++)
		{
			if(m_apPlayers[i] != nullptr)
			{
				if(Team == TEAM_SPECTATORS)
				{
					if(m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS)
					{
						Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
					}
				}
				else
				{
					if(pTeams->Team(i) == Team && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS)
					{
						Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
					}
				}
			}
		}
	}
}

void CGameContext::SendStartWarning(int ClientId, const char *pMessage)
{
	CCharacter *pChr = GetPlayerChar(ClientId);
	if(pChr && pChr->m_LastStartWarning < Server()->Tick() - 3 * Server()->TickSpeed())
	{
		SendChatTarget(ClientId, pMessage);
		pChr->m_LastStartWarning = Server()->Tick();
	}
}

void CGameContext::SendEmoticon(int ClientId, int Emoticon, int TargetClientId) const
{
	CNetMsg_Sv_Emoticon Msg;
	Msg.m_ClientId = ClientId;
	Msg.m_Emoticon = Emoticon;
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, TargetClientId);
}

void CGameContext::SendWeaponPickup(int ClientId, int Weapon) const
{
	CNetMsg_Sv_WeaponPickup Msg;
	Msg.m_Weapon = Weapon;
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CGameContext::SendMotd(int ClientId) const
{
	CNetMsg_Sv_Motd Msg;
	Msg.m_pMessage = g_Config.m_SvMotd;
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CGameContext::SendSettings(int ClientId) const
{
	protocol7::CNetMsg_Sv_ServerSettings Msg;
	Msg.m_KickVote = g_Config.m_SvVoteKick;
	Msg.m_KickMin = g_Config.m_SvVoteKickMin;
	Msg.m_SpecVote = g_Config.m_SvVoteSpectate;
	Msg.m_TeamLock = 0;
	Msg.m_TeamBalance = 0;
	Msg.m_PlayerSlots = Server()->MaxClients() - g_Config.m_SvSpectatorSlots;
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
}

void CGameContext::SendServerAlert(const char *pMessage)
{
	for(int ClientId = 0; ClientId < Server()->MaxClients(); ClientId++)
	{
		if(!m_apPlayers[ClientId])
		{
			continue;
		}

		if(m_apPlayers[ClientId]->GetClientVersion() >= VERSION_DDNET_IMPORTANT_ALERT)
		{
			CNetMsg_Sv_ServerAlert Msg;
			Msg.m_pMessage = pMessage;
			Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
		}
		else
		{
			char aBroadcastText[1024 + 32];
			str_copy(aBroadcastText, "SERVER ALERT\n\n");
			str_append(aBroadcastText, pMessage);
			SendBroadcast(aBroadcastText, ClientId, true);
		}
	}

	// Record server alert to demos exactly once
	// TODO: Workaround https://github.com/ddnet/ddnet/issues/11144 by using client ID 0,
	//       otherwise the message is recorded multiple times.
	CNetMsg_Sv_ServerAlert Msg;
	Msg.m_pMessage = pMessage;
	Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, 0);
}

void CGameContext::SendModeratorAlert(int ToClientId, const char *pMessage)
{
	dbg_assert(in_range(ToClientId, 0, MAX_CLIENTS - 1), "SendImportantAlert ToClientId invalid: %d", ToClientId);
	dbg_assert(m_apPlayers[ToClientId] != nullptr, "Client not online: %d", ToClientId);

	if(m_apPlayers[ToClientId]->GetClientVersion() >= VERSION_DDNET_IMPORTANT_ALERT)
	{
		CNetMsg_Sv_ModeratorAlert Msg;
		Msg.m_pMessage = pMessage;
		Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ToClientId);
	}
	else
	{
		char aBroadcastText[1024 + 32];
		str_copy(aBroadcastText, "MODERATOR ALERT\n\n");
		str_append(aBroadcastText, pMessage);
		SendBroadcast(aBroadcastText, ToClientId, true);
		log_info("moderator_alert", "Notice: player uses an old client version and may not see moderator alerts: %s (ID %d)", Server()->ClientName(ToClientId), ToClientId);
	}
}

void CGameContext::SendBroadcast(const char *pText, int ClientId, bool IsImportant)
{
	CNetMsg_Sv_Broadcast Msg;
	Msg.m_pMessage = pText;

	if(ClientId == -1)
	{
		dbg_assert(IsImportant, "broadcast messages to all players must be important");
		Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);

		for(auto &pPlayer : m_apPlayers)
		{
			if(pPlayer)
			{
				pPlayer->m_LastBroadcastImportance = true;
				pPlayer->m_LastBroadcast = Server()->Tick();
			}
		}
		return;
	}

	if(!m_apPlayers[ClientId])
		return;

	if(!IsImportant && m_apPlayers[ClientId]->m_LastBroadcastImportance && m_apPlayers[ClientId]->m_LastBroadcast > Server()->Tick() - Server()->TickSpeed() * 10)
		return;

	// Broadcasts to individual players are not recorded in demos
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
	m_apPlayers[ClientId]->m_LastBroadcast = Server()->Tick();
	m_apPlayers[ClientId]->m_LastBroadcastImportance = IsImportant;
}

void CGameContext::SendRename7(int ClientId)
{
	dbg_assert(in_range(ClientId, 0, MAX_CLIENTS - 1), "Invalid ClientId: %d", ClientId);
	dbg_assert(m_apPlayers[ClientId] != nullptr, "Client not online: %d", ClientId);

	CPlayer *pPlayer = m_apPlayers[ClientId];

	protocol7::CNetMsg_Sv_ClientDrop Drop;
	Drop.m_ClientId = ClientId;
	Drop.m_pReason = "";
	Drop.m_Silent = true;

	protocol7::CNetMsg_Sv_ClientInfo Info;
	Info.m_ClientId = ClientId;
	Info.m_pName = Server()->ClientName(ClientId);
	Info.m_Country = Server()->ClientCountry(ClientId);
	Info.m_pClan = Server()->ClientClan(ClientId);
	Info.m_Local = 0;
	Info.m_Silent = true;
	Info.m_Team = pPlayer->GetTeam();

	for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
	{
		Info.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_aaSkinPartNames[p];
		Info.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p];
		Info.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p];
	}

	for(int i = 0; i < Server()->MaxClients(); i++)
	{
		if(i != ClientId)
		{
			Server()->SendPackMsg(&Drop, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
			Server()->SendPackMsg(&Info, MSGFLAG_VITAL | MSGFLAG_NORECORD, i);
		}
	}
}

void CGameContext::SendSkinChange7(int ClientId)
{
	dbg_assert(in_range(ClientId, 0, MAX_CLIENTS - 1), "Invalid ClientId: %d", ClientId);
	dbg_assert(m_apPlayers[ClientId] != nullptr, "Client not online: %d", ClientId);

	const CTeeInfo &Info = m_apPlayers[ClientId]->m_TeeInfos;
	protocol7::CNetMsg_Sv_SkinChange Msg;
	Msg.m_ClientId = ClientId;
	for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
	{
		Msg.m_apSkinPartNames[Part] = Info.m_aaSkinPartNames[Part];
		Msg.m_aSkinPartColors[Part] = Info.m_aSkinPartColors[Part];
		Msg.m_aUseCustomColors[Part] = Info.m_aUseCustomColors[Part];
	}

	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);
}

void CGameContext::StartVote(const char *pDesc, const char *pCommand, const char *pReason, const char *pSixupDesc)
{
	// reset votes
	m_VoteEnforce = VOTE_ENFORCE_UNKNOWN;
	for(auto &pPlayer : m_apPlayers)
	{
		if(pPlayer)
		{
			pPlayer->m_Vote = 0;
			pPlayer->m_VotePos = 0;
		}
	}

	// start vote
	m_VoteCloseTime = time_get() + time_freq() * g_Config.m_SvVoteTime;
	str_copy(m_aVoteDescription, pDesc);
	str_copy(m_aSixupVoteDescription, pSixupDesc);
	str_copy(m_aVoteCommand, pCommand);
	str_copy(m_aVoteReason, pReason);
	SendVoteSet(-1);
	m_VoteUpdate = true;
}

void CGameContext::EndVote()
{
	m_VoteCloseTime = 0;
	SendVoteSet(-1);
}

void CGameContext::SendVoteSet(int ClientId)
{
	::CNetMsg_Sv_VoteSet Msg6;
	protocol7::CNetMsg_Sv_VoteSet Msg7;

	Msg7.m_ClientId = m_VoteCreator;
	if(m_VoteCloseTime)
	{
		Msg6.m_Timeout = Msg7.m_Timeout = (m_VoteCloseTime - time_get()) / time_freq();
		Msg6.m_pDescription = m_aVoteDescription;
		Msg6.m_pReason = Msg7.m_pReason = m_aVoteReason;

		Msg7.m_pDescription = m_aSixupVoteDescription;
		if(IsKickVote())
			Msg7.m_Type = protocol7::VOTE_START_KICK;
		else if(IsSpecVote())
			Msg7.m_Type = protocol7::VOTE_START_SPEC;
		else if(IsOptionVote())
			Msg7.m_Type = protocol7::VOTE_START_OP;
		else
			Msg7.m_Type = protocol7::VOTE_UNKNOWN;
	}
	else
	{
		Msg6.m_Timeout = Msg7.m_Timeout = 0;
		Msg6.m_pDescription = Msg7.m_pDescription = "";
		Msg6.m_pReason = Msg7.m_pReason = "";

		if(m_VoteEnforce == VOTE_ENFORCE_NO || m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN)
			Msg7.m_Type = protocol7::VOTE_END_FAIL;
		else if(m_VoteEnforce == VOTE_ENFORCE_YES || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
			Msg7.m_Type = protocol7::VOTE_END_PASS;
		else if(m_VoteEnforce == VOTE_ENFORCE_ABORT || m_VoteEnforce == VOTE_ENFORCE_CANCEL)
			Msg7.m_Type = protocol7::VOTE_END_ABORT;
		else
			Msg7.m_Type = protocol7::VOTE_UNKNOWN;

		if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
			Msg7.m_ClientId = -1;
	}

	if(ClientId == -1)
	{
		for(int i = 0; i < Server()->MaxClients(); i++)
		{
			if(!m_apPlayers[i])
				continue;
			if(!Server()->IsSixup(i))
			{
				Server()->SendPackMsg(&Msg6, MSGFLAG_VITAL, i);
			}
			else
			{
				// 0.7 clients need the client id in order to show the vote and the name of the caller, so its important that we get him in
				m_PlayerMapping.ForceInsertPlayer(m_VoteCreator, i);
				Server()->SendPackMsg(&Msg7, MSGFLAG_VITAL, i);
			}
		}
	}
	else
	{
		if(!Server()->IsSixup(ClientId))
		{
			Server()->SendPackMsg(&Msg6, MSGFLAG_VITAL, ClientId);
		}
		else
		{
			// 0.7 clients need the client id in order to show the vote and the name of the caller, so its important that we get him in
			m_PlayerMapping.ForceInsertPlayer(m_VoteCreator, ClientId);
			Server()->SendPackMsg(&Msg7, MSGFLAG_VITAL, ClientId);
		}
	}
}

void CGameContext::SendVoteStatus(int ClientId, int Total, int Yes, int No)
{
	if(ClientId == -1)
	{
		for(int i = 0; i < MAX_CLIENTS; ++i)
			if(Server()->ClientIngame(i))
				SendVoteStatus(i, Total, Yes, No);
		return;
	}

	const int MaxClients = Server()->GetMaxClients(ClientId);
	if(Total > MaxClients && !Server()->ClientSupportsServerMaxClients(ClientId))
	{
		Yes = (Yes * MaxClients) / (float)Total;
		No = (No * MaxClients) / (float)Total;
		Total = MaxClients;
	}

	CNetMsg_Sv_VoteStatus Msg = {0};
	Msg.m_Total = Total;
	Msg.m_Yes = Yes;
	Msg.m_No = No;
	Msg.m_Pass = Total - (Yes + No);

	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CGameContext::AbortVoteKickOnDisconnect(int ClientId)
{
	if(m_VoteCloseTime && ((str_startswith(m_aVoteCommand, "kick ") && str_toint(&m_aVoteCommand[5]) == ClientId) ||
				      (str_startswith(m_aVoteCommand, "set_team ") && str_toint(&m_aVoteCommand[9]) == ClientId)))
		m_VoteEnforce = VOTE_ENFORCE_ABORT;
}

void CGameContext::SendTuningParams(int ClientId, int Zone)
{
	if(ClientId == -1)
	{
		for(int i = 0; i < MAX_CLIENTS; ++i)
		{
			if(m_apPlayers[i])
			{
				if(m_apPlayers[i]->GetCharacter())
				{
					if(m_apPlayers[i]->GetCharacter()->m_TuneZone == Zone)
						SendTuningParams(i, Zone);
				}
				else if(m_apPlayers[i]->m_TuneZone == Zone)
				{
					SendTuningParams(i, Zone);
				}
			}
		}
		return;
	}

	dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "Invalid ClientId: %d", ClientId);
	dbg_assert(m_apPlayers[ClientId], "client %d without player", ClientId);

	CTuningParams Params = m_aTuningList[Zone];

	CCharacter *pCharacter = m_apPlayers[ClientId]->GetCharacter();
	int NeededFakeTuning = pCharacter ? pCharacter->NeededFaketuning() : 0;

	if(NeededFakeTuning & FAKETUNE_SOLO)
	{
		Params.m_PlayerCollision = 0;
		Params.m_PlayerHooking = 0;
	}

	if(NeededFakeTuning & FAKETUNE_NOCOLL)
	{
		Params.m_PlayerCollision = 0;
	}

	if(NeededFakeTuning & FAKETUNE_NOHOOK)
	{
		Params.m_PlayerHooking = 0;
	}

	if(NeededFakeTuning & FAKETUNE_NOJUMP)
	{
		Params.m_GroundJumpImpulse = 0;
	}

	if(NeededFakeTuning & FAKETUNE_JETPACK)
	{
		Params.m_JetpackStrength = 0;
	}

	if(NeededFakeTuning & FAKETUNE_NOHAMMER)
	{
		Params.m_HammerStrength = 0;
	}

	CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
	const int *pParams = Params.NetworkArray();
	for(int i = 0; i < CTuningParams::Num(); i++)
	{
		static_assert(offsetof(CTuningParams, m_LaserDamage) / sizeof(CTuneParam) == 30);
		if(i == 30 && Server()->IsSixup(ClientId)) // laser_damage was removed in 0.7
		{
			continue;
		}
		Msg.AddInt(pParams[i]);
	}
	Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CGameContext::OnPreTickTeehistorian()
{
	if(!m_TeeHistorianActive)
		return;

	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		if(m_apPlayers[i] != nullptr)
			m_TeeHistorian.RecordPlayerTeam(i, GetDDRaceTeam(i));
		else
			m_TeeHistorian.RecordPlayerTeam(i, 0);
	}
	for(int i = 0; i < TEAM_SUPER; i++)
	{
		m_TeeHistorian.RecordTeamPractice(i, m_pController->Teams().IsPractice(i));
	}
}

void CGameContext::OnTick()
{
	if(m_TeeHistorianActive)
	{
		int Error = aio_error(m_pTeeHistorianFile);
		if(Error)
		{
			dbg_msg("teehistorian", "error writing to file, err=%d", Error);
			Server()->SetErrorShutdown("teehistorian io error");
		}

		if(!m_TeeHistorian.Starting())
		{
			m_TeeHistorian.EndInputs();
			m_TeeHistorian.EndTick();
		}
		m_TeeHistorian.BeginTick(Server()->Tick());
		m_TeeHistorian.BeginPlayers();
	}

	// copy tuning
	*m_World.GetTuning(0) = m_aTuningList[0];
	m_World.Tick();
	m_PlayerMapping.Tick();

	m_pController->Tick();

	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		if(m_apPlayers[i])
		{
			IServer::CClientInfo Info;
			if(m_apPlayers[i]->m_DDNetVersionKickTick > 0 && Server()->Tick() >= m_apPlayers[i]->m_DDNetVersionKickTick && !Server()->IsSixup(i) &&
				Server()->GetClientInfo(i, &Info) && Info.m_DDNetVersion < VERSION_DDNET_OLD)
			{
				if(!g_Config.m_SvVanillaConnections)
				{
					Server()->Kick(i, "Old Teeworlds 0.6 versions are unsupported. Use DDNet client or Teeworlds 0.7");
					continue;
				}
				m_apPlayers[i]->m_DDNetVersionKickTick = -1;
			}

			// send vote options
			ProgressVoteOptions(i);

			m_apPlayers[i]->Tick();
			m_apPlayers[i]->PostTick();
		}
	}

	for(auto &pPlayer : m_apPlayers)
	{
		if(pPlayer)
			pPlayer->PostPostTick();
	}

	// update voting
	if(m_VoteCloseTime)
	{
		// abort the kick-vote on player-leave
		if(m_VoteEnforce == VOTE_ENFORCE_ABORT)
		{
			SendChat(-1, TEAM_ALL, "Vote aborted");
			EndVote();
		}
		else if(m_VoteEnforce == VOTE_ENFORCE_CANCEL)
		{
			char aBuf[64];
			if(m_VoteCreator == -1)
			{
				str_copy(aBuf, "Vote canceled");
			}
			else
			{
				str_format(aBuf, sizeof(aBuf), "'%s' canceled their vote", Server()->ClientName(m_VoteCreator));
			}
			SendChat(-1, TEAM_ALL, aBuf);
			EndVote();
		}
		else
		{
			int Total = 0, Yes = 0, No = 0;
			bool Veto = false, VetoStop = false;
			if(m_VoteUpdate)
			{
				// count votes
				const NETADDR *apAddresses[MAX_CLIENTS] = {nullptr};
				const NETADDR *pFirstAddress = nullptr;
				bool SinglePlayer = true;
				for(int i = 0; i < MAX_CLIENTS; i++)
				{
					if(m_apPlayers[i])
					{
						apAddresses[i] = Server()->ClientAddr(i);
						if(!pFirstAddress)
						{
							pFirstAddress = apAddresses[i];
						}
						else if(SinglePlayer && net_addr_comp_noport(pFirstAddress, apAddresses[i]) != 0)
						{
							SinglePlayer = false;
						}
					}
				}

				// remember checked players, only the first player with a specific ip will be handled
				bool aVoteChecked[MAX_CLIENTS] = {false};
				int64_t Now = Server()->Tick();
				for(int i = 0; i < MAX_CLIENTS; i++)
				{
					if(!m_apPlayers[i] || aVoteChecked[i])
						continue;

					if((IsKickVote() || IsSpecVote()) && (m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS ||
										     (GetPlayerChar(m_VoteCreator) && GetPlayerChar(i) &&
											     GetPlayerChar(m_VoteCreator)->Team() != GetPlayerChar(i)->Team())))
						continue;

					if(m_apPlayers[i]->IsAfk() && i != m_VoteCreator)
						continue;

					// can't vote in kick and spec votes in the beginning after joining
					if((IsKickVote() || IsSpecVote()) && Now < m_apPlayers[i]->m_FirstVoteTick)
						continue;

					// connecting clients with spoofed ips can clog slots without being ingame
					if(!Server()->ClientIngame(i))
						continue;

					// don't count votes by blacklisted clients
					if(g_Config.m_SvDnsblVote && !m_pServer->DnsblWhite(i) && !SinglePlayer)
						continue;

					int CurVote = m_apPlayers[i]->m_Vote;
					int CurVotePos = m_apPlayers[i]->m_VotePos;

					// only allow IPs to vote once, but keep veto ability
					// check for more players with the same ip (only use the vote of the one who voted first)
					for(int j = i + 1; j < MAX_CLIENTS; j++)
					{
						if(!m_apPlayers[j] || aVoteChecked[j] || net_addr_comp_noport(apAddresses[j], apAddresses[i]) != 0)
							continue;

						// count the latest vote by this ip
						if(CurVotePos < m_apPlayers[j]->m_VotePos)
						{
							CurVote = m_apPlayers[j]->m_Vote;
							CurVotePos = m_apPlayers[j]->m_VotePos;
						}

						aVoteChecked[j] = true;
					}

					Total++;
					if(CurVote > 0)
						Yes++;
					else if(CurVote < 0)
						No++;

					// veto right for players who have been active on server for long and who're not afk
					if(!IsKickVote() && !IsSpecVote() && g_Config.m_SvVoteVetoTime)
					{
						// look through all players with same IP again, including the current player
						for(int j = i; j < MAX_CLIENTS; j++)
						{
							// no need to check ip address of current player
							if(i != j && (!m_apPlayers[j] || net_addr_comp_noport(apAddresses[j], apAddresses[i]) != 0))
								continue;

							if(m_apPlayers[j] && !m_apPlayers[j]->IsAfk() && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS &&
								((Server()->Tick() - m_apPlayers[j]->m_JoinTick) / (Server()->TickSpeed() * 60) > g_Config.m_SvVoteVetoTime ||
									(m_apPlayers[j]->GetCharacter() && m_apPlayers[j]->GetCharacter()->m_DDRaceState == ERaceState::STARTED &&
										(Server()->Tick() - m_apPlayers[j]->GetCharacter()->m_StartTime) / (Server()->TickSpeed() * 60) > g_Config.m_SvVoteVetoTime)))
							{
								if(CurVote == 0)
									Veto = true;
								else if(CurVote < 0)
									VetoStop = true;
								break;
							}
						}
					}
				}

				if(g_Config.m_SvVoteMaxTotal && Total > g_Config.m_SvVoteMaxTotal &&
					(IsKickVote() || IsSpecVote()))
					Total = g_Config.m_SvVoteMaxTotal;

				if((Yes > Total / (100.0f / g_Config.m_SvVoteYesPercentage)) && !Veto)
					m_VoteEnforce = VOTE_ENFORCE_YES;
				else if(No >= Total - Total / (100.0f / g_Config.m_SvVoteYesPercentage))
					m_VoteEnforce = VOTE_ENFORCE_NO;

				if(VetoStop)
					m_VoteEnforce = VOTE_ENFORCE_NO;

				m_VoteWillPass = Yes > (Yes + No) / (100.0f / g_Config.m_SvVoteYesPercentage);
			}

			if(time_get() > m_VoteCloseTime && !g_Config.m_SvVoteMajority)
				m_VoteEnforce = (m_VoteWillPass && !Veto) ? VOTE_ENFORCE_YES : VOTE_ENFORCE_NO;

			// / Ensure minimum time for vote to end when moderating.
			if(m_VoteEnforce == VOTE_ENFORCE_YES && !(PlayerModerating() &&
									(IsKickVote() || IsSpecVote()) && time_get() < m_VoteCloseTime))
			{
				Server()->SetRconCid(IServer::RCON_CID_VOTE);
				Console()->ExecuteLine(m_aVoteCommand, IConsole::CLIENT_ID_UNSPECIFIED);
				Server()->SetRconCid(IServer::RCON_CID_SERV);
				EndVote();
				SendChat(-1, TEAM_ALL, "Vote passed", -1, FLAG_SIX);

				if(m_VoteCreator != -1 && m_apPlayers[m_VoteCreator] && !IsKickVote() && !IsSpecVote())
					m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0;
			}
			else if(m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
			{
				Server()->SetRconCid(IServer::RCON_CID_VOTE);
				Console()->ExecuteLine(m_aVoteCommand, IConsole::CLIENT_ID_UNSPECIFIED);
				Server()->SetRconCid(IServer::RCON_CID_SERV);
				EndVote();
				SendChat(-1, TEAM_ALL, "Vote passed enforced by authorized player", -1, FLAG_SIX);

				if(m_VoteCreator != -1 && m_apPlayers[m_VoteCreator])
					m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0;
			}
			else if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN)
			{
				EndVote();
				SendChat(-1, TEAM_ALL, "Vote failed enforced by authorized player", -1, FLAG_SIX);
			}
			else if(m_VoteEnforce == VOTE_ENFORCE_NO || (time_get() > m_VoteCloseTime && g_Config.m_SvVoteMajority))
			{
				EndVote();
				if(VetoStop || (m_VoteWillPass && Veto))
					SendChat(-1, TEAM_ALL, "Vote failed because of veto. Find an empty server instead", -1, FLAG_SIX);
				else
					SendChat(-1, TEAM_ALL, "Vote failed", -1, FLAG_SIX);
			}
			else if(m_VoteUpdate)
			{
				m_VoteUpdate = false;
				SendVoteStatus(-1, Total, Yes, No);
			}
		}
	}

	if(Server()->Tick() % (Server()->TickSpeed() / 2) == 0)
	{
		m_Mutes.UnmuteExpired();
		m_VoteMutes.UnmuteExpired();
	}

	if(Server()->Tick() % (g_Config.m_SvAnnouncementInterval * Server()->TickSpeed() * 60) == 0)
	{
		const char *pLine = Server()->GetAnnouncementLine();
		if(pLine)
			SendChat(-1, TEAM_ALL, pLine);
	}

	for(auto &Switcher : Switchers())
	{
		for(int j = 0; j < NUM_DDRACE_TEAMS; ++j)
		{
			if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDOPEN)
			{
				Switcher.m_aStatus[j] = false;
				Switcher.m_aEndTick[j] = 0;
				Switcher.m_aType[j] = TILE_SWITCHCLOSE;
			}
			else if(Switcher.m_aEndTick[j] <= Server()->Tick() && Switcher.m_aType[j] == TILE_SWITCHTIMEDCLOSE)
			{
				Switcher.m_aStatus[j] = true;
				Switcher.m_aEndTick[j] = 0;
				Switcher.m_aType[j] = TILE_SWITCHOPEN;
			}
		}
	}

	if(m_SqlRandomMapResult != nullptr && m_SqlRandomMapResult->m_Completed)
	{
		if(m_SqlRandomMapResult->m_Success)
		{
			if(m_SqlRandomMapResult->m_ClientId != -1 && m_apPlayers[m_SqlRandomMapResult->m_ClientId] && m_SqlRandomMapResult->m_aMessage[0] != '\0')
				SendChat(-1, TEAM_ALL, m_SqlRandomMapResult->m_aMessage);
			if(m_SqlRandomMapResult->m_aMap[0] != '\0')
				Server()->ChangeMap(m_SqlRandomMapResult->m_aMap);
			else
				m_LastMapVote = 0;
		}
		m_SqlRandomMapResult = nullptr;
	}

	// check for map info result from database
	if(m_pLoadMapInfoResult != nullptr && m_pLoadMapInfoResult->m_Completed)
	{
		if(m_pLoadMapInfoResult->m_Success && m_pLoadMapInfoResult->m_Data.m_aaMessages[0][0] != '\0')
		{
			str_copy(m_aMapInfoMessage, m_pLoadMapInfoResult->m_Data.m_aaMessages[0]);
			CNetMsg_Sv_MapInfo MapInfoMsg;
			MapInfoMsg.m_pDescription = m_aMapInfoMessage;
			Server()->SendPackMsg(&MapInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);
		}
		m_pLoadMapInfoResult = nullptr;
	}

	// Record player position at the end of the tick
	if(m_TeeHistorianActive)
	{
		for(int i = 0; i < MAX_CLIENTS; i++)
		{
			if(m_apPlayers[i] && m_apPlayers[i]->GetCharacter())
			{
				CNetObj_CharacterCore Char;
				m_apPlayers[i]->GetCharacter()->GetCore().Write(&Char);
				m_TeeHistorian.RecordPlayer(i, &Char);
			}
			else
			{
				m_TeeHistorian.RecordDeadPlayer(i);
			}
		}
		m_TeeHistorian.EndPlayers();
		m_TeeHistorian.BeginInputs();
	}
	// Warning: do not put code in this function directly above or below this comment
}

void CGameContext::PreInputClients(int ClientId, bool *pClients)
{
	if(!pClients || !m_apPlayers[ClientId])
		return;

	CCharacter *pInputChr = m_apPlayers[ClientId]->GetCharacter();
	if(!pInputChr || m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS || m_apPlayers[ClientId]->IsAfk())
		return;

	const int Team = GetDDRaceTeam(ClientId);

	for(int Id = 0; Id < MAX_CLIENTS; Id++)
	{
		if(ClientId == Id)
			continue;

		CPlayer *pPlayer = m_apPlayers[Id];
		if(!pPlayer)
			continue;

		if(pPlayer->GetTeam() == TEAM_SPECTATORS || Team != GetDDRaceTeam(Id) || pPlayer->IsAfk())
			continue;

		if(Server()->GetClientVersion(Id) < VERSION_DDNET_PREINPUT)
			continue;

		if(!pInputChr->CanSnapCharacter(Id) || pInputChr->NetworkClipped(Id))
			continue;

		pClients[Id] = true;
	}
}

// Server hooks
void CGameContext::OnClientPrepareInput(int ClientId, void *pInput)
{
	CNetObj_PlayerInput *pPlayerInput = static_cast<CNetObj_PlayerInput *>(pInput);

	if(Server()->IsSixup(ClientId))
		pPlayerInput->m_PlayerFlags = PlayerFlags_SevenToSix(pPlayerInput->m_PlayerFlags);
}

void CGameContext::OnClientDirectInput(int ClientId, const void *pInput)
{
	const CNetObj_PlayerInput *pPlayerInput = static_cast<const CNetObj_PlayerInput *>(pInput);

	if(!m_pController->IsGamePaused())
		m_apPlayers[ClientId]->OnDirectInput(pPlayerInput);

	int Flags = pPlayerInput->m_PlayerFlags;
	if((Flags & 256) || (Flags & 512))
	{
		Server()->Kick(ClientId, "please update your client or use DDNet client");
	}
}

void CGameContext::OnClientPredictedInput(int ClientId, const void *pInput)
{
	const CNetObj_PlayerInput *pApplyInput = static_cast<const CNetObj_PlayerInput *>(pInput);

	if(pApplyInput == nullptr)
	{
		// early return if no input at all has been sent by a player
		if(!m_aPlayerHasInput[ClientId])
		{
			return;
		}
		// set to last sent input when no new input has been sent
		pApplyInput = &m_aLastPlayerInput[ClientId];
	}

	if(!m_pController->IsGamePaused())
		m_apPlayers[ClientId]->OnPredictedInput(pApplyInput);
}

void CGameContext::OnClientPredictedEarlyInput(int ClientId, const void *pInput)
{
	const CNetObj_PlayerInput *pApplyInput = static_cast<const CNetObj_PlayerInput *>(pInput);

	if(pApplyInput == nullptr)
	{
		// early return if no input at all has been sent by a player
		if(!m_aPlayerHasInput[ClientId])
		{
			return;
		}
		// set to last sent input when no new input has been sent
		pApplyInput = &m_aLastPlayerInput[ClientId];
	}
	else
	{
		// Store input in this function and not in `OnClientPredictedInput`,
		// because this function is called on all inputs, while
		// `OnClientPredictedInput` is only called on the first input of each
		// tick.
		mem_copy(&m_aLastPlayerInput[ClientId], pApplyInput, sizeof(m_aLastPlayerInput[ClientId]));
		m_aPlayerHasInput[ClientId] = true;
	}

	if(!m_pController->IsGamePaused())
		m_apPlayers[ClientId]->OnPredictedEarlyInput(pApplyInput);

	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerInput(ClientId, m_apPlayers[ClientId]->GetUniqueCid(), pApplyInput);
	}
}

const CVoteOptionServer *CGameContext::GetVoteOption(int Index) const
{
	const CVoteOptionServer *pCurrent;
	for(pCurrent = m_pVoteOptionFirst;
		Index > 0 && pCurrent;
		Index--, pCurrent = pCurrent->m_pNext)
		;

	if(Index > 0)
		return nullptr;
	return pCurrent;
}

void CGameContext::ProgressVoteOptions(int ClientId)
{
	CPlayer *pPl = m_apPlayers[ClientId];

	if(pPl->m_SendVoteIndex == -1)
		return; // we didn't start sending options yet

	if(pPl->m_SendVoteIndex > m_NumVoteOptions)
		return; // shouldn't happen / fail silently

	int VotesLeft = m_NumVoteOptions - pPl->m_SendVoteIndex;
	int NumVotesToSend = std::min(g_Config.m_SvSendVotesPerTick, VotesLeft);

	if(!VotesLeft)
	{
		// player has up to date vote option list
		return;
	}

	// build vote option list msg
	int CurIndex = 0;

	CNetMsg_Sv_VoteOptionListAdd OptionMsg;
	OptionMsg.m_pDescription0 = "";
	OptionMsg.m_pDescription1 = "";
	OptionMsg.m_pDescription2 = "";
	OptionMsg.m_pDescription3 = "";
	OptionMsg.m_pDescription4 = "";
	OptionMsg.m_pDescription5 = "";
	OptionMsg.m_pDescription6 = "";
	OptionMsg.m_pDescription7 = "";
	OptionMsg.m_pDescription8 = "";
	OptionMsg.m_pDescription9 = "";
	OptionMsg.m_pDescription10 = "";
	OptionMsg.m_pDescription11 = "";
	OptionMsg.m_pDescription12 = "";
	OptionMsg.m_pDescription13 = "";
	OptionMsg.m_pDescription14 = "";

	// get current vote option by index
	const CVoteOptionServer *pCurrent = GetVoteOption(pPl->m_SendVoteIndex);

	while(CurIndex < NumVotesToSend && pCurrent != nullptr)
	{
		switch(CurIndex)
		{
		case 0: OptionMsg.m_pDescription0 = pCurrent->m_aDescription; break;
		case 1: OptionMsg.m_pDescription1 = pCurrent->m_aDescription; break;
		case 2: OptionMsg.m_pDescription2 = pCurrent->m_aDescription; break;
		case 3: OptionMsg.m_pDescription3 = pCurrent->m_aDescription; break;
		case 4: OptionMsg.m_pDescription4 = pCurrent->m_aDescription; break;
		case 5: OptionMsg.m_pDescription5 = pCurrent->m_aDescription; break;
		case 6: OptionMsg.m_pDescription6 = pCurrent->m_aDescription; break;
		case 7: OptionMsg.m_pDescription7 = pCurrent->m_aDescription; break;
		case 8: OptionMsg.m_pDescription8 = pCurrent->m_aDescription; break;
		case 9: OptionMsg.m_pDescription9 = pCurrent->m_aDescription; break;
		case 10: OptionMsg.m_pDescription10 = pCurrent->m_aDescription; break;
		case 11: OptionMsg.m_pDescription11 = pCurrent->m_aDescription; break;
		case 12: OptionMsg.m_pDescription12 = pCurrent->m_aDescription; break;
		case 13: OptionMsg.m_pDescription13 = pCurrent->m_aDescription; break;
		case 14: OptionMsg.m_pDescription14 = pCurrent->m_aDescription; break;
		}

		CurIndex++;
		pCurrent = pCurrent->m_pNext;
	}

	// send msg
	if(pPl->m_SendVoteIndex == 0)
	{
		CNetMsg_Sv_VoteOptionGroupStart StartMsg;
		Server()->SendPackMsg(&StartMsg, MSGFLAG_VITAL, ClientId);
	}

	OptionMsg.m_NumOptions = NumVotesToSend;
	Server()->SendPackMsg(&OptionMsg, MSGFLAG_VITAL, ClientId);

	pPl->m_SendVoteIndex += NumVotesToSend;

	if(pPl->m_SendVoteIndex == m_NumVoteOptions)
	{
		CNetMsg_Sv_VoteOptionGroupEnd EndMsg;
		Server()->SendPackMsg(&EndMsg, MSGFLAG_VITAL, ClientId);
	}
}

void CGameContext::OnClientEnter(int ClientId)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerReady(ClientId);
	}
	m_pController->OnPlayerConnect(m_apPlayers[ClientId]);

	IServer::CClientInfo Info;
	if(Server()->GetClientInfo(ClientId, &Info))
	{
		// 0.7 clients can send ddnet message, F-Client does that for example
		if(Info.m_GotDDNetVersion)
		{
			if(OnClientDDNetVersionKnown(ClientId))
			{
				return; // kicked
			}
		}
	}

	if(!Server()->ClientPrevIngame(ClientId))
	{
		if(g_Config.m_SvWelcome[0] != 0)
			SendChatTarget(ClientId, g_Config.m_SvWelcome);

		if(g_Config.m_SvShowOthersDefault > SHOW_OTHERS_OFF)
		{
			if(g_Config.m_SvShowOthers)
				SendChatTarget(ClientId, "You can see other players. To disable this use DDNet client and type /showothers");

			m_apPlayers[ClientId]->m_ShowOthers = g_Config.m_SvShowOthersDefault;
		}
	}
	m_VoteUpdate = true;

	// the player map has to be initialized before anything can be mapped into it
	m_PlayerMapping.InitPlayerMap(ClientId);

	SendStartMessages(ClientId);

	Server()->ExpireServerInfo();

	// send map info if loaded from database
	if(m_aMapInfoMessage[0] != '\0')
	{
		CNetMsg_Sv_MapInfo MapInfoMsg;
		MapInfoMsg.m_pDescription = m_aMapInfoMessage;
		Server()->SendPackMsg(&MapInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
	}

	mem_zero(&m_aLastPlayerInput[ClientId], sizeof(m_aLastPlayerInput[ClientId]));
	m_aPlayerHasInput[ClientId] = false;

	// initial chat delay
	if(g_Config.m_SvChatInitialDelay != 0 && m_apPlayers[ClientId]->m_JoinTick > m_NonEmptySince + 10 * Server()->TickSpeed())
	{
		char aBuf[128];
		str_format(aBuf, sizeof(aBuf), "This server has an initial chat delay, you will need to wait %d seconds before talking.", g_Config.m_SvChatInitialDelay);
		SendChatTarget(ClientId, aBuf);
		m_Mutes.Mute(Server()->ClientAddr(ClientId), g_Config.m_SvChatInitialDelay, "Initial chat delay", Server()->ClientName(ClientId), true);
	}

	LogEvent("Connect", ClientId);
}

bool CGameContext::OnClientDataPersist(int ClientId, void *pData)
{
	CPersistentClientData *pPersistent = (CPersistentClientData *)pData;
	if(!m_apPlayers[ClientId])
	{
		return false;
	}
	new(pPersistent) CPersistentClientData();
	pPersistent->m_IsSpectator = m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS;
	pPersistent->m_IsAfk = m_apPlayers[ClientId]->IsAfk();
	pPersistent->m_LastWhisperTo = m_apPlayers[ClientId]->m_LastWhisperTo;
	return true;
}

void CGameContext::OnClientConnected(int ClientId, void *pData)
{
	CPersistentClientData *pPersistentData = (CPersistentClientData *)pData;
	bool Spec = false;
	bool Afk = true;
	int LastWhisperTo = -1;
	if(pPersistentData)
	{
		Spec = pPersistentData->m_IsSpectator;
		Afk = pPersistentData->m_IsAfk;
		LastWhisperTo = pPersistentData->m_LastWhisperTo;
	}
	else
	{
		// new player connected, clear whispers waiting for the old player with this id
		for(auto &pPlayer : m_apPlayers)
		{
			if(pPlayer && pPlayer->m_LastWhisperTo == ClientId)
				pPlayer->m_LastWhisperTo = -1;
		}
	}

	{
		bool Empty = true;
		for(auto &pPlayer : m_apPlayers)
		{
			// connecting clients with spoofed ips can clog slots without being ingame
			if(pPlayer && Server()->ClientIngame(pPlayer->GetCid()))
			{
				Empty = false;
				break;
			}
		}
		if(Empty)
		{
			m_NonEmptySince = Server()->Tick();
		}
	}

	// Check which team the player should be on
	const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(ClientId);
	CreatePlayer(ClientId, StartTeam, Afk, LastWhisperTo);

	SendMotd(ClientId);
	SendSettings(ClientId);

	Server()->ExpireServerInfo();
}

void CGameContext::OnClientDrop(int ClientId, const char *pReason)
{
	LogEvent("Disconnect", ClientId);

	AbortVoteKickOnDisconnect(ClientId);
	m_pController->OnPlayerDisconnect(m_apPlayers[ClientId], pReason);
	delete m_apPlayers[ClientId];
	m_apPlayers[ClientId] = nullptr;

	delete m_apSavedTeams[ClientId];
	m_apSavedTeams[ClientId] = nullptr;

	delete m_apSavedTees[ClientId];
	m_apSavedTees[ClientId] = nullptr;

	m_aTeamMapping[ClientId] = -1;

	if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && PracticeByDefault())
		m_pController->Teams().SetPractice(GetDDRaceTeam(ClientId), true);

	m_VoteUpdate = true;
	if(m_VoteCreator == ClientId)
	{
		m_VoteCreator = -1;
	}

	// update spectator modes
	for(auto &pPlayer : m_apPlayers)
	{
		if(pPlayer && pPlayer->SpectatorId() == ClientId)
			pPlayer->SetSpectatorId(SPEC_FREEVIEW);
	}

	// update conversation targets
	for(auto &pPlayer : m_apPlayers)
	{
		if(pPlayer && pPlayer->m_LastWhisperTo == ClientId)
			pPlayer->m_LastWhisperTo = -1;
	}

	protocol7::CNetMsg_Sv_ClientDrop Msg;
	Msg.m_ClientId = ClientId;
	Msg.m_pReason = pReason;
	Msg.m_Silent = true;
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1);

	Server()->ExpireServerInfo();
}

void CGameContext::TeehistorianRecordAntibot(const void *pData, int DataSize)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordAntibot(pData, DataSize);
	}
}

void CGameContext::TeehistorianRecordPlayerJoin(int ClientId, bool Sixup)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerJoin(ClientId, !Sixup ? CTeeHistorian::PROTOCOL_6 : CTeeHistorian::PROTOCOL_7);
	}
}

void CGameContext::TeehistorianRecordPlayerDrop(int ClientId, const char *pReason)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerDrop(ClientId, pReason);
	}
}

void CGameContext::TeehistorianRecordPlayerRejoin(int ClientId)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerRejoin(ClientId);
	}
}

void CGameContext::TeehistorianRecordPlayerName(int ClientId, const char *pName)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerName(ClientId, pName);
	}
}

void CGameContext::TeehistorianRecordPlayerFinish(int ClientId, int TimeTicks)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordPlayerFinish(ClientId, TimeTicks);
	}
}

void CGameContext::TeehistorianRecordTeamFinish(int TeamId, int TimeTicks)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordTeamFinish(TeamId, TimeTicks);
	}
}

void CGameContext::TeehistorianRecordAuthLogin(int ClientId, int Level, const char *pAuthName)
{
	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.RecordAuthLogin(ClientId, Level, pAuthName);
	}
}

bool CGameContext::OnClientDDNetVersionKnown(int ClientId)
{
	IServer::CClientInfo Info;
	dbg_assert(Server()->GetClientInfo(ClientId, &Info), "failed to get client info");
	int ClientVersion = Info.m_DDNetVersion;
	dbg_msg("ddnet", "cid=%d version=%d", ClientId, ClientVersion);

	if(m_TeeHistorianActive)
	{
		if(Info.m_pConnectionId && Info.m_pDDNetVersionStr)
		{
			m_TeeHistorian.RecordDDNetVersion(ClientId, *Info.m_pConnectionId, ClientVersion, Info.m_pDDNetVersionStr);
		}
		else
		{
			m_TeeHistorian.RecordDDNetVersionOld(ClientId, ClientVersion);
		}
	}

	// Autoban known bot versions.
	if(g_Config.m_SvBannedVersions[0] != '\0' && IsVersionBanned(ClientVersion))
	{
		Server()->Kick(ClientId, "unsupported client");
		return true;
	}

	CPlayer *pPlayer = m_apPlayers[ClientId];
	if(ClientVersion >= VERSION_DDNET_GAMETICK)
		pPlayer->m_TimerType = g_Config.m_SvDefaultTimerType;

	// First update the teams state.
	m_pController->Teams().SendTeamsState(ClientId);

	// Then send records.
	SendRecord(ClientId);

	// And report correct tunings.
	if(ClientVersion < VERSION_DDNET_EARLY_VERSION)
		SendTuningParams(ClientId, pPlayer->m_TuneZone);

	// Tell old clients to update.
	if(ClientVersion < VERSION_DDNET_UPDATER_FIXED && g_Config.m_SvClientSuggestionOld[0] != '\0')
		SendBroadcast(g_Config.m_SvClientSuggestionOld, ClientId);
	// Tell known bot clients that they're botting and we know it.
	if(((ClientVersion >= 15 && ClientVersion < 100) || ClientVersion == 502) && g_Config.m_SvClientSuggestionBot[0] != '\0')
		SendBroadcast(g_Config.m_SvClientSuggestionBot, ClientId);

	m_PlayerMapping.UpdateTeamsState(ClientId);

	return false;
}

void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientId)
{
	if(Server()->IsSixup(ClientId) && *pMsgId < OFFSET_UUID)
	{
		void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgId, pUnpacker);
		if(!pRawMsg)
			return nullptr;

		CPlayer *pPlayer = m_apPlayers[ClientId];
		static char s_aRawMsg[1024];

		if(*pMsgId == protocol7::NETMSGTYPE_CL_SAY)
		{
			protocol7::CNetMsg_Cl_Say *pMsg7 = (protocol7::CNetMsg_Cl_Say *)pRawMsg;
			// Should probably use a placement new to start the lifetime of the object to avoid future weirdness
			::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg;

			if(pMsg7->m_Mode == protocol7::CHAT_WHISPER)
			{
				if(!Server()->ReverseTranslate(pMsg7->m_Target, ClientId))
					return nullptr;
				if(!CheckClientId(pMsg7->m_Target) || !Server()->ClientIngame(pMsg7->m_Target))
					return nullptr;
				if(ProcessSpamProtection(ClientId))
					return nullptr;

				WhisperId(ClientId, pMsg7->m_Target, pMsg7->m_pMessage);
				return nullptr;
			}
			else
			{
				pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM;
				pMsg->m_pMessage = pMsg7->m_pMessage;
			}
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_STARTINFO)
		{
			protocol7::CNetMsg_Cl_StartInfo *pMsg7 = (protocol7::CNetMsg_Cl_StartInfo *)pRawMsg;
			::CNetMsg_Cl_StartInfo *pMsg = (::CNetMsg_Cl_StartInfo *)s_aRawMsg;

			pMsg->m_pName = pMsg7->m_pName;
			pMsg->m_pClan = pMsg7->m_pClan;
			pMsg->m_Country = pMsg7->m_Country;

			pPlayer->m_TeeInfos = CTeeInfo(pMsg7->m_apSkinPartNames, pMsg7->m_aUseCustomColors, pMsg7->m_aSkinPartColors);
			pPlayer->m_TeeInfos.FromSixup();

			str_copy(s_aRawMsg + sizeof(*pMsg), pPlayer->m_TeeInfos.m_aSkinName, sizeof(s_aRawMsg) - sizeof(*pMsg));

			pMsg->m_pSkin = s_aRawMsg + sizeof(*pMsg);
			pMsg->m_UseCustomColor = pPlayer->m_TeeInfos.m_UseCustomColor;
			pMsg->m_ColorBody = pPlayer->m_TeeInfos.m_ColorBody;
			pMsg->m_ColorFeet = pPlayer->m_TeeInfos.m_ColorFeet;
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_SKINCHANGE)
		{
			protocol7::CNetMsg_Cl_SkinChange *pMsg = (protocol7::CNetMsg_Cl_SkinChange *)pRawMsg;
			if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo &&
				pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
				return nullptr;

			pPlayer->m_LastChangeInfo = Server()->Tick();

			CTeeInfo Info(pMsg->m_apSkinPartNames, pMsg->m_aUseCustomColors, pMsg->m_aSkinPartColors);
			Info.FromSixup();
			pPlayer->m_TeeInfos = Info;
			SendSkinChange7(ClientId);

			return nullptr;
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE)
		{
			protocol7::CNetMsg_Cl_SetSpectatorMode *pMsg7 = (protocol7::CNetMsg_Cl_SetSpectatorMode *)pRawMsg;
			::CNetMsg_Cl_SetSpectatorMode *pMsg = (::CNetMsg_Cl_SetSpectatorMode *)s_aRawMsg;

			if(pMsg7->m_SpecMode == protocol7::SPEC_FREEVIEW)
				pMsg->m_SpectatorId = SPEC_FREEVIEW;
			else if(pMsg7->m_SpecMode == protocol7::SPEC_PLAYER)
				pMsg->m_SpectatorId = pMsg7->m_SpectatorId;
			else
				pMsg->m_SpectatorId = SPEC_FREEVIEW; // Probably not needed
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETTEAM)
		{
			protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg;
			::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg;

			pMsg->m_Team = pMsg7->m_Team;
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_COMMAND)
		{
			protocol7::CNetMsg_Cl_Command *pMsg7 = (protocol7::CNetMsg_Cl_Command *)pRawMsg;
			::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg;

			str_format(s_aRawMsg + sizeof(*pMsg), sizeof(s_aRawMsg) - sizeof(*pMsg), "/%s %s", pMsg7->m_pName, pMsg7->m_pArguments);
			pMsg->m_pMessage = s_aRawMsg + sizeof(*pMsg);
			pMsg->m_Team = 0;

			*pMsgId = NETMSGTYPE_CL_SAY;
			return s_aRawMsg;
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_CALLVOTE)
		{
			protocol7::CNetMsg_Cl_CallVote *pMsg7 = (protocol7::CNetMsg_Cl_CallVote *)pRawMsg;

			if(pMsg7->m_Force)
			{
				if(!Server()->IsRconAuthed(ClientId))
				{
					return nullptr;
				}
				char aCommand[IConsole::CMDLINE_LENGTH];
				str_format(aCommand, sizeof(aCommand), "force_vote \"%s\" \"%s\" \"%s\"", pMsg7->m_pType, pMsg7->m_pValue, pMsg7->m_pReason);
				Console()->ExecuteLine(aCommand, ClientId, false);
				return nullptr;
			}

			::CNetMsg_Cl_CallVote *pMsg = (::CNetMsg_Cl_CallVote *)s_aRawMsg;
			pMsg->m_pValue = pMsg7->m_pValue;
			pMsg->m_pReason = pMsg7->m_pReason;
			pMsg->m_pType = pMsg7->m_pType;
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_EMOTICON)
		{
			protocol7::CNetMsg_Cl_Emoticon *pMsg7 = (protocol7::CNetMsg_Cl_Emoticon *)pRawMsg;
			::CNetMsg_Cl_Emoticon *pMsg = (::CNetMsg_Cl_Emoticon *)s_aRawMsg;

			pMsg->m_Emoticon = pMsg7->m_Emoticon;
		}
		else if(*pMsgId == protocol7::NETMSGTYPE_CL_VOTE)
		{
			protocol7::CNetMsg_Cl_Vote *pMsg7 = (protocol7::CNetMsg_Cl_Vote *)pRawMsg;
			::CNetMsg_Cl_Vote *pMsg = (::CNetMsg_Cl_Vote *)s_aRawMsg;

			pMsg->m_Vote = pMsg7->m_Vote;
		}

		*pMsgId = Msg_SevenToSix(*pMsgId);

		return s_aRawMsg;
	}
	else
		return m_NetObjHandler.SecureUnpackMsg(*pMsgId, pUnpacker);
}

void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, int Size)
{
	str_copy(pCensoredMessage, pMessage, Size);

	for(auto &Item : m_vCensorlist)
	{
		char *pCurLoc = pCensoredMessage;
		while(true)
		{
			const char *pEndMatch;
			pCurLoc = (char *)str_utf8_find_nocase(pCurLoc, Item.c_str(), &pEndMatch);
			if(!pCurLoc)
			{
				break;
			}
			while(pCurLoc < pEndMatch)
			{
				*pCurLoc = '*';
				pCurLoc++;
			}
		}
	}
}

void CGameContext::OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId)
{
	if(m_TeeHistorianActive)
	{
		if(m_NetObjHandler.TeeHistorianRecordMsg(MsgId))
		{
			m_TeeHistorian.RecordPlayerMessage(ClientId, pUnpacker->CompleteData(), pUnpacker->CompleteSize());
		}
	}

	void *pRawMsg = PreProcessMsg(&MsgId, pUnpacker, ClientId);

	if(!pRawMsg)
		return;

	if(Server()->ClientIngame(ClientId))
	{
		switch(MsgId)
		{
		case NETMSGTYPE_CL_SAY:
			OnSayNetMessage(static_cast<CNetMsg_Cl_Say *>(pRawMsg), ClientId, pUnpacker);
			break;
		case NETMSGTYPE_CL_CALLVOTE:
			OnCallVoteNetMessage(static_cast<CNetMsg_Cl_CallVote *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_VOTE:
			OnVoteNetMessage(static_cast<CNetMsg_Cl_Vote *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_SETTEAM:
			OnSetTeamNetMessage(static_cast<CNetMsg_Cl_SetTeam *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_ISDDNETLEGACY:
			OnIsDDNetLegacyNetMessage(static_cast<CNetMsg_Cl_IsDDNetLegacy *>(pRawMsg), ClientId, pUnpacker);
			break;
		case NETMSGTYPE_CL_SHOWOTHERSLEGACY:
			OnShowOthersLegacyNetMessage(static_cast<CNetMsg_Cl_ShowOthersLegacy *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_SHOWOTHERS:
			OnShowOthersNetMessage(static_cast<CNetMsg_Cl_ShowOthers *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_SHOWDISTANCE:
			OnShowDistanceNetMessage(static_cast<CNetMsg_Cl_ShowDistance *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_CAMERAINFO:
			OnCameraInfoNetMessage(static_cast<CNetMsg_Cl_CameraInfo *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_SETSPECTATORMODE:
			OnSetSpectatorModeNetMessage(static_cast<CNetMsg_Cl_SetSpectatorMode *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_CHANGEINFO:
			OnChangeInfoNetMessage(static_cast<CNetMsg_Cl_ChangeInfo *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_EMOTICON:
			OnEmoticonNetMessage(static_cast<CNetMsg_Cl_Emoticon *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_KILL:
			OnKillNetMessage(static_cast<CNetMsg_Cl_Kill *>(pRawMsg), ClientId);
			break;
		case NETMSGTYPE_CL_ENABLESPECTATORCOUNT:
			OnEnableSpectatorCountNetMessage(static_cast<CNetMsg_Cl_EnableSpectatorCount *>(pRawMsg), ClientId);
		default:
			break;
		}
	}
	if(MsgId == NETMSGTYPE_CL_STARTINFO)
	{
		OnStartInfoNetMessage(static_cast<CNetMsg_Cl_StartInfo *>(pRawMsg), ClientId);
	}
}

void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, const CUnpacker *pUnpacker)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];
	bool Check = !pPlayer->m_NotEligibleForFinish && pPlayer->m_EligibleForFinishCheck + 10 * time_freq() >= time_get();
	if(Check && str_comp(pMsg->m_pMessage, "xd sure chillerbot.png is lyfe") == 0 && pMsg->m_Team == 0)
	{
		if(m_TeeHistorianActive)
		{
			m_TeeHistorian.RecordPlayerMessage(ClientId, pUnpacker->CompleteData(), pUnpacker->CompleteSize());
		}

		pPlayer->m_NotEligibleForFinish = true;
		dbg_msg("hack", "bot detected, cid=%d", ClientId);
		return;
	}
	int Team = pMsg->m_Team;

	// trim right and set maximum length to 256 utf8-characters
	int Length = 0;
	const char *p = pMsg->m_pMessage;
	const char *pEnd = nullptr;
	while(*p)
	{
		const char *pStrOld = p;
		int Code = str_utf8_decode(&p);

		// check if unicode is not empty
		if(!str_utf8_isspace(Code))
		{
			pEnd = nullptr;
		}
		else if(pEnd == nullptr)
			pEnd = pStrOld;

		if(++Length >= MAX_CHAT_LENGTH)
		{
			*(const_cast<char *>(p)) = 0;
			break;
		}
	}
	if(pEnd != nullptr)
		*(const_cast<char *>(pEnd)) = 0;

	// drop empty and autocreated spam messages (more than 32 characters per second)
	if(Length == 0 || (pMsg->m_pMessage[0] != '/' && (g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat + Server()->TickSpeed() * ((31 + Length) / 32) > Server()->Tick())))
		return;

	int GameTeam = GetDDRaceTeam(pPlayer->GetCid());
	if(Team)
		Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? TEAM_SPECTATORS : GameTeam);
	else
		Team = TEAM_ALL;

	if(pMsg->m_pMessage[0] == '/')
	{
		const char *pWhisper;
		if((pWhisper = str_startswith_nocase(pMsg->m_pMessage + 1, "w ")))
		{
			Whisper(pPlayer->GetCid(), const_cast<char *>(pWhisper));
		}
		else if((pWhisper = str_startswith_nocase(pMsg->m_pMessage + 1, "whisper ")))
		{
			Whisper(pPlayer->GetCid(), const_cast<char *>(pWhisper));
		}
		else if((pWhisper = str_startswith_nocase(pMsg->m_pMessage + 1, "c ")))
		{
			Converse(pPlayer->GetCid(), const_cast<char *>(pWhisper));
		}
		else if((pWhisper = str_startswith_nocase(pMsg->m_pMessage + 1, "converse ")))
		{
			Converse(pPlayer->GetCid(), const_cast<char *>(pWhisper));
		}
		else
		{
			if(g_Config.m_SvSpamprotection && !str_startswith(pMsg->m_pMessage + 1, "timeout ") && pPlayer->m_aLastCommands[0] && pPlayer->m_aLastCommands[0] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[1] && pPlayer->m_aLastCommands[1] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[2] && pPlayer->m_aLastCommands[2] + Server()->TickSpeed() > Server()->Tick() && pPlayer->m_aLastCommands[3] && pPlayer->m_aLastCommands[3] + Server()->TickSpeed() > Server()->Tick())
				return;

			int64_t Now = Server()->Tick();
			pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now;
			pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4;

			Console()->SetFlagMask(CFGFLAG_CHAT);
			{
				CClientChatLogger Logger(this, ClientId, log_get_scope_logger());
				CLogScope Scope(&Logger);
				Console()->ExecuteLine(pMsg->m_pMessage + 1, ClientId, false);
			}
			// m_apPlayers[ClientId] can be nullptr, if the player used a
			// timeout code and replaced another client.
			char aBuf[256];
			str_format(aBuf, sizeof(aBuf), "%d used %s", ClientId, pMsg->m_pMessage);
			Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "chat-command", aBuf);

			Console()->SetFlagMask(CFGFLAG_SERVER);
		}
	}
	else
	{
		pPlayer->UpdatePlaytime();
		char aCensoredMessage[256];
		CensorMessage(aCensoredMessage, pMsg->m_pMessage, sizeof(aCensoredMessage));
		SendChat(ClientId, Team, aCensoredMessage, ClientId);
	}
}

void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientId)
{
	if(str_comp_nocase(pMsg->m_pType, "option") != 0 && m_PlayerMapping.DoSeeOthers(ClientId, str_toint(pMsg->m_pValue), true))
		return;

	if(RateLimitPlayerVote(ClientId) || m_VoteCloseTime)
		return;

	m_apPlayers[ClientId]->UpdatePlaytime();

	m_VoteType = VOTE_TYPE_UNKNOWN;
	char aChatmsg[512] = {0};
	char aDesc[VOTE_DESC_LENGTH] = {0};
	char aSixupDesc[VOTE_DESC_LENGTH] = {0};
	char aCmd[VOTE_CMD_LENGTH] = {0};
	char aReason[VOTE_REASON_LENGTH] = "No reason given";
	if(pMsg->m_pReason[0])
	{
		str_copy(aReason, pMsg->m_pReason);
	}

	if(str_comp_nocase(pMsg->m_pType, "option") == 0)
	{
		CVoteOptionServer *pOption = m_pVoteOptionFirst;
		while(pOption)
		{
			if(str_comp_nocase(pMsg->m_pValue, pOption->m_aDescription) == 0)
			{
				if(!Console()->LineIsValid(pOption->m_aCommand))
				{
					SendChatTarget(ClientId, "Invalid option");
					return;
				}
				if((str_find(pOption->m_aCommand, "sv_map ") != nullptr || str_find(pOption->m_aCommand, "change_map ") != nullptr || str_find(pOption->m_aCommand, "random_map") != nullptr || str_find(pOption->m_aCommand, "random_unfinished_map") != nullptr) && RateLimitPlayerMapVote(ClientId))
				{
					return;
				}

				str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s' (%s)", Server()->ClientName(ClientId),
					pOption->m_aDescription, aReason);
				str_copy(aDesc, pOption->m_aDescription);

				if((str_endswith(pOption->m_aCommand, "random_map") || str_endswith(pOption->m_aCommand, "random_unfinished_map")))
				{
					if(str_length(aReason) == 1 && aReason[0] >= '0' && aReason[0] <= '5')
					{
						int Stars = aReason[0] - '0';
						str_format(aCmd, sizeof(aCmd), "%s %d", pOption->m_aCommand, Stars);
					}
					else if(str_length(aReason) == 3 && aReason[1] == '-' && aReason[0] >= '0' && aReason[0] <= '5' && aReason[2] >= '0' && aReason[2] <= '5')
					{
						int Start = aReason[0] - '0';
						int End = aReason[2] - '0';
						str_format(aCmd, sizeof(aCmd), "%s %d %d", pOption->m_aCommand, Start, End);
					}
					else
					{
						str_copy(aCmd, pOption->m_aCommand);
					}
				}
				else
				{
					str_copy(aCmd, pOption->m_aCommand);
				}

				m_LastMapVote = time_get();
				break;
			}

			pOption = pOption->m_pNext;
		}

		if(!pOption)
		{
			if(!Server()->IsRconAuthedAdmin(ClientId)) // allow admins to call any vote they want
			{
				str_format(aChatmsg, sizeof(aChatmsg), "'%s' isn't an option on this server", pMsg->m_pValue);
				SendChatTarget(ClientId, aChatmsg);
				return;
			}
			else
			{
				str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s'", Server()->ClientName(ClientId), pMsg->m_pValue);
				str_copy(aDesc, pMsg->m_pValue);
				str_copy(aCmd, pMsg->m_pValue);
			}
		}

		m_VoteType = VOTE_TYPE_OPTION;
	}
	else if(str_comp_nocase(pMsg->m_pType, "kick") == 0)
	{
		if(!g_Config.m_SvVoteKick && !Server()->IsRconAuthed(ClientId)) // allow admins to call kick votes even if they are forbidden
		{
			SendChatTarget(ClientId, "Server does not allow voting to kick players");
			return;
		}
		if(!Server()->IsRconAuthed(ClientId) && time_get() < m_apPlayers[ClientId]->m_LastKickVote + (time_freq() * g_Config.m_SvVoteKickDelay))
		{
			str_format(aChatmsg, sizeof(aChatmsg), "There's a %d second wait time between kick votes for each player please wait %d second(s)",
				g_Config.m_SvVoteKickDelay,
				(int)((m_apPlayers[ClientId]->m_LastKickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq()));
			SendChatTarget(ClientId, aChatmsg);
			return;
		}

		if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientId))
		{
			const NETADDR *apAddresses[MAX_CLIENTS];
			for(int i = 0; i < MAX_CLIENTS; i++)
			{
				if(m_apPlayers[i])
				{
					apAddresses[i] = Server()->ClientAddr(i);
				}
			}
			int NumPlayers = 0;
			for(int i = 0; i < MAX_CLIENTS; ++i)
			{
				if(m_apPlayers[i] && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(i))
				{
					NumPlayers++;
					for(int j = 0; j < i; j++)
					{
						if(m_apPlayers[j] && m_apPlayers[j]->GetTeam() != TEAM_SPECTATORS && !GetDDRaceTeam(j))
						{
							if(!net_addr_comp_noport(apAddresses[i], apAddresses[j]))
							{
								NumPlayers--;
								break;
							}
						}
					}
				}
			}

			if(NumPlayers < g_Config.m_SvVoteKickMin)
			{
				str_format(aChatmsg, sizeof(aChatmsg), "Kick voting requires %d players", g_Config.m_SvVoteKickMin);
				SendChatTarget(ClientId, aChatmsg);
				return;
			}
		}

		int KickId = str_toint(pMsg->m_pValue);

		if(!Server()->ReverseTranslate(KickId, ClientId))
		{
			return;
		}
		if(KickId < 0 || KickId >= MAX_CLIENTS || !m_apPlayers[KickId])
		{
			SendChatTarget(ClientId, "Invalid client id to kick");
			return;
		}
		if(KickId == ClientId)
		{
			SendChatTarget(ClientId, "You can't kick yourself");
			return;
		}

		int Authed = Server()->GetAuthedState(ClientId);
		int KickedAuthed = Server()->GetAuthedState(KickId);
		if(KickedAuthed > Authed)
		{
			SendChatTarget(ClientId, "You can't kick authorized players");
			char aBufKick[128];
			str_format(aBufKick, sizeof(aBufKick), "'%s' called for vote to kick you", Server()->ClientName(ClientId));
			SendChatTarget(KickId, aBufKick);
			return;
		}

		// Don't allow kicking if a player has no character
		if(!GetPlayerChar(ClientId) || !GetPlayerChar(KickId))
		{
			SendChatTarget(ClientId, "You can kick only your team member");
			return;
		}

		if(GetDDRaceTeam(ClientId) != GetDDRaceTeam(KickId))
		{
			if(!g_Config.m_SvVoteKickMuteTime)
			{
				str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to mute '%s' (%s)", Server()->ClientName(ClientId), Server()->ClientName(KickId), aReason);
				str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", KickId, Server()->ClientName(KickId));
				str_format(aCmd, sizeof(aCmd), "muteid %d %d Muted by vote", KickId, g_Config.m_SvVoteKickMuteTime);
				str_format(aDesc, sizeof(aDesc), "Mute '%s'", Server()->ClientName(KickId));
			}
			else
			{
				SendChatTarget(ClientId, "You can kick only your team member");
				return;
			}
		}
		else
		{
			str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to kick '%s' (%s)", Server()->ClientName(ClientId), Server()->ClientName(KickId), aReason);
			str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", KickId, Server()->ClientName(KickId));
			if(!GetDDRaceTeam(ClientId))
			{
				if(!g_Config.m_SvVoteKickBantime)
				{
					str_format(aCmd, sizeof(aCmd), "kick %d Kicked by vote", KickId);
					str_format(aDesc, sizeof(aDesc), "Kick '%s'", Server()->ClientName(KickId));
				}
				else
				{
					str_format(aCmd, sizeof(aCmd), "ban %s %d Banned by vote", Server()->ClientAddrString(KickId, false), g_Config.m_SvVoteKickBantime);
					str_format(aDesc, sizeof(aDesc), "Ban '%s'", Server()->ClientName(KickId));
				}
			}
			else
			{
				str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team_ddr %d 0", KickId, GetDDRaceTeam(KickId), KickId);
				str_format(aDesc, sizeof(aDesc), "Move '%s' to team 0", Server()->ClientName(KickId));
			}
		}
		m_apPlayers[ClientId]->m_LastKickVote = time_get();
		m_VoteType = VOTE_TYPE_KICK;
		m_VoteVictim = KickId;
	}
	else if(str_comp_nocase(pMsg->m_pType, "spectate") == 0)
	{
		if(!g_Config.m_SvVoteSpectate)
		{
			SendChatTarget(ClientId, "Server does not allow voting to move players to spectators");
			return;
		}

		int SpectateId = str_toint(pMsg->m_pValue);

		if(!Server()->ReverseTranslate(SpectateId, ClientId))
		{
			return;
		}
		if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !m_apPlayers[SpectateId] || m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS)
		{
			SendChatTarget(ClientId, "Invalid client id to move to spectators");
			return;
		}
		if(SpectateId == ClientId)
		{
			SendChatTarget(ClientId, "You can't move yourself to spectators");
			return;
		}
		int Authed = Server()->GetAuthedState(ClientId);
		int SpectateAuthed = Server()->GetAuthedState(SpectateId);
		if(SpectateAuthed > Authed)
		{
			SendChatTarget(ClientId, "You can't move authorized players to spectators");
			char aBufSpectate[128];
			str_format(aBufSpectate, sizeof(aBufSpectate), "'%s' called for vote to move you to spectators", Server()->ClientName(ClientId));
			SendChatTarget(SpectateId, aBufSpectate);
			return;
		}

		if(!GetPlayerChar(ClientId) || !GetPlayerChar(SpectateId) || GetDDRaceTeam(ClientId) != GetDDRaceTeam(SpectateId))
		{
			SendChatTarget(ClientId, "You can only move your team member to spectators");
			return;
		}

		str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", SpectateId, Server()->ClientName(SpectateId));
		if(g_Config.m_SvPauseable && g_Config.m_SvVotePause)
		{
			str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to pause '%s' for %d seconds (%s)", Server()->ClientName(ClientId), Server()->ClientName(SpectateId), g_Config.m_SvVotePauseTime, aReason);
			str_format(aDesc, sizeof(aDesc), "Pause '%s' (%ds)", Server()->ClientName(SpectateId), g_Config.m_SvVotePauseTime);
			str_format(aCmd, sizeof(aCmd), "uninvite %d %d; force_pause %d %d", SpectateId, GetDDRaceTeam(SpectateId), SpectateId, g_Config.m_SvVotePauseTime);
		}
		else
		{
			str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to move '%s' to spectators (%s)", Server()->ClientName(ClientId), Server()->ClientName(SpectateId), aReason);
			str_format(aDesc, sizeof(aDesc), "Move '%s' to spectators", Server()->ClientName(SpectateId));
			str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team %d -1 %d", SpectateId, GetDDRaceTeam(SpectateId), SpectateId, g_Config.m_SvVoteSpectateRejoindelay);
		}
		m_VoteType = VOTE_TYPE_SPECTATE;
		m_VoteVictim = SpectateId;
	}

	if(aCmd[0] && str_comp_nocase(aCmd, "info") != 0)
		CallVote(ClientId, aDesc, aCmd, aReason, aChatmsg, aSixupDesc[0] ? aSixupDesc : nullptr);
}

void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientId)
{
	if(!m_VoteCloseTime)
		return;

	CPlayer *pPlayer = m_apPlayers[ClientId];

	if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + Server()->TickSpeed() * 3 > Server()->Tick())
		return;

	pPlayer->m_LastVoteTry = Server()->Tick();
	pPlayer->UpdatePlaytime();

	if(!pMsg->m_Vote)
		return;

	// Allow the vote creator to cancel the vote
	if(pPlayer->GetCid() == m_VoteCreator && pMsg->m_Vote == -1)
	{
		m_VoteEnforce = VOTE_ENFORCE_CANCEL;
		return;
	}

	pPlayer->m_Vote = pMsg->m_Vote;
	pPlayer->m_VotePos = ++m_VotePos;
	m_VoteUpdate = true;

	CNetMsg_Sv_YourVote Msg = {pMsg->m_Vote};
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientId)
{
	if(m_pController->IsGamePaused())
		return;

	CPlayer *pPlayer = m_apPlayers[ClientId];

	if(pPlayer->GetTeam() == pMsg->m_Team)
		return;
	if(g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick())
		return;

	// Kill Protection
	CCharacter *pChr = pPlayer->GetCharacter();
	if(pChr)
	{
		int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
		if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED)
		{
			SendChatTarget(ClientId, "Kill Protection enabled. If you really want to join the spectators, first type /kill");
			return;
		}
	}

	if(pPlayer->m_TeamChangeTick > Server()->Tick())
	{
		pPlayer->m_LastSetTeam = Server()->Tick();
		int TimeLeft = (pPlayer->m_TeamChangeTick - Server()->Tick()) / Server()->TickSpeed();
		char aTime[32];
		str_time((int64_t)TimeLeft * 100, ETimeFormat::HOURS, aTime, sizeof(aTime));
		char aBuf[128];
		str_format(aBuf, sizeof(aBuf), "Time to wait before changing team: %s", aTime);
		SendBroadcast(aBuf, ClientId);
		return;
	}

	// Switch team on given client and kill/respawn them
	char aTeamJoinError[512];
	if(m_pController->CanJoinTeam(pMsg->m_Team, ClientId, aTeamJoinError, sizeof(aTeamJoinError)))
	{
		if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS)
			m_VoteUpdate = true;
		m_pController->DoTeamChange(pPlayer, pMsg->m_Team, true);
		pPlayer->m_TeamChangeTick = Server()->Tick();
	}
	else
		SendBroadcast(aTeamJoinError, ClientId);
}

void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientId, CUnpacker *pUnpacker)
{
	IServer::CClientInfo Info;
	if(Server()->GetClientInfo(ClientId, &Info) && Info.m_GotDDNetVersion)
	{
		return;
	}
	int DDNetVersion = pUnpacker->GetInt();
	if(pUnpacker->Error() || DDNetVersion < 0)
	{
		DDNetVersion = VERSION_DDRACE;
	}
	Server()->SetClientDDNetVersion(ClientId, DDNetVersion);
	if(OnClientDDNetVersionKnown(ClientId))
		return;
	// Initial identification, currently identified as 16p client, make sure we allow 64 slots
	CPlayerMapping::CSixupCfg SixupCfg;
	SixupCfg.m_ClearSlots = true;
	m_PlayerMapping.InitPlayerMap(ClientId, SixupCfg);
}

void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId)
{
	if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
	{
		CPlayer *pPlayer = m_apPlayers[ClientId];
		pPlayer->m_ShowOthers = pMsg->m_Show;
	}
}

void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId)
{
	if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault)
	{
		CPlayer *pPlayer = m_apPlayers[ClientId];
		pPlayer->m_ShowOthers = pMsg->m_Show;
	}
}

void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];
	pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y);
}

void CGameContext::OnCameraInfoNetMessage(const CNetMsg_Cl_CameraInfo *pMsg, int ClientId)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];
	pPlayer->m_CameraInfo.Write(pMsg);
}

void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId)
{
	if(m_pController->IsGamePaused())
		return;

	CPlayer *pPlayer = m_apPlayers[ClientId];
	if((g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode + Server()->TickSpeed() / 4 > Server()->Tick()))
		return;

	pPlayer->m_LastSetSpectatorMode = Server()->Tick();
	int SpectatorId = std::clamp(pMsg->m_SpectatorId, (int)SPEC_FOLLOW, MAX_CLIENTS - 1);

	if(m_PlayerMapping.DoSeeOthers(ClientId, SpectatorId))
		return;

	if(SpectatorId >= 0)
		if(!Server()->ReverseTranslate(SpectatorId, ClientId))
			return;

	pPlayer->UpdatePlaytime();
	if(SpectatorId >= 0 && (!m_apPlayers[SpectatorId] || m_apPlayers[SpectatorId]->GetTeam() == TEAM_SPECTATORS))
		SendChatTarget(ClientId, "Invalid spectator id used");
	else
		pPlayer->SetSpectatorId(SpectatorId);
}

void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];
	if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick())
		return;

	bool SixupNeedsUpdate = false;

	pPlayer->m_LastChangeInfo = Server()->Tick();
	pPlayer->UpdatePlaytime();

	if(g_Config.m_SvSpamprotection)
	{
		CNetMsg_Sv_ChangeInfoCooldown ChangeInfoCooldownMsg;
		ChangeInfoCooldownMsg.m_WaitUntil = Server()->Tick() + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay;
		Server()->SendPackMsg(&ChangeInfoCooldownMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
	}

	// set infos
	if(Server()->WouldClientNameChange(ClientId, pMsg->m_pName) && !ProcessSpamProtection(ClientId))
	{
		char aOldName[MAX_NAME_LENGTH];
		str_copy(aOldName, Server()->ClientName(ClientId));

		Server()->SetClientName(ClientId, pMsg->m_pName);

		char aChatText[256];
		str_format(aChatText, sizeof(aChatText), "'%s' changed name to '%s'", aOldName, Server()->ClientName(ClientId));
		SendChat(-1, TEAM_ALL, aChatText);

		// reload scores
		Score()->PlayerData(ClientId)->Reset();
		Server()->SetClientScore(ClientId, std::nullopt);
		Score()->LoadPlayerData(ClientId);

		SixupNeedsUpdate = true;

		LogEvent("Name change", ClientId);
	}

	if(Server()->WouldClientClanChange(ClientId, pMsg->m_pClan))
	{
		SixupNeedsUpdate = true;
		Server()->SetClientClan(ClientId, pMsg->m_pClan);
	}

	if(Server()->ClientCountry(ClientId) != pMsg->m_Country)
	{
		SixupNeedsUpdate = true;
		Server()->SetClientCountry(ClientId, pMsg->m_Country);
	}

	str_copy(pPlayer->m_TeeInfos.m_aSkinName, pMsg->m_pSkin);
	pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
	pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
	pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
	if(!Server()->IsSixup(ClientId))
		pPlayer->m_TeeInfos.ToSixup();

	if(SixupNeedsUpdate)
	{
		SendRename7(ClientId);
	}
	else
	{
		SendSkinChange7(ClientId);
	}

	Server()->ExpireServerInfo();
}

void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId)
{
	if(m_pController->IsGamePaused())
		return;

	CPlayer *pPlayer = m_apPlayers[ClientId];

	auto &&CheckPreventEmote = [&](int64_t LastEmote, int64_t DelayInMs) {
		return (LastEmote * (int64_t)1000) + (int64_t)Server()->TickSpeed() * DelayInMs > ((int64_t)Server()->Tick() * (int64_t)1000);
	};

	if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmote, (int64_t)g_Config.m_SvEmoticonMsDelay))
		return;

	CCharacter *pChr = pPlayer->GetCharacter();

	// player needs a character to send emotes
	if(!pChr)
		return;

	pPlayer->m_LastEmote = Server()->Tick();
	pPlayer->UpdatePlaytime();

	// check if the global emoticon is prevented and emotes are only send to nearby players
	if(g_Config.m_SvSpamprotection && CheckPreventEmote((int64_t)pPlayer->m_LastEmoteGlobal, (int64_t)g_Config.m_SvGlobalEmoticonMsDelay))
	{
		for(int i = 0; i < MAX_CLIENTS; ++i)
		{
			if(m_apPlayers[i] && pChr->CanSnapCharacter(i) && pChr->IsSnappingCharacterInView(i))
			{
				SendEmoticon(ClientId, pMsg->m_Emoticon, i);
			}
		}
	}
	else
	{
		// else send emoticons to all players
		pPlayer->m_LastEmoteGlobal = Server()->Tick();
		SendEmoticon(ClientId, pMsg->m_Emoticon, -1);
	}

	if(g_Config.m_SvEmotionalTees == 1 && pPlayer->m_EyeEmoteEnabled)
	{
		int EmoteType = EMOTE_NORMAL;
		switch(pMsg->m_Emoticon)
		{
		case EMOTICON_EXCLAMATION:
		case EMOTICON_GHOST:
		case EMOTICON_QUESTION:
		case EMOTICON_WTF:
			EmoteType = EMOTE_SURPRISE;
			break;
		case EMOTICON_DOTDOT:
		case EMOTICON_DROP:
		case EMOTICON_ZZZ:
			EmoteType = EMOTE_BLINK;
			break;
		case EMOTICON_EYES:
		case EMOTICON_HEARTS:
		case EMOTICON_MUSIC:
			EmoteType = EMOTE_HAPPY;
			break;
		case EMOTICON_OOP:
		case EMOTICON_SORRY:
		case EMOTICON_SUSHI:
			EmoteType = EMOTE_PAIN;
			break;
		case EMOTICON_DEVILTEE:
		case EMOTICON_SPLATTEE:
		case EMOTICON_ZOMG:
			EmoteType = EMOTE_ANGRY;
			break;
		default:
			break;
		}
		pChr->SetEmote(EmoteType, Server()->Tick() + 2 * Server()->TickSpeed());
	}
}

void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientId)
{
	if(m_pController->IsGamePaused())
		return;

	if(IsRunningKickOrSpecVote(ClientId) && GetDDRaceTeam(ClientId))
	{
		SendChatTarget(ClientId, "You are running a vote please try again after the vote is done!");
		return;
	}
	CPlayer *pPlayer = m_apPlayers[ClientId];
	if(pPlayer->m_LastKill && pPlayer->m_LastKill + Server()->TickSpeed() * g_Config.m_SvKillDelay > Server()->Tick())
		return;
	if(pPlayer->IsPaused())
		return;

	CCharacter *pChr = pPlayer->GetCharacter();
	if(!pChr)
		return;

	// Kill Protection
	int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed();
	if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == ERaceState::STARTED)
	{
		SendChatTarget(ClientId, "Kill Protection enabled. If you really want to kill, type /kill");
		return;
	}

	pPlayer->m_LastKill = Server()->Tick();
	pPlayer->KillCharacter(WEAPON_SELF);
	pPlayer->Respawn();
}

void CGameContext::OnEnableSpectatorCountNetMessage(const CNetMsg_Cl_EnableSpectatorCount *pMsg, int ClientId)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];
	if(!pPlayer)
		return;

	pPlayer->m_EnableSpectatorCount = pMsg->m_Enable;
}

void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientId)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];

	if(pPlayer->m_IsReady)
		return;

	pPlayer->m_LastChangeInfo = Server()->Tick();

	// set start infos
	Server()->SetClientName(ClientId, pMsg->m_pName);
	// trying to set client name can delete the player object, check if it still exists
	if(!m_apPlayers[ClientId])
	{
		return;
	}
	Server()->SetClientClan(ClientId, pMsg->m_pClan);
	// trying to set client clan can delete the player object, check if it still exists
	if(!m_apPlayers[ClientId])
	{
		return;
	}
	Server()->SetClientCountry(ClientId, pMsg->m_Country);
	str_copy(pPlayer->m_TeeInfos.m_aSkinName, pMsg->m_pSkin);
	pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor;
	pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody;
	pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet;
	if(!Server()->IsSixup(ClientId))
		pPlayer->m_TeeInfos.ToSixup();

	// send clear vote options
	CNetMsg_Sv_VoteClearOptions ClearMsg;
	Server()->SendPackMsg(&ClearMsg, MSGFLAG_VITAL, ClientId);

	// begin sending vote options
	pPlayer->m_SendVoteIndex = 0;

	// send tuning parameters to client
	SendTuningParams(ClientId, pPlayer->m_TuneZone);

	// client is ready to enter
	pPlayer->m_IsReady = true;
	CNetMsg_Sv_ReadyToEnter ReadyMsg;
	Server()->SendPackMsg(&ReadyMsg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId);

	Server()->ExpireServerInfo();
}

void CGameContext::ConTuneParam(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	const char *pParamName = pResult->GetString(0);

	char aBuf[256];
	if(pResult->NumArguments() == 2)
	{
		float NewValue = pResult->GetFloat(1);
		if(pSelf->GlobalTuning()->Set(pParamName, NewValue) && pSelf->GlobalTuning()->Get(pParamName, &NewValue))
		{
			str_format(aBuf, sizeof(aBuf), "%s changed to %.2f", pParamName, NewValue);
			pSelf->SendTuningParams(-1);
		}
		else
		{
			str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
		}
	}
	else
	{
		float Value;
		if(pSelf->GlobalTuning()->Get(pParamName, &Value))
		{
			str_format(aBuf, sizeof(aBuf), "%s %.2f", pParamName, Value);
		}
		else
		{
			str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
		}
	}
	pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
}

void CGameContext::ConToggleTuneParam(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	const char *pParamName = pResult->GetString(0);
	float OldValue;

	char aBuf[256];
	if(!pSelf->GlobalTuning()->Get(pParamName, &OldValue))
	{
		str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
		return;
	}

	float NewValue = absolute(OldValue - pResult->GetFloat(1)) < 0.0001f ? pResult->GetFloat(2) : pResult->GetFloat(1);

	pSelf->GlobalTuning()->Set(pParamName, NewValue);
	pSelf->GlobalTuning()->Get(pParamName, &NewValue);

	str_format(aBuf, sizeof(aBuf), "%s changed to %.2f", pParamName, NewValue);
	pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
	pSelf->SendTuningParams(-1);
}

void CGameContext::ConTuneReset(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	if(pResult->NumArguments())
	{
		const char *pParamName = pResult->GetString(0);
		float DefaultValue = 0.0f;
		char aBuf[256];

		if(CTuningParams::DEFAULT.Get(pParamName, &DefaultValue) && pSelf->GlobalTuning()->Set(pParamName, DefaultValue) && pSelf->GlobalTuning()->Get(pParamName, &DefaultValue))
		{
			str_format(aBuf, sizeof(aBuf), "%s reset to %.2f", pParamName, DefaultValue);
			pSelf->SendTuningParams(-1);
		}
		else
		{
			str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
		}
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
	}
	else
	{
		pSelf->ResetTuning();
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", "Tuning reset");
	}
}

void CGameContext::ConTunes(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	char aBuf[256];
	for(int i = 0; i < CTuningParams::Num(); i++)
	{
		float Value;
		pSelf->GlobalTuning()->Get(i, &Value);
		str_format(aBuf, sizeof(aBuf), "%s %.2f", CTuningParams::Name(i), Value);
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
	}
}

void CGameContext::ConTuneZone(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	int List = pResult->GetInteger(0);
	const char *pParamName = pResult->GetString(1);
	float NewValue = pResult->GetFloat(2);

	if(List >= 0 && List < TuneZone::NUM)
	{
		char aBuf[256];
		if(pSelf->TuningList()[List].Set(pParamName, NewValue) && pSelf->TuningList()[List].Get(pParamName, &NewValue))
		{
			str_format(aBuf, sizeof(aBuf), "%s in zone %d changed to %.2f", pParamName, List, NewValue);
			pSelf->SendTuningParams(-1, List);
		}
		else
		{
			str_format(aBuf, sizeof(aBuf), "No such tuning parameter: %s", pParamName);
		}
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
	}
}

void CGameContext::ConTuneDumpZone(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	int List = pResult->GetInteger(0);
	char aBuf[256];
	if(List >= 0 && List < TuneZone::NUM)
	{
		for(int i = 0; i < CTuningParams::Num(); i++)
		{
			float Value;
			pSelf->TuningList()[List].Get(i, &Value);
			str_format(aBuf, sizeof(aBuf), "zone %d: %s %.2f", List, CTuningParams::Name(i), Value);
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
		}
	}
}

void CGameContext::ConTuneResetZone(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	if(pResult->NumArguments())
	{
		int List = pResult->GetInteger(0);
		if(List >= 0 && List < TuneZone::NUM)
		{
			pSelf->TuningList()[List] = CTuningParams::DEFAULT;
			char aBuf[256];
			str_format(aBuf, sizeof(aBuf), "Tunezone %d reset", List);
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", aBuf);
			pSelf->SendTuningParams(-1, List);
		}
	}
	else
	{
		for(int i = 0; i < TuneZone::NUM; i++)
		{
			*(pSelf->TuningList() + i) = CTuningParams::DEFAULT;
			pSelf->SendTuningParams(-1, i);
		}
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tuning", "All Tunezones reset");
	}
}

void CGameContext::ConTuneSetZoneMsgEnter(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	if(pResult->NumArguments())
	{
		int List = pResult->GetInteger(0);
		if(List >= 0 && List < TuneZone::NUM)
		{
			str_copy(pSelf->m_aaZoneEnterMsg[List], pResult->GetString(1));
		}
	}
}

void CGameContext::ConTuneSetZoneMsgLeave(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	if(pResult->NumArguments())
	{
		int List = pResult->GetInteger(0);
		if(List >= 0 && List < TuneZone::NUM)
		{
			str_copy(pSelf->m_aaZoneLeaveMsg[List], pResult->GetString(1));
		}
	}
}

void CGameContext::ConMapbug(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	if(pSelf->m_pController)
	{
		log_info("mapbugs", "can't add map bugs after the game started");
		return;
	}

	const char *pMapBugName = pResult->GetString(0);
	switch(pSelf->m_MapBugs.Update(pMapBugName))
	{
	case EMapBugUpdate::OK:
		break;
	case EMapBugUpdate::OVERRIDDEN:
		log_info("mapbugs", "map-internal setting overridden by database");
		break;
	case EMapBugUpdate::NOTFOUND:
		log_info("mapbugs", "unknown map bug '%s', ignoring", pMapBugName);
		break;
	default:
		dbg_assert_failed("unreachable");
	}
}

void CGameContext::ConSwitchOpen(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	int Switch = pResult->GetInteger(0);

	if(in_range(Switch, (int)pSelf->Switchers().size() - 1))
	{
		pSelf->Switchers()[Switch].m_Initial = false;
		char aBuf[256];
		str_format(aBuf, sizeof(aBuf), "switch %d opened by default", Switch);
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
	}
}

void CGameContext::ConPause(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	pSelf->m_pController->SetGamePaused(!pSelf->m_pController->IsGamePaused());
}

void CGameContext::ConChangeMap(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	pSelf->m_pController->ChangeMap(pResult->GetString(0));
}

void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId;
	int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(0) : -1;
	int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(1) : MinStars;

	if(!in_range(MinStars, -1, 5) || !in_range(MaxStars, -1, 5))
		return;

	pSelf->m_pScore->RandomMap(ClientId, MinStars, MaxStars);
}

void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	const int ClientId = pResult->m_ClientId == -1 ? pSelf->m_VoteCreator : pResult->m_ClientId;
	int MinStars = pResult->NumArguments() > 0 ? pResult->GetInteger(0) : -1;
	int MaxStars = pResult->NumArguments() > 1 ? pResult->GetInteger(1) : MinStars;

	if(!in_range(MinStars, -1, 5) || !in_range(MaxStars, -1, 5))
		return;

	pSelf->m_pScore->RandomUnfinishedMap(ClientId, MinStars, MaxStars);
}

void CGameContext::ConRestart(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	if(pResult->NumArguments())
		pSelf->m_pController->DoWarmup(pResult->GetInteger(0));
	else
		pSelf->m_pController->StartRound();
}

static void UnescapeNewlines(char *pBuf)
{
	int i, j;
	for(i = 0, j = 0; pBuf[i]; i++, j++)
	{
		if(pBuf[i] == '\\' && pBuf[i + 1] == 'n')
		{
			pBuf[j] = '\n';
			i++;
		}
		else if(i != j)
		{
			pBuf[j] = pBuf[i];
		}
	}
	pBuf[j] = '\0';
}

void CGameContext::ConServerAlert(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	char aBuf[1024];
	str_copy(aBuf, pResult->GetString(0));
	UnescapeNewlines(aBuf);

	pSelf->SendServerAlert(aBuf);
}

void CGameContext::ConModAlert(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	const int Victim = pResult->GetVictim(0);
	if(!CheckClientId(Victim) || !pSelf->m_apPlayers[Victim])
	{
		log_info("moderator_alert", "Client ID not found: %d", Victim);
		return;
	}

	char aBuf[1024];
	str_copy(aBuf, pResult->GetString(1));
	UnescapeNewlines(aBuf);

	pSelf->SendModeratorAlert(Victim, aBuf);
}

void CGameContext::ConBroadcast(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	char aBuf[1024];
	str_copy(aBuf, pResult->GetString(0));
	UnescapeNewlines(aBuf);

	pSelf->SendBroadcast(aBuf, -1);
}

void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	pSelf->SendChat(-1, TEAM_ALL, pResult->GetString(0));
}

void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	int Team = pResult->GetInteger(1);
	if(!pSelf->m_pController->IsValidTeam(Team))
	{
		log_info("server", "Invalid Team: %d", Team);
		return;
	}

	int ClientId = pResult->GetVictim(0);
	int Delay = pResult->NumArguments() > 2 ? pResult->GetInteger(2) : 0;
	if(!pSelf->m_apPlayers[ClientId])
		return;

	char aBuf[256];
	str_format(aBuf, sizeof(aBuf), "moved client %d to the %s", ClientId, pSelf->m_pController->GetTeamName(Team));
	pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);

	pSelf->m_apPlayers[ClientId]->Pause(CPlayer::PAUSE_NONE, false); // reset /spec and /pause to allow rejoin
	pSelf->m_apPlayers[ClientId]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60;
	pSelf->m_pController->DoTeamChange(pSelf->m_apPlayers[ClientId], Team, true);
	if(Team == TEAM_SPECTATORS)
		pSelf->m_apPlayers[ClientId]->Pause(CPlayer::PAUSE_NONE, true);
}

void CGameContext::ConHotReload(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		if(!pSelf->GetPlayerChar(i))
			continue;

		CCharacter *pChar = pSelf->GetPlayerChar(i);

		// Save the tee individually
		pSelf->m_apSavedTees[i] = new CSaveHotReloadTee();
		pSelf->m_apSavedTees[i]->Save(pChar, false);

		// Save the team state
		pSelf->m_aTeamMapping[i] = pSelf->GetDDRaceTeam(i);
		if(pSelf->m_aTeamMapping[i] == TEAM_SUPER)
			pSelf->m_aTeamMapping[i] = pChar->m_TeamBeforeSuper;

		if(pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]])
			continue;

		pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]] = new CSaveTeam();
		pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]->Save(pSelf, pSelf->m_aTeamMapping[i], true, true);
	}
	pSelf->Server()->ReloadMap();
}

void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	const char *pDescription = pResult->GetString(0);
	const char *pCommand = pResult->GetString(1);

	pSelf->AddVote(pDescription, pCommand);
}

void CGameContext::AddVote(const char *pDescription, const char *pCommand)
{
	if(m_NumVoteOptions == MAX_VOTE_OPTIONS)
	{
		Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "maximum number of vote options reached");
		return;
	}

	// check for valid option
	if(!Console()->LineIsValid(pCommand) || str_length(pCommand) >= VOTE_CMD_LENGTH)
	{
		char aBuf[256];
		str_format(aBuf, sizeof(aBuf), "skipped invalid command '%s'", pCommand);
		Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
		return;
	}
	while(*pDescription == ' ')
		pDescription++;
	if(str_length(pDescription) >= VOTE_DESC_LENGTH || *pDescription == 0)
	{
		char aBuf[256];
		str_format(aBuf, sizeof(aBuf), "skipped invalid option '%s'", pDescription);
		Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
		return;
	}

	// check for duplicate entry
	CVoteOptionServer *pOption = m_pVoteOptionFirst;
	while(pOption)
	{
		if(str_comp_nocase(pDescription, pOption->m_aDescription) == 0)
		{
			char aBuf[256];
			str_format(aBuf, sizeof(aBuf), "option '%s' already exists", pDescription);
			Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
			return;
		}
		pOption = pOption->m_pNext;
	}

	// add the option
	++m_NumVoteOptions;
	int Len = str_length(pCommand);

	pOption = (CVoteOptionServer *)m_pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len, alignof(CVoteOptionServer));
	pOption->m_pNext = nullptr;
	pOption->m_pPrev = m_pVoteOptionLast;
	if(pOption->m_pPrev)
		pOption->m_pPrev->m_pNext = pOption;
	m_pVoteOptionLast = pOption;
	if(!m_pVoteOptionFirst)
		m_pVoteOptionFirst = pOption;

	str_copy(pOption->m_aDescription, pDescription);
	str_copy(pOption->m_aCommand, pCommand, Len + 1);
}

void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	const char *pDescription = pResult->GetString(0);

	// check for valid option
	CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
	while(pOption)
	{
		if(str_comp_nocase(pDescription, pOption->m_aDescription) == 0)
			break;
		pOption = pOption->m_pNext;
	}
	if(!pOption)
	{
		char aBuf[256];
		str_format(aBuf, sizeof(aBuf), "option '%s' does not exist", pDescription);
		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
		return;
	}

	// start reloading vote option list
	// clear vote options
	CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
	pSelf->Server()->SendPackMsg(&VoteClearOptionsMsg, MSGFLAG_VITAL, -1);

	// reset sending of vote options
	for(auto &pPlayer : pSelf->m_apPlayers)
	{
		if(pPlayer)
			pPlayer->m_SendVoteIndex = 0;
	}

	// TODO: improve this
	// remove the option
	--pSelf->m_NumVoteOptions;

	CHeap *pVoteOptionHeap = new CHeap();
	CVoteOptionServer *pVoteOptionFirst = nullptr;
	CVoteOptionServer *pVoteOptionLast = nullptr;
	int NumVoteOptions = pSelf->m_NumVoteOptions;
	for(CVoteOptionServer *pSrc = pSelf->m_pVoteOptionFirst; pSrc; pSrc = pSrc->m_pNext)
	{
		if(pSrc == pOption)
			continue;

		// copy option
		int Len = str_length(pSrc->m_aCommand);
		CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len, alignof(CVoteOptionServer));
		pDst->m_pNext = nullptr;
		pDst->m_pPrev = pVoteOptionLast;
		if(pDst->m_pPrev)
			pDst->m_pPrev->m_pNext = pDst;
		pVoteOptionLast = pDst;
		if(!pVoteOptionFirst)
			pVoteOptionFirst = pDst;

		str_copy(pDst->m_aDescription, pSrc->m_aDescription);
		str_copy(pDst->m_aCommand, pSrc->m_aCommand, Len + 1);
	}

	// clean up
	delete pSelf->m_pVoteOptionHeap;
	pSelf->m_pVoteOptionHeap = pVoteOptionHeap;
	pSelf->m_pVoteOptionFirst = pVoteOptionFirst;
	pSelf->m_pVoteOptionLast = pVoteOptionLast;
	pSelf->m_NumVoteOptions = NumVoteOptions;
}

void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;
	const char *pType = pResult->GetString(0);
	const char *pValue = pResult->GetString(1);
	const char *pReason = pResult->NumArguments() > 2 && pResult->GetString(2)[0] ? pResult->GetString(2) : "No reason given";
	char aBuf[128] = {0};

	if(str_comp_nocase(pType, "option") == 0)
	{
		CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst;
		while(pOption)
		{
			if(str_comp_nocase(pValue, pOption->m_aDescription) == 0)
			{
				str_format(aBuf, sizeof(aBuf), "authorized player forced server option '%s' (%s)", pValue, pReason);
				pSelf->SendChatTarget(-1, aBuf, FLAG_SIX);
				// m_VoteCreator must be a valid client id or -1, but the command can also be executed by console pseudo clients (e.g. map configs)
				pSelf->m_VoteCreator = pResult->m_ClientId >= 0 ? pResult->m_ClientId : -1;
				pSelf->Console()->ExecuteLine(pOption->m_aCommand, IConsole::CLIENT_ID_UNSPECIFIED);
				break;
			}

			pOption = pOption->m_pNext;
		}

		if(!pOption)
		{
			str_format(aBuf, sizeof(aBuf), "'%s' isn't an option on this server", pValue);
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
			return;
		}
	}
	else if(str_comp_nocase(pType, "kick") == 0)
	{
		if(!pSelf->Server()->ClientUsesRealClientIds(pResult->m_ClientId))
		{
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Your client does not see the real client IDs of this server. Use a more recent DDNet client.");
			return;
		}
		int KickId = str_toint(pValue);
		if(KickId < 0 || KickId >= MAX_CLIENTS || !pSelf->m_apPlayers[KickId])
		{
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Invalid client id to kick");
			return;
		}

		if(!g_Config.m_SvVoteKickBantime)
		{
			str_format(aBuf, sizeof(aBuf), "kick %d %s", KickId, pReason);
			pSelf->Console()->ExecuteLine(aBuf, IConsole::CLIENT_ID_UNSPECIFIED, false);
		}
		else
		{
			str_format(aBuf, sizeof(aBuf), "ban %s %d %s", pSelf->Server()->ClientAddrString(KickId, false), g_Config.m_SvVoteKickBantime, pReason);
			pSelf->Console()->ExecuteLine(aBuf, IConsole::CLIENT_ID_UNSPECIFIED, false);
		}
	}
	else if(str_comp_nocase(pType, "spectate") == 0)
	{
		if(!pSelf->Server()->ClientUsesRealClientIds(pResult->m_ClientId))
		{
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Your client does not see the real client IDs of this server. Use a more recent DDNet client.");
			return;
		}
		int SpectateId = str_toint(pValue);
		if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateId] || pSelf->m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS)
		{
			pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Invalid client id to move");
			return;
		}

		str_format(aBuf, sizeof(aBuf), "'%s' was moved to spectator (%s)", pSelf->Server()->ClientName(SpectateId), pReason);
		pSelf->SendChatTarget(-1, aBuf);
		str_format(aBuf, sizeof(aBuf), "set_team %d -1 %d", SpectateId, g_Config.m_SvVoteSpectateRejoindelay);
		pSelf->Console()->ExecuteLine(aBuf, IConsole::CLIENT_ID_UNSPECIFIED, false);
	}
}

void CGameContext::ConClearVotes(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	CNetMsg_Sv_VoteClearOptions VoteClearOptionsMsg;
	pSelf->Server()->SendPackMsg(&VoteClearOptionsMsg, MSGFLAG_VITAL, -1);
	pSelf->m_pVoteOptionHeap->Reset();
	pSelf->m_pVoteOptionFirst = nullptr;
	pSelf->m_pVoteOptionLast = nullptr;
	pSelf->m_NumVoteOptions = 0;

	// reset sending of vote options
	for(auto &pPlayer : pSelf->m_apPlayers)
	{
		if(pPlayer)
			pPlayer->m_SendVoteIndex = 0;
	}
}

struct CMapNameItem
{
	char m_aName[IO_MAX_PATH_LENGTH - 4];
	bool m_IsDirectory;

	static bool CompareFilenameAscending(const CMapNameItem Lhs, const CMapNameItem Rhs)
	{
		if(str_comp(Rhs.m_aName, "..") == 0)
			return false;
		if(str_comp(Lhs.m_aName, "..") == 0)
			return true;
		if(Lhs.m_IsDirectory != Rhs.m_IsDirectory)
			return Lhs.m_IsDirectory;
		return str_comp_filenames(Lhs.m_aName, Rhs.m_aName) < 0;
	}
};

void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	std::vector<CMapNameItem> vMapList;
	const char *pDirectory = pResult->GetString(0);

	// Don't allow moving to parent directories
	if(str_find_nocase(pDirectory, ".."))
		return;

	char aPath[IO_MAX_PATH_LENGTH] = "maps/";
	str_append(aPath, pDirectory);
	pSelf->Storage()->ListDirectory(IStorage::TYPE_ALL, aPath, MapScan, &vMapList);
	std::sort(vMapList.begin(), vMapList.end(), CMapNameItem::CompareFilenameAscending);

	for(auto &Item : vMapList)
	{
		if(!str_comp(Item.m_aName, "..") && (!str_comp(aPath, "maps/")))
			continue;

		char aDescription[VOTE_DESC_LENGTH];
		str_format(aDescription, sizeof(aDescription), "%s: %s%s", Item.m_IsDirectory ? "Directory" : "Map", Item.m_aName, Item.m_IsDirectory ? "/" : "");

		char aCommand[VOTE_CMD_LENGTH];
		char aOptionEscaped[IO_MAX_PATH_LENGTH * 2];
		char *pDst = aOptionEscaped;
		str_escape(&pDst, Item.m_aName, aOptionEscaped + sizeof(aOptionEscaped));

		char aDirectory[IO_MAX_PATH_LENGTH] = "";
		if(pResult->NumArguments())
			str_copy(aDirectory, pDirectory);

		if(!str_comp(Item.m_aName, ".."))
		{
			dbg_assert(fs_parent_dir(aDirectory) == 0, "Parent folder vote selected but there is no parent folder");
			str_format(aCommand, sizeof(aCommand), "clear_votes; add_map_votes \"%s\"", aDirectory);
		}
		else if(Item.m_IsDirectory)
		{
			str_append(aDirectory, "/");
			str_append(aDirectory, aOptionEscaped);

			str_format(aCommand, sizeof(aCommand), "clear_votes; add_map_votes \"%s\"", aDirectory);
		}
		else
			str_format(aCommand, sizeof(aCommand), "change_map \"%s%s%s\"", pDirectory, pDirectory[0] == '\0' ? "" : "/", aOptionEscaped);

		pSelf->AddVote(aDescription, aCommand);
	}

	pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "added maps to votes");
}

int CGameContext::MapScan(const char *pName, int IsDir, int DirType, void *pUserData)
{
	if((!IsDir && !str_endswith(pName, ".map")) || !str_comp(pName, "."))
		return 0;

	CMapNameItem Item;
	Item.m_IsDirectory = IsDir;
	if(!IsDir)
		str_truncate(Item.m_aName, sizeof(Item.m_aName), pName, str_length(pName) - str_length(".map"));
	else
		str_copy(Item.m_aName, pName);
	static_cast<std::vector<CMapNameItem> *>(pUserData)->push_back(Item);

	return 0;
}

void CGameContext::ConVote(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	if(str_comp_nocase(pResult->GetString(0), "yes") == 0)
		pSelf->ForceVote(true);
	else if(str_comp_nocase(pResult->GetString(0), "no") == 0)
		pSelf->ForceVote(false);
}

void CGameContext::ConVotes(IConsole::IResult *pResult, void *pUserData)
{
	CGameContext *pSelf = (CGameContext *)pUserData;

	int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(0) : 0;
	static const int s_EntriesPerPage = 20;
	const int Start = Page * s_EntriesPerPage;
	const int End = (Page + 1) * s_EntriesPerPage;

	char aBuf[512];
	int Count = 0;
	for(CVoteOptionServer *pOption = pSelf->m_pVoteOptionFirst; pOption; pOption = pOption->m_pNext, Count++)
	{
		if(Count < Start || Count >= End)
		{
			continue;
		}

		str_copy(aBuf, "add_vote \"");
		char *pDst = aBuf + str_length(aBuf);
		str_escape(&pDst, pOption->m_aDescription, aBuf + sizeof(aBuf));
		str_append(aBuf, "\" \"");
		pDst = aBuf + str_length(aBuf);
		str_escape(&pDst, pOption->m_aCommand, aBuf + sizeof(aBuf));
		str_append(aBuf, "\"");

		pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "votes", aBuf);
	}
	str_format(aBuf, sizeof(aBuf), "%d %s, showing entries %d - %d", Count, Count == 1 ? "vote" : "votes", Start, End - 1);
	pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "votes", aBuf);
}

void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
	pfnCallback(pResult, pCallbackUserData);
	if(pResult->NumArguments())
	{
		CGameContext *pSelf = (CGameContext *)pUserData;
		pSelf->SendMotd(-1);
	}
}

void CGameContext::ConchainSettingUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
	pfnCallback(pResult, pCallbackUserData);
	if(pResult->NumArguments())
	{
		CGameContext *pSelf = (CGameContext *)pUserData;
		pSelf->SendSettings(-1);
	}
}

void CGameContext::ConchainPracticeByDefaultUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
	const int OldValue = g_Config.m_SvPracticeByDefault;
	pfnCallback(pResult, pCallbackUserData);

	if(pResult->NumArguments() && g_Config.m_SvTestingCommands)
	{
		CGameContext *pSelf = (CGameContext *)pUserData;

		if(pSelf->m_pController == nullptr)
			return;

		const int Enable = pResult->GetInteger(0);
		if(Enable == OldValue)
			return;

		char aBuf[256];
		str_format(aBuf, sizeof(aBuf), "Practice is %s by default.", Enable ? "enabled" : "disabled");
		if(Enable)
			str_append(aBuf, " Join a team and /unpractice to turn it off for your team.");

		pSelf->SendChat(-1, TEAM_ALL, aBuf);

		for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++)
		{
			if(Team == TEAM_FLOCK || pSelf->m_pController->Teams().TeamSize(Team) == 0)
			{
				pSelf->m_pController->Teams().SetPractice(Team, Enable);
			}
		}
	}
}

void CGameContext::OnConsoleInit()
{
	m_pServer = Kernel()->RequestInterface<IServer>();
	m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
	m_pConfig = m_pConfigManager->Values();
	m_pConsole = Kernel()->RequestInterface<IConsole>();
	m_pEngine = Kernel()->RequestInterface<IEngine>();
	m_pStorage = Kernel()->RequestInterface<IStorage>();

	Console()->Register("tune", "s[tuning] ?f[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneParam, this, "Tune variable to value or show current value");
	Console()->Register("toggle_tune", "s[tuning] f[value 1] f[value 2]", CFGFLAG_SERVER, ConToggleTuneParam, this, "Toggle tune variable");
	Console()->Register("tune_reset", "?s[tuning]", CFGFLAG_SERVER, ConTuneReset, this, "Reset all or one tuning variable to default");
	Console()->Register("tunes", "", CFGFLAG_SERVER, ConTunes, this, "List all tuning variables and their values");
	Console()->Register("tune_zone", "i[zone] s[tuning] f[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value");
	Console()->Register("tune_zone_dump", "i[zone]", CFGFLAG_SERVER, ConTuneDumpZone, this, "Dump zone tuning in zone x");
	Console()->Register("tune_zone_reset", "?i[zone]", CFGFLAG_SERVER, ConTuneResetZone, this, "Reset zone tuning in zone x or in all zones");
	Console()->Register("tune_zone_enter", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgEnter, this, "Which message to display on zone enter; use 0 for normal area");
	Console()->Register("tune_zone_leave", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgLeave, this, "Which message to display on zone leave; use 0 for normal area");
	Console()->Register("mapbug", "s[mapbug]", CFGFLAG_SERVER | CFGFLAG_GAME, ConMapbug, this, "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)");
	Console()->Register("switch_open", "i[switch]", CFGFLAG_SERVER | CFGFLAG_GAME, ConSwitchOpen, this, "Whether a switch is deactivated by default (otherwise activated)");
	Console()->Register("pause_game", "", CFGFLAG_SERVER, ConPause, this, "Pause/unpause game");
	Console()->Register("change_map", "r[map]", CFGFLAG_SERVER | CFGFLAG_STORE, ConChangeMap, this, "Change map");
	Console()->Register("random_map", "?i[stars] ?i[max stars]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRandomMap, this, "Random map");
	Console()->Register("random_unfinished_map", "?i[stars] ?i[max stars]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRandomUnfinishedMap, this, "Random unfinished map");
	Console()->Register("restart", "?i[seconds]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRestart, this, "Restart in x seconds (0 = abort)");
	Console()->Register("server_alert", "r[message]", CFGFLAG_SERVER, ConServerAlert, this, "Send a server alert message to all players");
	Console()->Register("mod_alert", "v[id] r[message]", CFGFLAG_SERVER, ConModAlert, this, "Send a moderator alert message to player");
	Console()->Register("broadcast", "r[message]", CFGFLAG_SERVER, ConBroadcast, this, "Broadcast message");
	Console()->Register("say", "r[message]", CFGFLAG_SERVER, ConSay, this, "Say in chat");
	Console()->Register("set_team", "v[id] i[team-id] ?i[delay in minutes]", CFGFLAG_SERVER, ConSetTeam, this, "Set team for a player (spectators = -1, game = 0)");
	Console()->Register("hot_reload", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConHotReload, this, "Reload the map while preserving the state of tees and teams");
	Console()->Register("reload_censorlist", "", CFGFLAG_SERVER, ConReloadCensorlist, this, "Reload the censorlist");

	Console()->Register("add_vote", "s[name] r[command]", CFGFLAG_SERVER, ConAddVote, this, "Add a voting option");
	Console()->Register("remove_vote", "r[name]", CFGFLAG_SERVER, ConRemoveVote, this, "remove a voting option");
	Console()->Register("force_vote", "s[name] s[command] ?r[reason]", CFGFLAG_SERVER, ConForceVote, this, "Force a voting option");
	Console()->Register("clear_votes", "", CFGFLAG_SERVER, ConClearVotes, this, "Clears the voting options");
	Console()->Register("add_map_votes", "?s[directory]", CFGFLAG_SERVER, ConAddMapVotes, this, "Automatically adds voting options for all maps");
	Console()->Register("vote", "r['yes'|'no']", CFGFLAG_SERVER, ConVote, this, "Force a vote to yes/no");
	Console()->Register("votes", "?i[page]", CFGFLAG_SERVER, ConVotes, this, "Show all votes (page 0 by default, 20 entries per page)");
	Console()->Register("dump_antibot", "", CFGFLAG_SERVER | CFGFLAG_STORE, ConDumpAntibot, this, "Dumps the antibot status");
	Console()->Register("antibot", "r[command]", CFGFLAG_SERVER | CFGFLAG_STORE, ConAntibot, this, "Sends a command to the antibot");

	Console()->Chain("sv_motd", ConchainSpecialMotdupdate, this);

	Console()->Chain("sv_vote_kick", ConchainSettingUpdate, this);
	Console()->Chain("sv_vote_kick_min", ConchainSettingUpdate, this);
	Console()->Chain("sv_vote_spectate", ConchainSettingUpdate, this);
	Console()->Chain("sv_spectator_slots", ConchainSettingUpdate, this);

	RegisterDDRaceCommands();
	RegisterChatCommands();
}

void CGameContext::RegisterDDRaceCommands()
{
	Console()->Register("kill_pl", "v[id] ?r[reason]", CFGFLAG_SERVER, ConKillPlayer, this, "Kills a player and announces the kill");
	Console()->Register("totele", "i[number]", CFGFLAG_SERVER | CMDFLAG_TEST, ConToTeleporter, this, "Teleports you to teleporter i");
	Console()->Register("totelecp", "i[number]", CFGFLAG_SERVER | CMDFLAG_TEST, ConToCheckTeleporter, this, "Teleports you to checkpoint teleporter i");
	Console()->Register("tele", "?v[id] ?v[id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConTeleport, this, "Teleports player i (or you) to player i (or you to where you look at)");
	Console()->Register("addweapon", "i[weapon-id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConAddWeapon, this, "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)");
	Console()->Register("removeweapon", "i[weapon-id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConRemoveWeapon, this, "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)");
	Console()->Register("shotgun", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConShotgun, this, "Gives a shotgun to you");
	Console()->Register("grenade", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConGrenade, this, "Gives a grenade launcher to you");
	Console()->Register("laser", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConLaser, this, "Gives a laser to you");
	Console()->Register("rifle", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConLaser, this, "Gives a laser to you");
	Console()->Register("jetpack", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConJetpack, this, "Gives jetpack to you");
	Console()->Register("setjumps", "i[jumps]", CFGFLAG_SERVER | CMDFLAG_TEST, ConSetJumps, this, "Gives you as many jumps as you specify");
	Console()->Register("weapons", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConWeapons, this, "Gives all weapons to you");
	Console()->Register("unshotgun", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnShotgun, this, "Removes the shotgun from you");
	Console()->Register("ungrenade", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnGrenade, this, "Removes the grenade launcher from you");
	Console()->Register("unlaser", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnLaser, this, "Removes the laser from you");
	Console()->Register("unrifle", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnLaser, this, "Removes the laser from you");
	Console()->Register("unjetpack", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnJetpack, this, "Removes the jetpack from you");
	Console()->Register("unweapons", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnWeapons, this, "Removes all weapons from you");
	Console()->Register("ninja", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConNinja, this, "Makes you a ninja");
	Console()->Register("unninja", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnNinja, this, "Removes ninja from you");
	Console()->Register("super", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConSuper, this, "Makes you super");
	Console()->Register("unsuper", "", CFGFLAG_SERVER, ConUnSuper, this, "Removes super from you");
	Console()->Register("invincible", "?i['0'|'1']", CFGFLAG_SERVER | CMDFLAG_TEST, ConToggleInvincible, this, "Toggles invincible mode");
	Console()->Register("infinite_jump", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConEndlessJump, this, "Gives you infinite jump");
	Console()->Register("uninfinite_jump", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnEndlessJump, this, "Removes infinite jump from you");
	Console()->Register("endless_hook", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConEndlessHook, this, "Gives you endless hook");
	Console()->Register("unendless_hook", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnEndlessHook, this, "Removes endless hook from you");
	Console()->Register("setswitch", "i[switch] ?i['0'|'1'] ?i[seconds]", CFGFLAG_SERVER | CMDFLAG_TEST, ConSetSwitch, this, "Toggle or set the switch on or off for the specified time (or indefinitely by default)");
	Console()->Register("solo", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConSolo, this, "Puts you into solo part");
	Console()->Register("unsolo", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnSolo, this, "Puts you out of solo part");
	Console()->Register("freeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConFreeze, this, "Puts you into freeze");
	Console()->Register("unfreeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnfreeze, this, "Puts you out of freeze");
	Console()->Register("deep", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConDeep, this, "Puts you into deep freeze");
	Console()->Register("undeep", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnDeep, this, "Puts you out of deep freeze");
	Console()->Register("livefreeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConLiveFreeze, this, "Makes you live frozen");
	Console()->Register("unlivefreeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnLiveFreeze, this, "Puts you out of live freeze");
	Console()->Register("left", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoLeft, this, "Makes you move 1 tile left");
	Console()->Register("right", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoRight, this, "Makes you move 1 tile right");
	Console()->Register("up", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoUp, this, "Makes you move 1 tile up");
	Console()->Register("down", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoDown, this, "Makes you move 1 tile down");

	Console()->Register("move", "i[x] i[y]", CFGFLAG_SERVER | CMDFLAG_TEST, ConMove, this, "Moves to the tile with x/y-number ii");
	Console()->Register("move_raw", "i[x] i[y]", CFGFLAG_SERVER | CMDFLAG_TEST, ConMoveRaw, this, "Moves to the point with x/y-coordinates ii");
	Console()->Register("force_pause", "v[id] i[seconds]", CFGFLAG_SERVER, ConForcePause, this, "Force i to pause for i seconds");
	Console()->Register("force_unpause", "v[id]", CFGFLAG_SERVER, ConForcePause, this, "Set force-pause timer of i to 0.");

	Console()->Register("set_team_ddr", "v[id] i[team]", CFGFLAG_SERVER, ConSetDDRTeam, this, "Set ddrace team for a player");
	Console()->Register("uninvite", "v[id] i[team]", CFGFLAG_SERVER, ConUninvite, this, "Uninvite player from team");

	Console()->Register("mute", "", CFGFLAG_SERVER, ConMute, this, "Deprecated. Use either 'muteid <client_id> <seconds> <reason>' or 'muteip <ip> <seconds> <reason>'");
	Console()->Register("muteid", "v[id] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConMuteId, this, "Mute player with client ID");
	Console()->Register("muteip", "s[ip] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConMuteIp, this, "Mute player with IP address");
	Console()->Register("unmute", "i[index]", CFGFLAG_SERVER, ConUnmute, this, "Unmute player with list index");
	Console()->Register("unmuteid", "v[id]", CFGFLAG_SERVER, ConUnmuteId, this, "Unmute player with client ID");
	Console()->Register("unmuteip", "s[ip]", CFGFLAG_SERVER, ConUnmuteIp, this, "Unmute player with IP address");
	Console()->Register("mutes", "?i[page]", CFGFLAG_SERVER, ConMutes, this, "Show list of mutes (page 1 by default, 20 entries per page)");

	Console()->Register("vote_mute", "", CFGFLAG_SERVER, ConVoteMute, this, "Deprecated. Use either 'vote_muteid <client_id> <seconds> <reason>' or 'vote_muteip <ip> <seconds> <reason>'");
	Console()->Register("vote_muteid", "v[id] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConVoteMuteId, this, "Remove right to vote from player with client ID");
	Console()->Register("vote_muteip", "s[ip] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConVoteMuteIp, this, "Remove right to vote from player with IP address");
	Console()->Register("vote_unmute", "i[index]", CFGFLAG_SERVER, ConVoteUnmute, this, "Give back right to vote to player with list index");
	Console()->Register("vote_unmuteid", "v[id]", CFGFLAG_SERVER, ConVoteUnmuteId, this, "Give back right to vote to player with client ID");
	Console()->Register("vote_unmuteip", "s[ip]", CFGFLAG_SERVER, ConVoteUnmuteIp, this, "Give back right to vote to player with IP address");
	Console()->Register("vote_mutes", "?i[page]", CFGFLAG_SERVER, ConVoteMutes, this, "Show list of vote mutes (page 1 by default, 20 entries per page)");

	Console()->Register("moderate", "", CFGFLAG_SERVER, ConModerate, this, "Enables/disables active moderator mode for the player");
	Console()->Register("vote_no", "", CFGFLAG_SERVER, ConVoteNo, this, "Same as \"vote no\"");
	Console()->Register("save_dry", "", CFGFLAG_SERVER, ConDrySave, this, "Dump the current savestring");
	Console()->Register("dump_log", "?i[seconds]", CFGFLAG_SERVER, ConDumpLog, this, "Show logs of the last i seconds");

	Console()->Chain("sv_practice_by_default", ConchainPracticeByDefaultUpdate, this);
}

void CGameContext::RegisterChatCommands()
{
	Console()->Register("rules", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConRules, this, "Shows the server rules");
	Console()->Register("emote", "?s[emote name] i[duration in seconds]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConEyeEmote, this, "Sets your tee's eye emote");
	Console()->Register("eyeemote", "?s['on'|'off'|'toggle']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSetEyeEmote, this, "Toggles use of standard eye-emotes on/off, eyeemote s, where s = on for on, off for off, toggle for toggle and nothing to show current status");
	Console()->Register("settings", "?s[configname]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSettings, this, "Shows gameplay information for this server");
	Console()->Register("help", "?r[command]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConHelp, this, "Shows help to command r, general help if left blank");
	Console()->Register("info", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConInfo, this, "Shows info about this server");
	Console()->Register("list", "?s[filter]", CFGFLAG_CHAT, ConList, this, "List connected players with optional case-insensitive substring matching filter");
	Console()->Register("w", "s[player name] r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConWhisper, this, "Whisper something to someone (private message)");
	Console()->Register("whisper", "s[player name] r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConWhisper, this, "Whisper something to someone (private message)");
	Console()->Register("c", "r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConConverse, this, "Converse with the last person you whispered to (private message)");
	Console()->Register("converse", "r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConConverse, this, "Converse with the last person you whispered to (private message)");
	Console()->Register("pause", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTogglePause, this, "Toggles pause");
	Console()->Register("spec", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConToggleSpec, this, "Toggles spec (if not available behaves as /pause)");
	Console()->Register("pausevoted", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTogglePauseVoted, this, "Toggles pause on the currently voted player");
	Console()->Register("specvoted", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConToggleSpecVoted, this, "Toggles spec on the currently voted player");
	Console()->Register("dnd", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConDND, this, "Toggle Do Not Disturb (no chat and server messages)");
	Console()->Register("whispers", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConWhispers, this, "Toggle receiving whispers");
	Console()->Register("mapinfo", "?r[map]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConMapInfo, this, "Show info about the map with name r gives (current map by default)");
	Console()->Register("timeout", "?s[code]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimeout, this, "Set timeout protection code s");
	Console()->Register("practice", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPractice, this, "Enable cheats for your current team's run, but you can't earn a rank");
	Console()->Register("unpractice", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConUnPractice, this, "Kills team and disables practice mode");
	Console()->Register("practicecmdlist", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPracticeCmdList, this, "List all commands that are available in practice mode");
	Console()->Register("swap", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSwap, this, "Request to swap your tee with another team member");
	Console()->Register("cancelswap", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConCancelSwap, this, "Cancel your swap request");
	Console()->Register("save", "?r[code]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSave, this, "Save team with code r.");
	Console()->Register("load", "?r[code]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConLoad, this, "Load with code r. /load to check your existing saves");
	Console()->Register("map", "?r[map]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConMap, this, "Vote a map by name");

	Console()->Register("rankteam", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamRank, this, "Shows the team rank of player with name r (your team rank by default)");
	Console()->Register("teamrank", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamRank, this, "Shows the team rank of player with name r (your team rank by default)");

	Console()->Register("rank", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConRank, this, "Shows the rank of player with name r (your rank by default)");
	Console()->Register("top5team", "?s[player name] ?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamTop5, this, "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)");
	Console()->Register("teamtop5", "?s[player name] ?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamTop5, this, "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)");
	Console()->Register("top", "?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTop, this, "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)");
	Console()->Register("top5", "?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTop, this, "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)");
	Console()->Register("times", "?s[player name] ?i[number of times to skip]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimes, this, "/times ?s?i shows last 5 times of the server or of a player beginning with name s starting with time i (i = 1 by default, -1 for first)");
	Console()->Register("points", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPoints, this, "Shows the global points of a player beginning with name r (your rank by default)");
	Console()->Register("top5points", "?i[number]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTopPoints, this, "Shows five points of the global point ladder beginning with rank i (1 by default)");
	Console()->Register("timecp", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimeCP, this, "Set your checkpoints based on another player");

	Console()->Register("team", "?i[id]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeam, this, "Lets you join team i (shows your team if left blank)");
	Console()->Register("lock", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConLock, this, "Toggle team lock so no one else can join and so the team restarts when a player dies. /lock 0 to unlock, /lock 1 to lock");
	Console()->Register("unlock", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConUnlock, this, "Unlock a team");
	Console()->Register("invite", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConInvite, this, "Invite a person to a locked team");
	Console()->Register("join", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConJoin, this, "Join the team of the specified player");
	Console()->Register("team0mode", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeam0Mode, this, "Toggle team between team 0 and team mode. This mode will make your team behave like team 0.");

	Console()->Register("showothers", "?i['0'|'1'|'2']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConShowOthers, this, "Whether to show players from other teams or not (off by default), optional i = 0 for off, i = 1 for on, i = 2 for own team only");
	Console()->Register("showall", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConShowAll, this, "Whether to show players at any distance (off by default), optional i = 0 for off else for on");
	Console()->Register("specteam", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSpecTeam, this, "Whether to show players from other teams when spectating (on by default), optional i = 0 for off else for on");
	Console()->Register("ninjajetpack", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConNinjaJetpack, this, "Whether to use ninja jetpack or not. Makes jetpack look more awesome");
	Console()->Register("saytime", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConSayTime, this, "Privately messages someone's current time in this current running race (your time by default)");
	Console()->Register("saytimeall", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConSayTimeAll, this, "Publicly messages everyone your current time in this current running race");
	Console()->Register("time", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTime, this, "Privately shows you your current time in this current running race in the broadcast message");
	Console()->Register("timer", "?s['gametimer'|'broadcast'|'both'|'none'|'cycle']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSetTimerType, this, "Personal Setting of showing time in either broadcast or game/round timer, timer s, where s = broadcast for broadcast, gametimer for game/round timer, cycle for cycle, both for both, none for no timer and nothing to show current status");
	Console()->Register("r", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescue, this, "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)");
	Console()->Register("rescue", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescue, this, "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)");
	Console()->Register("back", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConBack, this, "Teleport yourself to the last auto rescue position before you died (use sv_rescue 1 to enable this feature)");
	Console()->Register("rescuemode", "?r['auto'|'manual']", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescueMode, this, "Sets one of the two rescue modes (auto or manual). Prints current mode if no arguments provided");
	Console()->Register("tp", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleTo, this, "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name");
	Console()->Register("teleport", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleTo, this, "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name");
	Console()->Register("tpxy", "s[x] s[y]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleXY, this, "Teleport yourself to the specified coordinates. A tilde (~) can be used to denote your current position, e.g. '/tpxy ~1 ~' to teleport one tile to the right");
	Console()->Register("lasttp", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConLastTele, this, "Teleport yourself to the last location you teleported to");
	Console()->Register("tc", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleCursor, this, "Teleport yourself to player or to where you are spectating/or looking if no player name is given");
	Console()->Register("telecursor", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleCursor, this, "Teleport yourself to player or to where you are spectating/or looking if no player name is given");
	Console()->Register("totele", "i[number]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeToTeleporter, this, "Teleports you to teleporter i");
	Console()->Register("totelecp", "i[number]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeToCheckTeleporter, this, "Teleports you to checkpoint teleporter i");
	Console()->Register("unsolo", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnSolo, this, "Puts you out of solo part");
	Console()->Register("solo", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeSolo, this, "Puts you into solo part");
	Console()->Register("undeep", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnDeep, this, "Puts you out of deep freeze");
	Console()->Register("deep", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeDeep, this, "Puts you into deep freeze");
	Console()->Register("unlivefreeze", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnLiveFreeze, this, "Puts you out of live freeze");
	Console()->Register("livefreeze", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeLiveFreeze, this, "Makes you live frozen");
	Console()->Register("addweapon", "i[weapon-id]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeAddWeapon, this, "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)");
	Console()->Register("removeweapon", "i[weapon-id]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeRemoveWeapon, this, "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)");
	Console()->Register("shotgun", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeShotgun, this, "Gives a shotgun to you");
	Console()->Register("grenade", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeGrenade, this, "Gives a grenade launcher to you");
	Console()->Register("laser", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeLaser, this, "Gives a laser to you");
	Console()->Register("rifle", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeLaser, this, "Gives a laser to you");
	Console()->Register("jetpack", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeJetpack, this, "Gives jetpack to you");
	Console()->Register("setjumps", "i[jumps]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeSetJumps, this, "Gives you as many jumps as you specify");
	Console()->Register("weapons", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeWeapons, this, "Gives all weapons to you");
	Console()->Register("unshotgun", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnShotgun, this, "Removes the shotgun from you");
	Console()->Register("ungrenade", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnGrenade, this, "Removes the grenade launcher from you");
	Console()->Register("unlaser", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnLaser, this, "Removes the laser from you");
	Console()->Register("unrifle", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnLaser, this, "Removes the laser from you");
	Console()->Register("unjetpack", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnJetpack, this, "Removes the jetpack from you");
	Console()->Register("unweapons", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnWeapons, this, "Removes all weapons from you");
	Console()->Register("ninja", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeNinja, this, "Makes you a ninja");
	Console()->Register("unninja", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnNinja, this, "Removes ninja from you");
	Console()->Register("infjump", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeEndlessJump, this, "Gives you infinite jump");
	Console()->Register("uninfjump", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnEndlessJump, this, "Removes infinite jump from you");
	Console()->Register("endless", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeEndlessHook, this, "Gives you endless hook");
	Console()->Register("unendless", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnEndlessHook, this, "Removes endless hook from you");
	Console()->Register("setswitch", "i[switch] ?i['0'|'1'] ?i[seconds]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeSetSwitch, this, "Toggle or set the switch on or off for the specified time (or indefinitely by default)");
	Console()->Register("invincible", "?i['0'|'1']", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeToggleInvincible, this, "Toggles invincible mode");
	Console()->Register("collision", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeToggleCollision, this, "Toggles collision");
	Console()->Register("hookcollision", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeToggleHookCollision, this, "Toggles hook collision");
	Console()->Register("hitothers", "?s['all'|'hammer'|'shotgun'|'grenade'|'laser']", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeToggleHitOthers, this, "Toggles hit others");

	Console()->Register("kill", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConProtectedKill, this, "Kill yourself when kill-protected during a long game (use f1, kill for regular kill)");
}

void CGameContext::OnInit(const void *pPersistentData)
{
	const CPersistentData *pPersistent = (const CPersistentData *)pPersistentData;

	m_pServer = Kernel()->RequestInterface<IServer>();
	m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
	m_pConfig = m_pConfigManager->Values();
	m_pConsole = Kernel()->RequestInterface<IConsole>();
	m_pEngine = Kernel()->RequestInterface<IEngine>();
	m_pStorage = Kernel()->RequestInterface<IStorage>();
	m_pAntibot = Kernel()->RequestInterface<IAntibot>();
	m_World.SetGameServer(this);
	m_Events.SetGameServer(this);
	m_PlayerMapping.Init(this);

	m_GameUuid = RandomUuid();
	Console()->SetGetVictimsCommandCallback(ClientsForVictim, this);
	Console()->SetTeeHistorianCommandCallback(CommandCallback, this);

	uint64_t aSeed[2];
	secure_random_fill(aSeed, sizeof(aSeed));
	m_Prng.Seed(aSeed);
	m_World.m_Core.m_pPrng = &m_Prng;

	DeleteTempfile();

	for(int i = 0; i < NUM_NETOBJTYPES; i++)
	{
		Server()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i));
	}

	// HACK: only set static size for items, which were available in the first 0.7 release
	// so new items don't break the snapshot delta
	static const int OLD_NUM_NETOBJTYPES = 23;
	for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++)
	{
		Server()->SnapSetStaticsize7(i, m_NetObjHandler7.GetObjSize(i));
	}

	m_Layers.Init(Map(), false, false);
	m_Collision.Init(&m_Layers);
	m_World.Init(&m_Collision, m_aTuningList);
	m_MapBugs = CMapBugs::Create(Map()->BaseName(), Map()->Size(), Map()->Sha256());

	// Reset Tunezones
	for(int i = 0; i < TuneZone::NUM; i++)
	{
		TuningList()[i] = CTuningParams::DEFAULT;
		TuningList()[i].Set("gun_curvature", 0);
		TuningList()[i].Set("gun_speed", 1400);
		TuningList()[i].Set("shotgun_curvature", 0);
		TuningList()[i].Set("shotgun_speed", 500);
		TuningList()[i].Set("shotgun_speeddiff", 0);
	}

	for(int i = 0; i < TuneZone::NUM; i++)
	{
		// Send no text by default when changing tune zones.
		m_aaZoneEnterMsg[i][0] = 0;
		m_aaZoneLeaveMsg[i][0] = 0;
	}
	// Reset Tuning
	if(g_Config.m_SvTuneReset)
	{
		ResetTuning();
	}
	else
	{
		GlobalTuning()->Set("gun_speed", 1400);
		GlobalTuning()->Set("gun_curvature", 0);
		GlobalTuning()->Set("shotgun_speed", 500);
		GlobalTuning()->Set("shotgun_speeddiff", 0);
		GlobalTuning()->Set("shotgun_curvature", 0);
	}

	if(g_Config.m_SvDDRaceTuneReset)
	{
		g_Config.m_SvHit = 1;
		g_Config.m_SvEndlessDrag = 0;
		g_Config.m_SvOldLaser = 0;
		g_Config.m_SvOldTeleportHook = 0;
		g_Config.m_SvOldTeleportWeapons = 0;
		g_Config.m_SvTeleportHoldHook = 0;
		g_Config.m_SvTeam = SV_TEAM_ALLOWED;
		g_Config.m_SvShowOthersDefault = SHOW_OTHERS_OFF;

		for(auto &Switcher : Switchers())
			Switcher.m_Initial = true;
	}

	m_pConfigManager->SetGameSettingsReadOnly(false);

	Console()->ExecuteFile(g_Config.m_SvResetFile, IConsole::CLIENT_ID_UNSPECIFIED);

	LoadMapSettings();

	m_pConfigManager->SetGameSettingsReadOnly(true);

	m_MapBugs.Dump();

	if(g_Config.m_SvSoloServer)
	{
		g_Config.m_SvTeam = SV_TEAM_FORCED_SOLO;
		g_Config.m_SvShowOthersDefault = SHOW_OTHERS_ON;

		GlobalTuning()->Set("player_collision", 0);
		GlobalTuning()->Set("player_hooking", 0);

		for(int i = 0; i < TuneZone::NUM; i++)
		{
			TuningList()[i].Set("player_collision", 0);
			TuningList()[i].Set("player_hooking", 0);
		}
	}

	if(!str_comp(Config()->m_SvGametype, "mod"))
		m_pController = new CGameControllerMod(this);
	else
		m_pController = new CGameControllerDDNet(this);

	for(const char *pReservedGameType : {"DM", "TDM", "CTF", "LMS", "LTS"})
	{
		dbg_assert(str_comp(m_pController->m_pGameType, pReservedGameType) != 0, "Using reserved gametype '%s' is not allowed", m_pController->m_pGameType);
	}

	ReadCensorList();

	m_TeeHistorianActive = g_Config.m_SvTeeHistorian;
	if(m_TeeHistorianActive)
	{
		char aGameUuid[UUID_MAXSTRSIZE];
		FormatUuid(m_GameUuid, aGameUuid, sizeof(aGameUuid));

		char aFilename[IO_MAX_PATH_LENGTH];
		str_format(aFilename, sizeof(aFilename), "teehistorian/%s.teehistorian", aGameUuid);

		IOHANDLE THFile = Storage()->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
		if(!THFile)
		{
			dbg_msg("teehistorian", "failed to open '%s'", aFilename);
			Server()->SetErrorShutdown("teehistorian open error");
			return;
		}
		else
		{
			dbg_msg("teehistorian", "recording to '%s'", aFilename);
		}
		m_pTeeHistorianFile = aio_new(THFile);

		char aVersion[128];
		if(GIT_SHORTREV_HASH)
		{
			str_format(aVersion, sizeof(aVersion), "%s (%s)", GAME_VERSION, GIT_SHORTREV_HASH);
		}
		else
		{
			str_copy(aVersion, GAME_VERSION);
		}
		CTeeHistorian::CGameInfo GameInfo;
		GameInfo.m_GameUuid = m_GameUuid;
		GameInfo.m_pServerVersion = aVersion;
		GameInfo.m_StartTime = time(nullptr);
		GameInfo.m_pPrngDescription = m_Prng.Description();

		GameInfo.m_pServerName = g_Config.m_SvName;
		GameInfo.m_ServerPort = Server()->Port();
		GameInfo.m_pGameType = m_pController->m_pGameType;

		GameInfo.m_pConfig = &g_Config;
		GameInfo.m_pTuning = GlobalTuning();
		GameInfo.m_pUuids = &g_UuidManager;

		GameInfo.m_pMapName = Map()->BaseName();
		GameInfo.m_MapSize = Map()->Size();
		GameInfo.m_MapSha256 = Map()->Sha256();
		GameInfo.m_MapCrc = Map()->Crc();

		if(pPersistent)
		{
			GameInfo.m_HavePrevGameUuid = true;
			GameInfo.m_PrevGameUuid = pPersistent->m_PrevGameUuid;
		}
		else
		{
			GameInfo.m_HavePrevGameUuid = false;
			mem_zero(&GameInfo.m_PrevGameUuid, sizeof(GameInfo.m_PrevGameUuid));
		}

		m_TeeHistorian.Reset(&GameInfo, TeeHistorianWrite, this);
	}

	Server()->DemoRecorder_HandleAutoStart();

	if(!m_pScore)
	{
		m_pScore = new CScore(this, ((CServer *)Server())->DbPool());
	}

	// load map info from database
	Score()->LoadMapInfo();

	// create all entities from the game layer
	CreateAllEntities(true);

	m_pAntibot->RoundStart(this);
}

void CGameContext::CreateAllEntities(bool Initial)
{
	const CTile *pTiles = m_Collision.GameLayer();
	const CTile *pFront = m_Collision.FrontLayer();
	const CSwitchTile *pSwitch = m_Collision.SwitchLayer();

	for(int y = 0; y < m_Collision.GetHeight(); y++)
	{
		for(int x = 0; x < m_Collision.GetWidth(); x++)
		{
			const int Index = y * m_Collision.GetWidth() + x;

			// Game layer
			{
				const int GameIndex = pTiles[Index].m_Index;
				if(GameIndex == TILE_OLDLASER)
				{
					g_Config.m_SvOldLaser = 1;
					dbg_msg("game_layer", "found old laser tile");
				}
				else if(GameIndex == TILE_NPC)
				{
					GlobalTuning()->Set("player_collision", 0);
					dbg_msg("game_layer", "found no collision tile");
				}
				else if(GameIndex == TILE_EHOOK)
				{
					g_Config.m_SvEndlessDrag = 1;
					dbg_msg("game_layer", "found unlimited hook time tile");
				}
				else if(GameIndex == TILE_NOHIT)
				{
					g_Config.m_SvHit = 0;
					dbg_msg("game_layer", "found no weapons hitting others tile");
				}
				else if(GameIndex == TILE_NPH)
				{
					GlobalTuning()->Set("player_hooking", 0);
					dbg_msg("game_layer", "found no player hooking tile");
				}
				else if(GameIndex >= ENTITY_OFFSET)
				{
					m_pController->OnEntity(GameIndex - ENTITY_OFFSET, x, y, LAYER_GAME, pTiles[Index].m_Flags, Initial);
				}
			}

			if(pFront)
			{
				const int FrontIndex = pFront[Index].m_Index;
				if(FrontIndex == TILE_OLDLASER)
				{
					g_Config.m_SvOldLaser = 1;
					dbg_msg("front_layer", "found old laser tile");
				}
				else if(FrontIndex == TILE_NPC)
				{
					GlobalTuning()->Set("player_collision", 0);
					dbg_msg("front_layer", "found no collision tile");
				}
				else if(FrontIndex == TILE_EHOOK)
				{
					g_Config.m_SvEndlessDrag = 1;
					dbg_msg("front_layer", "found unlimited hook time tile");
				}
				else if(FrontIndex == TILE_NOHIT)
				{
					g_Config.m_SvHit = 0;
					dbg_msg("front_layer", "found no weapons hitting others tile");
				}
				else if(FrontIndex == TILE_NPH)
				{
					GlobalTuning()->Set("player_hooking", 0);
					dbg_msg("front_layer", "found no player hooking tile");
				}
				else if(FrontIndex >= ENTITY_OFFSET)
				{
					m_pController->OnEntity(FrontIndex - ENTITY_OFFSET, x, y, LAYER_FRONT, pFront[Index].m_Flags, Initial);
				}
			}

			if(pSwitch)
			{
				const int SwitchType = pSwitch[Index].m_Type;
				// TODO: Add off by default door here
				// if(SwitchType == TILE_DOOR_OFF)
				if(SwitchType >= ENTITY_OFFSET)
				{
					m_pController->OnEntity(SwitchType - ENTITY_OFFSET, x, y, LAYER_SWITCH, pSwitch[Index].m_Flags, Initial, pSwitch[Index].m_Number);
				}
			}
		}
	}
}

CPlayer *CGameContext::CreatePlayer(int ClientId, int StartTeam, bool Afk, int LastWhisperTo)
{
	if(m_apPlayers[ClientId])
		delete m_apPlayers[ClientId];
	m_apPlayers[ClientId] = new(ClientId) CPlayer(this, m_NextUniqueClientId, ClientId, StartTeam);
	m_apPlayers[ClientId]->SetInitialAfk(Afk);
	m_apPlayers[ClientId]->m_LastWhisperTo = LastWhisperTo;
	m_NextUniqueClientId += 1;
	return m_apPlayers[ClientId];
}

void CGameContext::DeleteTempfile()
{
	if(m_aDeleteTempfile[0] != 0)
	{
		Storage()->RemoveFile(m_aDeleteTempfile, IStorage::TYPE_SAVE);
		m_aDeleteTempfile[0] = 0;
	}
}

bool CGameContext::OnMapChange(char *pNewMapName, int MapNameSize)
{
	char aConfig[IO_MAX_PATH_LENGTH];
	str_format(aConfig, sizeof(aConfig), "maps/%s.cfg", g_Config.m_SvMap);

	CLineReader LineReader;
	if(!LineReader.OpenFile(Storage()->OpenFile(aConfig, IOFLAG_READ, IStorage::TYPE_ALL)))
	{
		// No map-specific config, just return.
		return true;
	}

	CDataFileReader Reader;
	if(!Reader.Open(g_Config.m_SvMap, Storage(), pNewMapName, IStorage::TYPE_ALL))
	{
		log_error("mapchange", "Failed to import settings from '%s': failed to open map '%s' for reading", aConfig, pNewMapName);
		return false;
	}

	std::vector<const char *> vpLines;
	int TotalLength = 0;
	while(const char *pLine = LineReader.Get())
	{
		vpLines.push_back(pLine);
		TotalLength += str_length(pLine) + 1;
	}

	char *pSettings = (char *)malloc(std::max(1, TotalLength));
	int Offset = 0;
	for(const char *pLine : vpLines)
	{
		int Length = str_length(pLine) + 1;
		mem_copy(pSettings + Offset, pLine, Length);
		Offset += Length;
	}

	CDataFileWriter Writer;

	int SettingsIndex = Reader.NumData();
	bool FoundInfo = false;
	for(int i = 0; i < Reader.NumItems(); i++)
	{
		int TypeId;
		int ItemId;
		void *pData = Reader.GetItem(i, &TypeId, &ItemId);
		int Size = Reader.GetItemSize(i);
		CMapItemInfoSettings MapInfo;
		if(TypeId == MAPITEMTYPE_INFO && ItemId == 0)
		{
			FoundInfo = true;
			if(Size >= (int)sizeof(CMapItemInfoSettings))
			{
				CMapItemInfoSettings *pInfo = (CMapItemInfoSettings *)pData;
				if(pInfo->m_Settings > -1)
				{
					SettingsIndex = pInfo->m_Settings;
					char *pMapSettings = (char *)Reader.GetData(SettingsIndex);
					int DataSize = Reader.GetDataSize(SettingsIndex);
					if(DataSize == TotalLength && mem_comp(pSettings, pMapSettings, DataSize) == 0)
					{
						// Configs coincide, no need to update map.
						free(pSettings);
						return true;
					}
					Reader.UnloadData(pInfo->m_Settings);
				}
				else
				{
					MapInfo = *pInfo;
					MapInfo.m_Settings = SettingsIndex;
					pData = &MapInfo;
					Size = sizeof(MapInfo);
				}
			}
			else
			{
				*(CMapItemInfo *)&MapInfo = *(CMapItemInfo *)pData;
				MapInfo.m_Settings = SettingsIndex;
				pData = &MapInfo;
				Size = sizeof(MapInfo);
			}
		}
		Writer.AddItem(TypeId, ItemId, Size, pData);
	}

	if(!FoundInfo)
	{
		CMapItemInfoSettings Info;
		Info.m_Version = 1;
		Info.m_Author = -1;
		Info.m_MapVersion = -1;
		Info.m_Credits = -1;
		Info.m_License = -1;
		Info.m_Settings = SettingsIndex;
		Writer.AddItem(MAPITEMTYPE_INFO, 0, sizeof(Info), &Info);
	}

	for(int i = 0; i < Reader.NumData() || i == SettingsIndex; i++)
	{
		if(i == SettingsIndex)
		{
			Writer.AddData(TotalLength, pSettings);
			continue;
		}
		const void *pData = Reader.GetData(i);
		int Size = Reader.GetDataSize(i);
		Writer.AddData(Size, pData);
		Reader.UnloadData(i);
	}

	free(pSettings);
	Reader.Close();

	char aTemp[IO_MAX_PATH_LENGTH];
	if(!Writer.Open(Storage(), IStorage::FormatTmpPath(aTemp, sizeof(aTemp), pNewMapName)))
	{
		log_error("mapchange", "Failed to import settings from '%s': failed to open map '%s' for writing", aConfig, aTemp);
		return false;
	}
	Writer.Finish();
	log_info("mapchange", "Imported settings from '%s' into '%s'", aConfig, aTemp);

	str_copy(pNewMapName, aTemp, MapNameSize);
	str_copy(m_aDeleteTempfile, aTemp);
	return true;
}

void CGameContext::OnShutdown(void *pPersistentData)
{
	CPersistentData *pPersistent = (CPersistentData *)pPersistentData;

	if(pPersistent)
	{
		new(pPersistent) CPersistentData();
		pPersistent->m_PrevGameUuid = m_GameUuid;
	}

	Antibot()->RoundEnd();

	if(m_TeeHistorianActive)
	{
		m_TeeHistorian.Finish();
		aio_close(m_pTeeHistorianFile);
		aio_wait(m_pTeeHistorianFile);
		int Error = aio_error(m_pTeeHistorianFile);
		if(Error)
		{
			dbg_msg("teehistorian", "error closing file, err=%d", Error);
			Server()->SetErrorShutdown("teehistorian close error");
		}
		aio_free(m_pTeeHistorianFile);
	}

	// Stop any demos being recorded.
	Server()->StopDemos();

	DeleteTempfile();
	ConfigManager()->ResetGameSettings();
	Collision()->Unload();
	Layers()->Unload();
	delete m_pController;
	m_pController = nullptr;
	Clear();
}

void CGameContext::LoadMapSettings()
{
	IMap *pMap = Map();
	int Start, Num;
	pMap->GetType(MAPITEMTYPE_INFO, &Start, &Num);
	for(int i = Start; i < Start + Num; i++)
	{
		int ItemId;
		CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemId);
		int ItemSize = pMap->GetItemSize(i);
		if(!pItem || ItemId != 0)
			continue;

		if(ItemSize < (int)sizeof(CMapItemInfoSettings))
			break;
		if(!(pItem->m_Settings > -1))
			break;

		int Size = pMap->GetDataSize(pItem->m_Settings);
		char *pSettings = (char *)pMap->GetData(pItem->m_Settings);
		char *pNext = pSettings;
		while(pNext < pSettings + Size)
		{
			int StrSize = str_length(pNext) + 1;
			Console()->ExecuteLine(pNext, IConsole::CLIENT_ID_GAME);
			pNext += StrSize;
		}
		pMap->UnloadData(pItem->m_Settings);
		break;
	}

	char aBuf[IO_MAX_PATH_LENGTH];
	str_format(aBuf, sizeof(aBuf), "maps/%s.map.cfg", g_Config.m_SvMap);
	Console()->ExecuteFile(aBuf, IConsole::CLIENT_ID_NO_GAME);
}

void CGameContext::OnSnap(int ClientId, bool GlobalSnap, bool RecordingDemo)
{
	// sixup should only snap during global snap
	dbg_assert(!Server()->IsSixup(ClientId) || GlobalSnap, "sixup should only snap during global snap");

	// add tuning to demo
	if(RecordingDemo && mem_comp(&CTuningParams::DEFAULT, &m_aTuningList[0], sizeof(CTuningParams)) != 0)
	{
		CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS);
		int *pParams = (int *)&m_aTuningList[0];
		for(int i = 0; i < CTuningParams::Num(); i++)
			Msg.AddInt(pParams[i]);
		Server()->SendMsg(&Msg, MSGFLAG_NOSEND, ClientId);
	}

	m_pController->Snap(ClientId);

	for(auto &pPlayer : m_apPlayers)
	{
		if(pPlayer)
			pPlayer->Snap(ClientId);
	}

	if(ClientId > -1)
		m_apPlayers[ClientId]->FakeSnap();

	m_World.Snap(ClientId);

	// events are only sent on global snapshots
	if(GlobalSnap)
	{
		m_Events.Snap(ClientId);
	}
}

void CGameContext::OnPostGlobalSnap()
{
	for(auto &pPlayer : m_apPlayers)
	{
		if(pPlayer && pPlayer->GetCharacter())
			pPlayer->GetCharacter()->PostGlobalSnap();
	}
	m_Events.Clear();
}

bool CGameContext::IsClientReady(int ClientId) const
{
	return m_apPlayers[ClientId] && m_apPlayers[ClientId]->m_IsReady;
}

bool CGameContext::IsClientPlayer(int ClientId) const
{
	return m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetTeam() != TEAM_SPECTATORS;
}

bool CGameContext::IsClientHighBandwidth(int ClientId) const
{
	// force high bandwidth is not supported for sixup
	return m_apPlayers[ClientId] && !Server()->IsSixup(ClientId) && Server()->IsRconAuthed(ClientId) &&
	       (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS || m_apPlayers[ClientId]->IsPaused());
}

CUuid CGameContext::GameUuid() const { return m_GameUuid; }
const char *CGameContext::GameType() const
{
	dbg_assert(m_pController, "no controller");
	dbg_assert(m_pController->m_pGameType, "no gametype");
	return m_pController->m_pGameType;
}
const char *CGameContext::Version() const { return m_aVersionString; }
const char *CGameContext::NetVersion() const { return GAME_NETVERSION; }

IGameServer *CreateGameServer() { return new CGameContext; }

void CGameContext::OnSetAuthed(int ClientId, int Level)
{
	if(m_apPlayers[ClientId] && m_VoteCloseTime && Level != AUTHED_NO)
	{
		char aBuf[512];
		str_format(aBuf, sizeof(aBuf), "ban %s %d Banned by vote", Server()->ClientAddrString(ClientId, false), g_Config.m_SvVoteKickBantime);
		if(!str_comp_nocase(m_aVoteCommand, aBuf) && (m_VoteCreator == -1 || Level > Server()->GetAuthedState(m_VoteCreator)))
		{
			m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN;
			Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "game", "Vote aborted by authorized login.");
		}
	}

	if(m_TeeHistorianActive)
	{
		if(Level != AUTHED_NO)
		{
			m_TeeHistorian.RecordAuthLogin(ClientId, Level, Server()->GetAuthName(ClientId));
		}
		else
		{
			m_TeeHistorian.RecordAuthLogout(ClientId);
		}
	}
}

bool CGameContext::IsRunningVote(int ClientId) const
{
	return m_VoteCloseTime && m_VoteCreator == ClientId;
}

bool CGameContext::IsRunningKickOrSpecVote(int ClientId) const
{
	return IsRunningVote(ClientId) && (IsKickVote() || IsSpecVote());
}

void CGameContext::SendRecord(int ClientId)
{
	if(Server()->IsSixup(ClientId) || GetClientVersion(ClientId) >= VERSION_DDNET_MAP_BESTTIME)
		return;

	CNetMsg_Sv_Record Msg;
	CNetMsg_Sv_RecordLegacy MsgLegacy;
	MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = round_to_int(Score()->PlayerData(ClientId)->m_BestTime.value_or(0.0f) * 100.0f);
	MsgLegacy.m_ServerTimeBest = Msg.m_ServerTimeBest = m_pController->m_CurrentRecord.has_value() && !g_Config.m_SvHideScore ? round_to_int(m_pController->m_CurrentRecord.value() * 100.0f) : 0;
	Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
	if(GetClientVersion(ClientId) < VERSION_DDNET_MSG_LEGACY)
	{
		Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientId);
	}
}

void CGameContext::SendFinish(int ClientId, float Time, std::optional<float> PreviousBestTime)
{
	int ClientVersion = m_apPlayers[ClientId]->GetClientVersion();

	if(!Server()->IsSixup(ClientId))
	{
		CNetMsg_Sv_DDRaceTime Msg;
		CNetMsg_Sv_DDRaceTimeLegacy MsgLegacy;
		MsgLegacy.m_Time = Msg.m_Time = (int)(Time * 100.0f);
		MsgLegacy.m_Check = Msg.m_Check = 0;
		MsgLegacy.m_Finish = Msg.m_Finish = 1;

		if(PreviousBestTime.has_value())
		{
			float Diff100 = (Time - PreviousBestTime.value()) * 100;
			MsgLegacy.m_Check = Msg.m_Check = (int)Diff100;
		}
		if(VERSION_DDRACE <= ClientVersion)
		{
			if(ClientVersion < VERSION_DDNET_MSG_LEGACY)
			{
				Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId);
			}
			else
			{
				Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientId);
			}
		}
	}

	CNetMsg_Sv_RaceFinish RaceFinishMsg;
	RaceFinishMsg.m_ClientId = ClientId;
	RaceFinishMsg.m_Time = Time * 1000;
	RaceFinishMsg.m_Diff = 0;
	if(PreviousBestTime.has_value())
	{
		float Diff = absolute(Time - PreviousBestTime.value());
		RaceFinishMsg.m_Diff = Diff * 1000 * (Time < PreviousBestTime.value() ? -1 : 1);
	}
	RaceFinishMsg.m_RecordPersonal = (!PreviousBestTime.has_value() || Time < PreviousBestTime.value());
	RaceFinishMsg.m_RecordServer = Time < m_pController->m_CurrentRecord;
	Server()->SendPackMsg(&RaceFinishMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, g_Config.m_SvHideScore ? ClientId : -1);
}

void CGameContext::SendSaveCode(int Team, int TeamSize, int State, const char *pError, const char *pSaveRequester, const char *pServerName, const char *pGeneratedCode, const char *pCode)
{
	char aBuf[512];

	CMsgPacker Msg(NETMSGTYPE_SV_SAVECODE);
	Msg.AddInt(State);
	Msg.AddString(pError);
	Msg.AddString(pSaveRequester);
	Msg.AddString(pServerName);
	Msg.AddString(pGeneratedCode);
	Msg.AddString(pCode);
	char aTeamMembers[1024];
	aTeamMembers[0] = '\0';
	int NumMembersSent = 0;
	for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++)
	{
		if(!m_apPlayers[MemberId])
			continue;
		if(GetDDRaceTeam(MemberId) != Team)
			continue;
		if(NumMembersSent++ > 10)
		{
			str_format(aBuf, sizeof(aBuf), " and %d others", (TeamSize - NumMembersSent) + 1);
			str_append(aTeamMembers, aBuf);
			break;
		}

		if(NumMembersSent > 1)
			str_append(aTeamMembers, ", ");
		str_append(aTeamMembers, Server()->ClientName(MemberId));
	}
	Msg.AddString(aTeamMembers);

	for(int MemberId = 0; MemberId < MAX_CLIENTS; MemberId++)
	{
		if(!m_apPlayers[MemberId])
			continue;
		if(GetDDRaceTeam(MemberId) != Team)
			continue;

		if(GetClientVersion(MemberId) >= VERSION_DDNET_SAVE_CODE)
		{
			Server()->SendMsg(&Msg, MSGFLAG_VITAL, MemberId);
		}
		else
		{
			switch(State)
			{
			case SAVESTATE_PENDING:
				if(pCode[0] == '\0')
				{
					str_format(aBuf,
						sizeof(aBuf),
						"Team save in progress. You'll be able to load with '/load %s'",
						pGeneratedCode);
				}
				else
				{
					str_format(aBuf,
						sizeof(aBuf),
						"Team save in progress. You'll be able to load with '/load %s' if save is successful or with '/load %s' if it fails",
						pCode,
						pGeneratedCode);
				}
				break;
			case SAVESTATE_DONE:
				if(str_comp(pServerName, g_Config.m_SvSqlServerName) == 0)
				{
					str_format(aBuf, sizeof(aBuf),
						"Team successfully saved by %s. Use '/load %s' to continue",
						pSaveRequester, pCode[0] ? pCode : pGeneratedCode);
				}
				else
				{
					str_format(aBuf, sizeof(aBuf),
						"Team successfully saved by %s. Use '/load %s' on %s to continue",
						pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName);
				}
				break;
			case SAVESTATE_FALLBACKFILE:
				SendBroadcast("Database connection failed, teamsave written to a file instead. On official DDNet servers this will automatically be inserted into the database every full hour.", MemberId);
				if(str_comp(pServerName, g_Config.m_SvSqlServerName) == 0)
				{
					str_format(aBuf, sizeof(aBuf),
						"Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' to continue",
						pSaveRequester, pCode[0] ? pCode : pGeneratedCode);
				}
				else
				{
					str_format(aBuf, sizeof(aBuf),
						"Team successfully saved by %s. The database connection failed, using generated save code instead to avoid collisions. Use '/load %s' on %s to continue",
						pSaveRequester, pCode[0] ? pCode : pGeneratedCode, pServerName);
				}
				break;
			case SAVESTATE_ERROR:
			case SAVESTATE_WARNING:
				str_copy(aBuf, pError);
				break;
			default:
				dbg_assert_failed("Unexpected save state %d", State);
			}
			SendChatTarget(MemberId, aBuf);
		}
	}
}

bool CGameContext::ProcessSpamProtection(int ClientId, bool RespectChatInitialDelay)
{
	if(!m_apPlayers[ClientId])
		return false;
	if(g_Config.m_SvSpamprotection && m_apPlayers[ClientId]->m_LastChat && m_apPlayers[ClientId]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick())
		return true;
	else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientId))
	{
		SendChatTarget(ClientId, "Players are not allowed to chat from VPNs at this time");
		return true;
	}
	else
		m_apPlayers[ClientId]->m_LastChat = Server()->Tick();

	const std::optional<CMute> Muted = m_Mutes.IsMuted(Server()->ClientAddr(ClientId), RespectChatInitialDelay);
	if(Muted.has_value())
	{
		char aChatMessage[128];
		if(Muted->m_InitialDelay)
		{
			str_format(aChatMessage, sizeof(aChatMessage), "This server has an initial chat delay, you will be able to talk in %d seconds.", Muted->SecondsLeft());
		}
		else
		{
			str_format(aChatMessage, sizeof(aChatMessage), "You are not permitted to talk for the next %d seconds.", Muted->SecondsLeft());
		}
		SendChatTarget(ClientId, aChatMessage);
		return true;
	}

	if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientId]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold)
	{
		MuteWithMessage(Server()->ClientAddr(ClientId), g_Config.m_SvSpamMuteDuration, "Spam protection", Server()->ClientName(ClientId));
		m_apPlayers[ClientId]->m_ChatScore = 0;
		return true;
	}

	return false;
}

int CGameContext::GetDDRaceTeam(int ClientId) const
{
	return m_pController->Teams().m_Core.Team(ClientId);
}

void CGameContext::ResetTuning()
{
	*GlobalTuning() = CTuningParams::DEFAULT;
	GlobalTuning()->Set("gun_speed", 1400);
	GlobalTuning()->Set("gun_curvature", 0);
	GlobalTuning()->Set("shotgun_speed", 500);
	GlobalTuning()->Set("shotgun_speeddiff", 0);
	GlobalTuning()->Set("shotgun_curvature", 0);
	SendTuningParams(-1);
}

void CGameContext::Whisper(int ClientId, char *pStr)
{
	if(ProcessSpamProtection(ClientId))
		return;

	pStr = str_skip_whitespaces(pStr);

	const char *pName;
	int Victim;
	bool Error = false;

	// add token
	if(*pStr == '"')
	{
		pStr++;

		pName = pStr;
		char *pDst = pStr; // we might have to process escape data
		while(true)
		{
			if(pStr[0] == '"')
			{
				break;
			}
			else if(pStr[0] == '\\')
			{
				if(pStr[1] == '\\')
					pStr++; // skip due to escape
				else if(pStr[1] == '"')
					pStr++; // skip due to escape
			}
			else if(pStr[0] == 0)
			{
				Error = true;
				break;
			}

			*pDst = *pStr;
			pDst++;
			pStr++;
		}

		if(!Error)
		{
			*pDst = '\0';
			pStr++;

			Victim = FindClientIdByName(pName).value_or(-1);
		}
	}
	else
	{
		pName = pStr;
		while(true)
		{
			if(pStr[0] == '\0')
			{
				Error = true;
				break;
			}
			if(pStr[0] == ' ')
			{
				pStr[0] = '\0';

				Victim = FindClientIdByName(pName).value_or(-1);

				pStr[0] = ' ';
				if(Victim != -1)
					break;
			}
			pStr++;
		}
	}

	if(pStr[0] != ' ')
	{
		Error = true;
	}

	*pStr = '\0';
	pStr++;

	if(Error)
	{
		SendChatTarget(ClientId, "Invalid whisper");
		return;
	}

	if(!CheckClientId(Victim))
	{
		char aBuf[256];
		str_format(aBuf, sizeof(aBuf), "No player with name \"%s\" found", pName);
		SendChatTarget(ClientId, aBuf);
		return;
	}

	WhisperId(ClientId, Victim, pStr);
}

// Whispers are only recorded into the server demo when sv_demo_chat is set. Messages to 0.7 clients are
// packed in the 0.7 format and can never be recorded into the 0.6 demo.
int CGameContext::WhisperRecordFlag(int ClientId) const
{
	return g_Config.m_SvDemoChat && !Server()->IsSixup(ClientId) ? 0 : MSGFLAG_NORECORD;
}

void CGameContext::WhisperId(int ClientId, int VictimId, const char *pMessage)
{
	dbg_assert(CheckClientId(ClientId) && m_apPlayers[ClientId] != nullptr, "ClientId invalid");
	dbg_assert(CheckClientId(VictimId) && m_apPlayers[VictimId] != nullptr, "VictimId invalid");

	m_apPlayers[ClientId]->m_LastWhisperTo = VictimId;

	char aCensoredMessage[256];
	CensorMessage(aCensoredMessage, pMessage, sizeof(aCensoredMessage));

	char aBuf[256];
	protocol7::CNetMsg_Sv_Chat Msg;
	Msg.m_ClientId = ClientId;
	Msg.m_Mode = protocol7::CHAT_WHISPER;
	Msg.m_pMessage = aCensoredMessage;
	Msg.m_TargetId = VictimId;

	if(Server()->IsSixup(ClientId) || GetClientVersion(ClientId) >= VERSION_DDNET_WHISPER)
	{
		// The translation layer will send the correct 0.6 packet after translating
		Msg.m_Mode = (int)protocol7::NUM_CHATS + TEAM_WHISPER_SEND;
		Server()->SendPackMsgTranslateChat(&Msg, MSGFLAG_VITAL | WhisperRecordFlag(ClientId), ClientId);
	}
	else
	{
		str_format(aBuf, sizeof(aBuf), "[→ %s] %s", Server()->ClientName(VictimId), aCensoredMessage);
		SendChatTarget(ClientId, aBuf);
	}

	if(!m_apPlayers[VictimId]->m_Whispers)
	{
		SendChatTarget(ClientId, "This person has disabled receiving whispers");
		return;
	}

	if(Server()->IsSixup(VictimId) || GetClientVersion(VictimId) >= VERSION_DDNET_WHISPER)
	{
		// The translation layer will send the correct 0.6 packet after translating
		Msg.m_Mode = (int)protocol7::NUM_CHATS + TEAM_WHISPER_RECV;
		Server()->SendPackMsgTranslateChat(&Msg, MSGFLAG_VITAL | WhisperRecordFlag(VictimId), VictimId);
	}
	else
	{
		str_format(aBuf, sizeof(aBuf), "[← %s] %s", Server()->ClientName(ClientId), aCensoredMessage);
		SendChatTarget(VictimId, aBuf);
	}
}

void CGameContext::Converse(int ClientId, char *pStr)
{
	CPlayer *pPlayer = m_apPlayers[ClientId];
	if(!pPlayer)
		return;

	if(ProcessSpamProtection(ClientId))
		return;

	if(pPlayer->m_LastWhisperTo < 0)
		SendChatTarget(ClientId, "You do not have an ongoing conversation. Whisper to someone to start one");
	else if(!m_apPlayers[pPlayer->m_LastWhisperTo])
		SendChatTarget(ClientId, "The player you were whispering to hasn't reconnected yet or left. Please wait or whisper to someone else");
	else
		WhisperId(ClientId, pPlayer->m_LastWhisperTo, pStr);
}

bool CGameContext::IsVersionBanned(int Version)
{
	char aVersion[16];
	str_format(aVersion, sizeof(aVersion), "%d", Version);

	return str_in_list(g_Config.m_SvBannedVersions, ",", aVersion);
}

void CGameContext::List(int ClientId, const char *pFilter)
{
	int Total = 0;
	char aBuf[256];
	int Bufcnt = 0;
	if(pFilter[0])
		str_format(aBuf, sizeof(aBuf), "Listing players with \"%s\" in name:", pFilter);
	else
		str_copy(aBuf, "Listing all players:");
	SendChatTarget(ClientId, aBuf);
	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		if(m_apPlayers[i])
		{
			Total++;
			const char *pName = Server()->ClientName(i);
			if(str_utf8_find_nocase(pName, pFilter) == nullptr)
				continue;
			if(Bufcnt + str_length(pName) + 4 > 256)
			{
				SendChatTarget(ClientId, aBuf);
				Bufcnt = 0;
			}
			if(Bufcnt != 0)
			{
				str_format(&aBuf[Bufcnt], sizeof(aBuf) - Bufcnt, ", %s", pName);
				Bufcnt += 2 + str_length(pName);
			}
			else
			{
				str_copy(&aBuf[Bufcnt], pName, sizeof(aBuf) - Bufcnt);
				Bufcnt += str_length(pName);
			}
		}
	}
	if(Bufcnt != 0)
		SendChatTarget(ClientId, aBuf);
	str_format(aBuf, sizeof(aBuf), "%d players online", Total);
	SendChatTarget(ClientId, aBuf);
}

int CGameContext::GetClientVersion(int ClientId) const
{
	return Server()->GetClientVersion(ClientId);
}

CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) const
{
	CClientMask Mask;
	for(int i = 0; i < MAX_CLIENTS; ++i)
	{
		if(GetClientVersion(i) >= Version)
			continue;
		Mask.set(i);
	}
	return Mask;
}

bool CGameContext::PlayerModerating() const
{
	return std::any_of(std::begin(m_apPlayers), std::end(m_apPlayers), [](const CPlayer *pPlayer) { return pPlayer && pPlayer->m_Moderating; });
}

void CGameContext::ForceVote(bool Success)
{
	// check if there is a vote running
	if(!m_VoteCloseTime)
		return;

	m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN;
	const char *pOption = Success ? "yes" : "no";

	char aChatMessage[256];
	str_format(aChatMessage, sizeof(aChatMessage), "Authorized player forced vote '%s'", pOption);
	SendChatTarget(-1, aChatMessage);

	log_info("server", "Forcing vote '%s'", pOption);
}

bool CGameContext::RateLimitPlayerVote(int ClientId)
{
	int64_t Now = Server()->Tick();
	int64_t TickSpeed = Server()->TickSpeed();
	CPlayer *pPlayer = m_apPlayers[ClientId];

	if(g_Config.m_SvRconVote && !Server()->IsRconAuthed(ClientId))
	{
		SendChatTarget(ClientId, "You can only vote after logging in.");
		return true;
	}

	if(g_Config.m_SvDnsblVote && Server()->DistinctClientCount() > 1)
	{
		if(m_pServer->DnsblPending(ClientId))
		{
			SendChatTarget(ClientId, "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds.");
			return true;
		}
		else if(m_pServer->DnsblBlack(ClientId))
		{
			SendChatTarget(ClientId, "You are not allowed to vote because you appear to be using a VPN. Try connecting without a VPN or contacting an admin if you think this is a mistake.");
			return true;
		}
	}

	if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + TickSpeed * 3 > Now)
		return true;

	pPlayer->m_LastVoteTry = Now;
	if(m_VoteCloseTime)
	{
		SendChatTarget(ClientId, "Wait for current vote to end before calling a new one.");
		return true;
	}

	if(Now < pPlayer->m_FirstVoteTick)
	{
		char aChatMessage[64];
		str_format(aChatMessage, sizeof(aChatMessage), "You must wait %d seconds before making your first vote.", (int)((pPlayer->m_FirstVoteTick - Now) / TickSpeed) + 1);
		SendChatTarget(ClientId, aChatMessage);
		return true;
	}

	int TimeLeft = pPlayer->m_LastVoteCall + TickSpeed * g_Config.m_SvVoteDelay - Now;
	if(pPlayer->m_LastVoteCall && TimeLeft > 0)
	{
		char aChatMessage[64];
		str_format(aChatMessage, sizeof(aChatMessage), "You must wait %d seconds before making another vote.", (int)(TimeLeft / TickSpeed) + 1);
		SendChatTarget(ClientId, aChatMessage);
		return true;
	}

	const NETADDR *pAddr = Server()->ClientAddr(ClientId);
	std::optional<CMute> Muted = m_VoteMutes.IsMuted(pAddr, true);
	if(!Muted.has_value())
	{
		Muted = m_Mutes.IsMuted(pAddr, true);
	}
	if(Muted.has_value())
	{
		char aChatMessage[64];
		str_format(aChatMessage, sizeof(aChatMessage), "You are not permitted to vote for the next %d seconds.", Muted->SecondsLeft());
		SendChatTarget(ClientId, aChatMessage);
		return true;
	}
	return false;
}

bool CGameContext::RateLimitPlayerMapVote(int ClientId) const
{
	if(!Server()->IsRconAuthed(ClientId) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay))
	{
		char aChatMessage[128];
		str_format(aChatMessage, sizeof(aChatMessage), "There's a %d second delay between map-votes, please wait %d seconds.",
			g_Config.m_SvVoteMapTimeDelay, (int)((m_LastMapVote + g_Config.m_SvVoteMapTimeDelay * time_freq() - time_get()) / time_freq()));
		SendChatTarget(ClientId, aChatMessage);
		return true;
	}
	return false;
}

void CGameContext::OnUpdatePlayerServerInfo(CJsonWriter *pJsonWriter, int ClientId)
{
	if(!m_apPlayers[ClientId])
		return;

	CTeeInfo &TeeInfo = m_apPlayers[ClientId]->m_TeeInfos;

	pJsonWriter->WriteAttribute("skin");
	pJsonWriter->BeginObject();

	// 0.6
	if(!Server()->IsSixup(ClientId))
	{
		pJsonWriter->WriteAttribute("name");
		pJsonWriter->WriteStrValue(TeeInfo.m_aSkinName);

		if(TeeInfo.m_UseCustomColor)
		{
			pJsonWriter->WriteAttribute("color_body");
			pJsonWriter->WriteIntValue(TeeInfo.m_ColorBody);

			pJsonWriter->WriteAttribute("color_feet");
			pJsonWriter->WriteIntValue(TeeInfo.m_ColorFeet);
		}
	}
	// 0.7
	else
	{
		const char *apPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"};

		for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i)
		{
			pJsonWriter->WriteAttribute(apPartNames[i]);
			pJsonWriter->BeginObject();

			pJsonWriter->WriteAttribute("name");
			pJsonWriter->WriteStrValue(TeeInfo.m_aaSkinPartNames[i]);

			if(TeeInfo.m_aUseCustomColors[i])
			{
				pJsonWriter->WriteAttribute("color");
				pJsonWriter->WriteIntValue(TeeInfo.m_aSkinPartColors[i]);
			}

			pJsonWriter->EndObject();
		}
	}

	pJsonWriter->EndObject();

	pJsonWriter->WriteAttribute("afk");
	pJsonWriter->WriteBoolValue(m_apPlayers[ClientId]->IsAfk());

	const int Team = m_pController->IsTeamPlay() ? m_apPlayers[ClientId]->GetTeam() : (m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS ? -1 : GetDDRaceTeam(ClientId));

	pJsonWriter->WriteAttribute("team");
	pJsonWriter->WriteIntValue(Team);
}

void CGameContext::ReadCensorList()
{
	const char *pCensorFilename = "censorlist.txt";
	CLineReader LineReader;
	m_vCensorlist.clear();
	if(LineReader.OpenFile(Storage()->OpenFile(pCensorFilename, IOFLAG_READ, IStorage::TYPE_ALL)))
	{
		while(const char *pLine = LineReader.Get())
		{
			if(pLine[0] == '\0')
			{
				continue;
			}
			m_vCensorlist.emplace_back(pLine);
		}
	}
	else
	{
		dbg_msg("censorlist", "failed to open '%s'", pCensorFilename);
	}
}

bool CGameContext::PracticeByDefault() const
{
	return g_Config.m_SvPracticeByDefault && g_Config.m_SvTestingCommands;
}

void CGameContext::SendStartMessages(int ClientId)
{
	if(Server()->IsSixup(ClientId))
	{
		{
			protocol7::CNetMsg_Sv_GameInfo Msg;
			Msg.m_GameFlags = m_pController->GameFlags();
			Msg.m_MatchCurrent = 1;
			Msg.m_MatchNum = 0;
			Msg.m_ScoreLimit = 0;
			Msg.m_TimeLimit = 0;
			Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
		}

		// /team is essential
		{
			protocol7::CNetMsg_Sv_CommandInfoRemove Msg;
			Msg.m_pName = "team";
			Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
		}
	}

	{
		CNetMsg_Sv_CommandInfoGroupStart Msg;
		Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
	}
	for(const IConsole::ICommandInfo *pCmd = Console()->FirstCommandInfo(ClientId, CFGFLAG_CHAT);
		pCmd; pCmd = Console()->NextCommandInfo(pCmd, ClientId, CFGFLAG_CHAT))
	{
		const char *pName = pCmd->Name();

		if(Server()->IsSixup(ClientId))
		{
			if(!str_comp_nocase(pName, "w") || !str_comp_nocase(pName, "whisper"))
				continue;

			if(!str_comp_nocase(pName, "r"))
				pName = "rescue";

			protocol7::CNetMsg_Sv_CommandInfo Msg;
			Msg.m_pName = pName;
			Msg.m_pArgsFormat = pCmd->Params();
			Msg.m_pHelpText = pCmd->Help();
			Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
		}
		else
		{
			CNetMsg_Sv_CommandInfo Msg;
			Msg.m_pName = pName;
			Msg.m_pArgsFormat = pCmd->Params();
			Msg.m_pHelpText = pCmd->Help();
			Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
		}
	}
	{
		CNetMsg_Sv_CommandInfoGroupEnd Msg;
		Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
	}

	// send active vote
	if(m_VoteCloseTime)
		SendVoteSet(ClientId);
}

void CGameContext::OnClientRejoin(int ClientId)
{
	if(!m_apPlayers[ClientId])
		return;

	ReinitPlayerMap(ClientId, false);
	SendStartMessages(ClientId);

	// send clear vote options
	CNetMsg_Sv_VoteClearOptions ClearMsg;
	Server()->SendPackMsg(&ClearMsg, MSGFLAG_VITAL, ClientId);

	// begin sending vote options
	m_apPlayers[ClientId]->m_SendVoteIndex = 0;
	SendTuningParams(ClientId, m_apPlayers[ClientId]->m_TuneZone);
}

void CGameContext::ReinitPlayerMap(int ClientId, bool Timeout)
{
	// Timeout, when calling InitPlayerMap because that will make sure each disconnect packet gets sent out to 0.7 clients correctly before inserting
	// new players on their slots. will also trigger teams state update, otherwise you wouldn't see teams correctly in some cases.
	CPlayerMapping::CSixupCfg SixupCfg;
	SixupCfg.m_SkipTimeoutedId = Timeout;
	SixupCfg.m_ClearSlots = true;
	m_PlayerMapping.InitPlayerMap(ClientId, SixupCfg);
}
