about summary refs log tree commit diff
path: root/src/engine/client/client.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/engine/client/client.cpp')
-rw-r--r--src/engine/client/client.cpp2062
1 files changed, 2062 insertions, 0 deletions
diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp
new file mode 100644
index 00000000..dbcaa1ed
--- /dev/null
+++ b/src/engine/client/client.cpp
@@ -0,0 +1,2062 @@
+// copyright (c) 2007 magnus auvinen, see licence.txt for more info
+
+#include <stdlib.h> // qsort
+#include <stdarg.h>
+#include <math.h>
+
+#include <base/system.h>
+#include <engine/shared/engine.h>
+
+#include <engine/shared/protocol.h>
+#include <engine/shared/snapshot.h>
+#include <engine/shared/compression.h>
+#include <engine/shared/network.h>
+#include <engine/shared/config.h>
+#include <engine/shared/packer.h>
+#include <engine/shared/memheap.h>
+#include <engine/shared/datafile.h>
+#include <engine/shared/ringbuffer.h>
+#include <engine/shared/protocol.h>
+
+#include <engine/shared/demorec.h>
+
+#include <mastersrv/mastersrv.h>
+#include <versionsrv/versionsrv.h>
+
+#include "client.h"
+
+
+void CGraph::Init(float Min, float Max)
+{
+	m_Min = Min;
+	m_Max = Max;
+	m_Index = 0;
+}
+
+void CGraph::ScaleMax()
+{
+	int i = 0;
+	m_Max = 0;
+	for(i = 0; i < MAX_VALUES; i++)
+	{
+		if(m_aValues[i] > m_Max)
+			m_Max = m_aValues[i];
+	}
+}
+
+void CGraph::ScaleMin()
+{
+	int i = 0;
+	m_Min = m_Max;
+	for(i = 0; i < MAX_VALUES; i++)
+	{
+		if(m_aValues[i] < m_Min)
+			m_Min = m_aValues[i];
+	}
+}
+
+void CGraph::Add(float v, float r, float g, float b)
+{
+	m_Index = (m_Index+1)&(MAX_VALUES-1);
+	m_aValues[m_Index] = v;
+	m_aColors[m_Index][0] = r;
+	m_aColors[m_Index][1] = g;
+	m_aColors[m_Index][2] = b;
+}
+
+void CGraph::Render(IGraphics *pGraphics, int Font, float x, float y, float w, float h, const char *pDescription)
+{
+	//m_pGraphics->BlendNormal();
+
+
+	pGraphics->TextureSet(-1);
+
+	pGraphics->QuadsBegin();
+	pGraphics->SetColor(0, 0, 0, 0.75f);
+	IGraphics::CQuadItem QuadItem(x, y, w, h);
+	pGraphics->QuadsDrawTL(&QuadItem, 1);
+	pGraphics->QuadsEnd();
+
+	pGraphics->LinesBegin();
+	pGraphics->SetColor(0.95f, 0.95f, 0.95f, 1.00f);
+	IGraphics::CLineItem LineItem(x, y+h/2, x+w, y+h/2);
+	pGraphics->LinesDraw(&LineItem, 1);
+	pGraphics->SetColor(0.5f, 0.5f, 0.5f, 0.75f);
+	IGraphics::CLineItem Array[2] = {
+		IGraphics::CLineItem(x, y+(h*3)/4, x+w, y+(h*3)/4),
+		IGraphics::CLineItem(x, y+h/4, x+w, y+h/4)};
+	pGraphics->LinesDraw(Array, 2);
+	for(int i = 1; i < MAX_VALUES; i++)
+	{
+		float a0 = (i-1)/(float)MAX_VALUES;
+		float a1 = i/(float)MAX_VALUES;
+		int i0 = (m_Index+i-1)&(MAX_VALUES-1);
+		int i1 = (m_Index+i)&(MAX_VALUES-1);
+
+		float v0 = (m_aValues[i0]-m_Min) / (m_Max-m_Min);
+		float v1 = (m_aValues[i1]-m_Min) / (m_Max-m_Min);
+
+		IGraphics::CColorVertex Array[2] = {
+			IGraphics::CColorVertex(0, m_aColors[i0][0], m_aColors[i0][1], m_aColors[i0][2], 0.75f),
+			IGraphics::CColorVertex(1, m_aColors[i1][0], m_aColors[i1][1], m_aColors[i1][2], 0.75f)};
+		pGraphics->SetColorVertex(Array, 2);
+		IGraphics::CLineItem LineItem(x+a0*w, y+h-v0*h, x+a1*w, y+h-v1*h);
+		pGraphics->LinesDraw(&LineItem, 1);
+
+	}
+	pGraphics->LinesEnd();
+
+	pGraphics->TextureSet(Font);
+	pGraphics->QuadsText(x+2, y+h-16, 16, 1,1,1,1, pDescription);
+
+	char aBuf[32];
+	str_format(aBuf, sizeof(aBuf), "%.2f", m_Max);
+	pGraphics->QuadsText(x+w-8*str_length(aBuf)-8, y+2, 16, 1,1,1,1, aBuf);
+
+	str_format(aBuf, sizeof(aBuf), "%.2f", m_Min);
+	pGraphics->QuadsText(x+w-8*str_length(aBuf)-8, y+h-16, 16, 1,1,1,1, aBuf);
+}
+
+
+void CSmoothTime::Init(int64 Target)
+{
+	m_Snap = time_get();
+	m_Current = Target;
+	m_Target = Target;
+	m_aAdjustSpeed[0] = 0.3f;
+	m_aAdjustSpeed[1] = 0.3f;
+	m_Graph.Init(0.0f, 0.5f);
+}
+
+void CSmoothTime::SetAdjustSpeed(int Direction, float Value)
+{
+	m_aAdjustSpeed[Direction] = Value;
+}
+
+int64 CSmoothTime::Get(int64 Now)
+{
+	int64 c = m_Current + (Now - m_Snap);
+	int64 t = m_Target + (Now - m_Snap);
+
+	// it's faster to adjust upward instead of downward
+	// we might need to adjust these abit
+
+	float AdjustSpeed = m_aAdjustSpeed[0];
+	if(t > c)
+		AdjustSpeed = m_aAdjustSpeed[1];
+
+	float a = ((Now-m_Snap)/(float)time_freq()) * AdjustSpeed;
+	if(a > 1.0f)
+		a = 1.0f;
+
+	int64 r = c + (int64)((t-c)*a);
+
+	m_Graph.Add(a+0.5f,1,1,1);
+
+	return r;
+}
+
+void CSmoothTime::UpdateInt(int64 Target)
+{
+	int64 Now = time_get();
+	m_Current = Get(Now);
+	m_Snap = Now;
+	m_Target = Target;
+}
+
+void CSmoothTime::Update(CGraph *pGraph, int64 Target, int TimeLeft, int AdjustDirection)
+{
+	int UpdateTimer = 1;
+
+	if(TimeLeft < 0)
+	{
+		int IsSpike = 0;
+		if(TimeLeft < -50)
+		{
+			IsSpike = 1;
+
+			m_SpikeCounter += 5;
+			if(m_SpikeCounter > 50)
+				m_SpikeCounter = 50;
+		}
+
+		if(IsSpike && m_SpikeCounter < 15)
+		{
+			// ignore this ping spike
+			UpdateTimer = 0;
+			pGraph->Add(TimeLeft, 1,1,0);
+		}
+		else
+		{
+			pGraph->Add(TimeLeft, 1,0,0);
+			if(m_aAdjustSpeed[AdjustDirection] < 30.0f)
+				m_aAdjustSpeed[AdjustDirection] *= 2.0f;
+		}
+	}
+	else
+	{
+		if(m_SpikeCounter)
+			m_SpikeCounter--;
+
+		pGraph->Add(TimeLeft, 0,1,0);
+
+		m_aAdjustSpeed[AdjustDirection] *= 0.95f;
+		if(m_aAdjustSpeed[AdjustDirection] < 2.0f)
+			m_aAdjustSpeed[AdjustDirection] = 2.0f;
+	}
+
+	if(UpdateTimer)
+		UpdateInt(Target);
+}
+
+
+CClient::CClient() : m_DemoPlayer(&m_SnapshotDelta), m_DemoRecorder(&m_SnapshotDelta)
+{
+	m_pEditor = 0;
+	m_pInput = 0;
+	m_pGraphics = 0;
+	m_pSound = 0;
+	m_pGameClient = 0;
+	m_pMap = 0;
+	m_pConsole = 0;
+
+	m_FrameTime = 0.0001f;
+	m_FrameTimeLow = 1.0f;
+	m_FrameTimeHigh = 0.0f;
+	m_Frames = 0;
+
+	m_GameTickSpeed = SERVER_TICK_SPEED;
+
+	m_WindowMustRefocus = 0;
+	m_SnapCrcErrors = 0;
+
+	m_AckGameTick = -1;
+	m_CurrentRecvTick = 0;
+	m_RconAuthed = 0;
+
+	// version-checking
+	m_aVersionStr[0] = '0';
+	m_aVersionStr[1] = 0;
+
+	// pinging
+	m_PingStartTime = 0;
+
+	//
+	m_aCurrentMap[0] = 0;
+	m_CurrentMapCrc = 0;
+
+	//
+	m_aCmdConnect[0] = 0;
+
+	// map download
+	m_aMapdownloadFilename[0] = 0;
+	m_aMapdownloadName[0] = 0;
+	m_MapdownloadFile = 0;
+	m_MapdownloadChunk = 0;
+	m_MapdownloadCrc = 0;
+	m_MapdownloadAmount = -1;
+	m_MapdownloadTotalsize = -1;
+
+	m_CurrentServerInfoRequestTime = -1;
+
+	m_CurrentInput = 0;
+
+	m_State = IClient::STATE_OFFLINE;
+	m_aServerAddressStr[0] = 0;
+
+	mem_zero(m_aSnapshots, sizeof(m_aSnapshots));
+	m_RecivedSnapshots = 0;
+
+	m_VersionInfo.m_State = 0;
+}
+
+// ----- send functions -----
+int CClient::SendMsg(CMsgPacker *pMsg, int Flags)
+{
+	return SendMsgEx(pMsg, Flags, false);
+}
+
+int CClient::SendMsgEx(CMsgPacker *pMsg, int Flags, bool System)
+{
+	CNetChunk Packet;
+
+	if(State() == IClient::STATE_OFFLINE)
+		return 0;
+
+	mem_zero(&Packet, sizeof(CNetChunk));
+
+	Packet.m_ClientID = 0;
+	Packet.m_pData = pMsg->Data();
+	Packet.m_DataSize = pMsg->Size();
+
+	// HACK: modify the message id in the packet and store the system flag
+	if(*((unsigned char*)Packet.m_pData) == 1 && System && Packet.m_DataSize == 1)
+		dbg_break();
+
+	*((unsigned char*)Packet.m_pData) <<= 1;
+	if(System)
+		*((unsigned char*)Packet.m_pData) |= 1;
+
+	if(Flags&MSGFLAG_VITAL)
+		Packet.m_Flags |= NETSENDFLAG_VITAL;
+	if(Flags&MSGFLAG_FLUSH)
+		Packet.m_Flags |= NETSENDFLAG_FLUSH;
+
+	if(Flags&MSGFLAG_RECORD)
+	{
+		if(m_DemoRecorder.IsRecording())
+			m_DemoRecorder.RecordMessage(Packet.m_pData, Packet.m_DataSize);
+	}
+
+	if(!(Flags&MSGFLAG_NOSEND))
+		m_NetClient.Send(&Packet);
+	return 0;
+}
+
+void CClient::SendInfo()
+{
+	CMsgPacker Msg(NETMSG_INFO);
+	Msg.AddString(GameClient()->NetVersion(), 128);
+	Msg.AddString(g_Config.m_PlayerName, 128);
+	Msg.AddString(g_Config.m_ClanName, 128);
+	Msg.AddString(g_Config.m_Password, 128);
+	SendMsgEx(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
+}
+
+
+void CClient::SendEnterGame()
+{
+	CMsgPacker Msg(NETMSG_ENTERGAME);
+	SendMsgEx(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
+}
+
+void CClient::SendReady()
+{
+	CMsgPacker Msg(NETMSG_READY);
+	SendMsgEx(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
+}
+
+bool CClient::RconAuthed()
+{
+	return m_RconAuthed;
+}
+
+void CClient::RconAuth(const char *pName, const char *pPassword)
+{
+	if(RconAuthed())
+		return;
+        
+	CMsgPacker Msg(NETMSG_RCON_AUTH);
+	Msg.AddString(pName, 32);
+	Msg.AddString(pPassword, 32);
+	SendMsgEx(&Msg, MSGFLAG_VITAL);
+}
+
+void CClient::Rcon(const char *pCmd)
+{
+	CMsgPacker Msg(NETMSG_RCON_CMD);
+	Msg.AddString(pCmd, 256);
+	SendMsgEx(&Msg, MSGFLAG_VITAL);
+}
+
+bool CClient::ConnectionProblems()
+{
+	return m_NetClient.GotProblems() != 0;
+}
+
+void CClient::DirectInput(int *pInput, int Size)
+{
+	int i;
+	CMsgPacker Msg(NETMSG_INPUT);
+	Msg.AddInt(m_AckGameTick);
+	Msg.AddInt(m_PredTick);
+	Msg.AddInt(Size);
+
+	for(i = 0; i < Size/4; i++)
+		Msg.AddInt(pInput[i]);
+
+	SendMsgEx(&Msg, 0);
+}
+
+
+void CClient::SendInput()
+{
+	int64 Now = time_get();
+
+	if(m_PredTick <= 0)
+		return;
+
+	// fetch input
+	int Size = GameClient()->OnSnapInput(m_aInputs[m_CurrentInput].m_aData);
+
+	if(!Size)
+		return;
+
+	// pack input
+	CMsgPacker Msg(NETMSG_INPUT);
+	Msg.AddInt(m_AckGameTick);
+	Msg.AddInt(m_PredTick);
+	Msg.AddInt(Size);
+
+	m_aInputs[m_CurrentInput].m_Tick = m_PredTick;
+	m_aInputs[m_CurrentInput].m_PredictedTime = m_PredictedTime.Get(Now);
+	m_aInputs[m_CurrentInput].m_Time = Now;
+
+	// pack it
+	for(int i = 0; i < Size/4; i++)
+		Msg.AddInt(m_aInputs[m_CurrentInput].m_aData[i]);
+
+	m_CurrentInput++;
+	m_CurrentInput%=200;
+
+	SendMsgEx(&Msg, MSGFLAG_FLUSH);
+}
+
+const char *CClient::LatestVersion()
+{
+	return m_aVersionStr;
+}
+
+// TODO: OPT: do this alot smarter!
+int *CClient::GetInput(int Tick)
+{
+	int Best = -1;
+	for(int i = 0; i < 200; i++)
+	{
+		if(m_aInputs[i].m_Tick <= Tick && (Best == -1 || m_aInputs[Best].m_Tick < m_aInputs[i].m_Tick))
+			Best = i;
+	}
+
+	if(Best != -1)
+		return (int *)m_aInputs[Best].m_aData;
+	return 0;
+}
+
+// ------ state handling -----
+void CClient::SetState(int s)
+{
+	int Old = m_State;
+	if(g_Config.m_Debug)
+		dbg_msg("client", "state change. last=%d current=%d", m_State, s);
+	m_State = s;
+	if(Old != s)
+		GameClient()->OnStateChange(m_State, Old);
+}
+
+// called when the map is loaded and we should init for a new round
+void CClient::OnEnterGame()
+{
+	// reset input
+	int i;
+	for(i = 0; i < 200; i++)
+		m_aInputs[i].m_Tick = -1;
+	m_CurrentInput = 0;
+
+	// reset snapshots
+	m_aSnapshots[SNAP_CURRENT] = 0;
+	m_aSnapshots[SNAP_PREV] = 0;
+	m_SnapshotStorage.PurgeAll();
+	m_RecivedSnapshots = 0;
+	m_SnapshotParts = 0;
+	m_PredTick = 0;
+	m_CurrentRecvTick = 0;
+	m_CurGameTick = 0;
+	m_PrevGameTick = 0;
+}
+
+void CClient::EnterGame()
+{
+	if(State() == IClient::STATE_DEMOPLAYBACK)
+		return;
+
+	// now we will wait for two snapshots
+	// to finish the connection
+	SendEnterGame();
+	OnEnterGame();
+}
+
+void CClient::Connect(const char *pAddress)
+{
+	char aBuf[512];
+	const char *pPortStr = 0;
+	int Port = 8303;
+
+	Disconnect();
+
+	str_copy(m_aServerAddressStr, pAddress, sizeof(m_aServerAddressStr));
+
+	dbg_msg("client", "connecting to '%s'", m_aServerAddressStr);
+
+	ServerInfoRequest();
+	str_copy(aBuf, m_aServerAddressStr, sizeof(aBuf));
+
+	for(int k = 0; aBuf[k]; k++)
+	{
+		if(aBuf[k] == ':')
+		{
+			pPortStr = &(aBuf[k+1]);
+			aBuf[k] = 0;
+			break;
+		}
+	}
+
+	if(pPortStr)
+		Port = str_toint(pPortStr);
+
+	// TODO: IPv6 support
+	if(net_host_lookup(aBuf, &m_ServerAddress, NETTYPE_IPV4) != 0)
+		dbg_msg("client", "could not find the address of %s, connecting to localhost", aBuf);
+
+	m_RconAuthed = 0;
+	m_ServerAddress.port = Port;
+	m_NetClient.Connect(&m_ServerAddress);
+	SetState(IClient::STATE_CONNECTING);
+
+	if(m_DemoRecorder.IsRecording())
+		m_DemoRecorder.Stop();
+
+	m_InputtimeMarginGraph.Init(-150.0f, 150.0f);
+	m_GametimeMarginGraph.Init(-150.0f, 150.0f);
+}
+
+void CClient::DisconnectWithReason(const char *pReason)
+{
+	// stop demo playback and recorder
+	m_DemoPlayer.Stop();
+	m_DemoRecorder.Stop();
+
+	//
+	m_RconAuthed = 0;
+	m_NetClient.Disconnect(pReason);
+	SetState(IClient::STATE_OFFLINE);
+	m_pMap->Unload();
+
+	// disable all downloads
+	m_MapdownloadChunk = 0;
+	if(m_MapdownloadFile)
+		io_close(m_MapdownloadFile);
+	m_MapdownloadFile = 0;
+	m_MapdownloadCrc = 0;
+	m_MapdownloadTotalsize = -1;
+	m_MapdownloadAmount = 0;
+
+	// clear the current server info
+	mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
+	mem_zero(&m_ServerAddress, sizeof(m_ServerAddress));
+
+	// clear snapshots
+	m_aSnapshots[SNAP_CURRENT] = 0;
+	m_aSnapshots[SNAP_PREV] = 0;
+	m_RecivedSnapshots = 0;
+}
+
+void CClient::Disconnect()
+{
+	DisconnectWithReason(0);
+}
+
+
+void CClient::GetServerInfo(CServerInfo *pServerInfo)
+{
+	mem_copy(pServerInfo, &m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
+}
+
+void CClient::ServerInfoRequest()
+{
+	mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
+	m_CurrentServerInfoRequestTime = 0;
+}
+
+int CClient::LoadData()
+{
+	m_DebugFont = Graphics()->LoadTexture("debug_font.png", CImageInfo::FORMAT_AUTO, IGraphics::TEXLOAD_NORESAMPLE);
+	return 1;
+}
+
+// ---
+
+void *CClient::SnapGetItem(int SnapId, int Index, CSnapItem *pItem)
+{
+	CSnapshotItem *i;
+	dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
+	i = m_aSnapshots[SnapId]->m_pAltSnap->GetItem(Index);
+	pItem->m_DataSize = m_aSnapshots[SnapId]->m_pAltSnap->GetItemSize(Index);
+	pItem->m_Type = i->Type();
+	pItem->m_Id = i->ID();
+	return (void *)i->Data();
+}
+
+void CClient::SnapInvalidateItem(int SnapId, int Index)
+{
+	CSnapshotItem *i;
+	dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
+	i = m_aSnapshots[SnapId]->m_pAltSnap->GetItem(Index);
+	if(i)
+	{
+		if((char *)i < (char *)m_aSnapshots[SnapId]->m_pAltSnap || (char *)i > (char *)m_aSnapshots[SnapId]->m_pAltSnap + m_aSnapshots[SnapId]->m_SnapSize)
+			dbg_msg("ASDFASDFASdf", "ASDFASDFASDF");
+		if((char *)i >= (char *)m_aSnapshots[SnapId]->m_pSnap && (char *)i < (char *)m_aSnapshots[SnapId]->m_pSnap + m_aSnapshots[SnapId]->m_SnapSize)
+			dbg_msg("ASDFASDFASdf", "ASDFASDFASDF");
+		i->m_TypeAndID = -1;
+	}
+}
+
+void *CClient::SnapFindItem(int SnapId, int Type, int Id)
+{
+	// TODO: linear search. should be fixed.
+	int i;
+
+	if(!m_aSnapshots[SnapId])
+		return 0x0;
+
+	for(i = 0; i < m_aSnapshots[SnapId]->m_pSnap->NumItems(); i++)
+	{
+		CSnapshotItem *pItem = m_aSnapshots[SnapId]->m_pAltSnap->GetItem(i);
+		if(pItem->Type() == Type && pItem->ID() == Id)
+			return (void *)pItem->Data();
+	}
+	return 0x0;
+}
+
+int CClient::SnapNumItems(int SnapId)
+{
+	dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
+	if(!m_aSnapshots[SnapId])
+		return 0;
+	return m_aSnapshots[SnapId]->m_pSnap->NumItems();
+}
+
+void CClient::SnapSetStaticsize(int ItemType, int Size)
+{
+	m_SnapshotDelta.SetStaticsize(ItemType, Size);
+}
+
+
+void CClient::DebugRender()
+{
+	static NETSTATS Prev, Current;
+	static int64 LastSnap = 0;
+	static float FrameTimeAvg = 0;
+	int64 Now = time_get();
+	char aBuffer[512];
+
+	if(!g_Config.m_Debug)
+		return;
+
+	//m_pGraphics->BlendNormal();
+	Graphics()->TextureSet(m_DebugFont);
+	Graphics()->MapScreen(0,0,Graphics()->ScreenWidth(),Graphics()->ScreenHeight());
+
+	if(time_get()-LastSnap > time_freq())
+	{
+		LastSnap = time_get();
+		Prev = Current;
+		net_stats(&Current);
+	}
+
+	/*
+		eth = 14
+		ip = 20
+		udp = 8
+		total = 42
+	*/
+	FrameTimeAvg = FrameTimeAvg*0.9f + m_FrameTime*0.1f;
+	str_format(aBuffer, sizeof(aBuffer), "ticks: %8d %8d mem %dk %d  gfxmem: %dk  fps: %3d",
+		m_CurGameTick, m_PredTick,
+		mem_stats()->allocated/1024,
+		mem_stats()->total_allocations,
+		Graphics()->MemoryUsage()/1024,
+		(int)(1.0f/FrameTimeAvg));
+	Graphics()->QuadsText(2, 2, 16, 1,1,1,1, aBuffer);
+
+
+	{
+		int SendPackets = (Current.sent_packets-Prev.sent_packets);
+		int SendBytes = (Current.sent_bytes-Prev.sent_bytes);
+		int SendTotal = SendBytes + SendPackets*42;
+		int RecvPackets = (Current.recv_packets-Prev.recv_packets);
+		int RecvBytes = (Current.recv_bytes-Prev.recv_bytes);
+		int RecvTotal = RecvBytes + RecvPackets*42;
+
+		if(!SendPackets) SendPackets++;
+		if(!RecvPackets) RecvPackets++;
+		str_format(aBuffer, sizeof(aBuffer), "send: %3d %5d+%4d=%5d (%3d kbps) avg: %5d\nrecv: %3d %5d+%4d=%5d (%3d kbps) avg: %5d",
+			SendPackets, SendBytes, SendPackets*42, SendTotal, (SendTotal*8)/1024, SendBytes/SendPackets,
+			RecvPackets, RecvBytes, RecvPackets*42, RecvTotal, (RecvTotal*8)/1024, RecvBytes/RecvPackets);
+		Graphics()->QuadsText(2, 14, 16, 1,1,1,1, aBuffer);
+	}
+
+	// render rates
+	{
+		int y = 0;
+		int i;
+		for(i = 0; i < 256; i++)
+		{
+			if(m_SnapshotDelta.GetDataRate(i))
+			{
+				str_format(aBuffer, sizeof(aBuffer), "%4d %20s: %8d %8d %8d", i, GameClient()->GetItemName(i), m_SnapshotDelta.GetDataRate(i)/8, m_SnapshotDelta.GetDataUpdates(i),
+					(m_SnapshotDelta.GetDataRate(i)/m_SnapshotDelta.GetDataUpdates(i))/8);
+				Graphics()->QuadsText(2, 100+y*12, 16, 1,1,1,1, aBuffer);
+				y++;
+			}
+		}
+	}
+
+	str_format(aBuffer, sizeof(aBuffer), "pred: %d ms",
+		(int)((m_PredictedTime.Get(Now)-m_GameTime.Get(Now))*1000/(float)time_freq()));
+	Graphics()->QuadsText(2, 70, 16, 1,1,1,1, aBuffer);
+
+	// render graphs
+	if(g_Config.m_DbgGraphs)
+	{
+		//Graphics()->MapScreen(0,0,400.0f,300.0f);
+		float w = Graphics()->ScreenWidth()/4.0f;
+		float h = Graphics()->ScreenHeight()/6.0f;
+		float sp = Graphics()->ScreenWidth()/100.0f;
+		float x = Graphics()->ScreenWidth()-w-sp;
+
+		m_FpsGraph.ScaleMax();
+		m_FpsGraph.ScaleMin();
+		m_FpsGraph.Render(Graphics(), m_DebugFont, x, sp*5, w, h, "FPS");
+		m_InputtimeMarginGraph.Render(Graphics(), m_DebugFont, x, sp*5+h+sp, w, h, "Prediction Margin");
+		m_GametimeMarginGraph.Render(Graphics(), m_DebugFont, x, sp*5+h+sp+h+sp, w, h, "Gametime Margin");
+	}
+}
+
+void CClient::Quit()
+{
+	SetState(IClient::STATE_QUITING);
+}
+
+const char *CClient::ErrorString()
+{
+	return m_NetClient.ErrorString();
+}
+
+void CClient::Render()
+{
+	if(g_Config.m_GfxClear)
+		Graphics()->Clear(1,1,0);
+
+	GameClient()->OnRender();
+	DebugRender();
+}
+
+const char *CClient::LoadMap(const char *pName, const char *pFilename, unsigned WantedCrc)
+{
+	static char aErrorMsg[128];
+
+	SetState(IClient::STATE_LOADING);
+
+	if(!m_pMap->Load(pFilename))
+	{
+		str_format(aErrorMsg, sizeof(aErrorMsg), "map '%s' not found", pFilename);
+		return aErrorMsg;
+	}
+
+	// get the crc of the map
+	if(m_pMap->Crc() != WantedCrc)
+	{
+		m_pMap->Unload();
+		str_format(aErrorMsg, sizeof(aErrorMsg), "map differs from the server. %08x != %08x", m_pMap->Crc(), WantedCrc);
+		return aErrorMsg;
+	}
+
+	// stop demo recording if we loaded a new map
+	m_DemoRecorder.Stop();
+
+	dbg_msg("client", "loaded map '%s'", pFilename);
+	m_RecivedSnapshots = 0;
+
+	str_copy(m_aCurrentMap, pName, sizeof(m_aCurrentMap));
+	m_CurrentMapCrc = m_pMap->Crc();
+
+	return 0x0;
+}
+
+
+
+const char *CClient::LoadMapSearch(const char *pMapName, int WantedCrc)
+{
+	const char *pError = 0;
+	char aBuf[512];
+	dbg_msg("client", "loading map, map=%s wanted crc=%08x", pMapName, WantedCrc);
+	SetState(IClient::STATE_LOADING);
+
+	// try the normal maps folder
+	str_format(aBuf, sizeof(aBuf), "maps/%s.map", pMapName);
+	pError = LoadMap(pMapName, aBuf, WantedCrc);
+	if(!pError)
+		return pError;
+
+	// try the downloaded maps
+	str_format(aBuf, sizeof(aBuf), "downloadedmaps/%s_%08x.map", pMapName, WantedCrc);
+	pError = LoadMap(pMapName, aBuf, WantedCrc);
+	return pError;
+}
+
+int CClient::PlayerScoreComp(const void *a, const void *b)
+{
+	CServerInfo::CPlayer *p0 = (CServerInfo::CPlayer *)a;
+	CServerInfo::CPlayer *p1 = (CServerInfo::CPlayer *)b;
+	if(p0->m_Score == p1->m_Score)
+		return 0;
+	if(p0->m_Score < p1->m_Score)
+		return 1;
+	return -1;
+}
+
+void CClient::ProcessPacket(CNetChunk *pPacket)
+{
+	if(pPacket->m_ClientID == -1)
+	{
+		// connectionlesss
+		if(pPacket->m_DataSize == (int)(sizeof(VERSIONSRV_VERSION) + sizeof(VERSION_DATA)) &&
+			mem_comp(pPacket->m_pData, VERSIONSRV_VERSION, sizeof(VERSIONSRV_VERSION)) == 0)
+		{
+			unsigned char *pVersionData = (unsigned char*)pPacket->m_pData + sizeof(VERSIONSRV_VERSION);
+			int VersionMatch = !mem_comp(pVersionData, VERSION_DATA, sizeof(VERSION_DATA));
+
+			dbg_msg("client/version", "version does %s (%d.%d.%d)",
+				VersionMatch ? "match" : "NOT match",
+				pVersionData[1], pVersionData[2], pVersionData[3]);
+
+			// assume version is out of date when version-data doesn't match
+			if (!VersionMatch)
+			{
+				str_format(m_aVersionStr, sizeof(m_aVersionStr), "%d.%d.%d", pVersionData[1], pVersionData[2], pVersionData[3]);
+			}
+		}
+
+		if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_LIST) &&
+			mem_comp(pPacket->m_pData, SERVERBROWSE_LIST, sizeof(SERVERBROWSE_LIST)) == 0)
+		{
+			int Size = pPacket->m_DataSize-sizeof(SERVERBROWSE_LIST);
+			int Num = Size/sizeof(MASTERSRV_ADDR);
+			MASTERSRV_ADDR *pAddrs = (MASTERSRV_ADDR *)((char*)pPacket->m_pData+sizeof(SERVERBROWSE_LIST));
+			int i;
+
+			for(i = 0; i < Num; i++)
+			{
+				NETADDR Addr;
+
+				// convert address
+				mem_zero(&Addr, sizeof(Addr));
+				Addr.type = NETTYPE_IPV4;
+				Addr.ip[0] = pAddrs[i].m_aIp[0];
+				Addr.ip[1] = pAddrs[i].m_aIp[1];
+				Addr.ip[2] = pAddrs[i].m_aIp[2];
+				Addr.ip[3] = pAddrs[i].m_aIp[3];
+				Addr.port = (pAddrs[i].m_aPort[1]<<8) | pAddrs[i].m_aPort[0];
+
+				m_ServerBrowser.Set(Addr, IServerBrowser::SET_MASTER_ADD, -1, 0x0);
+			}
+		}
+
+		{
+			int PacketType = 0;
+			if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_INFO) && mem_comp(pPacket->m_pData, SERVERBROWSE_INFO, sizeof(SERVERBROWSE_INFO)) == 0)
+				PacketType = 2;
+
+			if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_OLD_INFO) && mem_comp(pPacket->m_pData, SERVERBROWSE_OLD_INFO, sizeof(SERVERBROWSE_OLD_INFO)) == 0)
+				PacketType = 1;
+
+			if(PacketType)
+			{
+				// we got ze info
+				CUnpacker Up;
+				CServerInfo Info = {0};
+				int Token = -1;
+
+				Up.Reset((unsigned char*)pPacket->m_pData+sizeof(SERVERBROWSE_INFO), pPacket->m_DataSize-sizeof(SERVERBROWSE_INFO));
+				if(PacketType >= 2)
+					Token = str_toint(Up.GetString());
+				str_copy(Info.m_aVersion, Up.GetString(), sizeof(Info.m_aVersion));
+				str_copy(Info.m_aName, Up.GetString(), sizeof(Info.m_aName));
+				str_copy(Info.m_aMap, Up.GetString(), sizeof(Info.m_aMap));
+				str_copy(Info.m_aGameType, Up.GetString(), sizeof(Info.m_aGameType));
+				Info.m_Flags = str_toint(Up.GetString());
+				Info.m_Progression = str_toint(Up.GetString());
+				Info.m_NumPlayers = str_toint(Up.GetString());
+				Info.m_MaxPlayers = str_toint(Up.GetString());
+
+				// don't add invalid info to the server browser list
+				if(Info.m_NumPlayers > MAX_CLIENTS || Info.m_MaxPlayers > MAX_CLIENTS)
+					return;
+
+				str_format(Info.m_aAddress, sizeof(Info.m_aAddress), "%d.%d.%d.%d:%d",
+					pPacket->m_Address.ip[0], pPacket->m_Address.ip[1], pPacket->m_Address.ip[2],
+					pPacket->m_Address.ip[3], pPacket->m_Address.port);
+
+				for(int i = 0; i < Info.m_NumPlayers; i++)
+				{
+					str_copy(Info.m_aPlayers[i].m_aName, Up.GetString(), sizeof(Info.m_aPlayers[i].m_aName));
+					Info.m_aPlayers[i].m_Score = str_toint(Up.GetString());
+				}
+
+				if(!Up.Error())
+				{
+					// sort players
+					qsort(Info.m_aPlayers, Info.m_NumPlayers, sizeof(*Info.m_aPlayers), PlayerScoreComp);
+
+					if(net_addr_comp(&m_ServerAddress, &pPacket->m_Address) == 0)
+					{
+						mem_copy(&m_CurrentServerInfo, &Info, sizeof(m_CurrentServerInfo));
+						m_CurrentServerInfo.m_NetAddr = m_ServerAddress;
+						m_CurrentServerInfoRequestTime = -1;
+					}
+					else
+					{
+						if(PacketType == 2)
+							m_ServerBrowser.Set(pPacket->m_Address, IServerBrowser::SET_TOKEN, Token, &Info);
+						else
+							m_ServerBrowser.Set(pPacket->m_Address, IServerBrowser::SET_OLD_INTERNET, -1, &Info);
+					}
+				}
+			}
+		}
+	}
+	else
+	{
+		CUnpacker Unpacker;
+		Unpacker.Reset(pPacket->m_pData, pPacket->m_DataSize);
+
+		// unpack msgid and system flag
+		int Msg = Unpacker.GetInt();
+		int Sys = Msg&1;
+		Msg >>= 1;
+
+		if(Unpacker.Error())
+			return;
+
+		if(Sys)
+		{
+			// system message
+			if(Msg == NETMSG_MAP_CHANGE)
+			{
+				const char *pMap = Unpacker.GetString();
+				int MapCrc = Unpacker.GetInt();
+				const char *pError = 0;
+
+				if(Unpacker.Error())
+					return;
+
+				for(int i = 0; pMap[i]; i++) // protect the player from nasty map names
+				{
+					if(pMap[i] == '/' || pMap[i] == '\\')
+						pError = "strange character in map name";
+				}
+
+				if(pError)
+					DisconnectWithReason(pError);
+				else
+				{
+					pError = LoadMapSearch(pMap, MapCrc);
+
+					if(!pError)
+					{
+						dbg_msg("client/network", "loading done");
+						SendReady();
+						GameClient()->OnConnected();
+					}
+					else
+					{
+						str_format(m_aMapdownloadFilename, sizeof(m_aMapdownloadFilename), "downloadedmaps/%s_%08x.map", pMap, MapCrc);
+
+						dbg_msg("client/network", "starting to download map to '%s'", m_aMapdownloadFilename);
+
+						m_MapdownloadChunk = 0;
+						str_copy(m_aMapdownloadName, pMap, sizeof(m_aMapdownloadName));
+						m_MapdownloadFile = Storage()->OpenFile(m_aMapdownloadFilename, IOFLAG_WRITE);
+						m_MapdownloadCrc = MapCrc;
+						m_MapdownloadTotalsize = -1;
+						m_MapdownloadAmount = 0;
+
+						CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA);
+						Msg.AddInt(m_MapdownloadChunk);
+						SendMsgEx(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
+
+						if(g_Config.m_Debug)
+							dbg_msg("client/network", "requested chunk %d", m_MapdownloadChunk);
+					}
+				}
+			}
+			else if(Msg == NETMSG_MAP_DATA)
+			{
+				int Last = Unpacker.GetInt();
+				int TotalSize = Unpacker.GetInt();
+				int Size = Unpacker.GetInt();
+				const unsigned char *pData = Unpacker.GetRaw(Size);
+
+				// check fior errors
+				if(Unpacker.Error() || Size <= 0 || TotalSize <= 0 || !m_MapdownloadFile)
+					return;
+
+				io_write(m_MapdownloadFile, pData, Size);
+
+				m_MapdownloadTotalsize = TotalSize;
+				m_MapdownloadAmount += Size;
+
+				if(Last)
+				{
+					const char *pError;
+					dbg_msg("client/network", "download complete, loading map");
+
+					io_close(m_MapdownloadFile);
+					m_MapdownloadFile = 0;
+					m_MapdownloadAmount = 0;
+					m_MapdownloadTotalsize = -1;
+
+					// load map
+					pError = LoadMap(m_aMapdownloadName, m_aMapdownloadFilename, m_MapdownloadCrc);
+					if(!pError)
+					{
+						dbg_msg("client/network", "loading done");
+						SendReady();
+						GameClient()->OnConnected();
+					}
+					else
+						DisconnectWithReason(pError);
+				}
+				else
+				{
+					// request new chunk
+					m_MapdownloadChunk++;
+
+					CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA);
+					Msg.AddInt(m_MapdownloadChunk);
+					SendMsgEx(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
+
+					if(g_Config.m_Debug)
+						dbg_msg("client/network", "requested chunk %d", m_MapdownloadChunk);
+				}
+			}
+			else if(Msg == NETMSG_PING)
+			{
+				CMsgPacker Msg(NETMSG_PING_REPLY);
+				SendMsgEx(&Msg, 0);
+			}
+			else if(Msg == NETMSG_RCON_AUTH_STATUS)
+			{
+				int Result = Unpacker.GetInt();
+				if(Unpacker.Error() == 0)
+					m_RconAuthed = Result;
+			}
+			else if(Msg == NETMSG_RCON_LINE)
+			{
+				const char *pLine = Unpacker.GetString();
+				if(Unpacker.Error() == 0)
+				{
+					//dbg_msg("remote", "%s", line);
+					GameClient()->OnRconLine(pLine);
+				}
+			}
+			else if(Msg == NETMSG_PING_REPLY)
+				dbg_msg("client/network", "latency %.2f", (time_get() - m_PingStartTime)*1000 / (float)time_freq());
+			else if(Msg == NETMSG_INPUTTIMING)
+			{
+				int InputPredTick = Unpacker.GetInt();
+				int TimeLeft = Unpacker.GetInt();
+
+				// adjust our prediction time
+				int64 Target = 0;
+				for(int k = 0; k < 200; k++)
+				{
+					if(m_aInputs[k].m_Tick == InputPredTick)
+					{
+						Target = m_aInputs[k].m_PredictedTime + (time_get() - m_aInputs[k].m_Time);
+						Target = Target - (int64)(((TimeLeft-PREDICTION_MARGIN)/1000.0f)*time_freq());
+						//st_update(&predicted_time, );
+						break;
+					}
+				}
+
+				if(Target)
+					m_PredictedTime.Update(&m_InputtimeMarginGraph, Target, TimeLeft, 1);
+			}
+			else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY)
+			{
+				//dbg_msg("client/network", "got snapshot");
+				int NumParts = 1;
+				int Part = 0;
+				int GameTick = Unpacker.GetInt();
+				int DeltaTick = GameTick-Unpacker.GetInt();
+				int PartSize = 0;
+				int Crc = 0;
+				int CompleteSize = 0;
+				const char *pData = 0;
+
+				// we are not allowed to process snapshot yet
+				if(State() < IClient::STATE_LOADING)
+					return;
+
+				if(Msg == NETMSG_SNAP)
+				{
+					NumParts = Unpacker.GetInt();
+					Part = Unpacker.GetInt();
+				}
+
+				if(Msg != NETMSG_SNAPEMPTY)
+				{
+					Crc = Unpacker.GetInt();
+					PartSize = Unpacker.GetInt();
+				}
+
+				pData = (const char *)Unpacker.GetRaw(PartSize);
+
+				if(Unpacker.Error())
+					return;
+
+				if(GameTick >= m_CurrentRecvTick)
+				{
+					if(GameTick != m_CurrentRecvTick)
+					{
+						m_SnapshotParts = 0;
+						m_CurrentRecvTick = GameTick;
+					}
+
+					// TODO: clean this up abit
+					mem_copy((char*)m_aSnapshotIncommingData + Part*MAX_SNAPSHOT_PACKSIZE, pData, PartSize);
+					m_SnapshotParts |= 1<<Part;
+
+					if(m_SnapshotParts == (unsigned)((1<<NumParts)-1))
+					{
+						static CSnapshot Emptysnap;
+						CSnapshot *pDeltaShot = &Emptysnap;
+						int PurgeTick;
+						void *pDeltaData;
+						int DeltaSize;
+						unsigned char aTmpBuffer2[CSnapshot::MAX_SIZE];
+						unsigned char aTmpBuffer3[CSnapshot::MAX_SIZE];
+						int SnapSize;
+
+						CompleteSize = (NumParts-1) * MAX_SNAPSHOT_PACKSIZE + PartSize;
+
+						// reset snapshoting
+						m_SnapshotParts = 0;
+
+						// find snapshot that we should use as delta
+						Emptysnap.Clear();
+
+						// find delta
+						if(DeltaTick >= 0)
+						{
+							int DeltashotSize = m_SnapshotStorage.Get(DeltaTick, 0, &pDeltaShot, 0);
+
+							if(DeltashotSize < 0)
+							{
+								// couldn't find the delta snapshots that the server used
+								// to compress this snapshot. force the server to resync
+								if(g_Config.m_Debug)
+									dbg_msg("client", "error, couldn't find the delta snapshot");
+
+								// ack snapshot
+								// TODO: combine this with the input message
+								m_AckGameTick = -1;
+								return;
+							}
+						}
+
+						// decompress snapshot
+						pDeltaData = m_SnapshotDelta.EmptyDelta();
+						DeltaSize = sizeof(int)*3;
+
+						if(CompleteSize)
+						{
+							int IntSize = CVariableInt::Decompress(m_aSnapshotIncommingData, CompleteSize, aTmpBuffer2);
+
+							if(IntSize < 0) // failure during decompression, bail
+								return;
+
+							pDeltaData = aTmpBuffer2;
+							DeltaSize = IntSize;
+						}
+
+						// unpack delta
+						PurgeTick = DeltaTick;
+						SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, (CSnapshot*)aTmpBuffer3, pDeltaData, DeltaSize);
+						if(SnapSize < 0)
+						{
+							dbg_msg("client", "delta unpack failed!");
+							return;
+						}
+
+						if(Msg != NETMSG_SNAPEMPTY && ((CSnapshot*)aTmpBuffer3)->Crc() != Crc)
+						{
+							if(g_Config.m_Debug)
+							{
+								dbg_msg("client", "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d",
+									m_SnapCrcErrors, GameTick, Crc, ((CSnapshot*)aTmpBuffer3)->Crc(), CompleteSize, DeltaTick);
+							}
+
+							m_SnapCrcErrors++;
+							if(m_SnapCrcErrors > 10)
+							{
+								// to many errors, send reset
+								m_AckGameTick = -1;
+								SendInput();
+								m_SnapCrcErrors = 0;
+							}
+							return;
+						}
+						else
+						{
+							if(m_SnapCrcErrors)
+								m_SnapCrcErrors--;
+						}
+
+						// purge old snapshots
+						PurgeTick = DeltaTick;
+						if(m_aSnapshots[SNAP_PREV] && m_aSnapshots[SNAP_PREV]->m_Tick < PurgeTick)
+							PurgeTick = m_aSnapshots[SNAP_PREV]->m_Tick;
+						if(m_aSnapshots[SNAP_CURRENT] && m_aSnapshots[SNAP_CURRENT]->m_Tick < PurgeTick)
+							PurgeTick = m_aSnapshots[SNAP_PREV]->m_Tick;
+						m_SnapshotStorage.PurgeUntil(PurgeTick);
+
+						// add new
+						m_SnapshotStorage.Add(GameTick, time_get(), SnapSize, (CSnapshot*)aTmpBuffer3, 1);
+
+						// add snapshot to demo
+						if(m_DemoRecorder.IsRecording())
+						{
+
+							// write tick marker
+							/*
+							DEMOREC_TICKMARKER marker;
+							marker.tick = game_tick;
+							swap_endian(&marker, sizeof(int), sizeof(marker)/sizeof(int));
+							demorec_record_write("TICK", sizeof(marker), &marker);
+							demorec_record_write("SNAP", snapsize, tmpbuffer3);
+							*/
+
+							// write snapshot
+							m_DemoRecorder.RecordSnapshot(GameTick, aTmpBuffer3, SnapSize);
+						}
+
+						// apply snapshot, cycle pointers
+						m_RecivedSnapshots++;
+
+						m_CurrentRecvTick = GameTick;
+
+						// we got two snapshots until we see us self as connected
+						if(m_RecivedSnapshots == 2)
+						{
+							// start at 200ms and work from there
+							m_PredictedTime.Init(GameTick*time_freq()/50);
+							m_PredictedTime.SetAdjustSpeed(1, 1000.0f);
+							m_GameTime.Init((GameTick-1)*time_freq()/50);
+							m_aSnapshots[SNAP_PREV] = m_SnapshotStorage.m_pFirst;
+							m_aSnapshots[SNAP_CURRENT] = m_SnapshotStorage.m_pLast;
+							m_LocalStartTime = time_get();
+							SetState(IClient::STATE_ONLINE);
+						}
+
+						// adjust game time
+						{
+							int64 Now = m_GameTime.Get(time_get());
+							int64 TickStart = GameTick*time_freq()/50;
+							int64 TimeLeft = (TickStart-Now)*1000 / time_freq();
+							//st_update(&game_time, (game_tick-1)*time_freq()/50);
+							m_GameTime.Update(&m_GametimeMarginGraph, (GameTick-1)*time_freq()/50, TimeLeft, 0);
+						}
+
+						// ack snapshot
+						m_AckGameTick = GameTick;
+					}
+				}
+			}
+		}
+		else
+		{
+			// game message
+			if(m_DemoRecorder.IsRecording())
+				m_DemoRecorder.RecordMessage(pPacket->m_pData, pPacket->m_DataSize);
+				// demorec_record_write("MESG", pPacket->data_size, );
+
+			GameClient()->OnMessage(Msg, &Unpacker);
+		}
+	}
+}
+
+void CClient::PumpNetwork()
+{
+	m_NetClient.Update();
+
+	if(State() != IClient::STATE_DEMOPLAYBACK)
+	{
+		// check for errors
+		if(State() != IClient::STATE_OFFLINE && m_NetClient.State() == NETSTATE_OFFLINE)
+		{
+			SetState(IClient::STATE_OFFLINE);
+			Disconnect();
+			dbg_msg("client", "offline error='%s'", m_NetClient.ErrorString());
+		}
+
+		//
+		if(State() == IClient::STATE_CONNECTING && m_NetClient.State() == NETSTATE_ONLINE)
+		{
+			// we switched to online
+			dbg_msg("client", "connected, sending info");
+			SetState(IClient::STATE_LOADING);
+			SendInfo();
+		}
+	}
+
+	// process packets
+	CNetChunk Packet;
+	while(m_NetClient.Recv(&Packet))
+		ProcessPacket(&Packet);
+}
+
+void CClient::OnDemoPlayerSnapshot(void *pData, int Size)
+{
+	// update ticks, they could have changed
+	const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
+	CSnapshotStorage::CHolder *pTemp;
+	m_CurGameTick = pInfo->m_Info.m_CurrentTick;
+	m_PrevGameTick = pInfo->m_PreviousTick;
+
+	// handle snapshots
+	pTemp = m_aSnapshots[SNAP_PREV];
+	m_aSnapshots[SNAP_PREV] = m_aSnapshots[SNAP_CURRENT];
+	m_aSnapshots[SNAP_CURRENT] = pTemp;
+
+	mem_copy(m_aSnapshots[SNAP_CURRENT]->m_pSnap, pData, Size);
+	mem_copy(m_aSnapshots[SNAP_CURRENT]->m_pAltSnap, pData, Size);
+
+	GameClient()->OnNewSnapshot();
+}
+
+void CClient::OnDemoPlayerMessage(void *pData, int Size)
+{
+	CUnpacker Unpacker;
+	Unpacker.Reset(pData, Size);
+
+	// unpack msgid and system flag
+	int Msg = Unpacker.GetInt();
+	int Sys = Msg&1;
+	Msg >>= 1;
+
+	if(Unpacker.Error())
+		return;
+
+	if(!Sys)
+		GameClient()->OnMessage(Msg, &Unpacker);
+}
+/*
+const IDemoPlayer::CInfo *client_demoplayer_getinfo()
+{
+	static DEMOPLAYBACK_INFO ret;
+	const DEMOREC_PLAYBACKINFO *info = m_DemoPlayer.Info();
+	ret.first_tick = info->first_tick;
+	ret.last_tick = info->last_tick;
+	ret.current_tick = info->current_tick;
+	ret.paused = info->paused;
+	ret.speed = info->speed;
+	return &ret;
+}*/
+
+/*
+void DemoPlayer()->SetPos(float percent)
+{
+	demorec_playback_set(percent);
+}
+
+void DemoPlayer()->SetSpeed(float speed)
+{
+	demorec_playback_setspeed(speed);
+}
+
+void DemoPlayer()->SetPause(int paused)
+{
+	if(paused)
+		demorec_playback_pause();
+	else
+		demorec_playback_unpause();
+}*/
+
+void CClient::Update()
+{
+	if(State() == IClient::STATE_DEMOPLAYBACK)
+	{
+		m_DemoPlayer.Update();
+		if(m_DemoPlayer.IsPlaying())
+		{
+			// update timers
+			const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
+			m_CurGameTick = pInfo->m_Info.m_CurrentTick;
+			m_PrevGameTick = pInfo->m_PreviousTick;
+			m_GameIntraTick = pInfo->m_IntraTick;
+			m_GameTickTime = pInfo->m_TickTime;
+		}
+		else
+		{
+			// disconnect on error
+			Disconnect();
+		}
+	}
+	else if(State() != IClient::STATE_OFFLINE && m_RecivedSnapshots >= 3)
+	{
+		// switch snapshot
+		int Repredict = 0;
+		int64 Freq = time_freq();
+		int64 Now = m_GameTime.Get(time_get());
+		int64 PredNow = m_PredictedTime.Get(time_get());
+
+		while(1)
+		{
+			CSnapshotStorage::CHolder *pCur = m_aSnapshots[SNAP_CURRENT];
+			int64 TickStart = (pCur->m_Tick)*time_freq()/50;
+
+			if(TickStart < Now)
+			{
+				CSnapshotStorage::CHolder *pNext = m_aSnapshots[SNAP_CURRENT]->m_pNext;
+				if(pNext)
+				{
+					m_aSnapshots[SNAP_PREV] = m_aSnapshots[SNAP_CURRENT];
+					m_aSnapshots[SNAP_CURRENT] = pNext;
+
+					// set ticks
+					m_CurGameTick = m_aSnapshots[SNAP_CURRENT]->m_Tick;
+					m_PrevGameTick = m_aSnapshots[SNAP_PREV]->m_Tick;
+
+					if(m_aSnapshots[SNAP_CURRENT] && m_aSnapshots[SNAP_PREV])
+					{
+						GameClient()->OnNewSnapshot();
+						Repredict = 1;
+					}
+				}
+				else
+					break;
+			}
+			else
+				break;
+		}
+
+		if(m_aSnapshots[SNAP_CURRENT] && m_aSnapshots[SNAP_PREV])
+		{
+			int64 CurtickStart = (m_aSnapshots[SNAP_CURRENT]->m_Tick)*time_freq()/50;
+			int64 PrevtickStart = (m_aSnapshots[SNAP_PREV]->m_Tick)*time_freq()/50;
+			//tg_add(&predicted_time_graph, pred_now, 0);
+			int PrevPredTick = (int)(PredNow*50/time_freq());
+			int NewPredTick = PrevPredTick+1;
+			static float LastPredintra = 0;
+
+			m_GameIntraTick = (Now - PrevtickStart) / (float)(CurtickStart-PrevtickStart);
+			m_GameTickTime = (Now - PrevtickStart) / (float)Freq; //(float)SERVER_TICK_SPEED);
+
+			CurtickStart = NewPredTick*time_freq()/50;
+			PrevtickStart = PrevPredTick*time_freq()/50;
+			m_PredIntraTick = (PredNow - PrevtickStart) / (float)(CurtickStart-PrevtickStart);
+
+			if(NewPredTick < m_aSnapshots[SNAP_PREV]->m_Tick-SERVER_TICK_SPEED || NewPredTick > m_aSnapshots[SNAP_PREV]->m_Tick+SERVER_TICK_SPEED)
+			{
+				dbg_msg("client", "prediction time reset!");
+				m_PredictedTime.Init(m_aSnapshots[SNAP_CURRENT]->m_Tick*time_freq()/50);
+			}
+
+			if(NewPredTick > m_PredTick)
+			{
+				LastPredintra = m_PredIntraTick;
+				m_PredTick = NewPredTick;
+				Repredict = 1;
+
+				// send input
+				SendInput();
+			}
+
+			LastPredintra = m_PredIntraTick;
+		}
+
+		// only do sane predictions
+		if(Repredict)
+		{
+			if(m_PredTick > m_CurGameTick && m_PredTick < m_CurGameTick+50)
+				GameClient()->OnPredict();
+		}
+
+		// fetch server info if we don't have it
+		if(State() >= IClient::STATE_LOADING &&
+			m_CurrentServerInfoRequestTime >= 0 &&
+			time_get() > m_CurrentServerInfoRequestTime)
+		{
+			m_ServerBrowser.Request(m_ServerAddress);
+			m_CurrentServerInfoRequestTime = time_get()+time_freq()*2;
+		}
+	}
+
+	// STRESS TEST: join the server again
+	if(g_Config.m_DbgStress)
+	{
+		static int64 ActionTaken = 0;
+		int64 Now = time_get();
+		if(State() == IClient::STATE_OFFLINE)
+		{
+			if(Now > ActionTaken+time_freq()*2)
+			{
+				dbg_msg("stress", "reconnecting!");
+				Connect(g_Config.m_DbgStressServer);
+				ActionTaken = Now;
+			}
+		}
+		else
+		{
+			/*if(now > action_taken+time_freq()*(10+config.dbg_stress))
+			{
+				dbg_msg("stress", "disconnecting!");
+				Disconnect();
+				action_taken = now;
+			}*/
+		}
+	}
+
+	// pump the network
+	PumpNetwork();
+
+	// update the maser server registry
+	MasterServer()->Update();
+
+	// update the server browser
+	m_ServerBrowser.Update();
+}
+
+const char *CClient::UserDirectory()
+{
+	static char saPath[1024] = {0};
+	fs_storage_path("Teeworlds", saPath, sizeof(saPath));
+	return saPath;
+}
+
+void CClient::VersionUpdate()
+{
+	if(m_VersionInfo.m_State == 0)
+	{
+		m_Engine.HostLookup(&m_VersionInfo.m_VersionServeraddr, g_Config.m_ClVersionServer);
+		m_VersionInfo.m_State++;
+	}
+	else if(m_VersionInfo.m_State == 1)
+	{
+		if(m_VersionInfo.m_VersionServeraddr.m_Job.Status() == CJob::STATE_DONE)
+		{
+			CNetChunk Packet;
+
+			mem_zero(&Packet, sizeof(Packet));
+
+			m_VersionInfo.m_VersionServeraddr.m_Addr.port = VERSIONSRV_PORT;
+
+			Packet.m_ClientID = -1;
+			Packet.m_Address = m_VersionInfo.m_VersionServeraddr.m_Addr;
+			Packet.m_pData = VERSIONSRV_GETVERSION;
+			Packet.m_DataSize = sizeof(VERSIONSRV_GETVERSION);
+			Packet.m_Flags = NETSENDFLAG_CONNLESS;
+
+			m_NetClient.Send(&Packet);
+			m_VersionInfo.m_State++;
+		}
+	}
+}
+
+void CClient::InitEngine(const char *pAppname)
+{
+	m_Engine.Init(pAppname);
+}
+
+void CClient::RegisterInterfaces()
+{
+	Kernel()->RegisterInterface(static_cast<IDemoPlayer*>(&m_DemoPlayer));
+	Kernel()->RegisterInterface(static_cast<IServerBrowser*>(&m_ServerBrowser));
+}
+
+void CClient::InitInterfaces()
+{
+	// fetch interfaces
+	m_pEditor = Kernel()->RequestInterface<IEditor>();
+	m_pGraphics = Kernel()->RequestInterface<IEngineGraphics>();
+	m_pSound = Kernel()->RequestInterface<IEngineSound>();
+	m_pGameClient = Kernel()->RequestInterface<IGameClient>();
+	m_pInput = Kernel()->RequestInterface<IEngineInput>();
+	m_pMap = Kernel()->RequestInterface<IEngineMap>();
+	m_pMasterServer = Kernel()->RequestInterface<IEngineMasterServer>();
+	m_pStorage = Kernel()->RequestInterface<IStorage>();
+
+	//
+	m_ServerBrowser.SetBaseInfo(&m_NetClient, m_pGameClient->NetVersion());
+}
+
+void CClient::Run()
+{
+	int64 ReportTime = time_get();
+	int64 ReportInterval = time_freq()*1;
+
+	m_LocalStartTime = time_get();
+	m_SnapshotParts = 0;
+
+	// init graphics
+	if(m_pGraphics->Init() != 0)
+		return;
+
+	// init font rendering
+	Kernel()->RequestInterface<IEngineTextRender>()->Init();
+
+	// init the input
+	Input()->Init();
+
+	// start refreshing addresses while we load
+	MasterServer()->RefreshAddresses();
+
+	// init the editor
+	m_pEditor->Init();
+
+	// init sound, allowed to fail
+	Sound()->Init();
+
+	// load data
+	if(!LoadData())
+		return;
+
+	GameClient()->OnInit();
+	dbg_msg("client", "version %s", GameClient()->NetVersion());
+
+	// open socket
+	{
+		NETADDR BindAddr;
+		mem_zero(&BindAddr, sizeof(BindAddr));
+		m_NetClient.Open(BindAddr, 0);
+	}
+
+	// connect to the server if wanted
+	/*
+	if(config.cl_connect[0] != 0)
+		Connect(config.cl_connect);
+	config.cl_connect[0] = 0;
+	*/
+
+	//
+	m_FpsGraph.Init(0.0f, 200.0f);
+
+	// never start with the editor
+	g_Config.m_ClEditor = 0;
+
+	Input()->MouseModeRelative();
+
+	while (1)
+	{
+		int64 FrameStartTime = time_get();
+		m_Frames++;
+
+		//
+		VersionUpdate();
+
+		// handle pending connects
+		if(m_aCmdConnect[0])
+		{
+			str_copy(g_Config.m_UiServerAddress, m_aCmdConnect, sizeof(g_Config.m_UiServerAddress));
+			Connect(m_aCmdConnect);
+			m_aCmdConnect[0] = 0;
+		}
+
+		// update input
+		Input()->Update();
+
+		// update sound
+		Sound()->Update();
+
+		// release focus
+		if(!m_pGraphics->WindowActive())
+		{
+			if(m_WindowMustRefocus == 0)
+				Input()->MouseModeAbsolute();
+			m_WindowMustRefocus = 1;
+		}
+		else if (g_Config.m_DbgFocus && Input()->KeyPressed(KEY_ESCAPE))
+		{
+			Input()->MouseModeAbsolute();
+			m_WindowMustRefocus = 1;
+		}
+
+		// refocus
+		if(m_WindowMustRefocus && m_pGraphics->WindowActive())
+		{
+			if(m_WindowMustRefocus < 3)
+			{
+				Input()->MouseModeAbsolute();
+				m_WindowMustRefocus++;
+			}
+
+			if(Input()->KeyPressed(KEY_MOUSE_1))
+			{
+				Input()->MouseModeRelative();
+				m_WindowMustRefocus = 0;
+			}
+		}
+
+		// panic quit button
+		if(Input()->KeyPressed(KEY_LCTRL) && Input()->KeyPressed(KEY_LSHIFT) && Input()->KeyPressed('q'))
+			break;
+
+		if(Input()->KeyPressed(KEY_LCTRL) && Input()->KeyPressed(KEY_LSHIFT) && Input()->KeyDown('d'))
+			g_Config.m_Debug ^= 1;
+
+		if(Input()->KeyPressed(KEY_LCTRL) && Input()->KeyPressed(KEY_LSHIFT) && Input()->KeyDown('g'))
+			g_Config.m_DbgGraphs ^= 1;
+
+		if(Input()->KeyPressed(KEY_LCTRL) && Input()->KeyPressed(KEY_LSHIFT) && Input()->KeyDown('e'))
+		{
+			g_Config.m_ClEditor = g_Config.m_ClEditor^1;
+			Input()->MouseModeRelative();
+		}
+
+		/*
+		if(!gfx_window_open())
+			break;
+		*/
+
+		// render
+		if(g_Config.m_ClEditor)
+		{
+			Update();
+			m_pEditor->UpdateAndRender();
+			m_pGraphics->Swap();
+		}
+		else
+		{
+			Update();
+
+			if(g_Config.m_DbgStress)
+			{
+				if((m_Frames%10) == 0)
+				{
+					Render();
+					m_pGraphics->Swap();
+				}
+			}
+			else
+			{
+				Render();
+				m_pGraphics->Swap();
+			}
+		}
+
+		// check conditions
+		if(State() == IClient::STATE_QUITING)
+			break;
+
+		// beNice
+		if(g_Config.m_DbgStress)
+			thread_sleep(5);
+		else if(g_Config.m_ClCpuThrottle || !m_pGraphics->WindowActive())
+			thread_sleep(1);
+
+		if(g_Config.m_DbgHitch)
+		{
+			thread_sleep(g_Config.m_DbgHitch);
+			g_Config.m_DbgHitch = 0;
+		}
+
+		if(ReportTime < time_get())
+		{
+			if(0 && g_Config.m_Debug)
+			{
+				dbg_msg("client/report", "fps=%.02f (%.02f %.02f) netstate=%d",
+					m_Frames/(float)(ReportInterval/time_freq()),
+					1.0f/m_FrameTimeHigh,
+					1.0f/m_FrameTimeLow,
+					m_NetClient.State());
+			}
+			m_FrameTimeLow = 1;
+			m_FrameTimeHigh = 0;
+			m_Frames = 0;
+			ReportTime += ReportInterval;
+		}
+
+		// update frametime
+		m_FrameTime = (time_get()-FrameStartTime)/(float)time_freq();
+		if(m_FrameTime < m_FrameTimeLow)
+			m_FrameTimeLow = m_FrameTime;
+		if(m_FrameTime > m_FrameTimeHigh)
+			m_FrameTimeHigh = m_FrameTime;
+
+		m_LocalTime = (time_get()-m_LocalStartTime)/(float)time_freq();
+
+		m_FpsGraph.Add(1.0f/m_FrameTime, 1,1,1);
+	}
+
+	GameClient()->OnShutdown();
+	Disconnect();
+
+	m_pGraphics->Shutdown();
+	m_pSound->Shutdown();
+}
+
+
+void CClient::Con_Connect(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	str_copy(pSelf->m_aCmdConnect, pResult->GetString(0), sizeof(pSelf->m_aCmdConnect));
+}
+
+void CClient::Con_Disconnect(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->Disconnect();
+}
+
+void CClient::Con_Quit(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->Quit();
+}
+
+void CClient::Con_Minimize(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->Graphics()->Minimize();
+}
+
+void CClient::Con_Ping(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+
+	CMsgPacker Msg(NETMSG_PING);
+	pSelf->SendMsgEx(&Msg, 0);
+	pSelf->m_PingStartTime = time_get();
+}
+
+void CClient::Con_Screenshot(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->Graphics()->TakeScreenshot();
+}
+
+void CClient::Con_Rcon(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->Rcon(pResult->GetString(0));
+}
+
+void CClient::Con_RconAuth(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->RconAuth("", pResult->GetString(0));
+}
+
+void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	NETADDR Addr;
+	if(net_addr_from_str(&Addr, pResult->GetString(0)) == 0)
+		pSelf->m_ServerBrowser.AddFavorite(Addr);
+}
+
+const char *CClient::DemoPlayer_Play(const char *pFilename)
+{
+	int Crc;
+	const char *pError;
+	Disconnect();
+	m_NetClient.ResetErrorString();
+
+	// try to start playback
+	m_DemoPlayer.SetListner(this);
+
+	if(m_DemoPlayer.Load(Storage(), pFilename))
+		return "error loading demo";
+
+	// load map
+	Crc = (m_DemoPlayer.Info()->m_Header.m_aCrc[0]<<24)|
+		(m_DemoPlayer.Info()->m_Header.m_aCrc[1]<<16)|
+		(m_DemoPlayer.Info()->m_Header.m_aCrc[2]<<8)|
+		(m_DemoPlayer.Info()->m_Header.m_aCrc[3]);
+	pError = LoadMapSearch(m_DemoPlayer.Info()->m_Header.m_aMap, Crc);
+	if(pError)
+	{
+		DisconnectWithReason(pError);
+		return pError;
+	}
+
+	GameClient()->OnConnected();
+
+	// setup buffers
+	mem_zero(m_aDemorecSnapshotData, sizeof(m_aDemorecSnapshotData));
+
+	m_aSnapshots[SNAP_CURRENT] = &m_aDemorecSnapshotHolders[SNAP_CURRENT];
+	m_aSnapshots[SNAP_PREV] = &m_aDemorecSnapshotHolders[SNAP_PREV];
+
+	m_aSnapshots[SNAP_CURRENT]->m_pSnap = (CSnapshot *)m_aDemorecSnapshotData[SNAP_CURRENT][0];
+	m_aSnapshots[SNAP_CURRENT]->m_pAltSnap = (CSnapshot *)m_aDemorecSnapshotData[SNAP_CURRENT][1];
+	m_aSnapshots[SNAP_CURRENT]->m_SnapSize = 0;
+	m_aSnapshots[SNAP_CURRENT]->m_Tick = -1;
+
+	m_aSnapshots[SNAP_PREV]->m_pSnap = (CSnapshot *)m_aDemorecSnapshotData[SNAP_PREV][0];
+	m_aSnapshots[SNAP_PREV]->m_pAltSnap = (CSnapshot *)m_aDemorecSnapshotData[SNAP_PREV][1];
+	m_aSnapshots[SNAP_PREV]->m_SnapSize = 0;
+	m_aSnapshots[SNAP_PREV]->m_Tick = -1;
+
+	// enter demo playback state
+	SetState(IClient::STATE_DEMOPLAYBACK);
+
+	m_DemoPlayer.Play();
+	GameClient()->OnEnterGame();
+
+	return 0;
+}
+
+void CClient::Con_Play(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->DemoPlayer_Play(pResult->GetString(0));
+}
+
+void CClient::Con_Record(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	if(pSelf->State() != IClient::STATE_ONLINE)
+		dbg_msg("demorec/record", "client is not online");
+	else
+	{
+		char aFilename[512];
+		str_format(aFilename, sizeof(aFilename), "demos/%s.demo", pResult->GetString(0));
+		pSelf->m_DemoRecorder.Start(pSelf->Storage(), aFilename, pSelf->GameClient()->NetVersion(), pSelf->m_aCurrentMap, pSelf->m_CurrentMapCrc, "client");
+	}
+}
+
+void CClient::Con_StopRecord(IConsole::IResult *pResult, void *pUserData)
+{
+	CClient *pSelf = (CClient *)pUserData;
+	pSelf->m_DemoRecorder.Stop();
+}
+
+void CClient::Con_ServerDummy(IConsole::IResult *pResult, void *pUserData)
+{
+	dbg_msg("client", "this command is not available on the client");
+}
+
+void CClient::RegisterCommands()
+{
+	m_pConsole = Kernel()->RequestInterface<IConsole>();
+	// register server dummy commands for tab completion
+	m_pConsole->Register("kick", "i", CFGFLAG_SERVER, Con_ServerDummy, this, "Kick player with specified id");
+	m_pConsole->Register("ban", "s?i", CFGFLAG_SERVER, Con_ServerDummy, this, "Ban player with ip/id for x minutes");
+	m_pConsole->Register("unban", "s", CFGFLAG_SERVER, Con_ServerDummy, this, "Unban ip");
+	m_pConsole->Register("bans", "", CFGFLAG_SERVER, Con_ServerDummy, this, "Show banlist");
+	m_pConsole->Register("status", "", CFGFLAG_SERVER, Con_ServerDummy, this, "List players");
+	m_pConsole->Register("shutdown", "", CFGFLAG_SERVER, Con_ServerDummy, this, "Shut down");
+	m_pConsole->Register("record", "s", CFGFLAG_SERVER, Con_ServerDummy, this, "Record to a file");
+	m_pConsole->Register("stoprecord", "", CFGFLAG_SERVER, Con_ServerDummy, this, "Stop recording");
+
+	m_pConsole->Register("quit", "", CFGFLAG_CLIENT, Con_Quit, this, "Quit Teeworlds");
+	m_pConsole->Register("exit", "", CFGFLAG_CLIENT, Con_Quit, this, "Quit Teeworlds");
+	m_pConsole->Register("minimize", "", CFGFLAG_CLIENT, Con_Minimize, this, "Minimize Teeworlds");
+	m_pConsole->Register("connect", "s", CFGFLAG_CLIENT, Con_Connect, this, "Connect to the specified host/ip");
+	m_pConsole->Register("disconnect", "", CFGFLAG_CLIENT, Con_Disconnect, this, "Disconnect from the server");
+	m_pConsole->Register("ping", "", CFGFLAG_CLIENT, Con_Ping, this, "Ping the current server");
+	m_pConsole->Register("screenshot", "", CFGFLAG_CLIENT, Con_Screenshot, this, "Take a screenshot");
+	m_pConsole->Register("rcon", "r", CFGFLAG_CLIENT, Con_Rcon, this, "Send specified command to rcon");
+	m_pConsole->Register("rcon_auth", "s", CFGFLAG_CLIENT, Con_RconAuth, this, "Authenticate to rcon");
+	m_pConsole->Register("play", "r", CFGFLAG_CLIENT, Con_Play, this, "Play the file specified");
+	m_pConsole->Register("record", "s", CFGFLAG_CLIENT, Con_Record, this, "Record to the file");
+	m_pConsole->Register("stoprecord", "", CFGFLAG_CLIENT, Con_StopRecord, this, "Stop recording");
+
+	m_pConsole->Register("add_favorite", "s", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite");
+}
+
+static CClient m_Client;
+
+/*
+	Server Time
+	Client Mirror Time
+	Client Predicted Time
+
+	Snapshot Latency
+		Downstream latency
+
+	Prediction Latency
+		Upstream latency
+*/
+
+#if defined(CONF_PLATFORM_MACOSX)
+int SDL_main(int argc, const char **argv) // ignore_convention
+#else
+int main(int argc, const char **argv) // ignore_convention
+#endif
+{
+	// init the engine
+	dbg_msg("client", "starting...");
+	m_Client.InitEngine("Teeworlds");
+
+	IKernel *pKernel = IKernel::Create();
+	pKernel->RegisterInterface(&m_Client);
+	m_Client.RegisterInterfaces();
+
+	// create the components
+	IConsole *pConsole = CreateConsole();
+	IStorage *pStorage = CreateStorage("Teeworlds", argv[0]); // ignore_convention
+	IConfig *pConfig = CreateConfig();
+	IEngineGraphics *pEngineGraphics = CreateEngineGraphics();
+	IEngineSound *pEngineSound = CreateEngineSound();
+	IEngineInput *pEngineInput = CreateEngineInput();
+	IEngineTextRender *pEngineTextRender = CreateEngineTextRender();
+	IEngineMap *pEngineMap = CreateEngineMap();
+	IEngineMasterServer *pEngineMasterServer = CreateEngineMasterServer();
+
+	{
+		bool RegisterFail = false;
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IConsole*>(pConsole));
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IConfig*>(pConfig));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IEngineGraphics*>(pEngineGraphics)); // register graphics as both
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IGraphics*>(pEngineGraphics));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IEngineSound*>(pEngineSound)); // register as both
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<ISound*>(pEngineSound));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IEngineInput*>(pEngineInput)); // register as both
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IInput*>(pEngineInput));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IEngineTextRender*>(pEngineTextRender)); // register as both
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<ITextRender*>(pEngineTextRender));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IEngineMap*>(pEngineMap)); // register as both
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IMap*>(pEngineMap));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IEngineMasterServer*>(pEngineMasterServer)); // register as both
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast<IMasterServer*>(pEngineMasterServer));
+
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateEditor());
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateGameClient());
+		RegisterFail = RegisterFail || !pKernel->RegisterInterface(pStorage);
+
+		if(RegisterFail)
+			return -1;
+	}
+
+	pConfig->Init();
+	pEngineMasterServer->Init(m_Client.Engine());
+	pEngineMasterServer->Load();
+
+	// register all console commands
+	m_Client.RegisterCommands();
+
+	pKernel->RequestInterface<IGameClient>()->OnConsoleInit();
+
+	// init client's interfaces
+	m_Client.InitInterfaces();
+
+	// execute autoexec file
+	pConsole->ExecuteFile("autoexec.cfg");
+
+	// parse the command line arguments
+	if(argc > 1) // ignore_convention
+		pConsole->ParseArguments(argc-1, &argv[1]); // ignore_convention
+
+	// execute config file
+	pConsole->ExecuteFile("settings.cfg");
+
+	// run the client
+	m_Client.Run();
+
+	// write down the config and quit
+	pConfig->Save();
+
+	return 0;
+}