/* Copyright (c) 2002-2012 Croteam Ltd. This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "stdh.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SESSIONSTATEVERSION_OLD 1 #define SESSIONSTATEVERSION_WITHBULLETTIME 2 #define SESSIONSTATEVERSION_CURRENT SESSIONSTATEVERSION_WITHBULLETTIME //#define DEBUG_LERPING 1 extern INDEX net_bLerping; extern FLOAT net_tmConnectionTimeout; extern FLOAT net_tmProblemsTimeOut; extern FLOAT net_tmDisconnectTimeout; // this is from ProgresHook.cpp - so we tell the progresshook to run client/srever updates extern BOOL _bRunNetUpdates; #if DEBUG_LERPING FLOAT avfStats[1000][4]; INDEX ctCounter=0; INDEX ctMax = sizeof(avfStats)/sizeof(avfStats[0]); #endif // DEBUG_LERPING // get a pseudo-random number (safe for network gaming) ULONG CSessionState::Rnd(void) { ASSERTMSG(ses_bAllowRandom, "Use RND only in entity AI!"); // NOTE: // using multiplicative congruent method with Greanberger's lambda = 2^18+3 ses_ulRandomSeed = ses_ulRandomSeed*262147; ASSERT(ses_ulRandomSeed!=0); return ses_ulRandomSeed; } // reset random number generator (always randomizes to same sequence!) void CSessionState::ResetRND(void) { BOOL bOldAllow = ses_bAllowRandom; ses_bAllowRandom = TRUE; // random must start at a number different than zero! ses_ulRandomSeed = 0x87654321; // run rnd a few times to make it go random for(INDEX i=0; i<32; i++) { Rnd(); } ses_bAllowRandom = bOldAllow; } /* * Constructor. */ CSessionState::CSessionState(void) { ses_bKeepingUpWithTime = TRUE; ses_tmLastUpdated = -100; ses_bAllowRandom = TRUE; // random allowed when not in game ses_bPredicting = FALSE; ses_tmPredictionHeadTick = -2.0f; ses_tmLastSyncCheck = 0; ses_tmLastPredictionProcessed = -200; ses_bPause = FALSE; ses_bWantPause = FALSE; ses_bGameFinished = FALSE; ses_bWaitingForServer = FALSE; ses_strDisconnected = ""; ses_ctMaxPlayers = 1; ses_bWaitAllPlayers = FALSE; ses_iLevel = 0; ses_fRealTimeFactor = 1.0f; ses_pstrm = NULL; // reset random number generator ResetRND(); ses_apltPlayers.New(NET_MAXGAMEPLAYERS); } /* * Destructor. */ CSessionState::~CSessionState() { } /* * Clear the object. */ void CSessionState::Stop(void) { #if DEBUG_SYNCSTREAMDUMPING ClearDumpStream(); #endif #if DEBUG_SYNCSTREAMDUMPING #endif ses_bKeepingUpWithTime = TRUE; ses_tmLastUpdated = -100; ses_bAllowRandom = TRUE; // random allowed when not in game ses_bPredicting = FALSE; ses_tmPredictionHeadTick = -2.0f; ses_tmLastSyncCheck = 0; ses_bPause = FALSE; ses_bWantPause = FALSE; ses_bGameFinished = FALSE; ses_bWaitingForServer = FALSE; ses_strDisconnected = ""; ses_ctMaxPlayers = 1; ses_fRealTimeFactor = 1.0f; ses_bWaitAllPlayers = FALSE; ses_apeEvents.PopAll(); // disable lerping _pTimer->DisableLerp(); #if DEBUG_LERPING CTFileStream f; f.Create_t(CTFILENAME("Temp\\Lerp.stats"), CTStream::CM_TEXT); for (INDEX i=0; iSendToServerReliable(nmConfirmDisconnect); } _cmiComm.Client_Close(); // clear all old levels ForgetOldLevels(); ses_apltPlayers.Clear(); ses_apltPlayers.New(NET_MAXGAMEPLAYERS); } /* * Initialize session state and wait for game to be started. */ void CSessionState::Start_t(INDEX ctLocalPlayers) { ses_bKeepingUpWithTime = TRUE; ses_tmLastUpdated = -100; // clear message stream ses_nsGameStream.Clear(); ses_bAllowRandom = FALSE; // random not allowed in game ses_bPredicting = FALSE; ses_tmPredictionHeadTick = -2.0f; ses_tmLastSyncCheck = 0; ses_bPause = FALSE; ses_bWantPause = FALSE; ses_bWaitingForServer = FALSE; ses_bGameFinished = FALSE; ses_strDisconnected = ""; ses_ctMaxPlayers = 1; ses_fRealTimeFactor = 1.0f; ses_bWaitAllPlayers = FALSE; ses_iMissingSequence = -1; ses_apeEvents.PopAll(); ses_tvMessageReceived.Clear(); _pNetwork->ga_strRequiredMod = ""; // reset random number generator ResetRND(); // clear all old levels ForgetOldLevels(); #if DEBUG_LERPING // clear lerp stats ctCounter = 0; #endif // DEBUG_LERPING // ses_LastProcessedTick and ses_LastReceivedTick tick counters are // irrelevant now, will be initialized when initialization message // from server is received, no need to set them here // if this computer is server if (_pNetwork->IsServer()) { // initialize local client _cmiComm.Client_Init_t(0UL); // connect as main session state try { Start_AtServer_t(); } catch(char *) { _cmiComm.Client_Close(); throw; } // if this computer is client } else { // connect client to server computer _cmiComm.Client_Init_t((char*)(const char*)_pNetwork->ga_strServerAddress); // connect as remote session state try { Start_AtClient_t(ctLocalPlayers); } catch(char *) { // if failed due to wrong mod if (strncmp(ses_strDisconnected, "MOD:", 4)==0) { // remember the mod _pNetwork->ga_strRequiredMod = ses_strDisconnected+4; // make sure that the string is never empty if (_pNetwork->ga_strRequiredMod=="") { _pNetwork->ga_strRequiredMod=" "; } } _cmiComm.Client_Close(); throw; } } } void CSessionState::Start_AtServer_t(void) // throw char * { // send registration request CNetworkMessage nmRegisterMainSessionState(MSG_REQ_CONNECTLOCALSESSIONSTATE); ses_sspParams.Update(); nmRegisterMainSessionState<SendToServerReliable(nmRegisterMainSessionState); for(TIME tmWait=0; tmWaitTimerLoop(); if (_cmiComm.Client_Update() == FALSE) { break; } // wait for message to come CNetworkMessage nmReceived; if (!_pNetwork->ReceiveFromServerReliable(nmReceived)) { continue; } // if this is the init message if (nmReceived.GetType() == MSG_REP_CONNECTLOCALSESSIONSTATE) { // just adjust your tick counters nmReceived>>ses_tmLastProcessedTick; nmReceived>>ses_iLastProcessedSequence; ses_tmInitializationTick = -1.0f; ses_tmInitializationTick2 = -1.0f; // finish waiting return; // otherwise } else { // it is invalid message ThrowF_t(TRANS("Invalid message while waiting for server session registration")); } // if client is disconnected if (!_cmiComm.Client_IsConnected()) { // quit ThrowF_t(TRANS("Client disconnected")); } } ThrowF_t(TRANS("Timeout while waiting for server session registration")); } void CSessionState::Start_AtClient_t(INDEX ctLocalPlayers) // throw char * { // send one unreliable packet to server to make the connection up and running CNetworkMessage nmKeepAlive(MSG_KEEPALIVE); _pNetwork->SendToServer(nmKeepAlive); // send registration request CNetworkMessage nmRegisterSessionState(MSG_REQ_CONNECTREMOTESESSIONSTATE); nmRegisterSessionState<SendToServerReliable(nmRegisterSessionState); // prepare file or memory stream for state CTFileStream strmStateFile; CTMemoryStream strmStateMem; CTStream *pstrmState; extern INDEX net_bDumpConnectionInfo; if (net_bDumpConnectionInfo) { strmStateFile.Create_t(CTString("Temp\\DefaultState.bin")); pstrmState = &strmStateFile; } else { pstrmState = &strmStateMem; } { // wait for server's response CTMemoryStream strmMessage; WaitStream_t(strmMessage, "reply", MSG_REP_CONNECTREMOTESESSIONSTATE); // get motd strmMessage>>ses_strMOTD; // get info for creating default state CTFileName fnmWorld; strmMessage>>fnmWorld; ULONG ulSpawnFlags; strmMessage>>ulSpawnFlags; UBYTE aubProperties[NET_MAXSESSIONPROPERTIES]; strmMessage.Read_t(aubProperties, NET_MAXSESSIONPROPERTIES); // create default state NET_MakeDefaultState_t(fnmWorld, ulSpawnFlags, aubProperties, *pstrmState); pstrmState->SetPos_t(0); } // send one unreliable packet to server to make the connection up and running {CNetworkMessage nmKeepAlive(MSG_KEEPALIVE); _pNetwork->SendToServer(nmKeepAlive); } // send data request CPrintF(TRANS("Sending statedelta request\n")); CNetworkMessage nmRequestDelta(MSG_REQ_STATEDELTA); _pNetwork->SendToServerReliable(nmRequestDelta); { // wait for server's response CTMemoryStream strmMessage; WaitStream_t(strmMessage, "data", MSG_REP_STATEDELTA); // decompress saved session state CTMemoryStream strmDelta; CzlibCompressor comp; comp.UnpackStream_t(strmMessage, strmDelta); CTMemoryStream strmNew; DIFF_Undiff_t(pstrmState, &strmDelta, &strmNew); strmNew.SetPos_t(0); // read the initialization information from the stream Read_t(&strmNew); ses_tmInitializationTick = -1.0f; ses_tmInitializationTick2 = -1.0f; } // send one unreliable packet to server to make the connection up and running {CNetworkMessage nmKeepAlive(MSG_KEEPALIVE); _pNetwork->SendToServer(nmKeepAlive); } CPrintF(TRANS("Sending CRC request\n")); // send data request CNetworkMessage nmRequestCRC(MSG_REQ_CRCLIST); _pNetwork->SendToServerReliable(nmRequestCRC); { // wait for CRC challenge CTMemoryStream strmMessage; WaitStream_t(strmMessage, "CRC", MSG_REQ_CRCCHECK); // make response CNetworkMessage nmCRC(MSG_REP_CRCCHECK); nmCRC<SendToServerReliable(nmCRC); } // send one unreliable packet to server to make the connection up and running {CNetworkMessage nmKeepAlive(MSG_KEEPALIVE); _pNetwork->SendToServer(nmKeepAlive); } } // notify entities of level change void CSessionState::SendLevelChangeNotification(CEntityEvent &ee) { // for each entity in the world {FOREACHINDYNAMICCONTAINER(_pNetwork->ga_World.wo_cenEntities, CEntity, iten) { // if it should be notified if (iten->en_ulFlags&ENF_NOTIFYLEVELCHANGE) { // send the event iten->SendEvent(ee); } }} } // wait for a stream to come from server void CSessionState::WaitStream_t(CTMemoryStream &strmMessage, const CTString &strName, INDEX iMsgCode) { // start waiting for server's response SetProgressDescription(TRANS("waiting for ")+strName); CallProgressHook_t(0.0f); SLONG slReceivedLast = 0; // yes, we need the client/server updates in the progres hook _bRunNetUpdates = TRUE; // repeat until timed out for(TIME tmWait=0; tmWaitReceiveFromServerReliable(strmMessage)) { // continue waiting continue; } // read message identifier strmMessage.SetPos_t(0); INDEX iID; strmMessage>>iID; // if this is the message if (iID == iMsgCode) { // all ok CallProgressHook_t(1.0f); // no more client/server updates in the progres hook _bRunNetUpdates = TRUE; return; // if disconnected } else if (iID == MSG_INF_DISCONNECTED) { // confirm disconnect CNetworkMessage nmConfirmDisconnect(MSG_REP_DISCONNECTED); _pNetwork->SendToServerReliable(nmConfirmDisconnect); // report the reason CTString strReason; strmMessage>>strReason; ses_strDisconnected = strReason; // no more client/server updates in the progres hook _bRunNetUpdates = FALSE; ThrowF_t(TRANS("Disconnected: %s\n"), strReason); // otherwise } else { // no more client/server updates in the progres hook _bRunNetUpdates = FALSE; // it is invalid message ThrowF_t(TRANS("Invalid stream while waiting for %s"), strName); } // if client is disconnected if (!_cmiComm.Client_IsConnected()) { // no more client/server updates in the progres hook _bRunNetUpdates = FALSE; // quit ThrowF_t(TRANS("Client disconnected")); } } // no more client/server updates in the progres hook _bRunNetUpdates = FALSE; // CNetworkMessage nmConfirmDisconnect(MSG_REP_DISCONNECTED); // _pNetwork->SendToServerReliable(nmConfirmDisconnect); ThrowF_t(TRANS("Timeout while waiting for %s"), strName); } // check if disconnected BOOL CSessionState::IsDisconnected(void) { return ses_strDisconnected!=""; } // print an incoming chat message to console void CSessionState::PrintChatMessage(ULONG ulFrom, const CTString &strFrom, const CTString &strMessage) { CTString strSender; // if no sender players if (ulFrom==0) { // take symbolic sender string strSender = strFrom; // if there are sender players } else { // for each sender player for(INDEX ipl=0; iplGetPlayerName(); } } } // let eventual script addon process the message extern CTString cmd_strChatSender ; extern CTString cmd_strChatMessage; extern CTString cmd_cmdOnChat; cmd_strChatSender = strSender; cmd_strChatMessage = strMessage; if (cmd_cmdOnChat!="") { _pShell->Execute(cmd_cmdOnChat); } // if proccessing didn't kill it if (cmd_strChatSender!="" && cmd_strChatMessage!="") { // print the message CPrintF("%s: ^o^cFFFFFF%s^r\n", (const char*)cmd_strChatSender, (const char*)cmd_strChatMessage); } extern INDEX net_ctChatMessages; net_ctChatMessages++; } /* NOTES: 1) New thinkers might be added by current ones, but it doesn't matter, since they must be added forward in time and the list is sorted, so they cannot be processed in this tick. 2) Thinkers/Movers can be removed during iteration, but the CEntityPointer guarantee that they are not freed from memory. */ // do physics for a game tick void CSessionState::HandleMovers(void) { _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_HANDLEMOVERS); // CPrintF("---- tick %g\n", _pTimer->CurrentTick()); // put all movers in active list, pushing ones first CListHead lhActiveMovers, lhDoneMovers, lhDummyMovers; {FORDELETELIST(CMovableEntity, en_lnInMovers, _pNetwork->ga_World.wo_lhMovers, itenMover) { CMovableEntity *pen = itenMover; pen->en_lnInMovers.Remove(); // if predicting, and it is not a predictor if (ses_bPredicting && !pen->IsPredictor()) { // skip it lhDummyMovers.AddTail(pen->en_lnInMovers); continue; } if (!(pen->en_ulFlags&ENF_DELETED)) { if ((pen->en_ulPhysicsFlags&EPF_ONBLOCK_MASK)==EPF_ONBLOCK_PUSH) { lhActiveMovers.AddHead(pen->en_lnInMovers); } else { lhActiveMovers.AddTail(pen->en_lnInMovers); } } }} // for each active mover {FORDELETELIST(CMovableEntity, en_lnInMovers, lhActiveMovers, itenMover) { // let it clear its temporary variables to prevent bad syncs itenMover->ClearMovingTemp(); }} // for each active mover {FORDELETELIST(CMovableEntity, en_lnInMovers, lhActiveMovers, itenMover) { // let it calculate its wanted parameters for this tick itenMover->PreMoving(); }} // while there are some active movers while(!lhActiveMovers.IsEmpty()) { // get first one CMovableEntity *penMoving = LIST_HEAD(lhActiveMovers, CMovableEntity, en_lnInMovers); CEntityPointer penCurrent = penMoving; // just to keep it alive around the loop // first move it to done list (if not done, it will move back to world's movers) penMoving->en_lnInMovers.Remove(); lhDoneMovers.AddTail(penMoving->en_lnInMovers); /* CPrintF("**%s(%08x)", penMoving->en_pecClass->ec_pdecDLLClass->dec_strName, penMoving->en_ulID); if (penMoving->IsPredictable()) { CPrintF(" predictable"); } if (penMoving->IsPredictor()) { CPrintF(" predictor"); } if (penMoving->IsPredicted()) { CPrintF(" predicted"); } if (penMoving->en_penReference!=NULL) { CPrintF("reference id%08x", penMoving->en_penReference->en_ulID); } CPrintF("\n"); */ // let it do its own physics penMoving->DoMoving(); // CPrintF("\n"); // if any mover is re-added, put it to the end of active list lhActiveMovers.MoveList(_pNetwork->ga_World.wo_lhMovers); } // for each done mover {FORDELETELIST(CMovableEntity, en_lnInMovers, lhDoneMovers, itenMover) { // if predicting, and it is not a predictor if (ses_bPredicting && !itenMover->IsPredictor()) { // skip it continue; } // let it calculate its parameters after all movement has been resolved itenMover->PostMoving(); }} // for each done mover {FORDELETELIST(CMovableEntity, en_lnInMovers, lhDoneMovers, itenMover) { CMovableEntity *pen = itenMover; // if predicting, and it is not a predictor if (ses_bPredicting && !itenMover->IsPredictor()) { // skip it continue; } // if marked for removing from list of movers if (pen->en_ulFlags&ENF_INRENDERING) { // remove it pen->en_ulFlags&=~ENF_INRENDERING; pen->en_lnInMovers.Remove(); } // let it clear its temporary variables to prevent bad syncs pen->ClearMovingTemp(); }} // return all done movers to the world's list _pNetwork->ga_World.wo_lhMovers.MoveList(lhDummyMovers); _pNetwork->ga_World.wo_lhMovers.MoveList(lhDoneMovers); // handle all the sent events CEntity::HandleSentEvents(); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_HANDLEMOVERS); } // do thinking for a game tick void CSessionState::HandleTimers(TIME tmCurrentTick) { #define TIME_EPSILON 0.0001f IFDEBUG(TIME tmLast = 0.0f); _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_HANDLETIMERS); // repeat CListHead &lhTimers = _pNetwork->ga_World.wo_lhTimers; FOREVER { // no entity found initially CRationalEntity *penTimer = NULL; // for each entity in list of timers FOREACHINLIST(CRationalEntity, en_lnInTimers, lhTimers, iten) { // if due after current time if(iten->en_timeTimer>tmCurrentTick+TIME_EPSILON) { // stop searching break; } // if now predicting and it is not a predictor if (ses_bPredicting && !iten->IsPredictor()) { // skip it continue; } // remember found entity, and stop searching penTimer = iten; break; } // if no entity is found if (penTimer==NULL) { // stop break; } // check that timers are propertly handled ASSERT(penTimer->en_timeTimer>tmCurrentTick-_pTimer->TickQuantum-TIME_EPSILON); //ASSERT(penTimer->en_timeTimer>=tmLast); IFDEBUG(tmLast=penTimer->en_timeTimer); // remove the timer from the list penTimer->en_timeTimer = THINKTIME_NEVER; penTimer->en_lnInTimers.Remove(); // send timer event to the entity penTimer->SendEvent(ETimer()); } // handle all the sent events CEntity::HandleSentEvents(); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_HANDLETIMERS); } // do a warm-up run of the world for a few ticks void CSessionState::WarmUpWorld(void) { #define WORLD_WORMUP_COUNT 20 // run 20 ticks to get stable ses_tmLastProcessedTick = _pNetwork->ga_srvServer.srv_tmLastProcessedTick = 0; ses_iLastProcessedSequence = _pNetwork->ga_srvServer.srv_iLastProcessedSequence = -1; // add a few empty all-action messages to the game stream for (INDEX iTick=0; iTickga_srvServer.srv_tmLastProcessedTick += _pTimer->TickQuantum; _pNetwork->ga_srvServer.srv_iLastProcessedSequence++; CNetworkStreamBlock nsbAllActions(MSG_SEQ_ALLACTIONS, _pNetwork->ga_srvServer.srv_iLastProcessedSequence); nsbAllActions<<_pNetwork->ga_srvServer.srv_tmLastProcessedTick; nsbAllActions.Rewind(); ses_nsGameStream.AddBlock(nsbAllActions); } // process the blocks ProcessGameStream(); } // create a checksum value for sync-check void CSessionState::ChecksumForSync(ULONG &ulCRC, INDEX iExtensiveSyncCheck) { CRC_AddLONG(ulCRC, ses_iLastProcessedSequence); CRC_AddLONG(ulCRC, ses_iLevel); CRC_AddLONG(ulCRC, ses_bPause); if (iExtensiveSyncCheck>0) { CRC_AddLONG(ulCRC, ses_bGameFinished); CRC_AddLONG(ulCRC, ses_ulRandomSeed); } // if all entities should be synced if (iExtensiveSyncCheck>1) { // for each active entity in the world {FOREACHINDYNAMICCONTAINER(_pNetwork->ga_World.wo_cenEntities, CEntity, iten) { if (iten->IsPredictor()) { continue; } iten->ChecksumForSync(ulCRC, iExtensiveSyncCheck); }} // for each entity in the world {FOREACHINDYNAMICCONTAINER(_pNetwork->ga_World.wo_cenAllEntities, CEntity, iten) { if (iten->IsPredictor()) { continue; } iten->ChecksumForSync(ulCRC, iExtensiveSyncCheck); }} } if (iExtensiveSyncCheck>0) { // checksum all movers {FOREACHINLIST(CMovableEntity, en_lnInMovers, _pNetwork->ga_World.wo_lhMovers, iten) { if (iten->IsPredictor()) { continue; } iten->ChecksumForSync(ulCRC, iExtensiveSyncCheck); }} } // checksum all active players {FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itclt) { CPlayerTarget &clt = *itclt; if (clt.IsActive()) { clt.plt_paPreLastAction.ChecksumForSync(ulCRC); clt.plt_paLastAction.ChecksumForSync(ulCRC); clt.plt_penPlayerEntity->ChecksumForSync(ulCRC, iExtensiveSyncCheck); } }} } // dump sync data to text file void CSessionState::DumpSync_t(CTStream &strm, INDEX iExtensiveSyncCheck) // throw char * { strm.FPrintF_t("Level: %d\n", ses_iLevel); strm.FPrintF_t("Sequence: %d\n", ses_iLastProcessedSequence); strm.FPrintF_t("Tick: %g\n", ses_tmLastProcessedTick); strm.FPrintF_t("Paused: %d\n", ses_bPause); if (iExtensiveSyncCheck>0) { strm.FPrintF_t("Finished: %d\n", ses_bGameFinished); strm.FPrintF_t("Random seed: 0x%08x\n", ses_ulRandomSeed); } _pNetwork->ga_World.LockAll(); strm.FPrintF_t("\n\n======================== players:\n"); // dump all active players {FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itclt) { CPlayerTarget &clt = *itclt; if (clt.IsActive()) { clt.plt_penPlayerEntity->DumpSync_t(strm, iExtensiveSyncCheck); strm.FPrintF_t("\n -- action:\n"); clt.plt_paPreLastAction.DumpSync_t(strm); clt.plt_paLastAction.DumpSync_t(strm); } }} if (iExtensiveSyncCheck>0) { strm.FPrintF_t("\n\n======================== movers:\n"); // dump all movers {FOREACHINLIST(CMovableEntity, en_lnInMovers, _pNetwork->ga_World.wo_lhMovers, iten) { if (iten->IsPredictor()) { continue; } iten->DumpSync_t(strm, iExtensiveSyncCheck); }} } // if all entities should be synced if (iExtensiveSyncCheck>1) { strm.FPrintF_t("\n\n======================== active entities (%d):\n", _pNetwork->ga_World.wo_cenEntities.Count()); // for each entity in the world {FOREACHINDYNAMICCONTAINER(_pNetwork->ga_World.wo_cenEntities, CEntity, iten) { if (iten->IsPredictor()) { continue; } iten->DumpSync_t(strm, iExtensiveSyncCheck); }} strm.FPrintF_t("\n\n======================== all entities (%d):\n", _pNetwork->ga_World.wo_cenEntities.Count()); // for each entity in the world {FOREACHINDYNAMICCONTAINER(_pNetwork->ga_World.wo_cenAllEntities, CEntity, iten) { if (iten->IsPredictor()) { continue; } iten->DumpSync_t(strm, iExtensiveSyncCheck); }} } _pNetwork->ga_World.UnlockAll(); } #if DEBUG_SYNCSTREAMDUMPING /* * Obtain valid session dump memory stream */ CTMemoryStream *GetDumpStream(void) { if( _pNetwork->ga_sesSessionState.ses_pstrm == NULL) { _pNetwork->ga_sesSessionState.ses_pstrm = new CTMemoryStream; } return _pNetwork->ga_sesSessionState.ses_pstrm; } void ClearDumpStream(void) { if( _pNetwork->ga_sesSessionState.ses_pstrm != NULL) { delete _pNetwork->ga_sesSessionState.ses_pstrm; _pNetwork->ga_sesSessionState.ses_pstrm = NULL; } } #endif /* * Process a game tick. */ extern INDEX cli_bEmulateDesync; void CSessionState::ProcessGameTick(CNetworkMessage &nmMessage, TIME tmCurrentTick) { ses_tmLastPredictionProcessed = -1; _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_PROCESSGAMETICK); ASSERT(this!=NULL); #if DEBUG_SYNCSTREAMDUMPING try { CTMemoryStream *pstrm = GetDumpStream(); pstrm->FPrintF_t("Time tick: %.2f\n", tmCurrentTick); } catch (char *strError) { CPrintF("Cannot dump sync data: %s\n", strError); } #endif //CPrintF("normal: %.2f\n", tmCurrentTick); // FPU must be in 24-bit mode CSetFPUPrecision FPUPrecision(FPT_24BIT); // copy the tick to process into tick used for all tasks _pTimer->SetCurrentTick(tmCurrentTick); _pfNetworkProfile.IncrementAveragingCounter(); _pfPhysicsProfile.IncrementAveragingCounter(); // random is allowed only here, during entity ai ses_bAllowRandom = TRUE; _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_APPLYACTIONS); // for all clients INDEX iClient = 0; FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itplt) { // if client is active if (itplt->IsActive()) { // player indices transmission is unneccessary unless if debugging // // check that it is present in message // INDEX iClientInMessage; // nmMessage>>iClientInMessage; // client index // ASSERT(iClient==iClientInMessage); // read the action CPlayerAction paAction; nmMessage>>paAction; // apply the action itplt->ApplyActionPacket(paAction); // desync emulation! if( cli_bEmulateDesync) { itplt->plt_penPlayerEntity->SetHealth(1.0f); } } iClient++; } cli_bEmulateDesync = FALSE; // handle all the sent events CEntity::HandleSentEvents(); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_APPLYACTIONS); // do thinking HandleTimers(tmCurrentTick); // do physics HandleMovers(); // notify all entities of level change as needed if (_lphCurrent==LCP_INITIATED) { EPreLevelChange ePreChange; ePreChange.iUserData = _pNetwork->ga_iNextLevelUserData; SendLevelChangeNotification(ePreChange); CEntity::HandleSentEvents(); _lphCurrent=LCP_SIGNALLED; } if (_lphCurrent==LCP_CHANGED) { EPostLevelChange ePostChange; ePostChange.iUserData = _pNetwork->ga_iNextLevelUserData; SendLevelChangeNotification(ePostChange); CEntity::HandleSentEvents(); _lphCurrent=LCP_NOCHANGE; } _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_WORLDBASETICK); // let the worldbase execute its tick function if (_pNetwork->ga_World.wo_pecWorldBaseClass!=NULL &&_pNetwork->ga_World.wo_pecWorldBaseClass->ec_pdecDLLClass!=NULL &&_pNetwork->ga_World.wo_pecWorldBaseClass->ec_pdecDLLClass->dec_OnWorldTick!=NULL) { _pNetwork->ga_World.wo_pecWorldBaseClass->ec_pdecDLLClass-> dec_OnWorldTick(&_pNetwork->ga_World); } // handle all the sent events CEntity::HandleSentEvents(); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_WORLDBASETICK); // make sync-check and send to server if needed MakeSynchronisationCheck(); #if DEBUG_SYNCSTREAMDUMPING extern INDEX cli_bDumpSyncEachTick; if( cli_bDumpSyncEachTick) { DumpSyncToMemory(); } #endif ses_bAllowRandom = FALSE; ses_tmPredictionHeadTick = Max(ses_tmPredictionHeadTick, tmCurrentTick); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_PROCESSGAMETICK); // assure that FPU precision was low all the rendering time ASSERT( GetFPUPrecision()==FPT_24BIT); // let eventual script do something on each tick extern FLOAT cmd_tmTick; extern CTString cmd_cmdOnTick; if (cmd_cmdOnTick!="") { cmd_tmTick = tmCurrentTick; _pShell->Execute(cmd_cmdOnTick); } } /* Process a predicted game tick. */ void CSessionState::ProcessPredictedGameTick(INDEX iPredictionStep, FLOAT fFactor, TIME tmCurrentTick) { _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_PROCESSGAMETICK); ASSERT(this!=NULL); //CPrintF("predicted: %.2f\n", tmCurrentTick); // FPU must be in 24-bit mode CSetFPUPrecision FPUPrecision(FPT_24BIT); // now predicting ses_bPredicting = TRUE; // copy the tick to process into tick used for all tasks _pTimer->SetCurrentTick(tmCurrentTick); _pfNetworkProfile.IncrementAveragingCounter(); _pfPhysicsProfile.IncrementAveragingCounter(); // random is allowed only here, during entity ai ses_bAllowRandom = TRUE; _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_APPLYACTIONS); // for all clients FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itplt) { // if client is active if (itplt->IsActive()) { // apply the predicted action itplt->ApplyPredictedAction(iPredictionStep, fFactor); } } // handle all the sent events CEntity::HandleSentEvents(); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_APPLYACTIONS); // do thinking HandleTimers(tmCurrentTick); // do physics HandleMovers(); _pfPhysicsProfile.StartTimer(CPhysicsProfile::PTI_WORLDBASETICK); // handle all the sent events CEntity::HandleSentEvents(); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_WORLDBASETICK); ses_bAllowRandom = FALSE; // not predicting any more ses_bPredicting = FALSE; ses_tmPredictionHeadTick = Max(ses_tmPredictionHeadTick, tmCurrentTick); _pfPhysicsProfile.StopTimer(CPhysicsProfile::PTI_PROCESSGAMETICK); // assure that FPU precision was low all the rendering time ASSERT( GetFPUPrecision()==FPT_24BIT); } /* * Process all eventual available gamestream blocks. */ void CSessionState::ProcessGameStream(void) { _pfNetworkProfile.StartTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); // must be in 24bit mode when managing entities CSetFPUPrecision FPUPrecision(FPT_24BIT); TIME tmDemoNow = _pNetwork->ga_fDemoTimer; // if playing a demo if (_pNetwork->ga_bDemoPlay) { // find how much ticks to step by now if (ses_tmLastDemoSequence<0.0f) { ses_tmLastDemoSequence=tmDemoNow; } // if it is finished if (_pNetwork->ga_bDemoPlayFinished) { _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); // do nothing return; } } // repeat FOREVER { // if playing a demo if (_pNetwork->ga_bDemoPlay) { // if finished for this pass if (ses_tmLastDemoSequence>=tmDemoNow) { _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); // end the loop return; } // try to try { // read a stream block from the demo CChunkID cid = _pNetwork->ga_strmDemoPlay.PeekID_t(); if (cid == CChunkID("DTCK")) { _pNetwork->ga_strmDemoPlay.ExpectID_t("DTCK");// demo tick CNetworkStreamBlock nsbBlock; nsbBlock.Read_t(_pNetwork->ga_strmDemoPlay); ses_nsGameStream.AddBlock(nsbBlock); _pNetwork->ga_bDemoPlayFinished = FALSE; } else { _pNetwork->ga_strmDemoPlay.ExpectID_t("DEND"); // demo end _pNetwork->ga_bDemoPlayFinished = TRUE; _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); return; } // if not successful } catch(char *strError) { // report error CPrintF(TRANS("Error while playing demo: %s"), strError); _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); return; } } // calculate index of next expected sequence INDEX iSequence = ses_iLastProcessedSequence+1; // get the stream block with that sequence CNetworkStreamBlock *pnsbBlock; CNetworkStream::Result res = ses_nsGameStream.GetBlockBySequence(iSequence, pnsbBlock); // if it is found if (res==CNetworkStream::R_OK) { // if recording a demo if (_pNetwork->ga_bDemoRec) { // try to try { // write the stream block to the demo _pNetwork->ga_strmDemoRec.WriteID_t("DTCK"); pnsbBlock->Write_t(_pNetwork->ga_strmDemoRec); // if not successful } catch(char *strError) { // report error CPrintF(TRANS("Error while recording demo: %s"), strError); // stop recording _pNetwork->StopDemoRec(); } } // remember the message type int iMsgType=pnsbBlock->GetType(); // remember the processed sequence ses_iLastProcessedSequence = iSequence; // process the stream block ProcessGameStreamBlock(*pnsbBlock); // remove the block from the stream pnsbBlock->RemoveFromStream(); delete pnsbBlock; // remove eventual resent blocks that have already been processed ses_nsGameStream.RemoveOlderBlocksBySequence(ses_iLastProcessedSequence-2); // if the message is all actions if (iMsgType==MSG_SEQ_ALLACTIONS) { // if playing a demo if (_pNetwork->ga_bDemoPlay) { // step demo sequence ses_tmLastDemoSequence+=_pTimer->TickQuantum; } } // if it is not avaliable yet } if (res==CNetworkStream::R_BLOCKNOTRECEIVEDYET) { // finish _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); return; // if it is missing } else if (res==CNetworkStream::R_BLOCKMISSING) { // if it is a new sequence if (iSequence>ses_iMissingSequence) { ses_iMissingSequence = iSequence; // setup timeout ses_tvResendTime = _pTimer->GetHighPrecisionTimer(); ses_tmResendTimeout = 0.1f; } // if timeout occured if (_pTimer->GetHighPrecisionTimer()>ses_tvResendTime+CTimerValue(ses_tmResendTimeout)) { _pNetwork->AddNetGraphValue(NGET_MISSING, 1.0f); // missing sequence // find how many are missing INDEX iNextValid = ses_nsGameStream.GetOldestSequenceAfter(iSequence); INDEX ctSequences = Max(iNextValid-iSequence, INDEX(1)); // create a request for resending the missing packet CNetworkMessage nmResendRequest(MSG_REQUESTGAMESTREAMRESEND); nmResendRequest<SendToServer(nmResendRequest); extern INDEX net_bReportMiscErrors; if (net_bReportMiscErrors) { CPrintF(TRANS("Session State: Missing sequences %d-%d(%d) timeout %g\n"), iSequence, iSequence+ctSequences-1, ctSequences, ses_tmResendTimeout); } // increase the timeout ses_tvResendTime = _pTimer->GetHighPrecisionTimer(); ses_tmResendTimeout *= 2.0f; } // finish _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_PROCESSGAMESTREAM); return; } } } // flush prediction actions that were already processed void CSessionState::FlushProcessedPredictions(void) { // for all clients FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itplt) { // flush itplt->FlushProcessedPredictions(); } } /* Find out how many prediction steps are currently pending. */ INDEX CSessionState::GetPredictionStepsCount(void) { // start with no prediction INDEX ctPredictionSteps = 0; // for all clients FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itplt) { // update maximum number of possible predictions ctPredictionSteps = Max(ctPredictionSteps, itplt->GetNumberOfPredictions()); } return ctPredictionSteps; } /* Process all eventual available prediction actions. */ void CSessionState::ProcessPrediction(void) { // FPU must be in 24-bit mode CSetFPUPrecision FPUPrecision(FPT_24BIT); // get number of steps that could be predicted INDEX ctSteps = GetPredictionStepsCount(); // limit prediction extern INDEX cli_iMaxPredictionSteps; ctSteps = ClampUp(ctSteps, cli_iMaxPredictionSteps); // if none if(ctSteps<=0) { // do nothing return; } // if this would not result in any new tick predicted TIME tmNow = ses_tmLastProcessedTick+ctSteps*_pTimer->TickQuantum; if (Abs(ses_tmLastPredictionProcessed-tmNow)<_pTimer->TickQuantum/10.0f) { // do nothing return; } // remeber what was predicted now ses_tmLastPredictionProcessed = tmNow; // remember random seed and entity ID ULONG ulOldRandom = ses_ulRandomSeed; ULONG ulEntityID = _pNetwork->ga_World.wo_ulNextEntityID; // delete all predictors (if any left from last time) _pNetwork->ga_World.DeletePredictors(); // create new predictors _pNetwork->ga_World.CreatePredictors(); // for each step TIME tmPredictedTick = ses_tmLastProcessedTick; for(INDEX iPredictionStep=0; iPredictionStepTickQuantum; //ses_tmPredictionHeadTick = Max(ses_tmPredictionHeadTick, tmPredictedTick); // predict it ProcessPredictedGameTick(iPredictionStep, FLOAT(iPredictionStep)/ctSteps, tmPredictedTick); } // restore random seed and entity ID ses_ulRandomSeed = ulOldRandom; _pNetwork->ga_World.wo_ulNextEntityID = ulEntityID; } /* * Process a gamestream block. */ void CSessionState::ProcessGameStreamBlock(CNetworkMessage &nmMessage) { // copy the tick to process into tick used for all tasks _pTimer->SetCurrentTick(ses_tmLastProcessedTick); // check message type switch (nmMessage.GetType()) { // if adding a new player case MSG_SEQ_ADDPLAYER: { _pNetwork->AddNetGraphValue(NGET_NONACTION, 1.0f); // non-action sequence INDEX iNewPlayer; CPlayerCharacter pcCharacter; nmMessage>>iNewPlayer; // player index nmMessage>>pcCharacter; // player character // delete all predictors _pNetwork->ga_World.DeletePredictors(); // activate the player ses_apltPlayers[iNewPlayer].Activate(); // if there is no entity with that character in the world CPlayerEntity *penNewPlayer = _pNetwork->ga_World.FindEntityWithCharacter(pcCharacter); if (penNewPlayer==NULL) { // create an entity for it CPlacement3D pl(FLOAT3D(0.0f,0.0f,0.0f), ANGLE3D(0,0,0)); try { CTFileName fnmPlayer = CTString("Classes\\Player.ecl"); // this must not be a dependency! penNewPlayer = (CPlayerEntity*)(_pNetwork->ga_World.CreateEntity_t(pl, fnmPlayer)); // attach entity to client data ses_apltPlayers[iNewPlayer].AttachEntity(penNewPlayer); // attach the character to it penNewPlayer->en_pcCharacter = pcCharacter; // prepare the entity penNewPlayer->Initialize(); } catch (char *strError) { (void)strError; FatalError(TRANS("Cannot load Player class:\n%s"), strError); } if (!_pNetwork->IsPlayerLocal(penNewPlayer)) { CPrintF(TRANS("%s joined\n"), penNewPlayer->GetPlayerName()); } } else { // attach entity to client data ses_apltPlayers[iNewPlayer].AttachEntity(penNewPlayer); // make it update its character penNewPlayer->CharacterChanged(pcCharacter); if (!_pNetwork->IsPlayerLocal(penNewPlayer)) { CPrintF(TRANS("%s rejoined\n"), penNewPlayer->GetPlayerName()); } } } break; // if removing a player case MSG_SEQ_REMPLAYER: { _pNetwork->AddNetGraphValue(NGET_NONACTION, 1.0f); // non-action sequence INDEX iPlayer; nmMessage>>iPlayer; // player index // delete all predictors _pNetwork->ga_World.DeletePredictors(); // inform entity of disconnnection CPrintF(TRANS("%s left\n"), ses_apltPlayers[iPlayer].plt_penPlayerEntity->GetPlayerName()); ses_apltPlayers[iPlayer].plt_penPlayerEntity->Disconnect(); // deactivate the player ses_apltPlayers[iPlayer].Deactivate(); // handle all the sent events ses_bAllowRandom = TRUE; CEntity::HandleSentEvents(); ses_bAllowRandom = FALSE; } break; // if changing character case MSG_SEQ_CHARACTERCHANGE: { _pNetwork->AddNetGraphValue(NGET_NONACTION, 1.0f); // non-action sequence INDEX iPlayer; CPlayerCharacter pcCharacter; nmMessage>>iPlayer>>pcCharacter; // delete all predictors _pNetwork->ga_World.DeletePredictors(); // change the character ses_apltPlayers[iPlayer].plt_penPlayerEntity->CharacterChanged(pcCharacter); // handle all the sent events ses_bAllowRandom = TRUE; CEntity::HandleSentEvents(); ses_bAllowRandom = FALSE; } break; // if receiving client actions case MSG_SEQ_ALLACTIONS: { // read time from packet TIME tmPacket; nmMessage>>tmPacket; // packet time // time must be greater by one than that on the last packet received TIME tmTickQuantum = _pTimer->TickQuantum; TIME tmPacketDelta = tmPacket-ses_tmLastProcessedTick; if(! (Abs(tmPacketDelta-tmTickQuantum) < (tmTickQuantum/10.0f)) ) { // report debug info CPrintF( TRANS("Session state: Mistimed MSG_ALLACTIONS: Last received tick %g, this tick %g\n"), ses_tmLastProcessedTick, tmPacket); } // remember the received tick ses_tmLastProcessedTick = tmPacket; // NOTE: if we got a tick, it means that all players have joined // don't wait for new players any more ses_bWaitAllPlayers = FALSE; // delete all predictors _pNetwork->ga_World.DeletePredictors(); // process the tick ProcessGameTick(nmMessage, tmPacket); } break; // if receiving pause message case MSG_SEQ_PAUSE: { _pNetwork->AddNetGraphValue(NGET_NONACTION, 1.0f); // non-action sequence // delete all predictors _pNetwork->ga_World.DeletePredictors(); BOOL bPauseBefore = ses_bPause; // read the pause state and pauser from it nmMessage>>(INDEX&)ses_bPause; CTString strPauser; nmMessage>>strPauser; // if paused by some other machine if (strPauser!=TRANS("Local machine")) { // report who paused if (ses_bPause!=bPauseBefore) { if (ses_bPause) { CPrintF(TRANS("Paused by '%s'\n"), strPauser); } else { CPrintF(TRANS("Unpaused by '%s'\n"), strPauser); } } } // must keep wanting current state ses_bWantPause = ses_bPause; } break; // otherwise default: // error ASSERT(FALSE); } } // Set lerping factor for current frame. void CSessionState::SetLerpFactor(CTimerValue tvNow) { // if no lerping if (!net_bLerping) { // set lerping factor without lerping _pTimer->SetLerp(1.0f); _pTimer->SetCurrentTick(ses_tmPredictionHeadTick); return; } FLOAT fFactor = 0.0f; FLOAT fFactor2 = 0.0f; // ---- primary factor - used for prediction { TIME tmLastTick = ses_tmPredictionHeadTick; // if lerping was never set before if (ses_tmInitializationTick<0) { // initialize it ses_tvInitialization = tvNow; ses_tmInitializationTick = tmLastTick; } // get passed time from session state starting in precise time and in ticks FLOAT tmRealDelta = FLOAT((tvNow-ses_tvInitialization).GetSeconds()) *_pNetwork->ga_fGameRealTimeFactor*_pNetwork->ga_sesSessionState.ses_fRealTimeFactor; FLOAT tmTickDelta = tmLastTick-ses_tmInitializationTick; // calculate factor fFactor = 1.0f-(tmTickDelta-tmRealDelta)/_pTimer->TickQuantum; // if the factor starts getting below zero if (fFactor<0) { //CPrintF("Lerp=%.2f <0 @ %.2fs\n", fFactor, tmLastTick); // clamp it fFactor = 0.0f; // readjust timers so that it gets better ses_tvInitialization = tvNow; ses_tmInitializationTick = tmLastTick-_pTimer->TickQuantum; } if (fFactor>1) { //CPrintF("Lerp=%.2f >1 @ %.2fs\n", fFactor, tmLastTick); // clamp it fFactor = 1.0f; // readjust timers so that it gets better ses_tvInitialization = tvNow; ses_tmInitializationTick = tmLastTick; } #if DEBUG_LERPING avfStats[ctCounter][0] = tmRealDelta/_pTimer->TickQuantum; avfStats[ctCounter][1] = tmTickDelta/_pTimer->TickQuantum; avfStats[ctCounter][2] = fFactor; avfStats[ctCounter][3] = (tmLastTick/_pTimer->TickQuantum-1.0f)+fFactor; ctCounter++; if (ctCounter>=ctMax) { ctCounter = 0; } #endif // DEBUG_LERPING } // ---- secondary factor - used for non-predicted movement { TIME tmLastTick = ses_tmLastProcessedTick; // if lerping was never set before if (ses_tmInitializationTick2<0) { // initialize it ses_tvInitialization2 = tvNow; ses_tmInitializationTick2 = tmLastTick; } // get passed time from session state starting in precise time and in ticks FLOAT tmRealDelta = FLOAT((tvNow-ses_tvInitialization2).GetSeconds()) *_pNetwork->ga_fGameRealTimeFactor*_pNetwork->ga_sesSessionState.ses_fRealTimeFactor; FLOAT tmTickDelta = tmLastTick-ses_tmInitializationTick2; // calculate factor fFactor2 = 1.0f-(tmTickDelta-tmRealDelta)/_pTimer->TickQuantum; // if the factor starts getting below zero if (fFactor2<0) { //CPrintF("Lerp2=%.2f <0 @ %.2fs\n", fFactor2, tmLastTick); // clamp it fFactor2 = 0.0f; // readjust timers so that it gets better ses_tvInitialization2 = tvNow; ses_tmInitializationTick2 = tmLastTick-_pTimer->TickQuantum; } if (fFactor2>1) { //CPrintF("Lerp2=%.2f >1 @ %.2fs\n", fFactor2, tmLastTick); // clamp it fFactor2 = 1.0f; // readjust timers so that it gets better ses_tvInitialization2 = tvNow; ses_tmInitializationTick2 = tmLastTick; } } // set lerping factor2 _pTimer->SetLerp(fFactor); _pTimer->SetLerp2(fFactor2); _pTimer->SetCurrentTick(ses_tmPredictionHeadTick); } /* * Read session state state from a stream. */ void CSessionState::Read_t(CTStream *pstr) // throw char * { #if DEBUG_SYNCSTREAMDUMPING CPrintF( "Session state read: Sequence %d, Time %g\n", ses_iLastProcessedSequence, ses_tmLastProcessedTick); #endif // read time information and random seed INDEX iVersion = SESSIONSTATEVERSION_OLD; if (pstr->PeekID_t()==CChunkID("SESV")) { pstr->ExpectID_t("SESV"); (*pstr)>>iVersion; } (*pstr)>>ses_tmLastProcessedTick; (*pstr)>>ses_iLastProcessedSequence; (*pstr)>>ses_iLevel; (*pstr)>>ses_ulRandomSeed; (*pstr)>>ses_ulSpawnFlags; (*pstr)>>ses_tmSyncCheckFrequency; (*pstr)>>ses_iExtensiveSyncCheck; (*pstr)>>ses_tmLastSyncCheck; (*pstr)>>ses_ctMaxPlayers; (*pstr)>>ses_bWaitAllPlayers; (*pstr)>>ses_bPause; (*pstr)>>ses_bGameFinished; if (iVersion>=SESSIONSTATEVERSION_WITHBULLETTIME) { (*pstr)>>ses_fRealTimeFactor; } ses_bWaitingForServer = FALSE; ses_bWantPause = ses_bPause; ses_strDisconnected = ""; _pTimer->SetCurrentTick(ses_tmLastProcessedTick); // read session properties from stream (*pstr)>>_pNetwork->ga_strSessionName; pstr->Read_t(_pNetwork->ga_aubProperties, NET_MAXSESSIONPROPERTIES); // read world and its state ReadWorldAndState_t(pstr); #if DEBUG_SYNCSTREAMDUMPING ClearDumpStream(); // dump sync data if needed extern INDEX cli_bDumpSyncEachTick; if( cli_bDumpSyncEachTick) { DumpSyncToMemory(); } #endif } void CSessionState::ReadWorldAndState_t(CTStream *pstr) // throw char * { // check engine build disallowing reinit BOOL bNeedsReinit; _pNetwork->CheckVersion_t(*pstr, FALSE, bNeedsReinit); ASSERT(!bNeedsReinit); // read world filename from stream (*pstr)>>_pNetwork->ga_fnmWorld; if (CTFileName(pstr->GetDescription()).FileExt()==".dem" && GetFileTimeStamp_t(pstr->GetDescription())<=GetFileTimeStamp_t(_pNetwork->ga_fnmWorld)) { ThrowF_t( TRANS("Cannot play demo because file '%s'\n" "is older than file '%s'!\n"), CTString(pstr->GetDescription()), CTString(_pNetwork->ga_fnmWorld)); } // prepare the world for loading _pNetwork->ga_World.DeletePredictors(); _pNetwork->ga_World.Clear(); _pNetwork->ga_World.LockAll(); // load the world brushes from the world file _pNetwork->ga_World.LoadBrushes_t(_pNetwork->ga_fnmWorld); // read world situation _pNetwork->ga_World.ReadState_t(pstr); // create an empty list for relinking timers CListHead lhNewTimers; // read number of entities in timer list pstr->ExpectID_t("TMRS"); // timers INDEX ctTimers; *pstr>>ctTimers; // ASSERT(ctTimers == _pNetwork->ga_World.wo_lhTimers.Count()); // for each entity in the timer list {for(INDEX ienTimer=0; ienTimer>ien; // get the entity CRationalEntity *pen = (CRationalEntity*)_pNetwork->ga_World.EntityFromID(ien); // remove it from the timer list and add it at the end of the new timer list if (pen->en_lnInTimers.IsLinked()) { pen->en_lnInTimers.Remove(); lhNewTimers.AddTail(pen->en_lnInTimers); } }} // use the new timer list instead the old one ASSERT(_pNetwork->ga_World.wo_lhTimers.IsEmpty()); _pNetwork->ga_World.wo_lhTimers.MoveList(lhNewTimers); // create an empty list for relinking movers CListHead lhNewMovers; // read number of entities in mover list pstr->ExpectID_t("MVRS"); // movers INDEX ctMovers; *pstr>>ctMovers; ASSERT(ctMovers == _pNetwork->ga_World.wo_lhMovers.Count()); // for each entity in the mover list {for(INDEX ienMover=0; ienMover>ien; // get the entity CMovableEntity *pen = (CMovableEntity*)_pNetwork->ga_World.EntityFromID(ien); // remove it from the mover list and add it at the end of the new mover list if (pen->en_lnInMovers.IsLinked()) { pen->en_lnInMovers.Remove(); } lhNewMovers.AddTail(pen->en_lnInMovers); }} // use the new mover list instead the old one ASSERT(_pNetwork->ga_World.wo_lhMovers.IsEmpty()); _pNetwork->ga_World.wo_lhMovers.MoveList(lhNewMovers); // read number of players INDEX ctPlayers; (*pstr)>>ctPlayers; ASSERT(ctPlayers==ses_apltPlayers.Count()); // for all clients FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itclt) { // read from stream itclt->Read_t(pstr); } _pNetwork->ga_World.UnlockAll(); } void CSessionState::ReadRememberedLevels_t(CTStream *pstr) { pstr->ExpectID_t("RLEV"); // remembered levels // read count of remembered levels INDEX ctLevels; (*pstr)>>ctLevels; // for each level for(INDEX iLevel=0; iLevel>prl->rl_strFileName; //prl->rl_strmSessionState. // use readstream() !!!! @@@@ } }; /* * Write session state state into a stream. */ void CSessionState::Write_t(CTStream *pstr) // throw char * { #if DEBUG_SYNCSTREAMDUMPING CPrintF( "Session state write: Sequence %d, Time %.2f\n", ses_iLastProcessedSequence, ses_tmLastProcessedTick); #endif pstr->WriteID_t("SESV"); (*pstr)<ga_strSessionName; pstr->Write_t(_pNetwork->ga_aubProperties, NET_MAXSESSIONPROPERTIES); // write world and its state WriteWorldAndState_t(pstr); } void CSessionState::WriteWorldAndState_t(CTStream *pstr) // throw char * { // delete all predictor entities before saving _pNetwork->ga_World.UnmarkForPrediction(); _pNetwork->ga_World.DeletePredictors(); // save engine build _pNetwork->WriteVersion_t(*pstr); // write world filename to stream (*pstr)<<_pNetwork->ga_fnmWorld; // write world situation _pNetwork->ga_World.LockAll(); _pNetwork->ga_World.WriteState_t(pstr, TRUE); // write number of entities in timer list pstr->WriteID_t("TMRS"); // timers CListHead &lhTimers = _pNetwork->ga_World.wo_lhTimers; *pstr<en_ulID; }} // write number of entities in mover list pstr->WriteID_t("MVRS"); // movers CListHead &lhMovers = _pNetwork->ga_World.wo_lhMovers; *pstr<en_ulID; }} // write number of clients (*pstr)<Write_t(pstr); } _pNetwork->ga_World.UnlockAll(); } void CSessionState::WriteRememberedLevels_t(CTStream *pstr) { // use writestream() !!!! @@@@ }; // remember current level void CSessionState::RememberCurrentLevel(const CTString &strFileName) { // if level is already remembered for(;;) { CRememberedLevel *prlOld = FindRememberedLevel(strFileName); if (prlOld==NULL) { break; } // remove it prlOld->rl_lnInSessionState.Remove(); delete prlOld; } // create new remembered level CRememberedLevel *prlNew = new CRememberedLevel; ses_lhRememberedLevels.AddTail(prlNew->rl_lnInSessionState); // remember it prlNew->rl_strFileName = strFileName; WriteWorldAndState_t(&prlNew->rl_strmSessionState); } // find a level if it is remembered CRememberedLevel *CSessionState::FindRememberedLevel(const CTString &strFileName) { {FOREACHINLIST(CRememberedLevel, rl_lnInSessionState, ses_lhRememberedLevels, itrl) { CRememberedLevel &rl = *itrl; if (rl.rl_strFileName==strFileName) { return &rl; } }} return NULL; } // restore some old level void CSessionState::RestoreOldLevel(const CTString &strFileName) { // find the level CRememberedLevel *prlOld = FindRememberedLevel(strFileName); // it must exist ASSERT(prlOld!=NULL); // restore it try { prlOld->rl_strmSessionState.SetPos_t(0); _pTimer->SetCurrentTick(0.0f); ReadWorldAndState_t(&prlOld->rl_strmSessionState); _pTimer->SetCurrentTick(ses_tmLastProcessedTick); } catch (char *strError) { FatalError(TRANS("Cannot restore old level '%s':\n%s"), prlOld->rl_strFileName, strError); } // delete it delete prlOld; } // make synchronization test message and send it to server (if client), or add to buffer (if server) void CSessionState::MakeSynchronisationCheck(void) { if (!_cmiComm.cci_bClientInitialized) return; // not yet time if(ses_tmLastSyncCheck+ses_tmSyncCheckFrequency > ses_tmLastProcessedTick) { // don't check yet return; } // make local checksum ULONG ulLocalCRC; CRC_Start(ulLocalCRC); ChecksumForSync(ulLocalCRC, ses_iExtensiveSyncCheck); CRC_Finish(ulLocalCRC); // create sync-check CSyncCheck sc; ses_tmLastSyncCheck = ses_tmLastProcessedTick; sc.sc_tmTick = ses_tmLastSyncCheck; sc.sc_iSequence = ses_iLastProcessedSequence; sc.sc_ulCRC = ulLocalCRC; sc.sc_iLevel = ses_iLevel; // NOTE: If local client, here we buffer the sync and send it to ourselves. // It's because synccheck is piggybacking session message buffer, so we acknowledge // the server to flush the buffer up to that sequence. // if on server if (_pNetwork->IsServer()) { // buffer the sync-check _pNetwork->ga_srvServer.AddSyncCheck(sc); } // create a message with sync check CNetworkMessage nmSyncCheck(MSG_SYNCCHECK); nmSyncCheck.Write(&sc, sizeof(sc)); // send it to server _pNetwork->SendToServer(nmSyncCheck); } // forget all remembered levels void CSessionState::ForgetOldLevels(void) { {FORDELETELIST(CRememberedLevel, rl_lnInSessionState, ses_lhRememberedLevels, itrl) { delete &*itrl; }} } extern INDEX cli_bDumpSync; extern INDEX cli_bDumpSyncEachTick; void CSessionState::DumpSyncToFile_t(CTStream &strm, INDEX iExtensiveSyncCheck) // throw char * { ULONG ulLocalCRC; CRC_Start(ulLocalCRC); ChecksumForSync(ulLocalCRC, iExtensiveSyncCheck); CRC_Finish(ulLocalCRC); strm.FPrintF_t("__________________________________________________________________________________\n", ulLocalCRC); strm.FPrintF_t("CRC: 0x%08X\n", ulLocalCRC); DumpSync_t(strm, iExtensiveSyncCheck); } #if DEBUG_SYNCSTREAMDUMPING void CSessionState::DumpSyncToMemory(void) { try { CTMemoryStream *pstrm = GetDumpStream(); DumpSyncToFile_t(*pstrm); } catch (char *strError) { CPrintF("Cannot dump sync data: %s\n", strError); } } #endif /* Session state loop. */ void CSessionState::SessionStateLoop(void) { _pfNetworkProfile.StartTimer(CNetworkProfile::PTI_SESSIONSTATE_LOOP); // while there is something to do BOOL bSomethingToDo = TRUE; while (bSomethingToDo && !IsDisconnected()) { bSomethingToDo = FALSE; // if client was disconnected without a notice if (!_cmiComm.Client_IsConnected()) { // quit ses_strDisconnected = TRANS("Link or server is down"); } CNetworkMessage nmMessage; // if there is some unreliable message if (_pNetwork->ReceiveFromServer(nmMessage)) { bSomethingToDo = TRUE; // if it is a gamestream message if (nmMessage.GetType() == MSG_GAMESTREAMBLOCKS) { ses_tvMessageReceived = _pTimer->GetHighPrecisionTimer(); ses_bWaitingForServer = FALSE; // unpack the message CNetworkMessage nmUnpackedBlocks(MSG_GAMESTREAMBLOCKS); nmMessage.UnpackDefault(nmUnpackedBlocks); // while there are some more blocks in the message while (!nmUnpackedBlocks.EndOfMessage()) { // read a block to the gamestream ses_nsGameStream.ReadBlock(nmUnpackedBlocks); } // if it is a keepalive } else if (nmMessage.GetType() == MSG_KEEPALIVE) { // just remember time ses_tvMessageReceived = _pTimer->GetHighPrecisionTimer(); _pNetwork->AddNetGraphValue(NGET_NONACTION, 0.5f); // non-action sequence // if it is pings message } else if (nmMessage.GetType() == MSG_INF_PINGS) { for(INDEX i=0; ien_tmPing = iPing/1000.0f; } } } // if it is chat message } else if (nmMessage.GetType() == MSG_CHAT_OUT) { // read the message ULONG ulFrom; CTString strFrom; nmMessage>>ulFrom; if (ulFrom==0) { nmMessage>>strFrom; } CTString strMessage; nmMessage>>strMessage; // print it PrintChatMessage(ulFrom, strFrom, strMessage); // otherwise } else { CPrintF(TRANS("Session state: Unexpected message during game: %s(%d)\n"), ErrorDescription(&MessageTypes, nmMessage.GetType()), nmMessage.GetType()); } } CNetworkMessage nmReliable; // if there is some reliable message if (_pNetwork->ReceiveFromServerReliable(nmReliable)) { bSomethingToDo = TRUE; // if this is disconnect message if (nmReliable.GetType() == MSG_INF_DISCONNECTED) { // confirm disconnect CNetworkMessage nmConfirmDisconnect(MSG_REP_DISCONNECTED); _pNetwork->SendToServerReliable(nmConfirmDisconnect); // report the reason CTString strReason; nmReliable>>strReason; ses_strDisconnected = strReason; CPrintF(TRANS("Disconnected: %s\n"), strReason); // disconnect _cmiComm.Client_Close(); // if this is recon response } else if (nmReliable.GetType() == MSG_ADMIN_RESPONSE) { // just print it CTString strResponse; nmReliable>>strResponse; CPrintF("%s", "|"+strResponse+"\n"); // otherwise } else { CPrintF(TRANS("Session state: Unexpected reliable message during game: %s(%d)\n"), ErrorDescription(&MessageTypes, nmReliable.GetType()), nmReliable.GetType()); } } } // if network client and not waiting for server if (_pNetwork->IsNetworkEnabled() && !_pNetwork->IsServer() && !ses_bWaitingForServer) { // check when last message was received. if (ses_tvMessageReceived.tv_llValue>0 && (_pTimer->GetHighPrecisionTimer()-ses_tvMessageReceived).GetSeconds()>net_tmDisconnectTimeout && ses_strDisconnected=="") { ses_strDisconnected = TRANS("Connection timeout"); CPrintF(TRANS("Disconnected: %s\n"), (const char*)ses_strDisconnected); } } // if pause state should be changed if (ses_bPause!=ses_bWantPause) { // send appropriate packet to server CNetworkMessage nmReqPause(MSG_REQ_PAUSE); nmReqPause<<(INDEX&)ses_bWantPause; _pNetwork->SendToServer(nmReqPause); } // dump sync data if needed if( cli_bDumpSync) { cli_bDumpSync = FALSE; try { CTFileStream strmFile; CTString strFileName = CTString("temp\\syncdump.txt"); strmFile.Create_t(CTString("temp\\syncdump.txt"), CTStream::CM_TEXT); #if DEBUG_SYNCSTREAMDUMPING if( cli_bDumpSyncEachTick) { // get size and buffer from the stream void *pvBuffer; SLONG slSize; ses_pstrm->LockBuffer(&pvBuffer, &slSize); strmFile.Write_t(pvBuffer, slSize); ses_pstrm->UnlockBuffer(); } else #endif { DumpSyncToFile_t(strmFile, ses_iExtensiveSyncCheck); } // inform user CPrintF("Sync data dumped to '%s'\n", strFileName); } catch (char *strError) { CPrintF("Cannot dump sync data: %s\n", strError); } } // if some client settings changed if (!ses_sspParams.IsUpToDate()) { // remember new settings ses_sspParams.Update(); // send message to server CNetworkMessage nmSet(MSG_SET_CLIENTSETTINGS); nmSet<SendToServerReliable(nmSet); } _pfNetworkProfile.StopTimer(CNetworkProfile::PTI_SESSIONSTATE_LOOP); } /* Get number of active players. */ INDEX CSessionState::GetPlayersCount(void) { INDEX ctPlayers = 0; FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itplt) { if (itplt->IsActive()) { ctPlayers++; } } return ctPlayers; } /* Remember predictor positions of all players. */ void CSessionState::RememberPlayerPredictorPositions(void) { // for each active player FOREACHINSTATICARRAY(ses_apltPlayers, CPlayerTarget, itplt) { CPlayerTarget &plt = *itplt; if (plt.IsActive()) { // remember its current, or predictor position CEntity *pen = plt.plt_penPlayerEntity; if (pen->IsPredicted()) { pen = pen->GetPredictor(); } plt.plt_vPredictorPos = pen->GetPlacement().pl_PositionVector; } } } /* Get player position. */ const FLOAT3D &CSessionState::GetPlayerPredictorPosition(INDEX iPlayer) { return ses_apltPlayers[iPlayer].plt_vPredictorPos; } CPredictedEvent::CPredictedEvent(void) { } // check an event for prediction, returns true if already predicted BOOL CSessionState::CheckEventPrediction(CEntity *pen, ULONG ulTypeID, ULONG ulEventID) { // if prediction is not involved if ( !( pen->GetFlags() & (ENF_PREDICTOR|ENF_PREDICTED|ENF_WILLBEPREDICTED) ) ){ // not predicted return FALSE; } // find eventual prediction tail if (pen->IsPredictor()) { pen = pen->GetPredictionTail(); } // gather all event relevant data ULONG ulEntityID = pen->en_ulID; TIME tmNow = _pTimer->CurrentTick(); BOOL bPredicted = FALSE; // for each active event INDEX ctpe = ses_apeEvents.Count(); {for(INDEX ipe=0; ipe