diff options
Diffstat (limited to 'src/engine/client/client.cpp')
| -rw-r--r-- | src/engine/client/client.cpp | 2062 |
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; +} |