ddnet/src/engine/client/client.cpp
Needs c43fa1f34a Make client launchable from the browser given a special link
It's the exact same mechanism than mailto:foo@bar.com links, adapted to
teeworlds.  Such links looks like:

	teeworlds:1.2.3.4:8300
	teeworlds:localhost

Browsers may require extra configuration to be able to open such
links[1].

Once everything is set up, clicking on a link in any webpage will prompt
the user to pick an application to open the link.  Choosing (a
compatible) teeworlds client will open it and automatically connect it
to the given address.

Behind the scene, the browser will run the client with only one
argument: the full address.  Hence it is the same than running the
teeworlds client with the following command line:

	teeworlds "teeworlds:1.2.3.4:8300"

The rational for this change is to allow websites like teerank.com,
teeworlds-stats.info, and others to provide a link to directly connect
to a server.  Also users or webpages could also link a server.

[1]: http://kb.mozillazine.org/Register_protocol
2017-12-10 18:29:19 +01:00

2483 lines
67 KiB
C++

/* (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 <new>
#include <stdlib.h> // qsort
#include <stdarg.h>
#include <base/math.h>
#include <base/system.h>
#include <engine/client.h>
#include <engine/config.h>
#include <engine/console.h>
#include <engine/editor.h>
#include <engine/engine.h>
#include <engine/graphics.h>
#include <engine/input.h>
#include <engine/keys.h>
#include <engine/map.h>
#include <engine/masterserver.h>
#include <engine/serverbrowser.h>
#include <engine/sound.h>
#include <engine/storage.h>
#include <engine/textrender.h>
#include <engine/shared/config.h>
#include <engine/shared/compression.h>
#include <engine/shared/datafile.h>
#include <engine/shared/demo.h>
#include <engine/shared/filecollection.h>
#include <engine/shared/mapchecker.h>
#include <engine/shared/network.h>
#include <engine/shared/packer.h>
#include <engine/shared/protocol.h>
#include <engine/shared/ringbuffer.h>
#include <engine/shared/snapshot.h>
#include <game/version.h>
#include <mastersrv/mastersrv.h>
#include <versionsrv/versionsrv.h>
#include "friends.h"
#include "serverbrowser.h"
#include "client.h"
#if defined(CONF_FAMILY_WINDOWS)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
#include "SDL.h"
#ifdef main
#undef main
#endif
void CGraph::Init(float Min, float Max)
{
m_MinRange = m_Min = Min;
m_MaxRange = m_Max = Max;
m_Index = 0;
}
void CGraph::ScaleMax()
{
int i = 0;
m_Max = m_MaxRange;
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_MinRange;
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, IGraphics::CTextureHandle FontTexture, float x, float y, float w, float h, const char *pDescription)
{
//m_pGraphics->BlendNormal();
pGraphics->TextureClear();
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(FontTexture);
pGraphics->QuadsBegin();
pGraphics->QuadsText(x+2, y+h-16, 16, pDescription);
char aBuf[32];
str_format(aBuf, sizeof(aBuf), "%.2f", m_Max);
pGraphics->QuadsText(x+w-8*str_length(aBuf)-8, y+2, 16, aBuf);
str_format(aBuf, sizeof(aBuf), "%.2f", m_Min);
pGraphics->QuadsText(x+w-8*str_length(aBuf)-8, y+h-16, 16, aBuf);
pGraphics->QuadsEnd();
}
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_RenderFrameTime = 0.0001f;
m_RenderFrameTimeLow = 1.0f;
m_RenderFrameTimeHigh = 0.0f;
m_RenderFrames = 0;
m_LastRenderTime = time_get();
m_GameTickSpeed = SERVER_TICK_SPEED;
m_WindowMustRefocus = 0;
m_SnapCrcErrors = 0;
m_AutoScreenshotRecycle = false;
m_EditorActive = false;
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_CurrentInput = 0;
m_State = IClient::STATE_OFFLINE;
m_aServerAddressStr[0] = 0;
mem_zero(m_aSnapshots, sizeof(m_aSnapshots));
m_SnapshotStorage.Init();
m_RecivedSnapshots = 0;
m_VersionInfo.m_State = CVersionInfo::STATE_INIT;
}
// ----- send functions -----
int CClient::SendMsg(CMsgPacker *pMsg, int Flags)
{
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();
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, true);
Msg.AddString(GameClient()->NetVersion(), 128);
Msg.AddString(g_Config.m_Password, 128);
SendMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
}
void CClient::SendEnterGame()
{
CMsgPacker Msg(NETMSG_ENTERGAME, true);
SendMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
}
void CClient::SendReady()
{
CMsgPacker Msg(NETMSG_READY, true);
SendMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
}
void CClient::RconAuth(const char *pName, const char *pPassword)
{
if(RconAuthed())
return;
CMsgPacker Msg(NETMSG_RCON_AUTH, true);
Msg.AddString(pPassword, 32);
SendMsg(&Msg, MSGFLAG_VITAL);
}
void CClient::Rcon(const char *pCmd)
{
CMsgPacker Msg(NETMSG_RCON_CMD, true);
Msg.AddString(pCmd, 256);
SendMsg(&Msg, MSGFLAG_VITAL);
}
bool CClient::ConnectionProblems() const
{
return m_NetClient.GotProblems() != 0;
}
void CClient::DirectInput(int *pInput, int Size)
{
CMsgPacker Msg(NETMSG_INPUT, true);
Msg.AddInt(m_AckGameTick);
Msg.AddInt(m_PredTick);
Msg.AddInt(Size);
for(int i = 0; i < Size/4; i++)
Msg.AddInt(pInput[i]);
SendMsg(&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, true);
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;
SendMsg(&Msg, MSGFLAG_FLUSH);
}
const char *CClient::LatestVersion() const
{
return m_aVersionStr;
}
// TODO: OPT: do this alot smarter!
const int *CClient::GetInput(int Tick) const
{
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 (const int *)m_aInputs[Best].m_aData;
return 0;
}
// ------ state handling -----
void CClient::SetState(int s)
{
if(m_State == IClient::STATE_QUITING)
return;
int Old = m_State;
if(g_Config.m_Debug)
{
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "state change. last=%d current=%d", m_State, s);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf);
}
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;
m_CurMenuTick = 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];
int Port = 8303;
Disconnect();
str_copy(m_aServerAddressStr, pAddress, sizeof(m_aServerAddressStr));
str_format(aBuf, sizeof(aBuf), "connecting to '%s'", m_aServerAddressStr);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
if(net_addr_from_str(&m_ServerAddress, m_aServerAddressStr) != 0 && net_host_lookup(m_aServerAddressStr, &m_ServerAddress, m_NetClient.NetType()) != 0)
{
char aBufMsg[256];
str_format(aBufMsg, sizeof(aBufMsg), "could not find the address of %s, connecting to localhost", aBuf);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBufMsg);
net_host_lookup("localhost", &m_ServerAddress, m_NetClient.NetType());
}
m_RconAuthed = 0;
if(m_ServerAddress.port == 0)
m_ServerAddress.port = Port;
m_NetClient.Connect(&m_ServerAddress);
SetState(IClient::STATE_CONNECTING);
if(m_DemoRecorder.IsRecording())
DemoRecorder_Stop();
m_InputtimeMarginGraph.Init(-150.0f, 150.0f);
m_GametimeMarginGraph.Init(-150.0f, 150.0f);
}
void CClient::DisconnectWithReason(const char *pReason)
{
char aBuf[512];
str_format(aBuf, sizeof(aBuf), "disconnecting. reason='%s'", pReason?pReason:"unknown");
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
// stop demo playback and recorder
m_DemoPlayer.Stop();
DemoRecorder_Stop();
//
m_RconAuthed = 0;
m_UseTempRconCommands = 0;
m_pConsole->DeregisterTempAll();
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) const
{
mem_copy(pServerInfo, &m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
}
int CClient::LoadData()
{
m_DebugFont = Graphics()->LoadTexture("ui/debug_font.png", IStorage::TYPE_ALL, CImageInfo::FORMAT_AUTO, IGraphics::TEXLOAD_NORESAMPLE);
return 1;
}
// ---
const void *CClient::SnapGetItem(int SnapID, int Index, CSnapItem *pItem) const
{
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)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", "snap invalidate problem");
if((char *)i >= (char *)m_aSnapshots[SnapID]->m_pSnap && (char *)i < (char *)m_aSnapshots[SnapID]->m_pSnap + m_aSnapshots[SnapID]->m_SnapSize)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", "snap invalidate problem");
i->m_TypeAndID = -1;
}
}
const void *CClient::SnapFindItem(int SnapID, int Type, int ID) const
{
// 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) const
{
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::SnapNewItem(int Type, int ID, int Size)
{
dbg_assert(Type >= 0 && Type <=0xffff, "incorrect type");
dbg_assert(ID >= 0 && ID <=0xffff, "incorrect id");
return ID < 0 ? 0 : m_DemoRecSnapshotBuilder.NewItem(Type, ID, Size);
}
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());
Graphics()->QuadsBegin();
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_RenderFrameTime*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 + 0.5f));
Graphics()->QuadsText(2, 2, 16, 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, 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, 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, aBuffer);
Graphics()->QuadsEnd();
// 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.ScaleMin();
m_InputtimeMarginGraph.ScaleMax();
m_InputtimeMarginGraph.Render(Graphics(), m_DebugFont, x, sp*5+h+sp, w, h, "Prediction Margin");
m_GametimeMarginGraph.ScaleMin();
m_GametimeMarginGraph.ScaleMax();
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() const
{
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)
{
str_format(aErrorMsg, sizeof(aErrorMsg), "map differs from the server. %08x != %08x", m_pMap->Crc(), WantedCrc);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aErrorMsg);
m_pMap->Unload();
return aErrorMsg;
}
// stop demo recording if we loaded a new map
DemoRecorder_Stop();
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "loaded map '%s'", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
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];
str_format(aBuf, sizeof(aBuf), "loading map, map=%s wanted crc=%08x", pMapName, WantedCrc);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
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);
if(!pError)
return pError;
// search for the map within subfolders
char aFilename[128];
str_format(aFilename, sizeof(aFilename), "%s.map", pMapName);
if(Storage()->FindFile(aFilename, "maps", IStorage::TYPE_ALL, aBuf, sizeof(aBuf)))
pError = LoadMap(pMapName, aBuf, WantedCrc);
return pError;
}
int CClient::PlayerScoreComp(const void *a, const void *b)
{
CServerInfo::CClient *p0 = (CServerInfo::CClient *)a;
CServerInfo::CClient *p1 = (CServerInfo::CClient *)b;
if(p0->m_Player && !p1->m_Player)
return -1;
if(!p0->m_Player && p1->m_Player)
return 1;
if(p0->m_Score == p1->m_Score)
return 0;
if(p0->m_Score < p1->m_Score)
return 1;
return -1;
}
int CClient::UnpackServerInfo(CUnpacker *pUnpacker, CServerInfo *pInfo, int *pToken)
{
if(pToken)
*pToken = pUnpacker->GetInt();
str_copy(pInfo->m_aVersion, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aVersion));
str_copy(pInfo->m_aName, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aName));
str_clean_whitespaces(pInfo->m_aName);
str_copy(pInfo->m_aHostname, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aHostname));
if(pInfo->m_aHostname[0] == 0)
str_copy(pInfo->m_aHostname, pInfo->m_aAddress, sizeof(pInfo->m_aHostname));
str_copy(pInfo->m_aMap, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aMap));
str_copy(pInfo->m_aGameType, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aGameType));
pInfo->m_Flags = (pUnpacker->GetInt()&SERVERINFO_FLAG_PASSWORD) ? IServerBrowser::FLAG_PASSWORD : 0;
pInfo->m_ServerLevel = clamp<int>(pUnpacker->GetInt(), SERVERINFO_LEVEL_MIN, SERVERINFO_LEVEL_MAX);
pInfo->m_NumPlayers = pUnpacker->GetInt();
pInfo->m_MaxPlayers = pUnpacker->GetInt();
pInfo->m_NumClients = pUnpacker->GetInt();
pInfo->m_MaxClients = pUnpacker->GetInt();
// don't add invalid info to the server browser list
if(pInfo->m_NumClients < 0 || pInfo->m_NumClients > MAX_CLIENTS || pInfo->m_MaxClients < 0 || pInfo->m_MaxClients > MAX_CLIENTS ||
pInfo->m_NumPlayers < 0 || pInfo->m_NumPlayers > pInfo->m_NumClients || pInfo->m_MaxPlayers < 0 || pInfo->m_MaxPlayers > pInfo->m_MaxClients)
return -1;
// use short version
if(!pToken)
return 0;
for(int i = 0; i < pInfo->m_NumClients; i++)
{
str_copy(pInfo->m_aClients[i].m_aName, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aClients[i].m_aName));
str_copy(pInfo->m_aClients[i].m_aClan, pUnpacker->GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES), sizeof(pInfo->m_aClients[i].m_aClan));
pInfo->m_aClients[i].m_Country = pUnpacker->GetInt();
pInfo->m_aClients[i].m_Score = pUnpacker->GetInt();
pInfo->m_aClients[i].m_Player = pUnpacker->GetInt() != 0 ? true : false;
}
return 0;
}
void CClient::ProcessConnlessPacket(CNetChunk *pPacket)
{
// version server
if(m_VersionInfo.m_State == CVersionInfo::STATE_READY && net_addr_comp(&pPacket->m_Address, &m_VersionInfo.m_VersionServeraddr.m_Addr) == 0)
{
// version info
if(pPacket->m_DataSize == (int)(sizeof(VERSIONSRV_VERSION) + sizeof(GAME_RELEASE_VERSION)) &&
mem_comp(pPacket->m_pData, VERSIONSRV_VERSION, sizeof(VERSIONSRV_VERSION)) == 0)
{
char *pVersionData = (char*)pPacket->m_pData + sizeof(VERSIONSRV_VERSION);
int VersionMatch = !mem_comp(pVersionData, GAME_RELEASE_VERSION, sizeof(GAME_RELEASE_VERSION));
char aVersion[sizeof(GAME_RELEASE_VERSION)];
str_copy(aVersion, pVersionData, sizeof(aVersion));
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "version does %s (%s)",
VersionMatch ? "match" : "NOT match",
aVersion);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/version", aBuf);
// assume version is out of date when version-data doesn't match
if(!VersionMatch)
{
str_copy(m_aVersionStr, aVersion, sizeof(m_aVersionStr));
}
// request the map version list now
CNetChunk Packet;
mem_zero(&Packet, sizeof(Packet));
Packet.m_ClientID = -1;
Packet.m_Address = m_VersionInfo.m_VersionServeraddr.m_Addr;
Packet.m_pData = VERSIONSRV_GETMAPLIST;
Packet.m_DataSize = sizeof(VERSIONSRV_GETMAPLIST);
Packet.m_Flags = NETSENDFLAG_CONNLESS;
m_ContactClient.Send(&Packet);
}
// map version list
if(pPacket->m_DataSize >= (int)sizeof(VERSIONSRV_MAPLIST) &&
mem_comp(pPacket->m_pData, VERSIONSRV_MAPLIST, sizeof(VERSIONSRV_MAPLIST)) == 0)
{
int Size = pPacket->m_DataSize-sizeof(VERSIONSRV_MAPLIST);
int Num = Size/sizeof(CMapVersion);
m_MapChecker.AddMaplist((CMapVersion *)((char*)pPacket->m_pData+sizeof(VERSIONSRV_MAPLIST)), Num);
}
}
// server list from master server
if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_LIST) &&
mem_comp(pPacket->m_pData, SERVERBROWSE_LIST, sizeof(SERVERBROWSE_LIST)) == 0)
{
// check for valid master server address
bool Valid = false;
for(int i = 0; i < IMasterServer::MAX_MASTERSERVERS; ++i)
{
if(m_pMasterServer->IsValid(i))
{
NETADDR Addr = m_pMasterServer->GetAddr(i);
if(net_addr_comp(&pPacket->m_Address, &Addr) == 0)
{
Valid = true;
break;
}
}
}
if(!Valid)
return;
int Size = pPacket->m_DataSize-sizeof(SERVERBROWSE_LIST);
int Num = Size/sizeof(CMastersrvAddr);
CMastersrvAddr *pAddrs = (CMastersrvAddr *)((char*)pPacket->m_pData+sizeof(SERVERBROWSE_LIST));
for(int i = 0; i < Num; i++)
{
NETADDR Addr;
static unsigned char s_aIPV4Mapping[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
// copy address
if(!mem_comp(s_aIPV4Mapping, pAddrs[i].m_aIp, sizeof(s_aIPV4Mapping)))
{
mem_zero(&Addr, sizeof(Addr));
Addr.type = NETTYPE_IPV4;
Addr.ip[0] = pAddrs[i].m_aIp[12];
Addr.ip[1] = pAddrs[i].m_aIp[13];
Addr.ip[2] = pAddrs[i].m_aIp[14];
Addr.ip[3] = pAddrs[i].m_aIp[15];
}
else
{
Addr.type = NETTYPE_IPV6;
mem_copy(Addr.ip, pAddrs[i].m_aIp, sizeof(Addr.ip));
}
Addr.port = (pAddrs[i].m_aPort[0]<<8) | pAddrs[i].m_aPort[1];
m_ServerBrowser.Set(Addr, CServerBrowser::SET_MASTER_ADD, -1, 0x0);
}
}
// server info
if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_INFO) && mem_comp(pPacket->m_pData, SERVERBROWSE_INFO, sizeof(SERVERBROWSE_INFO)) == 0)
{
CUnpacker Up;
CServerInfo Info = {0};
Up.Reset((unsigned char*)pPacket->m_pData+sizeof(SERVERBROWSE_INFO), pPacket->m_DataSize-sizeof(SERVERBROWSE_INFO));
net_addr_str(&pPacket->m_Address, Info.m_aAddress, sizeof(Info.m_aAddress), true);
int Token;
if(!UnpackServerInfo(&Up, &Info, &Token) && !Up.Error())
{
qsort(Info.m_aClients, Info.m_NumClients, sizeof(*Info.m_aClients), PlayerScoreComp);
m_ServerBrowser.Set(pPacket->m_Address, CServerBrowser::SET_TOKEN, Token, &Info);
}
}
}
void CClient::ProcessServerPacket(CNetChunk *pPacket)
{
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((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_CHANGE)
{
const char *pMap = Unpacker.GetString(CUnpacker::SANITIZE_CC|CUnpacker::SKIP_START_WHITESPACES);
int MapCrc = Unpacker.GetInt();
int MapSize = Unpacker.GetInt();
int MapChunkNum = Unpacker.GetInt();
int MapChunkSize = Unpacker.GetInt();
const char *pError = 0;
if(Unpacker.Error())
return;
// check for valid standard map
if(!m_MapChecker.IsMapValid(pMap, MapCrc, MapSize))
pError = "invalid standard map";
// protect the player from nasty map names
for(int i = 0; pMap[i]; i++)
{
if(pMap[i] == '/' || pMap[i] == '\\')
pError = "strange character in map name";
}
if(MapSize <= 0)
pError = "invalid map size";
if(pError)
DisconnectWithReason(pError);
else
{
pError = LoadMapSearch(pMap, MapCrc);
if(!pError)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done");
SendReady();
}
else
{
// start map download
str_format(m_aMapdownloadFilename, sizeof(m_aMapdownloadFilename), "downloadedmaps/%s_%08x.map", pMap, MapCrc);
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "starting to download map to '%s'", m_aMapdownloadFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", aBuf);
str_copy(m_aMapdownloadName, pMap, sizeof(m_aMapdownloadName));
if(m_MapdownloadFile)
io_close(m_MapdownloadFile);
m_MapdownloadFile = Storage()->OpenFile(m_aMapdownloadFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
m_MapdownloadChunk = 0;
m_MapdownloadChunkNum = MapChunkNum;
m_MapDownloadChunkSize = MapChunkSize;
m_MapdownloadCrc = MapCrc;
m_MapdownloadTotalsize = MapSize;
m_MapdownloadAmount = 0;
// request first chunk package of map data
CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true);
SendMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
if(g_Config.m_Debug)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client/network", "requested first chunk package");
}
}
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_DATA)
{
if(!m_MapdownloadFile)
return;
int Size = min(m_MapDownloadChunkSize, m_MapdownloadTotalsize-m_MapdownloadAmount);
const unsigned char *pData = Unpacker.GetRaw(Size);
if(Unpacker.Error())
return;
io_write(m_MapdownloadFile, pData, Size);
++m_MapdownloadChunk;
m_MapdownloadAmount += Size;
if(m_MapdownloadAmount == m_MapdownloadTotalsize)
{
// map download complete
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "download complete, loading map");
if(m_MapdownloadFile)
io_close(m_MapdownloadFile);
m_MapdownloadFile = 0;
m_MapdownloadAmount = 0;
m_MapdownloadTotalsize = -1;
// load map
const char *pError = LoadMap(m_aMapdownloadName, m_aMapdownloadFilename, m_MapdownloadCrc);
if(!pError)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done");
SendReady();
}
else
DisconnectWithReason(pError);
}
else if(m_MapdownloadChunk%m_MapdownloadChunkNum == 0)
{
// request next chunk package of map data
CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true);
SendMsg(&Msg, MSGFLAG_VITAL|MSGFLAG_FLUSH);
if(g_Config.m_Debug)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client/network", "requested next chunk package");
}
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_SERVERINFO)
{
CServerInfo Info = {0};
net_addr_str(&pPacket->m_Address, Info.m_aAddress, sizeof(Info.m_aAddress), true);
if(!UnpackServerInfo(&Unpacker, &Info, 0) && !Unpacker.Error())
{
qsort(Info.m_aClients, Info.m_NumClients, sizeof(*Info.m_aClients), PlayerScoreComp);
mem_copy(&m_CurrentServerInfo, &Info, sizeof(m_CurrentServerInfo));
m_CurrentServerInfo.m_NetAddr = m_ServerAddress;
}
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CON_READY)
{
GameClient()->OnConnected();
}
else if(Msg == NETMSG_PING)
{
CMsgPacker Msg(NETMSG_PING_REPLY, true);
SendMsg(&Msg, 0);
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD)
{
const char *pName = Unpacker.GetString(CUnpacker::SANITIZE_CC);
const char *pHelp = Unpacker.GetString(CUnpacker::SANITIZE_CC);
const char *pParams = Unpacker.GetString(CUnpacker::SANITIZE_CC);
if(Unpacker.Error() == 0)
m_pConsole->RegisterTemp(pName, pParams, CFGFLAG_SERVER, pHelp);
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_REM)
{
const char *pName = Unpacker.GetString(CUnpacker::SANITIZE_CC);
if(Unpacker.Error() == 0)
m_pConsole->DeregisterTemp(pName);
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_AUTH_ON)
{
m_RconAuthed = 1;
m_UseTempRconCommands = 1;
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_AUTH_OFF)
{
m_RconAuthed = 0;
if(m_UseTempRconCommands)
m_pConsole->DeregisterTempAll();
m_UseTempRconCommands = 0;
}
else if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_LINE)
{
const char *pLine = Unpacker.GetString();
if(Unpacker.Error() == 0)
GameClient()->OnRconLine(pLine);
}
else if(Msg == NETMSG_PING_REPLY)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "latency %.2f", (time_get() - m_PingStartTime)*1000 / (float)time_freq());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client/network", aBuf);
}
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());
break;
}
}
if(Target)
m_PredictedTime.Update(&m_InputtimeMarginGraph, Target, TimeLeft, 1);
}
else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY)
{
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() || NumParts < 1 || NumParts > CSnapshot::MAX_PARTS || Part < 0 || Part >= NumParts || PartSize < 0 || PartSize > MAX_SNAPSHOT_PACKSIZE)
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];
CSnapshot *pTmpBuffer3 = (CSnapshot*)aTmpBuffer3; // Fix compiler warning for strict-aliasing
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)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "error, couldn't find the delta snapshot");
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf);
}
// 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, sizeof(aTmpBuffer2));
if(IntSize < 0) // failure during decompression, bail
return;
pDeltaData = aTmpBuffer2;
DeltaSize = IntSize;
}
// unpack delta
SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, pTmpBuffer3, pDeltaData, DeltaSize);
if(SnapSize < 0)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", "delta unpack failed!");
return;
}
if(Msg != NETMSG_SNAPEMPTY && pTmpBuffer3->Crc() != Crc)
{
if(g_Config.m_Debug)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d",
m_SnapCrcErrors, GameTick, Crc, pTmpBuffer3->Crc(), CompleteSize, DeltaTick);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf);
}
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_CURRENT]->m_Tick;
m_SnapshotStorage.PurgeUntil(PurgeTick);
// add new
m_SnapshotStorage.Add(GameTick, time_get(), SnapSize, pTmpBuffer3, 1);
// add snapshot to demo
if(m_DemoRecorder.IsRecording())
{
// build up snapshot and add local messages
m_DemoRecSnapshotBuilder.Init(pTmpBuffer3);
GameClient()->OnDemoRecSnap();
SnapSize = m_DemoRecSnapshotBuilder.Finish(pTmpBuffer3);
// write snapshot
m_DemoRecorder.RecordSnapshot(GameTick, pTmpBuffer3, 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);
DemoRecorder_HandleAutoStart();
}
// adjust game time
if(m_RecivedSnapshots > 2)
{
int64 Now = m_GameTime.Get(time_get());
int64 TickStart = GameTick*time_freq()/50;
int64 TimeLeft = (TickStart-Now)*1000 / time_freq();
m_GameTime.Update(&m_GametimeMarginGraph, (GameTick-1)*time_freq()/50, TimeLeft, 0);
}
// ack snapshot
m_AckGameTick = GameTick;
}
}
}
}
else
{
if((pPacket->m_Flags&NET_CHUNKFLAG_VITAL) != 0)
{
// game message
GameClient()->OnMessage(Msg, &Unpacker);
if(m_RecordGameMessage && m_DemoRecorder.IsRecording())
m_DemoRecorder.RecordMessage(pPacket->m_pData, pPacket->m_DataSize);
}
}
}
void CClient::PumpNetwork()
{
m_NetClient.Update();
if(State() != IClient::STATE_DEMOPLAYBACK)
{
// check for errors
if(State() != IClient::STATE_OFFLINE && State() != IClient::STATE_QUITING && m_NetClient.State() == NETSTATE_OFFLINE)
{
SetState(IClient::STATE_OFFLINE);
Disconnect();
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "offline error='%s'", m_NetClient.ErrorString());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
}
//
if(State() == IClient::STATE_CONNECTING && m_NetClient.State() == NETSTATE_ONLINE)
{
// we switched to online
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", "connected, sending info");
SetState(IClient::STATE_LOADING);
SendInfo();
}
}
// process non-connless packets
CNetChunk Packet;
while(m_NetClient.Recv(&Packet))
{
if(!(Packet.m_Flags&NETSENDFLAG_CONNLESS))
ProcessServerPacket(&Packet);
}
// process connless packets data
m_ContactClient.Update();
while(m_ContactClient.Recv(&Packet))
{
if(Packet.m_Flags&NETSENDFLAG_CONNLESS)
ProcessConnlessPacket(&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_ONLINE && 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;
int PrevPredTick = (int)(PredNow*50/time_freq());
int NewPredTick = PrevPredTick+1;
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)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", "prediction time reset!");
m_PredictedTime.Init(m_aSnapshots[SNAP_CURRENT]->m_Tick*time_freq()/50);
}
if(NewPredTick > m_PredTick)
{
m_PredTick = NewPredTick;
Repredict = 1;
// send input
SendInput();
}
}
// only do sane predictions
if(Repredict)
{
if(m_PredTick > m_CurGameTick && m_PredTick < m_CurGameTick+50)
GameClient()->OnPredict();
}
}
// 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)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "stress", "reconnecting!");
Connect(g_Config.m_DbgStressServer);
ActionTaken = Now;
}
}
else
{
if(Now > ActionTaken+time_freq()*(10+g_Config.m_DbgStress))
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "stress", "disconnecting!");
Disconnect();
ActionTaken = Now;
}
}
}
// pump the network
PumpNetwork();
// update the maser server registry
MasterServer()->Update();
// update the server browser
m_ServerBrowser.Update(m_ResortServerBrowser);
m_ResortServerBrowser = false;
// update gameclient
if(!m_EditorActive)
GameClient()->OnUpdate();
}
void CClient::VersionUpdate()
{
if(m_VersionInfo.m_State == CVersionInfo::STATE_INIT)
{
Engine()->HostLookup(&m_VersionInfo.m_VersionServeraddr, g_Config.m_ClVersionServer, m_ContactClient.NetType());
m_VersionInfo.m_State = CVersionInfo::STATE_START;
}
else if(m_VersionInfo.m_State == CVersionInfo::STATE_START)
{
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_ContactClient.Send(&Packet);
m_VersionInfo.m_State = CVersionInfo::STATE_READY;
}
}
}
void CClient::RegisterInterfaces()
{
Kernel()->RegisterInterface(static_cast<IDemoRecorder*>(&m_DemoRecorder));
Kernel()->RegisterInterface(static_cast<IDemoPlayer*>(&m_DemoPlayer));
Kernel()->RegisterInterface(static_cast<IServerBrowser*>(&m_ServerBrowser));
Kernel()->RegisterInterface(static_cast<IFriends*>(&m_Friends));
}
void CClient::InitInterfaces()
{
// fetch interfaces
m_pEngine = Kernel()->RequestInterface<IEngine>();
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.Init(&m_ContactClient, m_pGameClient->NetVersion());
m_Friends.Init();
}
void CClient::Run()
{
m_LocalStartTime = time_get();
m_SnapshotParts = 0;
// init SDL
{
if(SDL_Init(0) < 0)
{
dbg_msg("client", "unable to init SDL base: %s", SDL_GetError());
return;
}
atexit(SDL_Quit); // ignore_convention
}
m_MenuStartTime = time_get();
// init graphics
{
m_pGraphics = CreateEngineGraphicsThreaded();
bool RegisterFail = false;
RegisterFail = RegisterFail || !Kernel()->RegisterInterface(static_cast<IEngineGraphics*>(m_pGraphics)); // register graphics as both
RegisterFail = RegisterFail || !Kernel()->RegisterInterface(static_cast<IGraphics*>(m_pGraphics));
if(RegisterFail || m_pGraphics->Init() != 0)
{
dbg_msg("client", "couldn't init graphics");
return;
}
}
// init sound, allowed to fail
m_SoundInitFailed = Sound()->Init() != 0;
Sound()->SetMaxDistance(1.5f*Graphics()->ScreenWidth()/2.0f);
// open socket
{
NETADDR BindAddr;
if(g_Config.m_Bindaddr[0] && net_host_lookup(g_Config.m_Bindaddr, &BindAddr, NETTYPE_ALL) == 0)
{
// got bindaddr
BindAddr.type = NETTYPE_ALL;
}
else
{
mem_zero(&BindAddr, sizeof(BindAddr));
BindAddr.type = NETTYPE_ALL;
}
if(!m_NetClient.Open(BindAddr, BindAddr.port ? 0 : NETCREATE_FLAG_RANDOMPORT))
{
dbg_msg("client", "couldn't open socket(net)");
return;
}
BindAddr.port = 0;
if(!m_ContactClient.Open(BindAddr, 0))
{
dbg_msg("client", "couldn't open socket(contact)");
return;
}
}
// init font rendering
Kernel()->RequestInterface<IEngineTextRender>()->Init();
// init the input
Input()->Init();
// start refreshing addresses while we load
MasterServer()->RefreshAddresses(m_ContactClient.NetType());
// init the editor
m_pEditor->Init();
// load data
if(!LoadData())
return;
GameClient()->OnInit();
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "version %s", GameClient()->NetVersion());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
// 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, 120.0f);
// never start with the editor
g_Config.m_ClEditor = 0;
// process pending commands
m_pConsole->StoreCommands(false);
while (1)
{
//
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
if(Input()->Update())
break; // SDL_QUIT
// 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()->KeyPress(KEY_ESCAPE, true))
{
Input()->MouseModeAbsolute();
m_WindowMustRefocus = 1;
}
// refocus
if(m_WindowMustRefocus && m_pGraphics->WindowActive())
{
if(m_WindowMustRefocus < 3)
{
Input()->MouseModeAbsolute();
m_WindowMustRefocus++;
}
if(m_WindowMustRefocus >= 3 || Input()->KeyPress(KEY_MOUSE_1, true))
{
Input()->MouseModeRelative();
m_WindowMustRefocus = 0;
// update screen in case it got moved
int ActScreen = Graphics()->GetWindowScreen();
if(ActScreen >= 0 && ActScreen != g_Config.m_GfxScreen)
g_Config.m_GfxScreen = ActScreen;
}
}
// panic quit button
if(Input()->KeyIsPressed(KEY_LCTRL) && Input()->KeyIsPressed(KEY_LSHIFT) && Input()->KeyPress(KEY_Q, true))
{
Quit();
break;
}
if(Input()->KeyIsPressed(KEY_LCTRL) && Input()->KeyIsPressed(KEY_LSHIFT) && Input()->KeyPress(KEY_D, true))
g_Config.m_Debug ^= 1;
if(Input()->KeyIsPressed(KEY_LCTRL) && Input()->KeyIsPressed(KEY_LSHIFT) && Input()->KeyPress(KEY_G, true))
g_Config.m_DbgGraphs ^= 1;
if(Input()->KeyIsPressed(KEY_LCTRL) && Input()->KeyIsPressed(KEY_LSHIFT) && Input()->KeyPress(KEY_E, true))
{
g_Config.m_ClEditor = g_Config.m_ClEditor^1;
Input()->MouseModeRelative();
}
// render
{
if(g_Config.m_ClEditor)
{
if(!m_EditorActive)
{
GameClient()->OnActivateEditor();
m_EditorActive = true;
}
}
else if(m_EditorActive)
m_EditorActive = false;
Update();
if(!g_Config.m_GfxAsyncRender || m_pGraphics->IsIdle())
{
m_RenderFrames++;
// update frametime
int64 Now = time_get();
m_RenderFrameTime = (Now - m_LastRenderTime) / (float)time_freq();
if(m_RenderFrameTime < m_RenderFrameTimeLow)
m_RenderFrameTimeLow = m_RenderFrameTime;
if(m_RenderFrameTime > m_RenderFrameTimeHigh)
m_RenderFrameTimeHigh = m_RenderFrameTime;
m_FpsGraph.Add(1.0f/m_RenderFrameTime, 1,1,1);
m_LastRenderTime = Now;
// when we are stress testing only render every 10th frame
if(!g_Config.m_DbgStress || (m_RenderFrames%10) == 0 )
{
if(!m_EditorActive)
Render();
else
{
m_pEditor->UpdateAndRender();
DebugRender();
}
m_pGraphics->Swap();
}
}
}
AutoScreenshot_Cleanup();
// check conditions
if(State() == IClient::STATE_QUITING)
break;
// menu tick
if(State() == IClient::STATE_OFFLINE)
{
int64 t = time_get();
while(t > TickStartTime(m_CurMenuTick+1))
m_CurMenuTick++;
}
// beNice
if(g_Config.m_ClCpuThrottle)
thread_sleep(g_Config.m_ClCpuThrottle);
else if(g_Config.m_DbgStress || !m_pGraphics->WindowActive())
thread_sleep(5);
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_RenderFrameTimeHigh,
1.0f/m_RenderFrameTimeLow,
m_NetClient.State());
}
m_RenderFrameTimeLow = 1;
m_RenderFrameTimeHigh = 0;
m_RenderFrames = 0;
ReportTime += ReportInterval;
}*/
// update local time
m_LocalTime = (time_get()-m_LocalStartTime)/(float)time_freq();
}
GameClient()->OnShutdown();
Disconnect();
m_pGraphics->Shutdown();
m_pSound->Shutdown();
// shutdown SDL
{
SDL_Quit();
}
}
int64 CClient::TickStartTime(int Tick)
{
return m_MenuStartTime + (time_freq()*Tick)/m_GameTickSpeed;
}
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, true);
pSelf->SendMsg(&Msg, 0);
pSelf->m_PingStartTime = time_get();
}
void CClient::AutoScreenshot_Start()
{
if(g_Config.m_ClAutoScreenshot)
{
Graphics()->TakeScreenshot("auto/autoscreen");
m_AutoScreenshotRecycle = true;
}
}
void CClient::AutoScreenshot_Cleanup()
{
if(m_AutoScreenshotRecycle)
{
if(g_Config.m_ClAutoScreenshotMax)
{
// clean up auto taken screens
CFileCollection AutoScreens;
AutoScreens.Init(Storage(), "screenshots/auto", "autoscreen", ".png", g_Config.m_ClAutoScreenshotMax);
}
m_AutoScreenshotRecycle = false;
}
}
void CClient::Con_Screenshot(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Graphics()->TakeScreenshot(0);
}
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));
}
const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType)
{
int Crc;
Disconnect();
m_NetClient.ResetErrorString();
// try to start playback
m_DemoPlayer.SetListner(this);
const char *pError = m_DemoPlayer.Load(Storage(), m_pConsole, pFilename, StorageType, GameClient()->NetVersion());
if(pError)
return pError;
// load map
Crc = (m_DemoPlayer.Info()->m_Header.m_aMapCrc[0]<<24)|
(m_DemoPlayer.Info()->m_Header.m_aMapCrc[1]<<16)|
(m_DemoPlayer.Info()->m_Header.m_aMapCrc[2]<<8)|
(m_DemoPlayer.Info()->m_Header.m_aMapCrc[3]);
pError = LoadMapSearch(m_DemoPlayer.Info()->m_Header.m_aMapName, 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), IStorage::TYPE_ALL);
}
void CClient::DemoRecorder_Start(const char *pFilename, bool WithTimestamp)
{
if(State() != IClient::STATE_ONLINE)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demorec/record", "client is not online");
else
{
char aFilename[128];
if(WithTimestamp)
{
char aDate[20];
str_timestamp(aDate, sizeof(aDate));
str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", pFilename, aDate);
}
else
str_format(aFilename, sizeof(aFilename), "demos/%s.demo", pFilename);
m_DemoRecorder.Start(Storage(), m_pConsole, aFilename, GameClient()->NetVersion(), m_aCurrentMap, m_CurrentMapCrc, "client");
}
}
void CClient::DemoRecorder_HandleAutoStart()
{
if(g_Config.m_ClAutoDemoRecord)
{
DemoRecorder_Stop();
DemoRecorder_Start("auto/autorecord", true);
if(g_Config.m_ClAutoDemoMax)
{
// clean up auto recorded demos
CFileCollection AutoDemos;
AutoDemos.Init(Storage(), "demos/auto", "autorecord", ".demo", g_Config.m_ClAutoDemoMax);
}
}
}
void CClient::DemoRecorder_Stop()
{
m_DemoRecorder.Stop();
}
void CClient::DemoRecorder_AddDemoMarker()
{
m_DemoRecorder.AddDemoMarker();
}
void CClient::Con_Record(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pResult->NumArguments())
pSelf->DemoRecorder_Start(pResult->GetString(0), false);
else
pSelf->DemoRecorder_Start("demo", true);
}
void CClient::Con_StopRecord(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->DemoRecorder_Stop();
}
void CClient::Con_AddDemoMarker(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->DemoRecorder_AddDemoMarker();
}
void CClient::ServerBrowserUpdate()
{
m_ResortServerBrowser = true;
}
void CClient::ConchainServerBrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
((CClient *)pUserData)->ServerBrowserUpdate();
}
void CClient::SwitchWindowScreen(int Index)
{
// Todo SDL: remove this when fixed (changing screen when in fullscreen is bugged)
if(g_Config.m_GfxFullscreen)
{
ToggleFullscreen();
if(Graphics()->SetWindowScreen(Index))
g_Config.m_GfxScreen = Index;
ToggleFullscreen();
}
else
{
if(Graphics()->SetWindowScreen(Index))
g_Config.m_GfxScreen = Index;
}
}
void CClient::ConchainWindowScreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(g_Config.m_GfxScreen != pResult->GetInteger(0))
pSelf->SwitchWindowScreen(pResult->GetInteger(0));
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::ToggleFullscreen()
{
if(Graphics()->Fullscreen(g_Config.m_GfxFullscreen^1))
g_Config.m_GfxFullscreen ^= 1;
}
void CClient::ConchainFullscreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(g_Config.m_GfxFullscreen != pResult->GetInteger(0))
pSelf->ToggleFullscreen();
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::ToggleWindowBordered()
{
g_Config.m_GfxBorderless ^= 1;
Graphics()->SetWindowBordered(!g_Config.m_GfxBorderless);
}
void CClient::ConchainWindowBordered(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(!g_Config.m_GfxFullscreen && (g_Config.m_GfxBorderless != pResult->GetInteger(0)))
pSelf->ToggleWindowBordered();
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::ToggleWindowVSync()
{
if(Graphics()->SetVSync(g_Config.m_GfxVsync^1))
g_Config.m_GfxVsync ^= 1;
}
void CClient::ConchainWindowVSync(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(g_Config.m_GfxVsync != pResult->GetInteger(0))
pSelf->ToggleWindowVSync();
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::RegisterCommands()
{
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pConsole->Register("quit", "", CFGFLAG_CLIENT|CFGFLAG_STORE, Con_Quit, this, "Quit Teeworlds");
m_pConsole->Register("exit", "", CFGFLAG_CLIENT|CFGFLAG_STORE, Con_Quit, this, "Quit Teeworlds");
m_pConsole->Register("minimize", "", CFGFLAG_CLIENT|CFGFLAG_STORE, 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|CFGFLAG_STORE, 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_demomarker", "", CFGFLAG_CLIENT, Con_AddDemoMarker, this, "Add demo timeline marker");
// used for server browser update
m_pConsole->Chain("br_filter_string", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("br_filter_gametype", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("br_filter_serveraddress", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("gfx_screen", ConchainWindowScreen, this);
m_pConsole->Chain("gfx_fullscreen", ConchainFullscreen, this);
m_pConsole->Chain("gfx_borderless", ConchainWindowBordered, this);
m_pConsole->Chain("gfx_vsync", ConchainWindowVSync, this);
}
static CClient *CreateClient()
{
CClient *pClient = static_cast<CClient *>(mem_alloc(sizeof(CClient), 1));
mem_zero(pClient, sizeof(CClient));
return new(pClient) CClient;
}
static bool IsTeeworldsConnectLink(const char *str)
{
const char *starter = "teeworlds:";
while(*str && *starter && *str == *starter)
{
str++;
starter++;
}
return *starter == '\0';
}
void CClient::HandleTeeworldsConnectLink(const char *clink)
{
str_copy(m_aCmdConnect, clink + sizeof("teeworlds:") - 1, sizeof(m_aCmdConnect));
}
/*
Server Time
Client Mirror Time
Client Predicted Time
Snapshot Latency
Downstream latency
Prediction Latency
Upstream latency
*/
int main(int argc, const char **argv) // ignore_convention
{
#if defined(CONF_FAMILY_WINDOWS)
for(int i = 1; i < argc; i++) // ignore_convention
{
if(str_comp("-s", argv[i]) == 0 || str_comp("--silent", argv[i]) == 0) // ignore_convention
{
FreeConsole();
break;
}
}
#endif
bool UseDefaultConfig = false;
for(int i = 1; i < argc; i++) // ignore_convention
{
if(str_comp("-d", argv[i]) == 0 || str_comp("--default", argv[i]) == 0) // ignore_convention
{
UseDefaultConfig = true;
break;
}
}
CClient *pClient = CreateClient();
IKernel *pKernel = IKernel::Create();
pKernel->RegisterInterface(pClient);
pClient->RegisterInterfaces();
// create the components
int FlagMask = CFGFLAG_CLIENT;
IEngine *pEngine = CreateEngine("Teeworlds");
IConsole *pConsole = CreateConsole(FlagMask);
IStorage *pStorage = CreateStorage("Teeworlds", IStorage::STORAGETYPE_CLIENT, argc, argv); // ignore_convention
IConfig *pConfig = CreateConfig();
IEngineSound *pEngineSound = CreateEngineSound();
IEngineInput *pEngineInput = CreateEngineInput();
IEngineTextRender *pEngineTextRender = CreateEngineTextRender();
IEngineMap *pEngineMap = CreateEngineMap();
IEngineMasterServer *pEngineMasterServer = CreateEngineMasterServer();
{
bool RegisterFail = false;
RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngine);
RegisterFail = RegisterFail || !pKernel->RegisterInterface(pConsole);
RegisterFail = RegisterFail || !pKernel->RegisterInterface(pConfig);
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;
}
pEngine->Init();
pConfig->Init(FlagMask);
pEngineMasterServer->Init();
pEngineMasterServer->Load();
// register all console commands
pClient->RegisterCommands();
// init client's interfaces
pClient->InitInterfaces();
pKernel->RequestInterface<IGameClient>()->OnConsoleInit();
if(!UseDefaultConfig)
{
// execute config file
pConsole->ExecuteFile("settings.cfg");
// execute autoexec file
pConsole->ExecuteFile("autoexec.cfg");
// parse the command line arguments
if(argc == 2 && IsTeeworldsConnectLink(argv[1]))
pClient->HandleTeeworldsConnectLink(argv[1]);
else if(argc > 1) // ignore_convention
pConsole->ParseArguments(argc-1, &argv[1]); // ignore_convention
}
// restore empty config strings to their defaults
pConfig->RestoreStrings();
pClient->Engine()->InitLogfile();
// run the client
dbg_msg("client", "starting...");
pClient->Run();
// write down the config and quit
pConfig->Save();
// free components
mem_free(pClient);
delete pKernel;
delete pEngine;
delete pConsole;
delete pStorage;
delete pConfig;
delete pEngineSound;
delete pEngineInput;
delete pEngineTextRender;
delete pEngineMap;
delete pEngineMasterServer;
return 0;
}