From f401f742ee075e2270e28e953c5922285018fef3 Mon Sep 17 00:00:00 2001 From: myT Date: Fri, 4 Nov 2022 05:00:39 +0100 Subject: [PATCH] added a new demo player with fast seeking support added cl_demoPlayer and cl_escapeAbortsDemo --- changelog.txt | 11 +- code/client/cl_avi.cpp | 2 +- code/client/cl_cgame.cpp | 104 +- code/client/cl_demo.cpp | 1232 +++++++++++++++++ code/client/cl_keys.cpp | 2 +- code/client/cl_main.cpp | 60 +- code/client/cl_ui.cpp | 11 +- code/client/client.h | 50 +- code/client/client_help.h | 5 + code/client/snd_dma.cpp | 7 + code/qcommon/cg_public.h | 9 +- code/qcommon/common.cpp | 24 + code/qcommon/files.cpp | 10 + code/qcommon/msg.cpp | 8 +- code/qcommon/q_shared.c | 4 +- code/qcommon/q_shared.h | 1 + code/qcommon/qcommon.h | 4 + code/renderer/tr_local.h | 22 +- code/renderer/tr_public.h | 2 +- code/renderer/tr_scene.cpp | 7 +- makefiles/linux_gmake/cnq3.make | 4 + makefiles/premake5.lua | 1 + makefiles/windows_vs2013/cnq3.vcxproj | 1 + makefiles/windows_vs2013/cnq3.vcxproj.filters | 3 + 24 files changed, 1530 insertions(+), 54 deletions(-) create mode 100644 code/client/cl_demo.cpp diff --git a/changelog.txt b/changelog.txt index a9ee5c5..6759dfc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,13 @@ See the end of this file for known issues. DD Mmm 20 - 1.53 +add: cl_demoPlayer <0|1> (default: 1) selects the demo playback system to use + cl_demoPlayer 0 = always uses the original demo player + cl_demoPlayer 1 = uses the new demo player with time rewind support when the mod supports it + +add: cl_escapeAbortsDemo <0|1> (default: 1) decides whether the escape key can abort demo playback + reminder: you can always use /disconnect to stop demo playback + add: r_depthClamp <0|1> (default: 0) disables vertex clipping against the near and far clip planes enabling this feature will raise the maximum allowed FoV value of CPMA 1.53 it turns the view frustum into a pyramid and prevents any vertex between the near clip plane @@ -43,8 +50,8 @@ add: r_alphaToCoverageMipBoost <0.0 to 0.5> (default: 0.125) boosts the alpha va with A2C enabled, it prevents alpha-tested surfaces from fading (too much) in the distance chg: r_gpuMipGen 0 now respects r_mipGenFilter but not r_mipGenGamma (gamma 2 is used) - the new code will slow down map loads in general but produces higher quality results - r_mipGenFilter BL is a special case without gamma correction that's much faster than before + the new code will slow down map loads in general but produces higher quality results + r_mipGenFilter BL is a special case without gamma correction that's much faster than before chg: image load/save errors now print warnings instead of triggering drop errors diff --git a/code/client/cl_avi.cpp b/code/client/cl_avi.cpp index 98a7aa4..d4858ba 100644 --- a/code/client/cl_avi.cpp +++ b/code/client/cl_avi.cpp @@ -648,7 +648,7 @@ static qbool CloseAVI( closeMode_t closeMode ) Z_Free( afd.eBuffer ); FS_FCloseFile( afd.f ); - Com_Printf( "Wrote %d:%d frames to %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName ); + Com_Printf( "Wrote %d:%d V:A frames to %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName ); if ( closeMode == CM_SEQUENCE_COMPLETE ) S_StopAllSounds(); diff --git a/code/client/cl_cgame.cpp b/code/client/cl_cgame.cpp index 6066c77..97acc6a 100644 --- a/code/client/cl_cgame.cpp +++ b/code/client/cl_cgame.cpp @@ -94,6 +94,11 @@ static int CL_GetCurrentCmdNumber() static void CL_GetCurrentSnapshotNumber( int* snapshotNumber, int* serverTime ) { + if ( clc.newDemoPlayer ) { + CL_NDP_GetCurrentSnapshotNumber( snapshotNumber, serverTime ); + return; + } + *snapshotNumber = cl.snap.messageNum; *serverTime = cl.snap.serverTime; } @@ -101,6 +106,10 @@ static void CL_GetCurrentSnapshotNumber( int* snapshotNumber, int* serverTime ) static qbool CL_GetSnapshot( int snapshotNumber, snapshot_t* snapshot ) { + if ( clc.newDemoPlayer ) { + return CL_NDP_GetSnapshot( snapshotNumber, snapshot ); + } + int i, count; if ( snapshotNumber > cl.snap.messageNum ) { @@ -154,7 +163,7 @@ static void CL_SetUserCmdValue( int userCmdValue, float sensitivityScale ) } -static void CL_ConfigstringModified() +void CL_ConfigstringModified() { int index = atoi( Cmd_Argv(1) ); if ( index < 0 || index >= MAX_CONFIGSTRINGS ) { @@ -209,6 +218,10 @@ static void CL_ConfigstringModified() static qbool CL_GetServerCommand( int serverCommandNumber ) { + if ( clc.newDemoPlayer ) { + return CL_NDP_GetServerCommand( serverCommandNumber ); + } + static char bigConfigString[BIG_INFO_STRING]; // if we have irretrievably lost a reliable command, drop the connection @@ -306,6 +319,7 @@ static void CL_CM_LoadMap( const char* mapname ) void CL_ShutdownCGame() { + Com_Memset( cls.cgvmCalls, 0, sizeof(cls.cgvmCalls) ); cls.keyCatchers &= ~KEYCATCH_CGAME; cls.cgameStarted = qfalse; cls.cgameForwardInput = 0; @@ -336,6 +350,12 @@ static qbool CL_CG_GetValue( char* value, int valueSize, const char* key ) { "trap_MatchAlertEvent", CG_EXT_MATCHALERTEVENT }, { "trap_Error2", CG_EXT_ERROR2 }, { "trap_IsRecordingDemo", CG_EXT_ISRECORDINGDEMO }, + { "trap_CNQ3_NDP_Enable", CG_EXT_NDP_ENABLE }, + { "trap_CNQ3_NDP_Seek", CG_EXT_NDP_SEEK }, + { "trap_CNQ3_NDP_ReadUntil", CG_EXT_NDP_READUNTIL }, + { "trap_CNQ3_NDP_StartVideo", CG_EXT_NDP_STARTVIDEO }, + { "trap_CNQ3_NDP_StopVideo", CG_EXT_NDP_STOPVIDEO }, + { "trap_CNQ3_R_RenderScene", CG_EXT_R_RENDERSCENE }, // commands { "screenshotnc", 1 }, { "screenshotncJPEG", 1 }, @@ -514,7 +534,7 @@ static intptr_t CL_CgameSystemCalls( intptr_t *args ) re.AddLightToScene( VMA(1), VMF(2), VMF(3), VMF(4), VMF(5) ); return 0; case CG_R_RENDERSCENE: - re.RenderScene( VMA(1) ); + re.RenderScene( VMA(1), 0 ); return 0; case CG_R_SETCOLOR: re.SetColor( VMA(1) ); @@ -656,6 +676,38 @@ static intptr_t CL_CgameSystemCalls( intptr_t *args ) case CG_EXT_ISRECORDINGDEMO: return clc.demorecording; + case CG_EXT_NDP_ENABLE: + if( clc.demoplaying && cl_demoPlayer->integer ) { + cls.cgameNewDemoPlayer = qtrue; + cls.cgvmCalls[CGVM_NDP_ANALYZE_COMMAND] = args[1]; + cls.cgvmCalls[CGVM_NDP_GENERATE_COMMANDS] = args[2]; + cls.cgvmCalls[CGVM_NDP_IS_CS_NEEDED] = args[3]; + cls.cgvmCalls[CGVM_NDP_ANALYZE_SNAPSHOT] = args[4]; + cls.cgvmCalls[CGVM_NDP_END_ANALYSIS] = args[5]; + return qtrue; + } else { + return qfalse; + } + + case CG_EXT_NDP_SEEK: + return CL_NDP_Seek( args[1] ); + + case CG_EXT_NDP_READUNTIL: + CL_NDP_ReadUntil( args[1] ); + return 0; + + case CG_EXT_NDP_STARTVIDEO: + Cvar_Set( cl_aviFrameRate->name, va( "%d", (int)args[2] ) ); + return CL_OpenAVIForWriting( va( "videos/%s", (const char*)VMA(1) ), qfalse ); + + case CG_EXT_NDP_STOPVIDEO: + CL_CloseAVI(); + return 0; + + case CG_EXT_R_RENDERSCENE: + re.RenderScene( VMA(1), args[2] ); + return 0; + default: Com_Error( ERR_DROP, "Bad cgame system trap: %i", args[0] ); } @@ -691,6 +743,7 @@ void CL_InitCGame() int t = Sys_Milliseconds(); cls.cgameForwardInput = 0; + cls.cgameNewDemoPlayer = qfalse; // put away the console Con_Close(); @@ -846,6 +899,11 @@ static void CL_FirstSnapshot() void CL_SetCGameTime() { + if ( clc.newDemoPlayer ) { + CL_NDP_SetCGameTime(); + return; + } + // getting a valid frame message ends the connection process if ( cls.state != CA_ACTIVE ) { if ( cls.state != CA_PRIMED ) { @@ -946,3 +1004,45 @@ void CL_SetCGameTime() } + +void CL_CGNDP_AnalyzeCommand( int serverTime ) +{ + Q_assert(cls.cgameNewDemoPlayer); + VM_Call(cgvm, cls.cgvmCalls[CGVM_NDP_ANALYZE_COMMAND], serverTime); +} + + +void CL_CGNDP_GenerateCommands( const char** commands, int* numCommandBytes ) +{ + Q_assert(cls.cgameNewDemoPlayer); + Q_assert(commands); + Q_assert(numCommandBytes); + VM_Call(cgvm, cls.cgvmCalls[CGVM_NDP_GENERATE_COMMANDS]); + *numCommandBytes = *(int*)interopBufferIn; + *commands = (const char*)interopBufferIn + 4; +} + + +qbool CL_CGNDP_IsConfigStringNeeded( int csIndex ) +{ + Q_assert(cls.cgameNewDemoPlayer); + Q_assert(csIndex >= 0 && csIndex < MAX_CONFIGSTRINGS); + return (qbool)VM_Call(cgvm, cls.cgvmCalls[CGVM_NDP_IS_CS_NEEDED], csIndex); +} + + +void CL_CGNDP_AnalyzeSnapshot( int progress ) +{ + Q_assert(cls.cgameNewDemoPlayer); + Q_assert(progress >= 0 && progress < 100); + VM_Call(cgvm, cls.cgvmCalls[CGVM_NDP_ANALYZE_SNAPSHOT], progress); +} + + +void CL_CGNDP_EndAnalysis( const char* filePath, int firstServerTime, int lastServerTime, qbool videoRestart ) +{ + Q_assert(cls.cgameNewDemoPlayer); + Q_assert(lastServerTime > firstServerTime); + Q_strncpyz((char*)interopBufferOut, filePath, interopBufferOutSize); + VM_Call(cgvm, cls.cgvmCalls[CGVM_NDP_END_ANALYSIS], firstServerTime, lastServerTime, videoRestart); +} diff --git a/code/client/cl_demo.cpp b/code/client/cl_demo.cpp new file mode 100644 index 0000000..e7c4ed8 --- /dev/null +++ b/code/client/cl_demo.cpp @@ -0,0 +1,1232 @@ +/* +=========================================================================== +Copyright (C) 2022 Gian 'myT' Schellenbaum + +This file is part of Challenge Quake 3 (CNQ3). + +Challenge Quake 3 is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Challenge Quake 3 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 Challenge Quake 3. If not, see . +=========================================================================== +*/ +// New Demo Player with fast seeking support + + +/* + +GENERAL + +When the client loads a demo with the new demo player enabled, +CL_NDP_PlayDemo is called with the demo file opened. +It will then proceed to generate a compressed and seekable encoding +of said demo in memory. +The input demo is read from disk exactly once in the process. + +STEP BY STEP BREAKDOWN + +Demo loading steps: +- Parse until a gamestate is found. +- Make gamestate config strings available to CGame. +- Create CGame VM and call CGAME_INIT. + - CGame loads the map, the assets, etc. + - CGame also starts demo analysis with the gamestate config strings. +- Parse commands and snapshots until there's an error, a new gamestate or there's nothing left to parse. + Command: + - Feed it to CGame for analysis. + Snapshot: + - Feed it to CGame for analysis. + - Decide whether this snapshot shall be delta-encoded or not based on server time. + - If full, ask CGame to generate a list of synchronization server commands. + - If full, save indexing data to be able to reference this snapshot during playback. + - Write the snapshot to the memory buffer. +- Close the input file and tell CGame to finish demo analysis. +- Jump to the desired time and read the first 2 snapshots from memory so we're ready for playback. + +Demo playback steps: +- Always keep a decoded snapshot for the current server time. +- If not at the end, keep a "next" decoded snapshot so CGame can interpolate. +- Once CGame queries a snapshot that exists, + generate and add all related commands to the CGame command queue. + +Seeking steps: +- Find the first full snapshot before the requested server time. +- Decode 2 snapshots so that "current" and "next" are both set, ready for playback. +- Queue commands and decode more snapshots until the playback time reaches the requested time. + +ERROR HANDLING + +Standard (forward-only) demo playback can afford to use DROP errors +since it doesn't prevent you from seeing everything that's valid before the bad data. +However, this new demo player needs to parse the full file to generate +a new representation that can be used during playback. +A drop error would therefore completely stop the processing and cancel playback. +The new custom handler allows errors to simply stop the parsing phase earlier. +If a gamestate and at least 1 snapshot were read, then loading can be finalized +and playback can happen as if the demo file was simply truncated. + +STORAGE + +Why store in memory and not on disk? +- Demo conversion is fast enough that a disk cache is not needed. + It would just waste disk space. +- Changes to either the client's code or CPMA can cause changes to + the final generated data. +- Cached files with original names are a pain. If you ever create a new cut of a frag + with the same file name or process the demo (e.g. LG/RG fixes), + then the game will load the cached version of the old data. +- Cached files based on a hash of the original input file are immune to this, + but now you don't know which files are safe to delete from the name alone. + +MULTIPLE GAMESTATES + +If the player has a demo with 2+ gamestates, he can trivially split it up using UDT. +There's no need to make the player more complex for such an edge case. + +*/ + + +#include "client.h" + +#include +#if defined(_MSC_VER) +#pragma warning(disable: 4611) // setjmp with C++ destructors +#endif + + +#if defined(_DEBUG) +#define FULL_SNAPSHOT_INTERVAL_MS (1 * 1000) +#else +#define FULL_SNAPSHOT_INTERVAL_MS (8 * 1000) +#endif + +#define MAX_COMMANDS ARRAY_LEN(demo.commands) + +#define VERBOSE_DEBUGGING 0 + +// VC++, GCC and Clang all seem to support ##__VA_ARGS__ just fine +#define Drop(Message, ...) Com_Error(ERR_DROP, "New Demo Player: " Message "\n", ##__VA_ARGS__) +#define Error(Message, ...) Com_Printf("ERROR: " Message "\n", ##__VA_ARGS__); longjmp(ndp.abortLoad, -1) +#define Warning(Message, ...) Com_Printf("WARNING: " Message "\n", ##__VA_ARGS__) + +#if defined(_DEBUG) && VERBOSE_DEBUGGING +#define PrintCmdListBegin(Message) Sys_DebugPrintf("BEGIN " Message "\n"); cmdName = Message +#define PrintCmdListEnd() Sys_DebugPrintf("END %s\n", cmdName) +#define PrintCmd(Cmd) Sys_DebugPrintf(" %8d: %s\n", demo.currSnap->serverTime, Cmd) +#else +#define PrintCmdListBegin(Message) +#define PrintCmdListEnd() +#define PrintCmd(Cmd) +#endif + + +struct configString_t { + int index; + int serverTime; +}; + +struct commandBuffer_t { + char data[128 * 1024]; + int numBytes; +}; + +struct ndpSnapshot_t { + // the first char of configStrings is '\0' + // so that empty entries can all use offset 0 + commandBuffer_t serverCommands; + commandBuffer_t synchCommands; + char configStrings[MAX_GAMESTATE_CHARS]; // matches the size in gameState_t + int configStringOffsets[MAX_CONFIGSTRINGS]; + int configStringTimes[MAX_CONFIGSTRINGS]; // last time it was changed + entityState_t entities[MAX_GENTITIES]; + byte areaMask[MAX_MAP_AREA_BYTES]; + playerState_t ps; + int numAreaMaskBytes; + int numConfigStringBytes; + int messageNum; + int serverTime; + int numEntities; + int numServerCommands; + int serverCommandSequence; + int snapFlags; + int ping; + qbool isFullSnap; +}; + +struct parser_t { + ndpSnapshot_t ndpSnapshots[2]; // current one and previous one for delta encoding + clSnapshot_t snapshots[PACKET_BACKUP]; + entityState_t entityBaselines[MAX_GENTITIES]; + entityState_t entities[MAX_PARSE_ENTITIES]; + char bigConfigString[BIG_INFO_STRING]; + msg_t inMsg; + msg_t outMsg; + ndpSnapshot_t* currSnap; + ndpSnapshot_t* prevSnap; + int entityWriteIndex; // indexes entities directly + int messageNum; // number of the current msg_t data packet + int bigConfigStringIndex; + int prevServerTime; + int lastMessageNum; + int nextFullSnapshotTime; // when server time is bigger, write a full snapshot + int serverCommandSequence; // the command number of the latest command we decoded + int progress; +}; + +struct demoIndex_t { + int byteOffset; + int serverTime; + int snapshotIndex; +}; + +struct memoryBuffer_t { + byte* data; + int capacity; // total number of bytes allocated + int position; // cursor or number of bytes read/written + int numBytes; // total number of bytes written for read mode + qbool isReadMode; +}; + +struct command_t { + char command[MAX_STRING_CHARS]; // the size used by MSG_ReadString +}; + +struct demo_t { + ndpSnapshot_t snapshots[2]; // current one and next one for CGame requests + demoIndex_t indices[4096]; + command_t commands[256]; + memoryBuffer_t buffer; + ndpSnapshot_t* currSnap; + ndpSnapshot_t* nextSnap; + int numSnapshots; + int numIndices; // the number of full snapshots (i.e. with no delta-encoding) + int firstServerTime; + int lastServerTime; + int snapshotIndex; // the index of currSnap for CGame requests + int numCommands; // number of commands written, must be modulo'd to index commands + qbool isLastSnapshot; +}; + +struct newDemoPlayer_t { + jmp_buf abortLoad; +}; + + +static parser_t parser; +static demo_t demo; +static newDemoPlayer_t ndp; +static const entityState_t nullEntityState = { 0 }; +static const playerState_t nullPlayerState = { 0 }; +#if defined(_DEBUG) && VERBOSE_DEBUGGING +static const char* cmdName; +#endif + + +// smallest time value comes first to keep chronological order +static int CompareConfigStringTimes( const void* aPtr, const void* bPtr ) +{ + const configString_t* const a = (const configString_t*)aPtr; + const configString_t* const b = (const configString_t*)bPtr; + return a->serverTime - b->serverTime; +} + + +static void MB_GrowTo( memoryBuffer_t* mb, int numBytes ) +{ + if (mb->data && mb->capacity < numBytes) { + byte* const oldData = mb->data; + numBytes = max(numBytes, mb->capacity * 2); + mb->data = (byte*)malloc(numBytes); + if (!mb->data) { + Drop("Ran out of memory"); + } + mb->capacity = numBytes; + Com_Memcpy(mb->data, oldData, mb->position); + free(oldData); + } else if (!mb->data) { + mb->data = (byte*)malloc(numBytes); + if (!mb->data) { + Drop("Ran out of memory"); + } + mb->capacity = numBytes; + } +} + + +static void MB_InitWrite( memoryBuffer_t* mb ) +{ + MB_GrowTo(mb, 4 << 20); + mb->numBytes = 0; + mb->position = 0; + mb->isReadMode = qfalse; +} + + +static void MB_Write( memoryBuffer_t* mb, const void* data, int numBytes ) +{ + Q_assert(!mb->isReadMode); + MB_GrowTo(mb, mb->position + numBytes); + Com_Memcpy(mb->data + mb->position, data, numBytes); + mb->position += numBytes; +} + + +static void MB_InitRead( memoryBuffer_t* mb ) +{ + Q_assert(mb->position > 0); + mb->numBytes = mb->position; + mb->position = 0; + mb->isReadMode = qtrue; +} + + +static void MB_InitMessage( memoryBuffer_t* mb, msg_t* inMsg ) +{ + const int numBytes = *(int*)(mb->data + mb->position); + Q_assert(numBytes > 0 && numBytes <= MAX_MSGLEN); + mb->position += 4; + MSG_Init(inMsg, mb->data + mb->position, numBytes); + mb->position += numBytes; + Q_assert(demo.buffer.position > 0); + MSG_BeginReading(inMsg); + inMsg->cursize = numBytes; +} + + +static void MB_Seek( memoryBuffer_t* mb, int position ) +{ + Q_assert(mb->isReadMode); + if (position < 0 || position >= mb->numBytes) { + Drop("Seeking out of range"); + } + mb->position = position; +} + + +static void WriteNDPSnapshot( msg_t* outMsg, const ndpSnapshot_t* prevSnap, ndpSnapshot_t* currSnap ) +{ + const qbool isFullSnap = prevSnap == NULL; + + // header + MSG_WriteBits(outMsg, isFullSnap, 1); + MSG_WriteLong(outMsg, currSnap->serverTime); + + // player state + const playerState_t* const oldPS = !prevSnap ? &nullPlayerState : &prevSnap->ps; + playerState_t* const newPS = &currSnap->ps; + MSG_WriteDeltaPlayerstate(outMsg, oldPS, newPS); + + // area mask + MSG_WriteByte(outMsg, currSnap->numAreaMaskBytes); + MSG_WriteData(outMsg, currSnap->areaMask, currSnap->numAreaMaskBytes); + + // server commands + MSG_WriteLong(outMsg, currSnap->serverCommands.numBytes); + MSG_WriteData(outMsg, currSnap->serverCommands.data, currSnap->serverCommands.numBytes); + + // synchronization commands + if (isFullSnap) { + MSG_WriteLong(outMsg, currSnap->synchCommands.numBytes); + MSG_WriteData(outMsg, currSnap->synchCommands.data, currSnap->synchCommands.numBytes); + } + + // config strings + for (int i = 0; i < MAX_CONFIGSTRINGS; ++i) { + const char* const oldString = !prevSnap ? "" : &prevSnap->configStrings[prevSnap->configStringOffsets[i]]; + const char* const newString = currSnap->configStrings + currSnap->configStringOffsets[i]; + if (strcmp(oldString, newString)) { + MSG_WriteShort(outMsg, i); + MSG_WriteLong(outMsg, currSnap->configStringTimes[i]); + MSG_WriteBigString(outMsg, newString); + } + } + MSG_WriteShort(outMsg, MAX_CONFIGSTRINGS); // end marker + + // entities + for (int i = 0; i < MAX_GENTITIES - 1; ++i) { + const entityState_t* oldEntity = NULL; + if (prevSnap && prevSnap->entities[i].number < MAX_GENTITIES - 1) { + oldEntity = &prevSnap->entities[i]; + } + + const entityState_t* newEntity = &currSnap->entities[i]; + if (newEntity->number != i || newEntity->number >= MAX_GENTITIES - 1) { + newEntity = NULL; + } + + if (newEntity && !oldEntity) { + oldEntity = &nullEntityState; + } + MSG_WriteDeltaEntity(outMsg, oldEntity, newEntity, qfalse); + } + MSG_WriteBits(outMsg, MAX_GENTITIES - 1, GENTITYNUM_BITS); // end marker +} + + +static void SaveConfigString( ndpSnapshot_t* snap, int index, const char* string, int serverTime ) +{ + snap->configStringTimes[index] = serverTime; + + if (string[0] == '\0') { + snap->configStringOffsets[index] = 0; + return; + } + + int numStringBytes = strlen(string) + 1; + if (snap->numConfigStringBytes + numStringBytes <= sizeof(snap->configStrings)) { + // we have enough space + Com_Memcpy(snap->configStrings + snap->numConfigStringBytes, string, numStringBytes); + snap->configStringOffsets[index] = snap->numConfigStringBytes; + snap->numConfigStringBytes += numStringBytes; + return; + } + + // we ran out of memory, it's time to compact the buffer + // we first create a new one locally then copy it into the snapshot + char cs[sizeof(snap->configStrings)]; + cs[0] = '\0'; + int numTotalBytes = 1; + for (int i = 0; i < ARRAY_LEN(snap->configStringOffsets); ++i) { + const char* const s = (i == index) ? string : snap->configStrings + snap->configStringOffsets[i]; + if (s[0] == '\0') { + snap->configStringOffsets[i] = 0; + continue; + } + + numStringBytes = strlen(s) + 1; + if (numTotalBytes + numStringBytes > sizeof(snap->configStrings)) { + Warning("Ran out of config string memory"); + break; + } + + Com_Memcpy(cs + numTotalBytes, s, numStringBytes); + snap->configStringOffsets[i] = numTotalBytes; + numTotalBytes += numStringBytes; + } + Com_Memcpy(snap->configStrings, cs, numTotalBytes); + snap->numConfigStringBytes = numTotalBytes; +} + + +static void SaveCommandString( commandBuffer_t* cmdBuf, const char* string ) +{ + const int numBytes = strlen(string) + 1; + if (cmdBuf->numBytes + numBytes > sizeof(cmdBuf->data)) { + Drop("Not enough memory for command data"); + } + + Com_Memcpy(cmdBuf->data + cmdBuf->numBytes, string, numBytes); + cmdBuf->numBytes += numBytes; +} + + +static void ParseServerCommand() +{ + msg_t* const inMsg = &parser.inMsg; + + const int commandNumber = MSG_ReadLong(inMsg); + const char* const s = MSG_ReadString(inMsg); + if (commandNumber <= parser.serverCommandSequence) { + return; + } + parser.serverCommandSequence = commandNumber; + + Cmd_TokenizeString(s); + + if (!Q_stricmp(Cmd_Argv(0), "bcs0")) { + parser.bigConfigStringIndex = atoi(Cmd_Argv(1)); + Q_strncpyz(parser.bigConfigString, Cmd_Argv(2), sizeof(parser.bigConfigString)); + } else if (!Q_stricmp(Cmd_Argv(0), "bcs1")) { + Q_strcat(parser.bigConfigString, sizeof(parser.bigConfigString), Cmd_Argv(2)); + } else if (!Q_stricmp(Cmd_Argv(0), "bcs2")) { + Q_strcat(parser.bigConfigString, sizeof(parser.bigConfigString), Cmd_Argv(2)); + // must tokenize again for the mod's benefit + Cmd_TokenizeString(parser.bigConfigString); + CL_CGNDP_AnalyzeCommand(parser.prevServerTime); + SaveConfigString(parser.currSnap, parser.bigConfigStringIndex, parser.bigConfigString, parser.prevServerTime); + } else if (!Q_stricmp(Cmd_Argv(0), "cs")) { + // already tokenized, so we're good to go + CL_CGNDP_AnalyzeCommand(parser.prevServerTime); + SaveConfigString(parser.currSnap, atoi(Cmd_Argv(1)), Cmd_ArgsFrom(2), parser.prevServerTime); + } else { + // already tokenized, so we're good to go + CL_CGNDP_AnalyzeCommand(parser.prevServerTime); + SaveCommandString(&parser.currSnap->serverCommands, s); + } +} + + +static void ParseGamestate() +{ + msg_t* const inMsg = &parser.inMsg; + MSG_ReadLong(inMsg); // skip message sequence + + ndpSnapshot_t* const currSnap = parser.currSnap; + for (;;) { + const int cmd = MSG_ReadByte(inMsg); + if (cmd == svc_EOF) { + break; + } else if (cmd == svc_configstring) { + const int index = MSG_ReadShort(inMsg); + const char* const string = MSG_ReadBigString(inMsg); + SaveConfigString(currSnap, index, string, -10000 + index + 1); + } else if (cmd == svc_baseline) { + const int index = MSG_ReadBits(inMsg, GENTITYNUM_BITS); + if (index < 0 || index >= MAX_GENTITIES) { + Error("Invalid baseline index: %d", index); + } + MSG_ReadDeltaEntity(inMsg, &nullEntityState, &parser.entityBaselines[index], index); + } else { + Error("Invalid gamestate command byte: %d", cmd); + } + } + + clc.clientNum = MSG_ReadLong(inMsg); + clc.checksumFeed = MSG_ReadLong(inMsg); + + // start up CGame now because we need it running for the command parser + clc.demoplaying = qtrue; + clc.newDemoPlayer = qtrue; + clc.serverMessageSequence = 0; + clc.lastExecutedServerCommand = 0; + Con_Close(); + CL_ClearState(); + cls.state = CA_LOADING; + Com_EventLoop(); + Cvar_Set("r_uiFullScreen", "0"); + CL_FlushMemory(); + Com_Memcpy(cl.gameState.stringOffsets, currSnap->configStringOffsets, sizeof(cl.gameState.stringOffsets)); + Com_Memcpy(cl.gameState.stringData, currSnap->configStrings, currSnap->numConfigStringBytes); + cl.gameState.dataCount = currSnap->numConfigStringBytes; + CL_InitCGame(); + if (!cls.cgameNewDemoPlayer) { + Drop("Sorry, this mod doesn't support the new demo player"); + } + cls.cgameStarted = qtrue; + cls.state = CA_ACTIVE; +} + + +static void ParseSnapshot() +{ + msg_t* const inMsg = &parser.inMsg; + + const int byteOffset = demo.buffer.position; + + const int newSnapIndex = parser.messageNum & PACKET_MASK; + clSnapshot_t* oldSnap = NULL; + clSnapshot_t* newSnap = &parser.snapshots[newSnapIndex]; + + const int currServerTime = MSG_ReadLong(inMsg); + + Com_Memset(newSnap, 0, sizeof(*newSnap)); + newSnap->deltaNum = MSG_ReadByte(inMsg); + newSnap->messageNum = parser.messageNum; + newSnap->snapFlags = MSG_ReadByte(inMsg); + + const int numAreaMaskBytes = MSG_ReadByte(inMsg); + if (numAreaMaskBytes > sizeof(newSnap->areamask)) { + Error("Invalid area mask size: %d", numAreaMaskBytes); + } + + if (newSnap->deltaNum == 0) { + // uncompressed data + newSnap->deltaNum = -1; + newSnap->valid = qtrue; + } else { + newSnap->deltaNum = newSnap->messageNum - newSnap->deltaNum; + oldSnap = &parser.snapshots[newSnap->deltaNum & PACKET_MASK]; + if (!oldSnap->valid) { + Error("The snapshot deltas against nothing"); + } else if (oldSnap->messageNum != newSnap->deltaNum) { + Warning("Delta frame too old"); + } else if (parser.entityWriteIndex - oldSnap->parseEntitiesNum > MAX_PARSE_ENTITIES - 128) { + Warning("Delta parseEntitiesNum too old"); + } else { + newSnap->valid = qtrue; + } + } + + MSG_ReadData(inMsg, newSnap->areamask, numAreaMaskBytes); + + MSG_ReadDeltaPlayerstate(inMsg, oldSnap ? &oldSnap->ps : NULL, &newSnap->ps); + + newSnap->parseEntitiesNum = parser.entityWriteIndex; + newSnap->numEntities = 0; + + entityState_t* oldstate; + entityState_t* newstate; + int oldnum, newnum; + int oldindex = 0; + newnum = MSG_ReadBits(inMsg, GENTITYNUM_BITS); + for (;;) { + if (oldSnap && oldindex < oldSnap->numEntities) { + // we still haven't dealt with all entities from oldSnap + const int index = (oldSnap->parseEntitiesNum + oldindex) & (MAX_PARSE_ENTITIES - 1); + oldstate = &parser.entities[index]; + oldnum = oldstate->number; + } else { + // we're done with all entities in oldSnap + oldstate = NULL; + oldnum = INT_MAX; + } + + newstate = &parser.entities[parser.entityWriteIndex]; + if (!oldstate && newnum == MAX_GENTITIES - 1) { + // we're done with all entities in oldSnap AND got the exit signal + break; + } else if (oldnum < newnum) { + // the old entity isn't present in the new list + *newstate = *oldstate; + oldindex++; + } else if (oldnum == newnum) { + // the entity changed + MSG_ReadDeltaEntity(inMsg, oldstate, newstate, newnum); + newnum = MSG_ReadBits(inMsg, GENTITYNUM_BITS); + oldindex++; + } else if (oldnum > newnum) { + // this is a new entity that was delta'd from the baseline + MSG_ReadDeltaEntity(inMsg, &parser.entityBaselines[newnum], newstate, newnum); + newnum = MSG_ReadBits(inMsg, GENTITYNUM_BITS); + } + + if (newstate->number == MAX_GENTITIES - 1) { + continue; + } + + parser.entityWriteIndex = (parser.entityWriteIndex + 1) & (MAX_PARSE_ENTITIES - 1); + newSnap->numEntities++; + } + + if (!newSnap->valid) { + return; + } + + // mark missing snapshots as invalid + if (newSnap->messageNum - parser.lastMessageNum >= PACKET_BACKUP) { + parser.lastMessageNum = newSnap->messageNum - (PACKET_BACKUP - 1); + } + while (parser.lastMessageNum < newSnap->messageNum) { + parser.snapshots[parser.lastMessageNum++ & PACKET_MASK].valid = qfalse; + } + parser.lastMessageNum = newSnap->messageNum + 1; + + if (currServerTime <= parser.prevServerTime) { + return; + } + + qbool isFullSnap = qfalse; + if (demo.numSnapshots == 0 || currServerTime > parser.nextFullSnapshotTime) { + isFullSnap = qtrue; + parser.nextFullSnapshotTime = currServerTime + FULL_SNAPSHOT_INTERVAL_MS; + } + + // update our custom snapshot data structure + ndpSnapshot_t* const currNDPSnap = parser.currSnap; + for (int i = 0; i < newSnap->numEntities; ++i) { + const int index = (newSnap->parseEntitiesNum + i) & (MAX_PARSE_ENTITIES - 1); + const entityState_t* const ent = &parser.entities[index]; + currNDPSnap->entities[ent->number] = *ent; + } + currNDPSnap->isFullSnap = isFullSnap; + currNDPSnap->ps = newSnap->ps; + currNDPSnap->serverTime = currServerTime; + currNDPSnap->numAreaMaskBytes = numAreaMaskBytes; + Com_Memcpy(currNDPSnap->areaMask, newSnap->areamask, sizeof(currNDPSnap->areaMask)); + + // add all synchronization commands + if (isFullSnap) { + const char* synchCommands; + int numSynchCommandBytes; + CL_CGNDP_GenerateCommands(&synchCommands, &numSynchCommandBytes); + int synchCommandStart = 0; + for (;;) { + if (synchCommandStart >= numSynchCommandBytes) { + break; + } + const char* const cmd = synchCommands + synchCommandStart; + SaveCommandString(&currNDPSnap->synchCommands, cmd); + synchCommandStart += strlen(cmd) + 1; + } + } + + // let CGame analyze the snapshot so it can do cool stuff + // e.g. store events of interest for the timeline overlay + demo.currSnap = currNDPSnap; + CL_CGNDP_AnalyzeSnapshot(parser.progress); + + // draw the current progress once in a while... + static int lastRefreshTime = Sys_Milliseconds(); + if (Sys_Milliseconds() > lastRefreshTime + 100) { + SCR_UpdateScreen(); + lastRefreshTime = Sys_Milliseconds(); + } + + // write it out (delta-)compressed + byte outMsgData[MAX_MSGLEN]; + msg_t writeMsg; + MSG_Init(&writeMsg, outMsgData, sizeof(outMsgData)); + MSG_Clear(&writeMsg); + MSG_Bitstream(&writeMsg); + ndpSnapshot_t* const prevNDPSnap = parser.prevSnap; + WriteNDPSnapshot(&writeMsg, isFullSnap ? NULL : prevNDPSnap, currNDPSnap); + MB_Write(&demo.buffer, &writeMsg.cursize, 4); + MB_Write(&demo.buffer, writeMsg.data, writeMsg.cursize); + + // prepare the next snapshot + ndpSnapshot_t* const nextNDPSnap = prevNDPSnap; + nextNDPSnap->isFullSnap = qfalse; + nextNDPSnap->serverTime = 0; + nextNDPSnap->serverCommands.numBytes = 0; + nextNDPSnap->synchCommands.numBytes = 0; + Com_Memcpy(nextNDPSnap->configStrings, currNDPSnap->configStrings, currNDPSnap->numConfigStringBytes); + Com_Memcpy(nextNDPSnap->configStringOffsets, currNDPSnap->configStringOffsets, sizeof(nextNDPSnap->configStringOffsets)); + Com_Memcpy(nextNDPSnap->configStringTimes, currNDPSnap->configStringTimes, sizeof(nextNDPSnap->configStringTimes)); + nextNDPSnap->numConfigStringBytes = currNDPSnap->numConfigStringBytes; + for (int i = 0; i < MAX_GENTITIES; ++i) { + nextNDPSnap->entities[i].number = MAX_GENTITIES - 1; + } + + // parser bookkeeping + parser.prevSnap = currNDPSnap; + parser.currSnap = nextNDPSnap; + parser.prevServerTime = currServerTime; + + // build the playback index table + if (isFullSnap) { + if (demo.numIndices >= ARRAY_LEN(demo.indices)) { + Error("Ran out of indices"); + } + demoIndex_t* const index = &demo.indices[demo.numIndices++]; + index->byteOffset = byteOffset; + index->snapshotIndex = demo.numSnapshots; + index->serverTime = currServerTime; + } + demo.numSnapshots++; + demo.firstServerTime = min(demo.firstServerTime, currServerTime); + demo.lastServerTime = max(demo.lastServerTime, currServerTime); +} + + +static void ParseDemo() +{ + byte oldData[MAX_MSGLEN]; + msg_t* const msg = &parser.inMsg; + const int fh = clc.demofile; + int numGamestates = 0; + + Com_Memset(&parser, 0, sizeof(parser)); + parser.currSnap = &parser.ndpSnapshots[0]; + parser.prevSnap = &parser.ndpSnapshots[1]; + parser.currSnap->numConfigStringBytes = 1; // first char is always '\0' + + Com_Memset(&demo, 0, sizeof(demo)); + demo.firstServerTime = INT_MAX; + demo.lastServerTime = INT_MIN; + demo.currSnap = &demo.snapshots[0]; + demo.nextSnap = &demo.snapshots[1]; + + for (int i = 0; i < MAX_CONFIGSTRINGS; ++i) { + parser.ndpSnapshots[0].configStringTimes[i] = INT_MIN; + parser.ndpSnapshots[1].configStringTimes[i] = INT_MIN; + demo.snapshots[0].configStringTimes[i] = INT_MIN; + demo.snapshots[1].configStringTimes[i] = INT_MIN; + } + + MB_InitWrite(&demo.buffer); + + // we won't have a working progress bar for demo files inside pk3 files + const int fileLength = FS_IsZipFile(clc.demofile) ? 0 : FS_filelength(clc.demofile); + + for (;;) { + parser.progress = fileLength > 0 ? ((100 * FS_FTell(clc.demofile)) / fileLength) : 0; + + MSG_Init(msg, oldData, sizeof(oldData)); + + if (FS_Read(&parser.messageNum, 4, fh) != 4) { + Error("File read: message number"); + } + + if (FS_Read(&msg->cursize, 4, fh) != 4) { + Error("File read: message size"); + } + if (msg->cursize < 0) { + // valid EOF marker + break; + } else if (msg->cursize > msg->maxsize) { + Error("Message too large: %d", msg->cursize); + } + + if (FS_Read(msg->data, msg->cursize, fh) != msg->cursize) { + Error("File read: message data"); + } + + MSG_BeginReading(msg); + MSG_ReadLong(msg); // ignore sequence number + + for (;;) { + if (msg->readcount > msg->cursize) { + Error("Read past end of message"); + } + + const byte cmd = MSG_ReadByte(msg); + if (cmd == svc_EOF) { + break; + } + + switch (cmd) { + case svc_nop: + break; + + case svc_gamestate: + ++numGamestates; + if (numGamestates >= 2) { + Warning("More than 1 gamestate found, only the first one was loaded"); + return; + } + ParseGamestate(); + break; + + case svc_snapshot: + ParseSnapshot(); + break; + + case svc_serverCommand: + ParseServerCommand(); + break; + + case svc_download: + default: + Error("Invalid command byte"); + break; + } + } + } +} + + +static void ReadNextSnapshot() +{ + if (demo.buffer.position >= demo.buffer.numBytes) { + demo.isLastSnapshot = qtrue; + return; + } + demo.isLastSnapshot = qfalse; + + msg_t inMsg; + MB_InitMessage(&demo.buffer, &inMsg); + + ndpSnapshot_t* currSnap = demo.currSnap; // destination + ndpSnapshot_t* prevSnap = demo.nextSnap; // source + demo.currSnap = prevSnap; + demo.nextSnap = currSnap; + + // header + const qbool isFullSnap = MSG_ReadBits(&inMsg, 1); + if (isFullSnap) { + prevSnap = NULL; + } + currSnap->isFullSnap = isFullSnap; + currSnap->serverTime = MSG_ReadLong(&inMsg); + + // player state + MSG_ReadDeltaPlayerstate(&inMsg, prevSnap ? &prevSnap->ps : &nullPlayerState, &currSnap->ps); + + // area mask + currSnap->numAreaMaskBytes = MSG_ReadByte(&inMsg); + MSG_ReadData(&inMsg, currSnap->areaMask, currSnap->numAreaMaskBytes); + + // server commands + currSnap->serverCommands.numBytes = MSG_ReadLong(&inMsg); + MSG_ReadData(&inMsg, currSnap->serverCommands.data, currSnap->serverCommands.numBytes); + + // synchronization commands + if (isFullSnap) { + currSnap->synchCommands.numBytes = MSG_ReadLong(&inMsg); + MSG_ReadData(&inMsg, currSnap->synchCommands.data, currSnap->synchCommands.numBytes); + } + + // config strings + currSnap->configStrings[0] = '\0'; + currSnap->numConfigStringBytes = 1; + if (isFullSnap) { + // if we have a full snapshot, the config strings from the message are all we need + // we can therefore safely leave everything else as empty (i.e. offset 0) + Com_Memset(currSnap->configStringOffsets, 0, sizeof(currSnap->configStringOffsets)); + Com_Memset(currSnap->configStringTimes, 0, sizeof(currSnap->configStringTimes)); + } + qbool csSet[MAX_CONFIGSTRINGS]; + Com_Memset(csSet, 0, sizeof(csSet)); + for (;;) { + const int index = MSG_ReadShort(&inMsg); + if (index == MAX_CONFIGSTRINGS) { + break; + } + + const int serverTime = MSG_ReadLong(&inMsg); + const char* const cs = MSG_ReadBigString(&inMsg); + SaveConfigString(currSnap, index, cs, serverTime); + csSet[index] = qtrue; + } + if (!isFullSnap) { + // we need to keep the unchanged config strings + for (int i = 0; i < MAX_CONFIGSTRINGS; ++i) { + if (!csSet[i]) { + const char* const cs = prevSnap->configStrings + prevSnap->configStringOffsets[i]; + const int serverTime = prevSnap->configStringTimes[i]; + SaveConfigString(currSnap, i, cs, serverTime); + } + } + } + + // entities + if (isFullSnap) { + // mark them all as invalid now + for (int i = 0; i < MAX_GENTITIES; ++i) { + currSnap->entities[i].number = MAX_GENTITIES - 1; + } + } + qbool entSet[MAX_GENTITIES]; + Com_Memset(entSet, 0, sizeof(entSet)); + for (;;) { + const int index = MSG_ReadBits(&inMsg, GENTITYNUM_BITS); + if (index == MAX_GENTITIES - 1) { + break; + } + const entityState_t* prevEnt = &nullEntityState; + entityState_t* currEnt = &currSnap->entities[index]; + if (prevSnap && prevSnap->entities[index].number == index) { + prevEnt = &prevSnap->entities[index]; + } + MSG_ReadDeltaEntity(&inMsg, prevEnt, currEnt, index); + entSet[index] = qtrue; + } + if (!isFullSnap) { + // copy over old valid entities and mark missing ones as invalid + for (int i = 0; i < MAX_GENTITIES; ++i) { + if (entSet[i]) { + continue; + } + if (prevSnap->entities[i].number == i) { + currSnap->entities[i] = prevSnap->entities[i]; + } else { + currSnap->entities[i].number = MAX_GENTITIES - 1; + } + } + } + + demo.snapshotIndex++; +} + + +static void AddCommand( const char* string ) +{ + PrintCmd(string); + const int index = ++demo.numCommands % MAX_COMMANDS; + Q_strncpyz(demo.commands[index].command, string, sizeof(demo.commands[index].command)); +} + + +static void AddAllCommands( const commandBuffer_t* cmdBuf ) +{ + int cmdStart = 0; + for (;;) { + if (cmdStart >= cmdBuf->numBytes) { + break; + } + const char* const cmd = cmdBuf->data + cmdStart; + AddCommand(cmd); + cmdStart += strlen(cmd) + 1; + } +} + + +static void SeekToIndex( int index ) +{ + if (index < 0) { + Warning("Attempted to seek out of range"); + index = 0; + } else if (index >= demo.numIndices) { + index = demo.numIndices - 1; + } + + const demoIndex_t* demoIndex = &demo.indices[index]; + MB_Seek(&demo.buffer, demoIndex->byteOffset); + ReadNextSnapshot(); // 1st in next, ??? in current + ReadNextSnapshot(); // 2nd in next, 1st in current + demo.snapshotIndex = demoIndex->snapshotIndex; + + ndpSnapshot_t* syncSnap = demo.currSnap; + if (!demo.currSnap->isFullSnap) { + // this is needed when trying to jump past the end + syncSnap = demo.nextSnap; + } + Q_assert(syncSnap->isFullSnap); + + // queue special config string changes as commands + // this will correct the red/blue scores, the game timer, etc. + configString_t cs[MAX_CONFIGSTRINGS]; + int numCS = 0; + for (int i = 0; i < MAX_CONFIGSTRINGS; ++i) { + if (syncSnap->configStringTimes[i] != INT_MIN && + CL_CGNDP_IsConfigStringNeeded(i)) { + cs[numCS].index = i; + cs[numCS].serverTime = syncSnap->configStringTimes[i]; + numCS++; + } + } + + // sort them so they're executed in the right order + // yes, it matters a great deal due to some old internal CPMA mechanisms + // for network bandwidth optimization + // this system has existed for at least 15 years before I got on board, + // so you don't get to blame me :-) + PrintCmdListBegin("synch CS commands"); + qsort(cs, numCS, sizeof(configString_t), &CompareConfigStringTimes); + for (int i = 0; i < numCS; ++i) { + const int index = cs[i].index; + const char* const csNew = syncSnap->configStrings + syncSnap->configStringOffsets[index]; + AddCommand(va("cs %d \"%s\"", index, csNew)); + } + PrintCmdListEnd(); + + // add special state synchronization commands + // this will correct item timers, make sure the scoreboard can be shown, etc. + PrintCmdListBegin("synch snap commands"); + AddAllCommands(&syncSnap->synchCommands); + PrintCmdListEnd(); +} + + +void CL_NDP_PlayDemo( qbool videoRestart ) +{ + const int startTime = Sys_Milliseconds(); + + if (setjmp(ndp.abortLoad)) { + goto after_parse; + } + + // read from file and write to memory + ParseDemo(); + +after_parse: + int fileSize = FS_FTell(clc.demofile); + fileSize = max(fileSize, 1); // FS_FTell doesn't work when the demo is in a pak file + FS_FCloseFile(clc.demofile); + clc.demofile = 0; + if (!clc.demoplaying || !clc.newDemoPlayer) { + Drop("Failed to parse the gamestate message"); + } + if (demo.numIndices <= 0 || demo.numSnapshots <= 0) { + Warning("Failed to get anything out of the demo file"); + return; + } + +#if defined(_DEBUG) + if (com_developer->integer) { + FS_WriteFile(va("demos/%s.bin", clc.demoName), demo.buffer.data, demo.buffer.position); + } +#endif + + // prepare to read from memory + demo.currSnap = &demo.snapshots[0]; + demo.nextSnap = &demo.snapshots[1]; + MB_InitRead(&demo.buffer); + + // finalize CGame load and set any extra info needed now + // CGame will also restore previous state when videoRestart is qtrue + CL_CGNDP_EndAnalysis(clc.demoName, demo.firstServerTime, demo.lastServerTime, videoRestart); + + const int duration = Sys_Milliseconds() - startTime; + Com_Printf("New Demo Player: loaded demo in %d.%03d seconds\n", duration / 1000, duration % 1000); + Com_DPrintf("New Demo Player: I-frame delay %d ms, %s -> %s (%.2fx)\n", + FULL_SNAPSHOT_INTERVAL_MS, Com_FormatBytes(fileSize), + Com_FormatBytes(demo.buffer.numBytes), (float)demo.buffer.numBytes / (float)fileSize); +} + + +void CL_NDP_SetCGameTime() +{ + // make sure we don't get timed out by CL_CheckTimeout + clc.lastPacketTime = cls.realtime; +} + + +void CL_NDP_GetCurrentSnapshotNumber( int* snapshotNumber, int* serverTime ) +{ + *snapshotNumber = demo.snapshotIndex; + *serverTime = demo.currSnap->serverTime; +} + + +qbool CL_NDP_GetSnapshot( int snapshotNumber, snapshot_t* snapshot ) +{ + // we don't give anything until CGame init is done + if (!cls.cgameStarted || cls.state != CA_ACTIVE) { + return qfalse; + } + + // we only allow CGame to get the current snapshot and the next one + const int playbackIndex = demo.snapshotIndex; + if (snapshotNumber < playbackIndex || + snapshotNumber > playbackIndex + 1) { + return qfalse; + } + + // there is no next snapshot when we're already at the end... + if (demo.isLastSnapshot && snapshotNumber == playbackIndex + 1) { + return qfalse; + } + + ndpSnapshot_t* const ndpSnap = snapshotNumber == playbackIndex ? demo.currSnap : demo.nextSnap; + + Com_Memcpy(snapshot->areamask, ndpSnap->areaMask, sizeof(snapshot->areamask)); + Com_Memcpy(&snapshot->ps, &ndpSnap->ps, sizeof(snapshot->ps)); + snapshot->snapFlags = 0; + snapshot->ping = 0; + snapshot->numServerCommands = 0; + snapshot->serverCommandSequence = demo.numCommands; + snapshot->serverTime = ndpSnap->serverTime; + snapshot->numEntities = 0; + + // copy over all valid entities + for (int i = 0; i < MAX_GENTITIES - 1 && snapshot->numEntities < MAX_ENTITIES_IN_SNAPSHOT; ++i) { + if (ndpSnap->entities[i].number == i) { + snapshot->entities[snapshot->numEntities++] = ndpSnap->entities[i]; + } + } + + const int prevNumCommands = demo.numCommands; + + // add config string changes as commands + PrintCmdListBegin("snap config string changes as commands"); + configString_t cs[MAX_CONFIGSTRINGS]; + int numCS = 0; + for (int i = 0; i < MAX_CONFIGSTRINGS; ++i) { + const char* const csOld = cl.gameState.stringData + cl.gameState.stringOffsets[i]; + const char* const csNew = ndpSnap->configStrings + ndpSnap->configStringOffsets[i]; + if (strcmp(csOld, csNew)) { + cs[numCS].index = i; + cs[numCS].serverTime = ndpSnap->configStringTimes[i]; + numCS++; + } + } + qsort(cs, numCS, sizeof(configString_t), &CompareConfigStringTimes); + for (int i = 0; i < numCS; ++i) { + const int index = cs[i].index; + const char* const csNew = ndpSnap->configStrings + ndpSnap->configStringOffsets[index]; + AddCommand(va("cs %d \"%s\"", index, csNew)); + } + PrintCmdListEnd(); + + // add commands + PrintCmdListBegin("add snap commands"); + AddAllCommands(&ndpSnap->serverCommands); + PrintCmdListEnd(); + + // make sure nothing is missing + if (demo.numCommands - prevNumCommands > MAX_COMMANDS) { + Warning("Too many commands for a single snapshot"); + } + + return qtrue; +} + + +qbool CL_NDP_GetServerCommand( int serverCommandNumber ) +{ + if (serverCommandNumber < demo.numCommands - MAX_COMMANDS || + serverCommandNumber > demo.numCommands) { + return qfalse; + } + + const int index = serverCommandNumber % MAX_COMMANDS; + const char* const cmd = demo.commands[index].command; + Cmd_TokenizeString(cmd); + + if (!strcmp(Cmd_Argv(0), "cs")) { + CL_ConfigstringModified(); + // reparse the string, because CL_ConfigstringModified may have done another Cmd_TokenizeString() + Cmd_TokenizeString(cmd); + } + + return qtrue; +} + + +int CL_NDP_Seek( int serverTime ) +{ + // rate-limit the command to avoid lags etc. + static int prevSeekTime = 0; + const int currSeekTime = Sys_Milliseconds(); + if (currSeekTime <= prevSeekTime + 100) { + return INT_MIN; + } + + // figure out which full snapshot we want to jump to + int index = 0; + if (serverTime >= demo.lastServerTime) { + index = demo.numIndices - 1; + } else if (serverTime <= 0) { + index = 0; + } else { + index = demo.numIndices - 1; + for (int i = 0; i < demo.numIndices; ++i) { + if (demo.indices[i].serverTime > serverTime) { + index = max(i - 1, 0); + break; + } + } + } + + // clear all pending commands before adding new ones + for (int i = 0; i < ARRAY_LEN(demo.commands); ++i) { + demo.commands[i].command[0] = '\0'; + } + + SeekToIndex(index); + + // read more snapshots until we're close to the target time for more precise jumps + int numSnapshotsRead = 2; + while (!demo.isLastSnapshot && demo.currSnap->serverTime < serverTime) { + // add all commands from the snapshot we're about to evict + PrintCmdListBegin("evicted snap commands"); + AddAllCommands(&demo.currSnap->serverCommands); + PrintCmdListEnd(); + ReadNextSnapshot(); + numSnapshotsRead++; + } + + Com_DPrintf("New Demo Player: sought and read %d snaps in %d ms\n", + numSnapshotsRead, Sys_Milliseconds() - currSeekTime); + + prevSeekTime = currSeekTime; + + return demo.currSnap->serverTime; +} + + +void CL_NDP_ReadUntil( int serverTime ) +{ + while (!demo.isLastSnapshot && demo.currSnap->serverTime < serverTime) { + ReadNextSnapshot(); + } +} + + +void CL_NDP_HandleError() +{ + // we use our handler for read errors during load + // read/write errors during playback aren't handled + if (clc.newDemoPlayer && clc.demofile != 0) { + longjmp(ndp.abortLoad, -1); + } +} diff --git a/code/client/cl_keys.cpp b/code/client/cl_keys.cpp index a1535c6..3b275f1 100644 --- a/code/client/cl_keys.cpp +++ b/code/client/cl_keys.cpp @@ -1118,7 +1118,7 @@ void CL_KeyEvent( int key, qbool down, unsigned time ) if ( cls.state == CA_ACTIVE && !clc.demoplaying ) { VM_Call( uivm, UI_SET_ACTIVE_MENU, UIMENU_INGAME ); } - else { + else if ( !clc.demoplaying || cl_escapeAbortsDemo->integer ) { CL_Disconnect_f(); S_StopAllSounds(); VM_Call( uivm, UI_SET_ACTIVE_MENU, UIMENU_MAIN ); diff --git a/code/client/cl_main.cpp b/code/client/cl_main.cpp index 5ea40aa..bce4f7d 100644 --- a/code/client/cl_main.cpp +++ b/code/client/cl_main.cpp @@ -46,6 +46,9 @@ cvar_t *cl_allowDownload; cvar_t *cl_inGameVideo; cvar_t *cl_matchAlerts; +cvar_t *cl_demoPlayer; +cvar_t *cl_escapeAbortsDemo; + cvar_t *net_proxy; cvar_t *r_khr_debug; @@ -89,7 +92,9 @@ not have future usercmd_t executed before it is executed ====================== */ void CL_AddReliableCommand( const char *cmd ) { - int index; + if ( clc.demoplaying ) { + return; + } // if we would be losing an old command that hasn't been acknowledged, // we must drop the connection @@ -97,7 +102,7 @@ void CL_AddReliableCommand( const char *cmd ) { Com_Error( ERR_DROP, "Client command overflow" ); } clc.reliableSequence++; - index = clc.reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); + const int index = clc.reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); Q_strncpyz( clc.reliableCommands[ index ], cmd, sizeof( clc.reliableCommands[ index ] ) ); } @@ -364,7 +369,7 @@ static void CL_WalkDemoExt( const char* path, fileHandle_t* fh ) } -void CL_PlayDemo_f() +static void CL_PlayDemo( qbool videoRestart ) { if (Cmd_Argc() != 2) { Com_Printf( "demo \n" ); @@ -395,6 +400,14 @@ void CL_PlayDemo_f() clc.demoplaying = qtrue; Q_strncpyz( cls.servername, shortPath, sizeof( cls.servername ) ); + if ( cl_demoPlayer->integer ) { + while ( CL_MapDownload_Active() ) { + Sys_Sleep( 50 ); + } + CL_NDP_PlayDemo( videoRestart ); + return; + } + // read demo messages until connected while ( cls.state >= CA_CONNECTED && cls.state < CA_PRIMED && !CL_MapDownload_Active() ) { CL_ReadDemoMessage(); @@ -730,6 +743,7 @@ void CL_Disconnect( qbool showMainMenu ) { FS_FCloseFile( clc.demofile ); clc.demofile = 0; } + clc.demoplaying = qfalse; if ( uivm && showMainMenu ) { VM_Call( uivm, UI_SET_ACTIVE_MENU, UIMENU_NONE ); @@ -1874,8 +1888,14 @@ static void CL_Vid_Restart_f() // startup all the client stuff CL_StartHunkUsers(); + // we don't really technically need to run everything again, + // but trying to optimize parts out is very likely to lead to nasty bugs + if ( clc.demoplaying && clc.newDemoPlayer ) { + Cmd_TokenizeString( va("demo %s", clc.demoName) ); + CL_PlayDemo( qtrue ); + } // start the cgame if connected - if ( cls.state > CA_CONNECTED && cls.state != CA_CINEMATIC ) { + else if ( cls.state > CA_CONNECTED && cls.state != CA_CINEMATIC ) { cls.cgameStarted = qtrue; CL_InitCGame(); // send pure checksums @@ -1903,30 +1923,42 @@ static void CL_Video_f() { char s[ MAX_OSPATH ]; - if( !clc.demoplaying ) + if( !clc.demoplaying || clc.newDemoPlayer ) { - Com_Printf( "ERROR: ^7/video is only enabled during demo playback\n" ); + Com_Printf( "^3ERROR: ^7/%s is only enabled in the old demo player\n", Cmd_Argv( 0 ) ); return; } if( Cmd_Argc( ) == 2 ) { - Com_sprintf( s, MAX_OSPATH, "videos/%s", Cmd_Argv( 1 ) ); + Q_strncpyz( s, Cmd_Argv( 1 ), sizeof( s ) ); } else { qtime_t t; Com_RealTime( &t ); - Com_sprintf( s, sizeof(s), "videos/%d_%02d_%02d-%02d_%02d_%02d", + Com_sprintf( s, sizeof( s ), "%d_%02d_%02d-%02d_%02d_%02d", 1900+t.tm_year, 1+t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec ); } - CL_OpenAVIForWriting( s, qfalse ); + CL_OpenAVIForWriting( va( "videos/%s", s ), qfalse ); } static void CL_StopVideo_f() { + if( !clc.demoplaying || clc.newDemoPlayer ) + { + Com_Printf( "^3ERROR: ^7/%s is only enabled in the old demo player\n", Cmd_Argv( 0 ) ); + return; + } + + if( !CL_VideoRecording() ) + { + Com_Printf( "No video is being recorded\n" ); + return; + } + CL_CloseAVI(); } @@ -2083,6 +2115,12 @@ static void CL_CancelDownload_f() } +static void CL_PlayDemo_f() +{ + CL_PlayDemo( qfalse ); +} + + static const cvarTableItem_t cl_cvars[] = { { &cl_timeout, "cl_timeout", "200", 0, CVART_INTEGER, "30", "300", "connection time-out, in seconds" }, @@ -2107,7 +2145,9 @@ static const cvarTableItem_t cl_cvars[] = { NULL, "password", "", CVAR_USERINFO, CVART_STRING, NULL, NULL, "used by /" S_COLOR_CMD "connect" }, { &cl_matchAlerts, "cl_matchAlerts", "7", CVAR_ARCHIVE, CVART_BITMASK, "0", XSTRING(MAF_MAX), help_cl_matchAlerts }, { &net_proxy, "net_proxy", "", CVAR_TEMP, CVART_STRING, NULL, NULL, help_net_proxy }, - { &r_khr_debug, "r_khr_debug", "2", CVAR_ARCHIVE, CVART_INTEGER, "0", "2", help_r_khr_debug } + { &r_khr_debug, "r_khr_debug", "2", CVAR_ARCHIVE, CVART_INTEGER, "0", "2", help_r_khr_debug }, + { &cl_demoPlayer, "cl_demoPlayer", "1", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, help_cl_demoPlayer }, + { &cl_escapeAbortsDemo, "cl_escapeAbortsDemo", "1", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, "pressing escape aborts demo playback" }, }; diff --git a/code/client/cl_ui.cpp b/code/client/cl_ui.cpp index b2334be..9999cb0 100644 --- a/code/client/cl_ui.cpp +++ b/code/client/cl_ui.cpp @@ -894,7 +894,7 @@ static intptr_t CL_UISystemCalls( intptr_t* args ) return 0; case UI_R_RENDERSCENE: - re.RenderScene( VMA(1) ); + re.RenderScene( VMA(1), 0 ); return 0; case UI_R_SETCOLOR: @@ -1144,7 +1144,7 @@ static intptr_t CL_UISystemCalls( intptr_t* args ) return 0; case UI_EXT_ENABLEERRORCALLBACK: - cls.uiErrorCallbackVMCall = (int)args[1]; + cls.uivmCalls[UIVM_ERROR_CALLBACK] = (int)args[1]; return 0; default: @@ -1157,7 +1157,7 @@ static intptr_t CL_UISystemCalls( intptr_t* args ) void CL_ShutdownUI() { - cls.uiErrorCallbackVMCall = 0; + Com_Memset( cls.uivmCalls, 0, sizeof(cls.uivmCalls) ); cls.keyCatchers &= ~KEYCATCH_UI; cls.uiStarted = qfalse; if ( !uivm ) @@ -1187,18 +1187,17 @@ void CL_InitUI() } // init for this gamestate - cls.uiErrorCallbackVMCall = 0; VM_Call( uivm, UI_INIT, (cls.state >= CA_AUTHORIZING && cls.state < CA_ACTIVE) ); } void CL_ForwardUIError( int level, int module, const char* error ) { - if ( uivm == NULL || cls.uiErrorCallbackVMCall == 0 ) + if ( uivm == NULL || cls.uivmCalls[UIVM_ERROR_CALLBACK] == 0 ) return; Q_strncpyz( (char*)interopBufferOut, error, interopBufferOutSize ); - VM_Call( uivm, cls.uiErrorCallbackVMCall, level, module ); + VM_Call( uivm, cls.uivmCalls[UIVM_ERROR_CALLBACK], level, module ); } diff --git a/code/client/client.h b/code/client/client.h index 1233136..623a542 100644 --- a/code/client/client.h +++ b/code/client/client.h @@ -173,7 +173,7 @@ typedef struct { int serverMessageSequence; // reliable messages received from server - int serverCommandSequence; + int serverCommandSequence; // the number of the latest available command int lastExecutedServerCommand; // last server command grabbed or executed with CL_GetServerCommand char serverCommands[MAX_RELIABLE_COMMANDS][MAX_STRING_CHARS]; qbool serverCommandsBad[MAX_RELIABLE_COMMANDS]; // non-zero means the command shouldn't be fed to cgame @@ -196,6 +196,7 @@ typedef struct { qbool demoplaying; qbool demowaiting; // don't record until a non-delta message is received qbool firstDemoFrameSkipped; + qbool newDemoPlayer; // running the new player with rewind support fileHandle_t demofile; int timeDemoFrames; // counter of rendered frames @@ -245,6 +246,22 @@ typedef struct { unsigned short port; } serverAddress_t; +// CGame VM calls that are extensions +enum { + CGVM_NDP_END_ANALYSIS, + CGVM_NDP_ANALYZE_SNAPSHOT, + CGVM_NDP_ANALYZE_COMMAND, + CGVM_NDP_GENERATE_COMMANDS, // generate synchronization commands + CGVM_NDP_IS_CS_NEEDED, // does this config string need to be re-submitted? + CGVM_COUNT +}; + +// UI VM calls that are extensions +enum { + UIVM_ERROR_CALLBACK, // forward errors to UI? + UIVM_COUNT +}; + typedef struct { connstate_t state; // connection status int keyCatchers; // bit flags @@ -261,12 +278,17 @@ typedef struct { qbool uiStarted; qbool cgameStarted; + // extensions VM calls indices + // 0 when not available + int cgvmCalls[CGVM_COUNT]; + int uivmCalls[UIVM_COUNT]; + + // extension: new demo player supported by the mod + qbool cgameNewDemoPlayer; + // extension: forward input to cgame regardless of keycatcher state? int cgameForwardInput; // 1=mouse, 2=keys (note: we don't forward the escape key) - // extension: forward errors to ui? - int uiErrorCallbackVMCall; // 0 when not available - int framecount; int frametime; // msec since last frame @@ -354,6 +376,8 @@ extern cvar_t *cl_allowDownload; // 0=off, 1=CNQ3, -1=id extern cvar_t *cl_inGameVideo; extern cvar_t *cl_matchAlerts; // bit mask, see the MAF_* constants +extern cvar_t *cl_demoPlayer; +extern cvar_t *cl_escapeAbortsDemo; extern cvar_t *r_khr_debug; @@ -496,6 +520,12 @@ void CL_InitCGame(); void CL_ShutdownCGame(); void CL_CGameRendering( stereoFrame_t stereo ); void CL_SetCGameTime(); +void CL_ConfigstringModified(); +void CL_CGNDP_EndAnalysis( const char* filePath, int firstServerTime, int lastServerTime, qbool videoRestart ); +void CL_CGNDP_AnalyzeSnapshot( int progress ); +void CL_CGNDP_AnalyzeCommand( int serverTime ); +void CL_CGNDP_GenerateCommands( const char** commands, int* numCommandBytes ); +qbool CL_CGNDP_IsConfigStringNeeded( int csIndex ); // // cl_ui.c @@ -546,6 +576,18 @@ void CL_MapDownload_CrashCleanUp(); qbool CL_GL_WantDebug(); // do we want a debug context from the platform layer? void CL_GL_Init(); // enables debug output if needed +// +// cl_demo.cpp +// +void CL_NDP_PlayDemo( qbool videoRestart ); +void CL_NDP_SetCGameTime(); +void CL_NDP_GetCurrentSnapshotNumber( int* snapshotNumber, int* serverTime ); +qbool CL_NDP_GetSnapshot( int snapshotNumber, snapshot_t* snapshot ); +qbool CL_NDP_GetServerCommand( int serverCommandNumber ); +int CL_NDP_Seek( int serverTime ); +void CL_NDP_ReadUntil( int serverTime ); +void CL_NDP_HandleError(); + // // OS-specific // diff --git a/code/client/client_help.h b/code/client/client_help.h index 474f9be..30638c8 100644 --- a/code/client/client_help.h +++ b/code/client/client_help.h @@ -146,3 +146,8 @@ S_COLOR_VAL " 2 " S_COLOR_HELP "= On in debug builds only" "Example: init lines starting with 'init'\n" \ "Example: *init lines containing 'init'\n" \ "Example: >*net lines starting with '>' AND containing 'net'" + +#define help_cl_demoPlayer \ +"1 enables demo rewind\n" \ +S_COLOR_VAL " 0 " S_COLOR_HELP "= Uses the original demo player\n" \ +S_COLOR_VAL " 1 " S_COLOR_HELP "= Uses the new demo player when the mod supports it" diff --git a/code/client/snd_dma.cpp b/code/client/snd_dma.cpp index af93845..cc314a2 100644 --- a/code/client/snd_dma.cpp +++ b/code/client/snd_dma.cpp @@ -233,6 +233,13 @@ static sfxHandle_t S_Base_RegisterSound( const char* name ) return 0; } + if ( name[0] == '\0' ) { + if ( !cls.cgameNewDemoPlayer ) { + Com_Printf( "Sound name is empty\n" ); + } + return 0; + } + if ( strlen( name ) >= MAX_QPATH ) { Com_Printf( "Sound name exceeds MAX_QPATH\n" ); return 0; diff --git a/code/qcommon/cg_public.h b/code/qcommon/cg_public.h index 81588ea..88144f8 100644 --- a/code/qcommon/cg_public.h +++ b/code/qcommon/cg_public.h @@ -196,7 +196,13 @@ typedef enum { CG_EXT_CMD_SETHELP, CG_EXT_MATCHALERTEVENT, CG_EXT_ERROR2, - CG_EXT_ISRECORDINGDEMO + CG_EXT_ISRECORDINGDEMO, + CG_EXT_NDP_ENABLE, + CG_EXT_NDP_SEEK, + CG_EXT_NDP_READUNTIL, + CG_EXT_NDP_STARTVIDEO, + CG_EXT_NDP_STOPVIDEO, + CG_EXT_R_RENDERSCENE } cgameImport_t; @@ -427,6 +433,7 @@ typedef enum { CG_MOUSE_EVENT, // void (*CG_MouseEvent)( int dx, int dy ); + CG_EVENT_HANDLING // void (*CG_EventHandling)(int type); } cgameExport_t; diff --git a/code/qcommon/common.cpp b/code/qcommon/common.cpp index facb5ab..921e834 100644 --- a/code/qcommon/common.cpp +++ b/code/qcommon/common.cpp @@ -250,6 +250,14 @@ void QDECL Com_ErrorExt( int code, int module, qbool realError, const char *fmt, static int lastErrorTime; static int errorCount; + if ( code == ERR_DROP_NDP ) { +#if !defined(DEDICATED) + void CL_NDP_HandleError(); + CL_NDP_HandleError(); +#endif + code = ERR_DROP; + } + // make sure we can get at our local stuff FS_PureServerSetLoadedPaks( "" ); @@ -3593,3 +3601,19 @@ static const char* Com_GetCompilerInfo() return "Unknown compiler"; #endif } + + +const char* Com_FormatBytes( int numBytes ) +{ + const char* units[] = { "bytes", "KB", "MB", "GB" }; + const float dividers[] = { 1.0f, float(1 << 10), float(1 << 20), float(1 << 30) }; + + int unit = 0; + for ( int vi = numBytes; vi >= 1024; vi >>= 10 ) { + unit++; + } + + const float vf = (float)numBytes / dividers[unit]; + + return va( "%.3f %s", vf, units[unit] ); +} diff --git a/code/qcommon/files.cpp b/code/qcommon/files.cpp index 6109e0a..bad8068 100644 --- a/code/qcommon/files.cpp +++ b/code/qcommon/files.cpp @@ -1294,6 +1294,16 @@ int FS_Seek( fileHandle_t f, long offset, int origin ) } +qbool FS_IsZipFile( fileHandle_t f ) +{ + if ( f < 0 || f >= MAX_FILE_HANDLES ) { + Com_Error( ERR_DROP, "FS_IsZipFile: out of range" ); + } + + return fsh[f].zipFile; +} + + /* ====================================================================================== diff --git a/code/qcommon/msg.cpp b/code/qcommon/msg.cpp index 4f6e54e..25cd093 100644 --- a/code/qcommon/msg.cpp +++ b/code/qcommon/msg.cpp @@ -180,7 +180,7 @@ int MSG_ReadBits( msg_t *msg, int bits ) { msg->readcount += 4; msg->bit += 32; } else { - Com_Error(ERR_DROP, "can't read %d bits\n", bits); + Com_Error(ERR_DROP_NDP, "can't read %d bits\n", bits); } } else { nbits = 0; @@ -741,7 +741,7 @@ void MSG_ReadDeltaEntity( msg_t* msg, const entityState_t* from, entityState_t* int startBit, endBit; if ( number < 0 || number >= MAX_GENTITIES ) { - Com_Error( ERR_DROP, "Bad delta entity number: %i", number ); + Com_Error( ERR_DROP_NDP, "Bad delta entity number: %i", number ); } if ( msg->bit == 0 ) { @@ -771,7 +771,7 @@ void MSG_ReadDeltaEntity( msg_t* msg, const entityState_t* from, entityState_t* lc = MSG_ReadByte(msg); if ( lc < 0 || lc > ARRAY_LEN(entityStateFields) ) { - Com_Error( ERR_DROP, "invalid entityState_t field count %d (max: %d)\n", lc, ARRAY_LEN(entityStateFields) ); + Com_Error( ERR_DROP_NDP, "invalid entityState_t field count %d (max: %d)\n", lc, ARRAY_LEN(entityStateFields) ); } // shownet 2/3 will interleave with other printed info, -1 will @@ -1095,7 +1095,7 @@ void MSG_ReadDeltaPlayerstate( msg_t* msg, const playerState_t* from, playerStat lc = MSG_ReadByte(msg); if ( lc < 0 || lc > ARRAY_LEN(playerStateFields) ) { - Com_Error( ERR_DROP, "invalid playerState_t field count %d (max: %d)\n", lc, ARRAY_LEN(playerStateFields) ); + Com_Error( ERR_DROP_NDP, "invalid playerState_t field count %d (max: %d)\n", lc, ARRAY_LEN(playerStateFields) ); } const netField_t* field; diff --git a/code/qcommon/q_shared.c b/code/qcommon/q_shared.c index 79929a3..6e09333 100644 --- a/code/qcommon/q_shared.c +++ b/code/qcommon/q_shared.c @@ -672,9 +672,9 @@ FIXME: make this buffer size safe someday */ const char* QDECL va( const char* format, ... ) { - static char string[2][32000]; // in case va is called by nested functions + static char string[4][32000]; // in case va is called by nested functions static int index = 0; - char* buf = string[index++ & 1]; + char* buf = string[index++ & 3]; va_list argptr; va_start( argptr, format ); diff --git a/code/qcommon/q_shared.h b/code/qcommon/q_shared.h index 05f83ef..7b1ae10 100644 --- a/code/qcommon/q_shared.h +++ b/code/qcommon/q_shared.h @@ -215,6 +215,7 @@ typedef int clipHandle_t; typedef enum { ERR_FATAL, // exit the entire game with a popup window ERR_DROP, // print to console and disconnect from game + ERR_DROP_NDP, // same but the NDP can run its own handler when active ERR_SERVERDISCONNECT, // don't kill server ERR_DISCONNECT, // client disconnected from the server ERR_NEED_CD // pop up the need-cd dialog diff --git a/code/qcommon/qcommon.h b/code/qcommon/qcommon.h index 117bdd0..d7dea44 100644 --- a/code/qcommon/qcommon.h +++ b/code/qcommon/qcommon.h @@ -710,6 +710,9 @@ int FS_FOpenFileByMode( const char *qpath, fileHandle_t *f, fsMode_t mode ); int FS_Seek( fileHandle_t f, long offset, int origin ); // seek on a file (doesn't work for zip files!!!!!!!!) +qbool FS_IsZipFile( fileHandle_t f ); +// tells us whether we opened a zip file + qbool FS_FilenameCompare( const char *s1, const char *s2 ); const char *FS_LoadedPakChecksums( void ); @@ -851,6 +854,7 @@ int Com_Filter( const char* filter, const char* name ); int Com_FilterPath( const char* filter, const char* name ); int Com_RealTime(qtime_t *qtime); qbool Com_SafeMode(); +const char *Com_FormatBytes( int numBytes ); void Com_StartupVariable( const char *match ); // checks for and removes command line "+set var arg" constructs diff --git a/code/renderer/tr_local.h b/code/renderer/tr_local.h index f7f9506..2b4fcde 100644 --- a/code/renderer/tr_local.h +++ b/code/renderer/tr_local.h @@ -623,24 +623,11 @@ struct srfTriangles_t { // trRefdef_t holds everything that comes in refdef_t, // as well as the locally generated scene information -typedef struct { - int x, y, width, height; - float fov_x, fov_y; - vec3_t vieworg; - vec3_t viewaxis[3]; // transformation matrix - - int time; // time in milliseconds for shader effects and other time dependent rendering issues - int rdflags; // RDF_NOWORLDMODEL, etc - - // 1 bits will prevent the associated area from rendering at all - byte areamask[MAX_MAP_AREA_BYTES]; +struct trRefdef_t : public refdef_t { qbool areamaskModified; // qtrue if areamask changed since last scene - + int microSeconds; // [0;999] micro-seconds to add to the timestamp double floatTime; // tr.refdef.time / 1000.0 - // text messages for deform text shaders - char text[MAX_RENDER_STRINGS][MAX_RENDER_STRING_LENGTH]; - int num_entities; trRefEntity_t *entities; @@ -655,8 +642,7 @@ typedef struct { int numLitSurfs; litSurf_t* litSurfs; - -} trRefdef_t; +}; /* @@ -1399,7 +1385,7 @@ void RE_ClearScene(); void RE_AddRefEntityToScene( const refEntity_t *ent, qbool intShaderTime ); void RE_AddPolyToScene( qhandle_t hShader , int numVerts, const polyVert_t *verts, int num ); void RE_AddLightToScene( const vec3_t org, float radius, float r, float g, float b ); -void RE_RenderScene( const refdef_t *fd ); +void RE_RenderScene( const refdef_t *fd, int us ); /* diff --git a/code/renderer/tr_public.h b/code/renderer/tr_public.h index eae6b48..4243997 100644 --- a/code/renderer/tr_public.h +++ b/code/renderer/tr_public.h @@ -131,7 +131,7 @@ typedef struct { void (*AddPolyToScene)( qhandle_t hShader, int numVerts, const polyVert_t *verts, int num ); qbool (*LightForPoint)( const vec3_t point, vec3_t ambientLight, vec3_t directedLight, vec3_t lightDir ); void (*AddLightToScene)( const vec3_t org, float radius, float r, float g, float b ); - void (*RenderScene)( const refdef_t *fd ); + void (*RenderScene)( const refdef_t *fd, int us ); void (*SetColor)( const float* rgba ); // NULL = 1,1,1,1 void (*DrawStretchPic)( float x, float y, float w, float h, diff --git a/code/renderer/tr_scene.cpp b/code/renderer/tr_scene.cpp index da96c81..b7b22d9 100644 --- a/code/renderer/tr_scene.cpp +++ b/code/renderer/tr_scene.cpp @@ -197,7 +197,7 @@ void RE_AddLightToScene( const vec3_t org, float radius, float r, float g, float // draw a 3D view into a part of the window, then return to 2D drawing // rendering a scene may require multiple views to be rendered to handle mirrors -void RE_RenderScene( const refdef_t* fd ) +void RE_RenderScene( const refdef_t* fd, int us ) { if ( !tr.registered ) { return; @@ -228,6 +228,7 @@ void RE_RenderScene( const refdef_t* fd ) VectorCopy( fd->viewaxis[2], tr.refdef.viewaxis[2] ); tr.refdef.time = fd->time; + tr.refdef.microSeconds = us; tr.refdef.rdflags = fd->rdflags; // copy the areamask data over and note if it has changed, which @@ -248,7 +249,9 @@ void RE_RenderScene( const refdef_t* fd ) // derived info - tr.refdef.floatTime = (double)tr.refdef.time / 1000.0; + tr.refdef.floatTime = + (double)tr.refdef.time / 1000.0 + + (double)tr.refdef.microSeconds / 1000000.0; tr.refdef.numDrawSurfs = r_firstSceneDrawSurf; tr.refdef.drawSurfs = backEndData->drawSurfs; diff --git a/makefiles/linux_gmake/cnq3.make b/makefiles/linux_gmake/cnq3.make index bc7a087..021b7bd 100644 --- a/makefiles/linux_gmake/cnq3.make +++ b/makefiles/linux_gmake/cnq3.make @@ -78,6 +78,7 @@ OBJECTS := \ $(OBJDIR)/cl_cgame.o \ $(OBJDIR)/cl_cin.o \ $(OBJDIR)/cl_console.o \ + $(OBJDIR)/cl_demo.o \ $(OBJDIR)/cl_download.o \ $(OBJDIR)/cl_gl.o \ $(OBJDIR)/cl_input.o \ @@ -205,6 +206,9 @@ $(OBJDIR)/cl_cin.o: ../../code/client/cl_cin.cpp $(OBJDIR)/cl_console.o: ../../code/client/cl_console.cpp @echo $(notdir $<) $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/cl_demo.o: ../../code/client/cl_demo.cpp + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" $(OBJDIR)/cl_download.o: ../../code/client/cl_download.cpp @echo $(notdir $<) $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" diff --git a/makefiles/premake5.lua b/makefiles/premake5.lua index 5164653..3cff0d8 100644 --- a/makefiles/premake5.lua +++ b/makefiles/premake5.lua @@ -369,6 +369,7 @@ local function ApplyExeProjectSettings(exeName, server) "client/cl_cgame.cpp", "client/cl_cin.cpp", "client/cl_console.cpp", + "client/cl_demo.cpp", "client/cl_download.cpp", "client/cl_gl.cpp", "client/cl_input.cpp", diff --git a/makefiles/windows_vs2013/cnq3.vcxproj b/makefiles/windows_vs2013/cnq3.vcxproj index fe1a153..54f8c66 100644 --- a/makefiles/windows_vs2013/cnq3.vcxproj +++ b/makefiles/windows_vs2013/cnq3.vcxproj @@ -314,6 +314,7 @@ copy "..\..\.bin\release_x32\cnq3-x86.pdb" "$(QUAKE3DIR)" + diff --git a/makefiles/windows_vs2013/cnq3.vcxproj.filters b/makefiles/windows_vs2013/cnq3.vcxproj.filters index 8310bf7..4a02e3f 100644 --- a/makefiles/windows_vs2013/cnq3.vcxproj.filters +++ b/makefiles/windows_vs2013/cnq3.vcxproj.filters @@ -248,6 +248,9 @@ client + + client + client