From ff998f1a071619fa801f26568c92b61d5e92677d Mon Sep 17 00:00:00 2001 From: Tom Arrow Date: Mon, 22 Aug 2022 13:09:24 +0200 Subject: [PATCH 01/12] Serverside demo pre-recording to not have the beginning of race demos cut off. --- codemp/qcommon/msg.cpp | 22 ++++++++++ codemp/qcommon/qcommon.h | 28 +++++++++++++ codemp/server/server.h | 13 ++++++ codemp/server/sv_ccmds.cpp | 59 +++++++++++++++++++++++++-- codemp/server/sv_init.cpp | 3 ++ codemp/server/sv_main.cpp | 3 ++ codemp/server/sv_snapshot.cpp | 75 +++++++++++++++++++++++++++++++++++ 7 files changed, 199 insertions(+), 4 deletions(-) diff --git a/codemp/qcommon/msg.cpp b/codemp/qcommon/msg.cpp index ac178d342f..b1b25d47a3 100644 --- a/codemp/qcommon/msg.cpp +++ b/codemp/qcommon/msg.cpp @@ -78,6 +78,28 @@ void MSG_Init( msg_t *buf, byte *data, int length ) { buf->maxsize = length; } +void MSG_ToBuffered(msg_t* src, bufferedMsg_t* dst) { + dst->allowoverflow = src->allowoverflow; + dst->overflowed = src->overflowed; + dst->oob = src->oob; + dst->maxsize = src->maxsize; + dst->cursize = src->cursize; + dst->readcount = src->readcount; + dst->bit = src->bit; + Com_Memcpy(dst->data, src->data, sizeof(dst->data)); +} + +void MSG_FromBuffered(msg_t* dst, bufferedMsg_t* src) { + dst->allowoverflow = src->allowoverflow; + dst->overflowed = src->overflowed; + dst->oob = src->oob; + dst->maxsize = src->maxsize; + dst->cursize = src->cursize; + dst->readcount = src->readcount; + dst->bit = src->bit; + Com_Memcpy(dst->data, src->data, sizeof(src->data)); +} + void MSG_InitOOB( msg_t *buf, byte *data, int length ) { if (!g_nOverrideChecked) { diff --git a/codemp/qcommon/qcommon.h b/codemp/qcommon/qcommon.h index e7e0283d3c..fee78d46b4 100755 --- a/codemp/qcommon/qcommon.h +++ b/codemp/qcommon/qcommon.h @@ -51,6 +51,7 @@ void MSG_Clear (msg_t *buf); void MSG_WriteData (msg_t *buf, const void *data, int length); void MSG_Bitstream( msg_t *buf ); + struct usercmd_s; struct entityState_s; struct playerState_s; @@ -170,6 +171,33 @@ void Sys_ShowIP(void); #define MAX_DOWNLOAD_BLKSIZE 2048 // 2048 byte block chunks +/* +* Buffered messages (structs that actually contain the data array) for buffering/prerecording +*/ + +typedef struct { + qboolean allowoverflow; // if false, do a Com_Error + qboolean overflowed; // set to true if the buffer size failed (with allowoverflow set) + qboolean oob; // set to true if the buffer size failed (with allowoverflow set) + byte data[MAX_MSGLEN]; + int maxsize; + int cursize; + int readcount; + int bit; // for bitwise reads and writes +} bufferedMsg_t; + +typedef struct { + bufferedMsg_t msg; + int msgNum; // Message number + int time; // We don't want to wait infinitely for old messages to arrive. + //qboolean containsFullSnapshot; // Doesn't matter for serverside pre-Recording because we have the keyframes. Comment back in for clientside buffered recording. + qboolean isKeyframe; // Is a gamestate message as typical for writing at the start of demos. +} bufferedMessageContainer_t; + +void MSG_ToBuffered(msg_t* src, bufferedMsg_t* dst); +void MSG_FromBuffered(msg_t* dst, bufferedMsg_t* src); + + /* Netchan handles packet fragmentation and out of order / duplicate suppression */ diff --git a/codemp/server/server.h b/codemp/server/server.h index be67475f8b..4afb804f31 100644 --- a/codemp/server/server.h +++ b/codemp/server/server.h @@ -132,9 +132,19 @@ typedef struct { fileHandle_t demofile; qboolean isBot; int botReliableAcknowledge; // for bots, need to maintain a separate reliableAcknowledge to record server messages into the demo file + struct { + // this is basically the equivalent of the demowaiting and minDeltaFrame values above, except it's for the demo pre-record feature and will be done every sv_demoPreRecordKeyframeDistance milliseconds. + qboolean keyframeWaiting; + int minDeltaFrame; + + int lastKeyframeTime; // When was the last keyframe (gamestate followed by non-delta frames) saved? If more than sv_demoPreRecordKeyframeDistance, we make a new keyframe. + } preRecord; } demoInfo_t; + +typedef std::vector::iterator demoPreRecordBufferIt; + typedef struct client_s { clientState_t state; char userinfo[MAX_INFO_STRING]; // name, etc @@ -302,6 +312,9 @@ extern cvar_t *sv_maxOOBRate; extern cvar_t *sv_maxOOBRateIP; extern cvar_t *sv_autoWhitelist; +extern cvar_t *sv_demoPreRecord; +extern cvar_t *sv_demoPreRecordKeyframeDistance; + extern cvar_t *sv_snapShotDuelCull; extern cvar_t *sv_pingFix; diff --git a/codemp/server/sv_ccmds.cpp b/codemp/server/sv_ccmds.cpp index 2ef7afe7e9..8695bd25a5 100755 --- a/codemp/server/sv_ccmds.cpp +++ b/codemp/server/sv_ccmds.cpp @@ -1524,6 +1524,21 @@ void SV_WriteDemoMessage ( client_t *cl, msg_t *msg, int headerBytes ) { FS_Write( msg->data + headerBytes, len, cl->demo.demofile ); } +void SV_WriteDemoMessage ( client_t *cl, msg_t *msg, int headerBytes, int messageNum ) { // Version that specifies messagenumber manually, for buffered (pre-record) messages. + int len, swlen; + + // write the packet sequence + len = messageNum; + swlen = LittleLong( len ); + FS_Write( &swlen, 4, cl->demo.demofile ); + + // skip the packet sequencing information + len = msg->cursize - headerBytes; + swlen = LittleLong( len ); + FS_Write( &swlen, 4, cl->demo.demofile ); + FS_Write( msg->data + headerBytes, len, cl->demo.demofile ); +} + void SV_StopRecordDemo( client_t *cl ) { int len; @@ -1646,7 +1661,7 @@ void SV_DemoFilename( char *buf, int bufSize ) { // defined in sv_client.cpp extern void SV_CreateClientGameStateMessage( client_t *client, msg_t* msg ); - +extern std::vector demoPreRecordBuffer[MAX_CLIENTS]; void SV_RecordDemo( client_t *cl, char *demoName ) { char name[MAX_OSPATH]; byte bufData[MAX_MSGLEN]; @@ -1678,12 +1693,48 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { } cl->demo.demorecording = qtrue; + cl->demo.isBot = (cl->netchan.remoteAddress.type == NA_BOT) ? qtrue : qfalse; + cl->demo.botReliableAcknowledge = cl->reliableSent; + + // Ok we have two options now. Either the classical way of starting with gamestate. + // OR, if enabled, we use our pre-recorded buffer to start recording a bit in the past, + // in which case we also don't have to worry about demowaiting since the pre-record + // already takes care of that + if (sv_demoPreRecord->integer) { + // Pre-recording is enabled. Let's check for the oldest available keyframe. + demoPreRecordBufferIt firstOldKeyframe; + qboolean firstOldKeyframeFound = qfalse; + for (demoPreRecordBufferIt it = demoPreRecordBuffer[cl - svs.clients].begin(); it != demoPreRecordBuffer[cl - svs.clients].end(); it++) { + if (it->isKeyframe && it->time < sv.time) { + firstOldKeyframe = it; + firstOldKeyframeFound = qtrue; + break; + } + } + if (firstOldKeyframeFound) { + int index = 0; + // Dump this keyframe (gamestate message) and all following non-keyframes into the demo. + for (demoPreRecordBufferIt it = firstOldKeyframe; it != demoPreRecordBuffer[cl - svs.clients].end(); it++,index++) { + static byte preRecordBufData[MAX_MSGLEN]; // I make these static so they don't sit on the stack. + static msg_t preRecordMsg; + + if (!it->isKeyframe || index == 0) { + // We only want a keyframe at the beginning of the demo, none after. + Com_Memset(&preRecordMsg, 0, sizeof(msg_t)); + Com_Memset(&preRecordBufData, 0, sizeof(preRecordBufData)); + preRecordMsg.data = preRecordBufData; + MSG_FromBuffered(&preRecordMsg, &it->msg); + MSG_WriteByte(&preRecordMsg, svc_EOF); // We didn't do that for the ones we put into the buffer, so we do it now. + SV_WriteDemoMessage(cl,&preRecordMsg,0,it->msgNum); + } + } + return; // No need to go through the whole normal demo procedure with demowaiting etc. + } + } + // don't start saving messages until a non-delta compressed message is received cl->demo.demowaiting = qtrue; - cl->demo.isBot = ( cl->netchan.remoteAddress.type == NA_BOT ) ? qtrue : qfalse; - cl->demo.botReliableAcknowledge = cl->reliableSent; - // write out the gamestate message MSG_Init( &msg, bufData, sizeof( bufData ) ); diff --git a/codemp/server/sv_init.cpp b/codemp/server/sv_init.cpp index fd6d17cd13..35bc390a85 100644 --- a/codemp/server/sv_init.cpp +++ b/codemp/server/sv_init.cpp @@ -1021,6 +1021,9 @@ void SV_Init (void) { sv_maxOOBRateIP = Cvar_Get("sv_maxOOBRateIP", "1", CVAR_ARCHIVE, "Maximum rate of handling incoming server commands per IP address" ); sv_autoWhitelist = Cvar_Get("sv_autoWhitelist", "1", CVAR_ARCHIVE, "Save player IPs to allow them using server during DOS attack" ); + sv_demoPreRecord = Cvar_Get("sv_demoPreRecord", "15000", CVAR_ARCHIVE, "If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration?"); + sv_demoPreRecordKeyframeDistance = Cvar_Get("sv_demoPreRecordKeyframeDistance", "5000", CVAR_ARCHIVE, "A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients."); + sv_snapShotDuelCull = Cvar_Get("sv_snapShotDuelCull", "1", CVAR_NONE, "Snapshot-based duel isolation"); sv_pingFix = Cvar_Get("sv_pingFix", "1", CVAR_ARCHIVE_ND, "Improved scoreboard client ping calculation"); diff --git a/codemp/server/sv_main.cpp b/codemp/server/sv_main.cpp index 4fa8b9ae0f..96374e9710 100644 --- a/codemp/server/sv_main.cpp +++ b/codemp/server/sv_main.cpp @@ -73,6 +73,9 @@ cvar_t *sv_maxOOBRate; cvar_t *sv_maxOOBRateIP; cvar_t *sv_autoWhitelist; +cvar_t *sv_demoPreRecord; // If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration? +cvar_t *sv_demoPreRecordKeyframeDistance; // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept. + cvar_t *sv_snapShotDuelCull; cvar_t *sv_pingFix; diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index 4a2a2c8405..6f42691091 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -24,6 +24,9 @@ along with this program; if not, see . #include "server.h" #include "qcommon/cm_public.h" + +std::vector demoPreRecordBuffer[MAX_CLIENTS]; + /* ============================================================================= @@ -158,6 +161,16 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { // non-delta frames until the client acks. oldframe = NULL; lastframe = 0; + } else if ( sv_demoPreRecord->integer && client->demo.preRecord.keyframeWaiting ) { + // demo is waiting for a non-delta-compressed frame for this client, so don't delta compress + oldframe = NULL; + lastframe = 0; + } else if ( client->demo.preRecord.minDeltaFrame > deltaMessage ) { + // we saved a non-delta frame to the pre-record buffer and sent it to the client, but the client didn't ack it + // we can't delta against an old frame that's not in the demo without breaking the demo. so send + // non-delta frames until the client acks. + oldframe = NULL; + lastframe = 0; } else { // we have a valid snapshot to delta from oldframe = &client->frames[ deltaMessage & PACKET_MASK ]; @@ -177,6 +190,11 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { client->demo.minDeltaFrame = client->netchan.outgoingSequence; } client->demo.demowaiting = qfalse; + if ( client->demo.preRecord.keyframeWaiting ) { + // this is a non-delta frame, so we can delta against it in the demo + client->demo.preRecord.minDeltaFrame = client->netchan.outgoingSequence; + } + client->demo.preRecord.keyframeWaiting = qfalse; } MSG_WriteByte (msg, svc_snapshot); @@ -737,6 +755,8 @@ static int SV_RateMsec( client_t *client, int messageSize ) { } extern void SV_WriteDemoMessage ( client_t *cl, msg_t *msg, int headerBytes ); +// defined in sv_client.cpp +extern void SV_CreateClientGameStateMessage(client_t* client, msg_t* msg); /* ======================= SV_SendMessageToClient @@ -763,6 +783,16 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageSent = (sv_pingFix->integer ? Sys_Milliseconds() : svs.time); client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageAcked = -1; + if (sv_demoPreRecord->integer) { // If pre record demo message buffering is enabled, we write this message to the buffer. + static bufferedMessageContainer_t bmt; // I make these static so they don't sit on the stack. + Com_Memset(&bmt, 0, sizeof(bufferedMessageContainer_t)); + MSG_ToBuffered(msg,&bmt.msg); + bmt.msgNum = client->netchan.outgoingSequence; + bmt.time = sv.time; + bmt.isKeyframe = qfalse; // In theory it might be a gamestate message, but we only call it a keyframe if we ourselves explicitly save a keyframe. + demoPreRecordBuffer[client - svs.clients].push_back(bmt); + } + // save the message to demo. this must happen before sending over network as that encodes the backing databuf if ( client->demo.demorecording && !client->demo.demowaiting ) { msg_t msgcopy = *msg; @@ -770,6 +800,51 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { SV_WriteDemoMessage( client, &msgcopy, 0 ); } + // Check for whether a new keyframe must be written in pre recording, and if so, do it. + if (sv_demoPreRecord->integer) { + if (client->demo.preRecord.lastKeyframeTime + sv_demoPreRecordKeyframeDistance->integer < sv.time) { + // Save a keyframe. + static byte keyframeBufData[MAX_MSGLEN]; // I make these static so they don't sit on the stack. + static msg_t keyframeMsg; + static bufferedMessageContainer_t bmt; + Com_Memset(&keyframeMsg, 0, sizeof(msg_t)); + Com_Memset(&bmt, 0, sizeof(bufferedMessageContainer_t)); + + MSG_Init(&keyframeMsg, keyframeBufData, sizeof(keyframeBufData)); + + int tmp = client->reliableSent; //Idk if this is still needed? Might have been from an older version of SV_CreateClientGameStateMessage that changed that? + SV_CreateClientGameStateMessage(client, &keyframeMsg); + client->reliableSent = tmp; + + MSG_ToBuffered(&keyframeMsg, &bmt.msg); + bmt.msgNum = client->netchan.outgoingSequence; // Yes the keyframe duplicates the messagenum of a message. This is (part of) why we dump only one keyframe at the start of the demo and discard future keyframes + bmt.time = sv.time; + bmt.isKeyframe = qtrue; // This is a keyframe (gamestate that will be followed by non-delta frames) + demoPreRecordBuffer[client - svs.clients].push_back(bmt); + client->demo.preRecord.keyframeWaiting = qtrue; + client->demo.preRecord.lastKeyframeTime = sv.time; + } + } + + // Clean up pre-record buffer, whether preRecord is enabled or not (because it might have just been disabled) + { + // The goal is to always maintain *at least* sv_demoPreRecord milliseconds of buffer. Rather more than less. + // So we find the last keyframe that is older than sv_demoPreRecord milliseconds (or just that old) and then delete everything *before* it. + demoPreRecordBufferIt lastTooOldKeyframe; + qboolean lastTooOldKeyframeFound = qfalse; + for (demoPreRecordBufferIt it = demoPreRecordBuffer[client - svs.clients].begin(); it != demoPreRecordBuffer[client - svs.clients].end(); it++) { + if (it->isKeyframe && (it->time + sv_demoPreRecord->integer) < sv.time) { + lastTooOldKeyframe = it; + lastTooOldKeyframeFound = qtrue; + } + } + if (lastTooOldKeyframeFound) { + // The lastTooOldKeyframe itself won't be erased because .erase()'s second parameter is not inclusive, + // aka it deletes up to that element, but not that element itself. + demoPreRecordBuffer[client - svs.clients].erase(demoPreRecordBuffer[client - svs.clients].begin(),lastTooOldKeyframe); + } + } + // bots need to have their snapshots built, but // they query them directly without needing to be sent if ( client->demo.isBot ) { From 065abd7dfced1a82a860ef782f07f80f0b3e8b6e Mon Sep 17 00:00:00 2001 From: Tom Arrow Date: Mon, 22 Aug 2022 15:40:36 +0200 Subject: [PATCH 02/12] Fix copy-pasted comment to make sense. --- codemp/qcommon/qcommon.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codemp/qcommon/qcommon.h b/codemp/qcommon/qcommon.h index fee78d46b4..43dfb0efb2 100755 --- a/codemp/qcommon/qcommon.h +++ b/codemp/qcommon/qcommon.h @@ -189,7 +189,7 @@ typedef struct { typedef struct { bufferedMsg_t msg; int msgNum; // Message number - int time; // We don't want to wait infinitely for old messages to arrive. + int time; // So we can discard very old buffered messages. Or for clientside recording, so we don't have to wait infinitely for old messages to arrive (which they never may). //qboolean containsFullSnapshot; // Doesn't matter for serverside pre-Recording because we have the keyframes. Comment back in for clientside buffered recording. qboolean isKeyframe; // Is a gamestate message as typical for writing at the start of demos. } bufferedMessageContainer_t; From 47c32e39bfb3cae4dc6b96728a725b828279be71 Mon Sep 17 00:00:00 2001 From: Tom Arrow Date: Tue, 23 Aug 2022 15:49:24 +0200 Subject: [PATCH 03/12] Fixed potential small bug when changing sv_demoPreRecord while server is running. --- codemp/server/sv_snapshot.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index 6f42691091..6385526412 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -824,10 +824,9 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { client->demo.preRecord.keyframeWaiting = qtrue; client->demo.preRecord.lastKeyframeTime = sv.time; } - } - // Clean up pre-record buffer, whether preRecord is enabled or not (because it might have just been disabled) - { + // Clean up pre-record buffer + // // The goal is to always maintain *at least* sv_demoPreRecord milliseconds of buffer. Rather more than less. // So we find the last keyframe that is older than sv_demoPreRecord milliseconds (or just that old) and then delete everything *before* it. demoPreRecordBufferIt lastTooOldKeyframe; @@ -841,9 +840,13 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { if (lastTooOldKeyframeFound) { // The lastTooOldKeyframe itself won't be erased because .erase()'s second parameter is not inclusive, // aka it deletes up to that element, but not that element itself. - demoPreRecordBuffer[client - svs.clients].erase(demoPreRecordBuffer[client - svs.clients].begin(),lastTooOldKeyframe); + demoPreRecordBuffer[client - svs.clients].erase(demoPreRecordBuffer[client - svs.clients].begin(), lastTooOldKeyframe); } } + else { // Pre-recording disabled. Clear buffer to prevent unexpected behavior if it is turned back on. + demoPreRecordBuffer[client - svs.clients].clear(); + } + // bots need to have their snapshots built, but // they query them directly without needing to be sent From 44eee09265ec268691564b44d0057d4461425bc6 Mon Sep 17 00:00:00 2001 From: Tom Arrow Date: Tue, 23 Aug 2022 16:24:30 +0200 Subject: [PATCH 04/12] Clear pre-record buffer when new client connects and reset lastKeyframeTime when sv_demoPreRecord is 0. --- codemp/server/sv_client.cpp | 5 +++++ codemp/server/sv_snapshot.cpp | 1 + 2 files changed, 6 insertions(+) diff --git a/codemp/server/sv_client.cpp b/codemp/server/sv_client.cpp index ddbcdc4075..10a0b6215f 100644 --- a/codemp/server/sv_client.cpp +++ b/codemp/server/sv_client.cpp @@ -128,6 +128,7 @@ SV_DirectConnect A "connect" OOB command has been received ================== */ +extern std::vector demoPreRecordBuffer[MAX_CLIENTS]; void SV_DirectConnect( netadr_t from ) { char userinfo[MAX_INFO_STRING]; int i; @@ -330,6 +331,10 @@ void SV_DirectConnect( netadr_t from ) { SV_UserinfoChanged( newcl ); + // When a new client connects, we reset the pre-record buffer for this client. + demoPreRecordBuffer[clientNum].clear(); + newcl->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer * 2; // Make sure that turning pre-recording on again will immediately cause creation of a keyframe + // send the connect packet to the client NET_OutOfBandPrint( NS_SERVER, from, "connectResponse" ); diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index 6385526412..b4d16a8c60 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -845,6 +845,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { } else { // Pre-recording disabled. Clear buffer to prevent unexpected behavior if it is turned back on. demoPreRecordBuffer[client - svs.clients].clear(); + client->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer*2; // Make sure that turning pre-recording on again will immediately cause creation of a keyframe } From 9fbfaa84c0ae0c9406f29dc7f270529f522ecb41 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 14 Jun 2023 16:48:50 +0200 Subject: [PATCH 05/12] Documentation for sv_demoPreRecord and sv_demoPreRecordKeyframeDistance --- japro_docs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/japro_docs.md b/japro_docs.md index 203f1e9430..b3aea96e80 100644 --- a/japro_docs.md +++ b/japro_docs.md @@ -129,6 +129,8 @@ g_raceLog 0//Log to races.log (incase database gets messed up). g_playerLog 0//Used by /amlookup sv_autoRaceDemo 0 //Requires custom server executable with "svrecord" command. + sv_demoPreRecord 15000 // If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration? + sv_demoPreRecordKeyframeDistance 5000 // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients. #### Bots bot_nochat 0 From 0a8dab604488c26430fa13f1c2ebd0e1ab4ac11b Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 14 Jun 2023 19:55:21 +0200 Subject: [PATCH 06/12] - Added game commands for clearing pre-record buffer per client - Added game commands for setting demo metadata - Updated docs to include serverside demo related cvars and commands including new ones - Bit of improvement to serverside demo pre-recording regarding clearing pre-record buffer - Implemented demo metadata using method from JK2DemoCutter - Added sv_demoWriteMeta to enable/disable metadata writing altogether. --- codemp/qcommon/qcommon.h | 1 + codemp/server/server.h | 4 + codemp/server/sv_bot.cpp | 2 + codemp/server/sv_ccmds.cpp | 254 +++++++++++++++++++++++++++++++++- codemp/server/sv_client.cpp | 7 +- codemp/server/sv_init.cpp | 2 + codemp/server/sv_main.cpp | 1 + codemp/server/sv_snapshot.cpp | 3 + japro_docs.md | 8 ++ 9 files changed, 278 insertions(+), 4 deletions(-) diff --git a/codemp/qcommon/qcommon.h b/codemp/qcommon/qcommon.h index 43dfb0efb2..492ed3a717 100755 --- a/codemp/qcommon/qcommon.h +++ b/codemp/qcommon/qcommon.h @@ -189,6 +189,7 @@ typedef struct { typedef struct { bufferedMsg_t msg; int msgNum; // Message number + int lastClientCommand; // Need this if we are writing metadata with pre-recording as it is the first thing writen in any message. int time; // So we can discard very old buffered messages. Or for clientside recording, so we don't have to wait infinitely for old messages to arrive (which they never may). //qboolean containsFullSnapshot; // Doesn't matter for serverside pre-Recording because we have the keyframes. Comment back in for clientside buffered recording. qboolean isKeyframe; // Is a gamestate message as typical for writing at the start of demos. diff --git a/codemp/server/server.h b/codemp/server/server.h index 4afb804f31..afac337ba3 100644 --- a/codemp/server/server.h +++ b/codemp/server/server.h @@ -314,6 +314,7 @@ extern cvar_t *sv_autoWhitelist; extern cvar_t *sv_demoPreRecord; extern cvar_t *sv_demoPreRecordKeyframeDistance; +extern cvar_t *sv_demoWriteMeta; extern cvar_t *sv_snapShotDuelCull; @@ -422,6 +423,9 @@ void SV_WriteDownloadToClient( client_t *cl , msg_t *msg ); void SV_Heartbeat_f( void ); void SV_RecordDemo( client_t *cl, char *demoName ); void SV_StopRecordDemo( client_t *cl ); +void SV_ClearClientDemoMeta( client_t *cl ); +void SV_ClearClientDemoPreRecord( client_t *cl ); +void SV_ClearAllDemoPreRecord( ); void SV_AutoRecordDemo( client_t *cl ); void SV_StopAutoRecordDemos(); void SV_BeginAutoRecordDemos(); diff --git a/codemp/server/sv_bot.cpp b/codemp/server/sv_bot.cpp index 3dfa473a63..1c53f5742e 100644 --- a/codemp/server/sv_bot.cpp +++ b/codemp/server/sv_bot.cpp @@ -232,6 +232,8 @@ void SV_BotFreeClient( int clientNum ) { if ( cl->demo.demorecording ) { SV_StopRecordDemo( cl ); + SV_ClearClientDemoPreRecord( cl ); + SV_ClearClientDemoMeta(cl); } } diff --git a/codemp/server/sv_ccmds.cpp b/codemp/server/sv_ccmds.cpp index 8695bd25a5..2bfe956dff 100755 --- a/codemp/server/sv_ccmds.cpp +++ b/codemp/server/sv_ccmds.cpp @@ -27,6 +27,14 @@ along with this program; if not, see . #include "server/sv_gameapi.h" #include "qcommon/game_version.h" +#include +#include + + +extern std::vector demoPreRecordBuffer[MAX_CLIENTS]; +extern std::map demoMetaData[MAX_CLIENTS]; + + /* =============================================================================== @@ -289,6 +297,7 @@ static void SV_MapRestart_f( void ) { } SV_StopAutoRecordDemos(); + SV_ClearAllDemoPreRecord(); // toggle the server bit so clients can detect that a // map_restart has happened @@ -1539,6 +1548,76 @@ void SV_WriteDemoMessage ( client_t *cl, msg_t *msg, int headerBytes, int messag FS_Write( msg->data + headerBytes, len, cl->demo.demofile ); } + + +constexpr char* postEOFMetadataMarker = "HIDDENMETA"; +constexpr int strlenConstExpr(const char* txt) { + int count = 0; + while (*txt != 0) { + count++; + txt++; + } + return count; +} +// Write an empty message at start of demo with metadata. +// Metadata must be in JSON format to maintain compatibility for this format, +// that way every demo writing tool/client/server can read/write different parameters +// without conflicting. +// But the JSON is not verified in this function, so just be good. +// "lastClientCommand" is client->lastClientCommand, but if you are pre-recording, +// you should get the older value +// "messageNum" is the messageNum of the metadata messsage. +// Normally the first message in a demo is the "header" or gamestate message with first message num -1 +// With metadata, this becomes sthe first message instead. So it's first demo message num - 2 +void SV_WriteEmptyMessageWithMetadata(int lastClientCommand, fileHandle_t f, const char* metaData, int messageNum) { + byte bufData[MAX_MSGLEN]; + msg_t buf; + int i; + int len; + entityState_t* ent; + entityState_t nullstate; + char* s; + + + MSG_Init(&buf, bufData, sizeof(bufData)); + MSG_Bitstream(&buf); + // NOTE, MRE: all server->client messages now acknowledge + MSG_WriteLong(&buf, lastClientCommand); + MSG_WriteByte(&buf, svc_EOF); + + // Normal demo readers will quit here. For all intents and purposes this demo message is over. But we're gonna put the metadata here now. Since it comes after svc_EOF, nobody will ever be bothered by it + // but we can read it if we want to. + constexpr int metaMarkerLength = strlenConstExpr(postEOFMetadataMarker); + // This is how the demo huffman operates. Worst case a byte can take almost 2 bytes to save, from what I understand. When reading past the end, we need to detect if we SHOULD read past the end. + // For each byte we need to read, thus, the message length must be at least 2 bytes longer still. Hence at the end we will artificially set the message length to be minimum that long. + // We will only read x amount of bytes (where x is the length of the meta marker) and see if the meta marker is present. If it is, we then proceeed to read a bigstring. + // This same thing is technically not true for the custom compressed types (as their size is always the real size of the data) but we'll just leave it like this to be universal and simple. + constexpr int maxBytePerByteSaved = 2; + constexpr int metaMarkerPresenceMinimumByteLengthExtra = metaMarkerLength * maxBytePerByteSaved; + + const int requiredCursize = buf.cursize + metaMarkerPresenceMinimumByteLengthExtra; // We'll just set it to this value at the end if it ends up smaller. + + for (int i = 0; i < metaMarkerLength; i++) { + MSG_WriteByte(&buf, postEOFMetadataMarker[i]); + } + MSG_WriteBigString(&buf, metaData); + + + MSG_WriteByte(&buf, svc_EOF); // Done. Not really needed but whatever. + + if (buf.cursize < requiredCursize) { + buf.cursize = requiredCursize; + } + + // write it to the demo file + len = LittleLong(messageNum); + FS_Write(&len, 4, f); + len = LittleLong(buf.cursize); + FS_Write(&len, 4, f); + + FS_Write(buf.data, buf.cursize, f); +} + void SV_StopRecordDemo( client_t *cl ) { int len; @@ -1558,6 +1637,29 @@ void SV_StopRecordDemo( client_t *cl ) { Com_Printf ("Stopped demo for client %d.\n", cl - svs.clients); } +void SV_ClearClientDemoMeta( client_t *cl ) { + int len; + + demoMetaData[cl - svs.clients].clear(); +} + +void SV_ClearClientDemoPreRecord( client_t *cl ) { + int len; + + demoPreRecordBuffer[cl - svs.clients].clear(); + cl->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer * 2; // Make sure that restarting recording will immediately create a keyframe. +} + +void SV_ClearAllDemoPreRecord( ) { + int len; + + if (svs.clients) { + for (client_t* client = svs.clients; client - svs.clients < sv_maxclients->integer; client++) { + SV_ClearClientDemoPreRecord( client ); + } + } +} + // stops all recording demos void SV_StopAutoRecordDemos() { if ( svs.clients && sv_autoDemo->integer ) { @@ -1661,12 +1763,12 @@ void SV_DemoFilename( char *buf, int bufSize ) { // defined in sv_client.cpp extern void SV_CreateClientGameStateMessage( client_t *client, msg_t* msg ); -extern std::vector demoPreRecordBuffer[MAX_CLIENTS]; void SV_RecordDemo( client_t *cl, char *demoName ) { char name[MAX_OSPATH]; byte bufData[MAX_MSGLEN]; msg_t msg; int len; + std::stringstream ssMeta; // JSON metadata if ( cl->demo.demorecording ) { Com_Printf( "Already recording.\n" ); @@ -1696,6 +1798,39 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { cl->demo.isBot = (cl->netchan.remoteAddress.type == NA_BOT) ? qtrue : qfalse; cl->demo.botReliableAcknowledge = cl->reliableSent; + // Save metadata message if desired + if (sv_demoWriteMeta->integer) { + int i; + ssMeta << "{"; + ssMeta << "\"wr\":\"EternalJK_Server\""; // Writer (keyword used by other tools too to identify origin of demo) + ssMeta << ",\"ost\":" << (int64_t)std::time(nullptr) << ""; // Original start time. When was demo recording started? + + // Go through manually set metadata and add it. + for (auto it = demoMetaData[cl - svs.clients].begin(); it != demoMetaData[cl - svs.clients].end(); it++) { + if (it->first != "wr" && it->first != "ost" && it->first != "prso") { // Can't overwrite default parameters (writer, original start time, pre-record start offset) + + ssMeta << ",\"" << it->first << "\":"; // JSON Key + + // Check if value is number + bool isNumber = true; + for (int i = 0; i < it->second.size(); i++) { + if (!(it->second[i] >= '0' && it->second[i] <= '9' || it->second[i] == '.')) { // Allow floating point numbers too + isNumber = false; + break; + } + } + + if (isNumber) { + ssMeta << it->second; // JSON Number value (no quotes) + } + else { + ssMeta << "\"" << it->second << "\""; // JSON String value (with quotes) + } + } + } + //ssMeta << "}"; // Don't end the json array here, we want to add extra info in the case of pre-recording + } + // Ok we have two options now. Either the classical way of starting with gamestate. // OR, if enabled, we use our pre-recorded buffer to start recording a bit in the past, // in which case we also don't have to worry about demowaiting since the pre-record @@ -1725,6 +1860,14 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { preRecordMsg.data = preRecordBufData; MSG_FromBuffered(&preRecordMsg, &it->msg); MSG_WriteByte(&preRecordMsg, svc_EOF); // We didn't do that for the ones we put into the buffer, so we do it now. + if (index == 0 && sv_demoWriteMeta->integer) { + // This goes before the first messsage + + ssMeta << ",\"prso\":" << (sv.time-it->time) << ""; // Pre-recording start offset. Offset from start of demo to when the command to start recording was called + + ssMeta << "}"; // End JSON object + SV_WriteEmptyMessageWithMetadata(it->lastClientCommand, cl->demo.demofile,ssMeta.str().c_str(),it->msgNum-1); + } SV_WriteDemoMessage(cl,&preRecordMsg,0,it->msgNum); } } @@ -1746,6 +1889,12 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { // finished writing the client packet MSG_WriteByte( &msg, svc_EOF ); + if (sv_demoWriteMeta->integer) { + // Write metadata first + ssMeta << "}"; // End JSON object + SV_WriteEmptyMessageWithMetadata(cl->lastClientCommand, cl->demo.demofile, ssMeta.str().c_str(), cl->netchan.outgoingSequence - 2); + } + // write it to the demo file len = LittleLong( cl->netchan.outgoingSequence - 1 ); FS_Write( &len, 4, cl->demo.demofile ); @@ -2021,6 +2170,106 @@ static void SV_Record_f( void ) { SV_RecordDemo( cl, demoName ); } +// Set metadata for demos of a particular client +static void SV_DemoMeta_f(void) { + int i,len; + client_t* cl; + const char* key = NULL; + const char* data = NULL; + + if (!svs.clients) { + Com_Printf("Can't set demo metadata, svs.clients is null\n"); + return; + } + + if (Cmd_Argc() != 4 && Cmd_Argc() != 3) { + Com_Printf("svdemometa , for example svdemometa 2 runstarttime 235254. Leave out to remove key.\n"); + return; + } + + int clIndex = atoi(Cmd_Argv(1)); + if (clIndex < 0 || clIndex >= sv_maxclients->integer) { + Com_Printf("Unknown client number %d.\n", clIndex); + return; + } + cl = &svs.clients[clIndex]; + + key = Cmd_Argv(2); + data = Cmd_Argc() == 4 ? Cmd_Argv(3) : ""; + + // Quick sanity check for key (must be valid json key) + // Let's be strict and only allow letters + len = strlen(key); + if (len == 0) { + Com_Printf("Metadata key must not be empty\n"); + return; + } + for (i = 0; i < len; i++) { + if (!(key[i] > 'a' && key[i] < 'z' || key[i] > 'A' && key[i] < 'Z')) { + Com_Printf("Metadata key must only contain a-z (lower or upper case)\n"); + return; + } + } + len = strlen(data); + if (len == 0) { + // Empty data provided. Remove key if it exists. + if (demoMetaData[cl - svs.clients].find(key) != demoMetaData[cl - svs.clients].end()) { + demoMetaData[cl - svs.clients].erase(key); + } + } + else { + demoMetaData[cl - svs.clients][key] = data; + } + +} +// Clear metadata for demos of a particular client +static void SV_DemoClearMeta_f(void) { + client_t* cl; + + if (!svs.clients) { + Com_Printf("Can't clear demo metadata, svs.clients is null\n"); + return; + } + + if (Cmd_Argc() != 2) { + Com_Printf("svdemoclearmeta \n"); + return; + } + + int clIndex = atoi(Cmd_Argv(1)); + if (clIndex < 0 || clIndex >= sv_maxclients->integer) { + Com_Printf("Unknown client number %d.\n", clIndex); + return; + } + cl = &svs.clients[clIndex]; + + SV_ClearClientDemoMeta(cl); +} +// Clear pre-record data for demos of a particular client +// Careful with overusage: This will force generation of new keyframes & non-delta snaps +static void SV_DemoClearPreRecord_f(void) { + client_t* cl; + + if (!svs.clients) { + Com_Printf("Can't clear demo pre-record, svs.clients is null\n"); + return; + } + + if (Cmd_Argc() != 2) { + Com_Printf("svdemoclearprerecord \n"); + return; + } + + int clIndex = atoi(Cmd_Argv(1)); + if (clIndex < 0 || clIndex >= sv_maxclients->integer) { + Com_Printf("Unknown client number %d.\n", clIndex); + return; + } + cl = &svs.clients[clIndex]; + + SV_ClearClientDemoPreRecord(cl); +} + /* ================= SV_WhitelistIP_f @@ -2097,6 +2346,9 @@ void SV_AddOperatorCommands( void ) { Cmd_AddCommand ("weapontoggle", SV_WeaponToggle_f, "Toggle g_weaponDisable bits" ); Cmd_AddCommand ("svrecord", SV_Record_f, "Record a server-side demo" ); Cmd_AddCommand ("svstoprecord", SV_StopRecord_f, "Stop recording a server-side demo" ); + Cmd_AddCommand ("svdemometa", SV_DemoMeta_f, "Sets a new metadata entry for server-side demos for one player. Call with clientnum, metakey, [data]"); + Cmd_AddCommand ("svdemoclearmeta", SV_DemoClearMeta_f, "Clears metadata for server-side demos for one player. Call with clientnum."); + Cmd_AddCommand ("svdemoclearprerecord", SV_DemoClearPreRecord_f, "Clears pre-record data for a particular client. Call with clientnum."); Cmd_AddCommand ("svrenamedemo", SV_RenameDemo_f, "Rename a server-side demo"); Cmd_AddCommand ("sv_rehashbans", SV_RehashBans_f, "Reloads banlist from file" ); Cmd_AddCommand ("sv_listbans", SV_ListBans_f, "Lists bans" ); diff --git a/codemp/server/sv_client.cpp b/codemp/server/sv_client.cpp index 10a0b6215f..6a2bc94540 100644 --- a/codemp/server/sv_client.cpp +++ b/codemp/server/sv_client.cpp @@ -128,7 +128,6 @@ SV_DirectConnect A "connect" OOB command has been received ================== */ -extern std::vector demoPreRecordBuffer[MAX_CLIENTS]; void SV_DirectConnect( netadr_t from ) { char userinfo[MAX_INFO_STRING]; int i; @@ -332,8 +331,8 @@ void SV_DirectConnect( netadr_t from ) { SV_UserinfoChanged( newcl ); // When a new client connects, we reset the pre-record buffer for this client. - demoPreRecordBuffer[clientNum].clear(); - newcl->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer * 2; // Make sure that turning pre-recording on again will immediately cause creation of a keyframe + SV_ClearClientDemoPreRecord(newcl); + SV_ClearClientDemoMeta(newcl); // send the connect packet to the client NET_OutOfBandPrint( NS_SERVER, from, "connectResponse" ); @@ -414,6 +413,8 @@ void SV_DropClient( client_t *drop, const char *reason ) { if ( drop->demo.demorecording ) { SV_StopRecordDemo( drop ); + SV_ClearClientDemoPreRecord( drop ); + SV_ClearClientDemoMeta( drop ); } // if this was the last client on the server, send a heartbeat diff --git a/codemp/server/sv_init.cpp b/codemp/server/sv_init.cpp index 35bc390a85..aa2de87d2b 100644 --- a/codemp/server/sv_init.cpp +++ b/codemp/server/sv_init.cpp @@ -456,6 +456,7 @@ void SV_SpawnServer( char *server, qboolean killBots, ForceReload_e eForceReload const char *p; SV_StopAutoRecordDemos(); + SV_ClearAllDemoPreRecord(); SV_SendMapChange(); @@ -1023,6 +1024,7 @@ void SV_Init (void) { sv_demoPreRecord = Cvar_Get("sv_demoPreRecord", "15000", CVAR_ARCHIVE, "If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration?"); sv_demoPreRecordKeyframeDistance = Cvar_Get("sv_demoPreRecordKeyframeDistance", "5000", CVAR_ARCHIVE, "A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients."); + sv_demoWriteMeta = Cvar_Get("sv_demoWriteMeta", "1", CVAR_ARCHIVE, "Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on."); sv_snapShotDuelCull = Cvar_Get("sv_snapShotDuelCull", "1", CVAR_NONE, "Snapshot-based duel isolation"); diff --git a/codemp/server/sv_main.cpp b/codemp/server/sv_main.cpp index 96374e9710..366eff8822 100644 --- a/codemp/server/sv_main.cpp +++ b/codemp/server/sv_main.cpp @@ -75,6 +75,7 @@ cvar_t *sv_autoWhitelist; cvar_t *sv_demoPreRecord; // If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration? cvar_t *sv_demoPreRecordKeyframeDistance; // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept. +cvar_t *sv_demoWriteMeta; // Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on. cvar_t *sv_snapShotDuelCull; diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index b4d16a8c60..065dfbf8f6 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -26,6 +26,7 @@ along with this program; if not, see . std::vector demoPreRecordBuffer[MAX_CLIENTS]; +std::map demoMetaData[MAX_CLIENTS]; /* ============================================================================= @@ -788,6 +789,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { Com_Memset(&bmt, 0, sizeof(bufferedMessageContainer_t)); MSG_ToBuffered(msg,&bmt.msg); bmt.msgNum = client->netchan.outgoingSequence; + bmt.lastClientCommand = client->lastClientCommand; bmt.time = sv.time; bmt.isKeyframe = qfalse; // In theory it might be a gamestate message, but we only call it a keyframe if we ourselves explicitly save a keyframe. demoPreRecordBuffer[client - svs.clients].push_back(bmt); @@ -818,6 +820,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { MSG_ToBuffered(&keyframeMsg, &bmt.msg); bmt.msgNum = client->netchan.outgoingSequence; // Yes the keyframe duplicates the messagenum of a message. This is (part of) why we dump only one keyframe at the start of the demo and discard future keyframes + bmt.lastClientCommand = client->lastClientCommand; bmt.time = sv.time; bmt.isKeyframe = qtrue; // This is a keyframe (gamestate that will be followed by non-delta frames) demoPreRecordBuffer[client - svs.clients].push_back(bmt); diff --git a/japro_docs.md b/japro_docs.md index b3aea96e80..521ccce176 100644 --- a/japro_docs.md +++ b/japro_docs.md @@ -131,6 +131,7 @@ sv_autoRaceDemo 0 //Requires custom server executable with "svrecord" command. sv_demoPreRecord 15000 // If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration? sv_demoPreRecordKeyframeDistance 5000 // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients. + sv_demoWriteMeta 1 // Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on. #### Bots bot_nochat 0 @@ -387,6 +388,13 @@ amlogout amlookup +#### Serverside demo recording + svrecord // Record a server-side demo (including pre-record time if sv_demoPreRecord is not 0 and metadata -default and custom set via svdemometa- if sv_demoWriteMeta is 1). + svstoprecord // Stop recording a server-side demo. + svdemometa // Sets a new metadata entry for server-side demos for one player. Call with clientnum, metakey, "[data]". If data is not provided, the key is cleared. metakey must be letters only, no numbers + svdemoclearmeta // Clears metadata for server-side demos for one player. Call with clientnum. + svdemoclearprerecord // Clears pre-record data for a particular client and forces new keyframe generation. Basically discards previously pre-recorded packets. Call with clientnum. + svrenamedemo // Rename a server-side demo ## ClientCvars ## From 6bb9708f20b236628659bfdcd333f0a2213164e7 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 14 Jun 2023 20:04:01 +0200 Subject: [PATCH 07/12] Small fix/improvement to "original start time" demo metadata. --- codemp/server/sv_ccmds.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codemp/server/sv_ccmds.cpp b/codemp/server/sv_ccmds.cpp index 2bfe956dff..53f24bd95b 100755 --- a/codemp/server/sv_ccmds.cpp +++ b/codemp/server/sv_ccmds.cpp @@ -1803,7 +1803,6 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { int i; ssMeta << "{"; ssMeta << "\"wr\":\"EternalJK_Server\""; // Writer (keyword used by other tools too to identify origin of demo) - ssMeta << ",\"ost\":" << (int64_t)std::time(nullptr) << ""; // Original start time. When was demo recording started? // Go through manually set metadata and add it. for (auto it = demoMetaData[cl - svs.clients].begin(); it != demoMetaData[cl - svs.clients].end(); it++) { @@ -1863,7 +1862,8 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { if (index == 0 && sv_demoWriteMeta->integer) { // This goes before the first messsage - ssMeta << ",\"prso\":" << (sv.time-it->time) << ""; // Pre-recording start offset. Offset from start of demo to when the command to start recording was called + ssMeta << ",\"ost\":" << ((int64_t)std::time(nullptr) - ((sv.time - it->time)/1000)); // Original start time. When was demo recording started? + ssMeta << ",\"prso\":" << (sv.time-it->time); // Pre-recording start offset. Offset from start of demo to when the command to start recording was called ssMeta << "}"; // End JSON object SV_WriteEmptyMessageWithMetadata(it->lastClientCommand, cl->demo.demofile,ssMeta.str().c_str(),it->msgNum-1); @@ -1891,6 +1891,7 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { if (sv_demoWriteMeta->integer) { // Write metadata first + ssMeta << ",\"ost\":" << (int64_t)std::time(nullptr); // Original start time. When was demo recording started? ssMeta << "}"; // End JSON object SV_WriteEmptyMessageWithMetadata(cl->lastClientCommand, cl->demo.demofile, ssMeta.str().c_str(), cl->netchan.outgoingSequence - 2); } From 311ab67a5df64a24b0f3aa582127a8062af3439d Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 14 Jun 2023 21:56:52 +0200 Subject: [PATCH 08/12] - Add sv_demoPreRecord->integer condition in WriteSnapshotToClient to prevent unexpected behavior when pre-recording is disabled. - Memset entire preRecord struct to 0 when clearing pre-record of a client. - Guard all serverside demo code with #ifdef DEDICATED, don't need it in client - Replace strlenConstExpr with sizeof() - Removed unused variables. - Set minDeltaFrame back to 0 in a few places, just in case. --- codemp/server/server.h | 12 ++++++-- codemp/server/sv_bot.cpp | 2 ++ codemp/server/sv_ccmds.cpp | 34 ++++++++++---------- codemp/server/sv_client.cpp | 6 ++++ codemp/server/sv_init.cpp | 17 +++++++--- codemp/server/sv_main.cpp | 4 +++ codemp/server/sv_snapshot.cpp | 58 ++++++++++++++++++++++++++++------- 7 files changed, 100 insertions(+), 33 deletions(-) diff --git a/codemp/server/server.h b/codemp/server/server.h index afac337ba3..7ba629a1b8 100644 --- a/codemp/server/server.h +++ b/codemp/server/server.h @@ -123,6 +123,7 @@ typedef enum { } clientState_t; +#ifdef DEDICATED // struct to hold demo data for a single demo typedef struct { char demoName[MAX_OSPATH]; @@ -140,10 +141,11 @@ typedef struct { int lastKeyframeTime; // When was the last keyframe (gamestate followed by non-delta frames) saved? If more than sv_demoPreRecordKeyframeDistance, we make a new keyframe. } preRecord; } demoInfo_t; +#endif - - +#ifdef DEDICATED typedef std::vector::iterator demoPreRecordBufferIt; +#endif typedef struct client_s { clientState_t state; @@ -203,7 +205,9 @@ typedef struct client_s { int oldServerTime; qboolean csUpdated[MAX_CONFIGSTRINGS]; +#ifdef DEDICATED demoInfo_t demo; +#endif #ifdef DEDICATED qboolean disableDuelCull; //set for clients with "Duel see others" option set in cp_pluginDisable on JA+ servers @@ -303,9 +307,11 @@ extern cvar_t *sv_newfloodProtect; extern cvar_t *sv_lanForceRate; extern cvar_t *sv_needpass; extern cvar_t *sv_filterCommands; +#ifdef DEDICATED extern cvar_t *sv_autoDemo; extern cvar_t *sv_autoDemoBots; extern cvar_t *sv_autoDemoMaxMaps; +#endif extern cvar_t *sv_legacyFixes; extern cvar_t *sv_banFile; extern cvar_t *sv_maxOOBRate; @@ -421,6 +427,7 @@ void SV_WriteDownloadToClient( client_t *cl , msg_t *msg ); // sv_ccmds.c // void SV_Heartbeat_f( void ); +#ifdef DEDICATED void SV_RecordDemo( client_t *cl, char *demoName ); void SV_StopRecordDemo( client_t *cl ); void SV_ClearClientDemoMeta( client_t *cl ); @@ -429,6 +436,7 @@ void SV_ClearAllDemoPreRecord( ); void SV_AutoRecordDemo( client_t *cl ); void SV_StopAutoRecordDemos(); void SV_BeginAutoRecordDemos(); +#endif // // sv_snapshot.c diff --git a/codemp/server/sv_bot.cpp b/codemp/server/sv_bot.cpp index 1c53f5742e..4df182fe5d 100644 --- a/codemp/server/sv_bot.cpp +++ b/codemp/server/sv_bot.cpp @@ -230,11 +230,13 @@ void SV_BotFreeClient( int clientNum ) { cl->gentity->r.svFlags &= ~SVF_BOT; } +#ifdef DEDICATED if ( cl->demo.demorecording ) { SV_StopRecordDemo( cl ); SV_ClearClientDemoPreRecord( cl ); SV_ClearClientDemoMeta(cl); } +#endif } /* diff --git a/codemp/server/sv_ccmds.cpp b/codemp/server/sv_ccmds.cpp index 53f24bd95b..113f5202a6 100755 --- a/codemp/server/sv_ccmds.cpp +++ b/codemp/server/sv_ccmds.cpp @@ -30,10 +30,10 @@ along with this program; if not, see . #include #include - +#ifdef DEDICATED extern std::vector demoPreRecordBuffer[MAX_CLIENTS]; extern std::map demoMetaData[MAX_CLIENTS]; - +#endif /* =============================================================================== @@ -296,8 +296,10 @@ static void SV_MapRestart_f( void ) { return; } +#ifdef DEDICATED SV_StopAutoRecordDemos(); SV_ClearAllDemoPreRecord(); +#endif // toggle the server bit so clients can detect that a // map_restart has happened @@ -382,7 +384,9 @@ static void SV_MapRestart_f( void ) { sv.time += 100; svs.time += 100; +#ifdef DEDICATED SV_BeginAutoRecordDemos(); +#endif } //=============================================================== @@ -1518,6 +1522,7 @@ static void SV_KillServer_f( void ) { SV_Shutdown( "killserver" ); } +#ifdef DEDICATED void SV_WriteDemoMessage ( client_t *cl, msg_t *msg, int headerBytes ) { int len, swlen; @@ -1550,15 +1555,8 @@ void SV_WriteDemoMessage ( client_t *cl, msg_t *msg, int headerBytes, int messag -constexpr char* postEOFMetadataMarker = "HIDDENMETA"; -constexpr int strlenConstExpr(const char* txt) { - int count = 0; - while (*txt != 0) { - count++; - txt++; - } - return count; -} +constexpr char postEOFMetadataMarker[] = "HIDDENMETA"; + // Write an empty message at start of demo with metadata. // Metadata must be in JSON format to maintain compatibility for this format, // that way every demo writing tool/client/server can read/write different parameters @@ -1587,7 +1585,7 @@ void SV_WriteEmptyMessageWithMetadata(int lastClientCommand, fileHandle_t f, con // Normal demo readers will quit here. For all intents and purposes this demo message is over. But we're gonna put the metadata here now. Since it comes after svc_EOF, nobody will ever be bothered by it // but we can read it if we want to. - constexpr int metaMarkerLength = strlenConstExpr(postEOFMetadataMarker); + constexpr int metaMarkerLength = sizeof(postEOFMetadataMarker)-1; // This is how the demo huffman operates. Worst case a byte can take almost 2 bytes to save, from what I understand. When reading past the end, we need to detect if we SHOULD read past the end. // For each byte we need to read, thus, the message length must be at least 2 bytes longer still. Hence at the end we will artificially set the message length to be minimum that long. // We will only read x amount of bytes (where x is the length of the meta marker) and see if the meta marker is present. If it is, we then proceeed to read a bigstring. @@ -1638,20 +1636,18 @@ void SV_StopRecordDemo( client_t *cl ) { } void SV_ClearClientDemoMeta( client_t *cl ) { - int len; demoMetaData[cl - svs.clients].clear(); } void SV_ClearClientDemoPreRecord( client_t *cl ) { - int len; demoPreRecordBuffer[cl - svs.clients].clear(); + Com_Memset(&cl->demo.preRecord,0,sizeof(cl->demo.preRecord)); cl->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer * 2; // Make sure that restarting recording will immediately create a keyframe. } void SV_ClearAllDemoPreRecord( ) { - int len; if (svs.clients) { for (client_t* client = svs.clients; client - svs.clients < sv_maxclients->integer; client++) { @@ -1976,6 +1972,7 @@ static int QDECL SV_DemoFolderTimeComparator( const void *arg1, const void *arg2 return rightTime - leftTime; } + // returns number of folders found. pass NULL result pointer for just a count. static int SV_FindLeafFolders( const char *baseFolder, char *result, int maxResults, int maxFolderLength ) { char *fileList = (char *)Z_Malloc( MAX_OSPATH * maxResults, TAG_FILESYS ); // too big for stack since this is recursive @@ -2271,6 +2268,9 @@ static void SV_DemoClearPreRecord_f(void) { SV_ClearClientDemoPreRecord(cl); } + +#endif + /* ================= SV_WhitelistIP_f @@ -2345,15 +2345,17 @@ void SV_AddOperatorCommands( void ) { Cmd_AddCommand ("svtell", SV_ConTell_f, "Private message from the server to a user" ); Cmd_AddCommand ("forcetoggle", SV_ForceToggle_f, "Toggle g_forcePowerDisable bits" ); Cmd_AddCommand ("weapontoggle", SV_WeaponToggle_f, "Toggle g_weaponDisable bits" ); +#ifdef DEDICATED Cmd_AddCommand ("svrecord", SV_Record_f, "Record a server-side demo" ); Cmd_AddCommand ("svstoprecord", SV_StopRecord_f, "Stop recording a server-side demo" ); Cmd_AddCommand ("svdemometa", SV_DemoMeta_f, "Sets a new metadata entry for server-side demos for one player. Call with clientnum, metakey, [data]"); Cmd_AddCommand ("svdemoclearmeta", SV_DemoClearMeta_f, "Clears metadata for server-side demos for one player. Call with clientnum."); Cmd_AddCommand ("svdemoclearprerecord", SV_DemoClearPreRecord_f, "Clears pre-record data for a particular client. Call with clientnum."); Cmd_AddCommand ("svrenamedemo", SV_RenameDemo_f, "Rename a server-side demo"); + Cmd_AddCommand("sv_listrecording", SV_ListRecording_f, "Lists demos being recorded"); +#endif Cmd_AddCommand ("sv_rehashbans", SV_RehashBans_f, "Reloads banlist from file" ); Cmd_AddCommand ("sv_listbans", SV_ListBans_f, "Lists bans" ); - Cmd_AddCommand( "sv_listrecording", SV_ListRecording_f, "Lists demos being recorded" ); Cmd_AddCommand ("sv_banaddr", SV_BanAddr_f, "Bans a user" ); Cmd_AddCommand ("sv_exceptaddr", SV_ExceptAddr_f, "Adds a ban exception for a user" ); Cmd_AddCommand ("sv_bandel", SV_BanDel_f, "Removes a ban" ); diff --git a/codemp/server/sv_client.cpp b/codemp/server/sv_client.cpp index 6a2bc94540..2ababde4fd 100644 --- a/codemp/server/sv_client.cpp +++ b/codemp/server/sv_client.cpp @@ -330,9 +330,11 @@ void SV_DirectConnect( netadr_t from ) { SV_UserinfoChanged( newcl ); +#ifdef DEDICATED // When a new client connects, we reset the pre-record buffer for this client. SV_ClearClientDemoPreRecord(newcl); SV_ClearClientDemoMeta(newcl); +#endif // send the connect packet to the client NET_OutOfBandPrint( NS_SERVER, from, "connectResponse" ); @@ -411,11 +413,13 @@ void SV_DropClient( client_t *drop, const char *reason ) { drop->state = CS_ZOMBIE; // become free in a few seconds } +#ifdef DEDICATED if ( drop->demo.demorecording ) { SV_StopRecordDemo( drop ); SV_ClearClientDemoPreRecord( drop ); SV_ClearClientDemoMeta( drop ); } +#endif // if this was the last client on the server, send a heartbeat // to the master so it is known the server is empty @@ -605,9 +609,11 @@ void SV_ClientEnterWorld( client_t *client, usercmd_t *cmd ) { // call the game begin function GVM_ClientBegin( client - svs.clients ); +#ifdef DEDICATED if (sv_autoDemo->integer == 1) { //Bots dont trigger this so whatever SV_BeginAutoRecordDemos(); } +#endif } /* diff --git a/codemp/server/sv_init.cpp b/codemp/server/sv_init.cpp index aa2de87d2b..60e38e8086 100644 --- a/codemp/server/sv_init.cpp +++ b/codemp/server/sv_init.cpp @@ -428,8 +428,11 @@ void SV_SendMapChange(void) { if (svs.clients[i].state >= CS_CONNECTED) { - if ( svs.clients[i].netchan.remoteAddress.type != NA_BOT || - svs.clients[i].demo.demorecording ) + if ( svs.clients[i].netchan.remoteAddress.type != NA_BOT +#ifdef DEDICATED + || svs.clients[i].demo.demorecording +#endif + ) { SV_SendClientMapChange( &svs.clients[i] ) ; } @@ -454,9 +457,10 @@ void SV_SpawnServer( char *server, qboolean killBots, ForceReload_e eForceReload qboolean isBot; char systemInfo[16384]; const char *p; - +#ifdef DEDICATED SV_StopAutoRecordDemos(); SV_ClearAllDemoPreRecord(); +#endif SV_SendMapChange(); @@ -749,6 +753,7 @@ Ghoul2 Insert End } */ +#ifdef DEDICATED for ( client_t *client = svs.clients; client - svs.clients < sv_maxclients->integer; client++) { // bots will not request gamestate, so it must be manually sent // cannot do this above where it says it will because mapname is not set at that time @@ -758,6 +763,7 @@ Ghoul2 Insert End } SV_BeginAutoRecordDemos(); +#endif } @@ -1005,10 +1011,11 @@ void SV_Init (void) { sv_filterCommands = Cvar_Get( "sv_filterCommands", "2", CVAR_ARCHIVE ); // sv_debugserver = Cvar_Get ("sv_debugserver", "0", 0); - +#ifdef DEDICATED sv_autoDemo = Cvar_Get( "sv_autoDemo", "0", CVAR_ARCHIVE_ND | CVAR_SERVERINFO, "Automatically take server-side demos" ); sv_autoDemoBots = Cvar_Get( "sv_autoDemoBots", "0", CVAR_ARCHIVE_ND, "Record server-side demos for bots" ); sv_autoDemoMaxMaps = Cvar_Get( "sv_autoDemoMaxMaps", "0", CVAR_ARCHIVE_ND ); +#endif #ifndef DEDICATED //Default this to off on client to avoid potential mod compatibility issues. sv_legacyFixes = Cvar_Get( "sv_legacyFixes", "0", CVAR_ARCHIVE ); @@ -1022,9 +1029,11 @@ void SV_Init (void) { sv_maxOOBRateIP = Cvar_Get("sv_maxOOBRateIP", "1", CVAR_ARCHIVE, "Maximum rate of handling incoming server commands per IP address" ); sv_autoWhitelist = Cvar_Get("sv_autoWhitelist", "1", CVAR_ARCHIVE, "Save player IPs to allow them using server during DOS attack" ); +#ifdef DEDICATED sv_demoPreRecord = Cvar_Get("sv_demoPreRecord", "15000", CVAR_ARCHIVE, "If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration?"); sv_demoPreRecordKeyframeDistance = Cvar_Get("sv_demoPreRecordKeyframeDistance", "5000", CVAR_ARCHIVE, "A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients."); sv_demoWriteMeta = Cvar_Get("sv_demoWriteMeta", "1", CVAR_ARCHIVE, "Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on."); +#endif sv_snapShotDuelCull = Cvar_Get("sv_snapShotDuelCull", "1", CVAR_NONE, "Snapshot-based duel isolation"); diff --git a/codemp/server/sv_main.cpp b/codemp/server/sv_main.cpp index 366eff8822..43e2c0c7d8 100644 --- a/codemp/server/sv_main.cpp +++ b/codemp/server/sv_main.cpp @@ -64,9 +64,11 @@ cvar_t *sv_newfloodProtect; cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491) cvar_t *sv_needpass; cvar_t *sv_filterCommands; // strict filtering on commands (1: strip ['\r', '\n'], 2: also strip ';') +#ifdef DEDICATED cvar_t *sv_autoDemo; cvar_t *sv_autoDemoBots; cvar_t *sv_autoDemoMaxMaps; +#endif cvar_t *sv_legacyFixes; cvar_t *sv_banFile; cvar_t *sv_maxOOBRate; @@ -551,7 +553,9 @@ void SVC_Info( netadr_t from ) { Info_SetValueForKey( infostring, "wdisable", va("%i", wDisable ) ); Info_SetValueForKey( infostring, "fdisable", va("%i", Cvar_VariableIntegerValue( "g_forcePowerDisable" ) ) ); //Info_SetValueForKey( infostring, "pure", va("%i", sv_pure->integer ) ); +#ifdef DEDICATED Info_SetValueForKey( infostring, "autodemo", va("%i", sv_autoDemo->integer ) ); +#endif if( sv_minPing->integer ) { Info_SetValueForKey( infostring, "minPing", va("%i", sv_minPing->integer) ); diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index 065dfbf8f6..a0b557f422 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -24,9 +24,10 @@ along with this program; if not, see . #include "server.h" #include "qcommon/cm_public.h" - +#ifdef DEDICATED std::vector demoPreRecordBuffer[MAX_CLIENTS]; std::map demoMetaData[MAX_CLIENTS]; +#endif /* ============================================================================= @@ -137,9 +138,11 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { // bots never acknowledge, but it doesn't matter since the only use case is for serverside demos // in which case we can delta against the very last message every time deltaMessage = client->deltaMessage; +#ifdef DEDICATED if ( client->demo.isBot ) { client->deltaMessage = client->netchan.outgoingSequence; } +#endif // try to use a previous frame as the source for delta compressing the snapshot if ( deltaMessage <= 0 || client->state != CS_ACTIVE ) { @@ -152,7 +155,9 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { Com_DPrintf ("%s: Delta request from out of date packet.\n", client->name); oldframe = NULL; lastframe = 0; - } else if ( client->demo.demorecording && client->demo.demowaiting ) { + } +#ifdef DEDICATED + else if ( client->demo.demorecording && client->demo.demowaiting ) { // demo is waiting for a non-delta-compressed frame for this client, so don't delta compress oldframe = NULL; lastframe = 0; @@ -166,13 +171,15 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { // demo is waiting for a non-delta-compressed frame for this client, so don't delta compress oldframe = NULL; lastframe = 0; - } else if ( client->demo.preRecord.minDeltaFrame > deltaMessage ) { + } else if (sv_demoPreRecord->integer && client->demo.preRecord.minDeltaFrame > deltaMessage ) { // we saved a non-delta frame to the pre-record buffer and sent it to the client, but the client didn't ack it // we can't delta against an old frame that's not in the demo without breaking the demo. so send // non-delta frames until the client acks. oldframe = NULL; lastframe = 0; - } else { + } +#endif + else { // we have a valid snapshot to delta from oldframe = &client->frames[ deltaMessage & PACKET_MASK ]; lastframe = client->netchan.outgoingSequence - deltaMessage; @@ -185,6 +192,7 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { } } +#ifdef DEDICATED if ( oldframe == NULL ) { if ( client->demo.demowaiting ) { // this is a non-delta frame, so we can delta against it in the demo @@ -197,6 +205,19 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { } client->demo.preRecord.keyframeWaiting = qfalse; } + else { + if (!client->demo.preRecord.keyframeWaiting) { + // We got the frame we needed acked, so reset this to 0 + // to avoid any potential shenanigans after map changes or so + client->demo.preRecord.minDeltaFrame = 0; + } + if (!client->demo.demowaiting) { + // We got the frame we needed acked, so reset this to 0 + // to avoid any potential shenanigans after map changes or so + client->demo.minDeltaFrame = 0; + } + } +#endif MSG_WriteByte (msg, svc_snapshot); @@ -206,8 +227,11 @@ static void SV_WriteSnapshotToClient( client_t *client, msg_t *msg ) { // send over the current server time so the client can drift // its view of time to try to match - if( client->oldServerTime && - !( client->demo.demorecording && client->demo.isBot ) ) { + if( client->oldServerTime +#ifdef DEDICATED + && !( client->demo.demorecording && client->demo.isBot ) +#endif + ) { // The server has not yet got an acknowledgement of the // new gamestate from this client, so continue to send it // a time as if the server has not restarted. Note from @@ -302,9 +326,12 @@ void SV_UpdateServerCommandsToClient( client_t *client, msg_t *msg ) { int i; int reliableAcknowledge; +#ifdef DEDICATED if ( client->demo.isBot && client->demo.demorecording ) { reliableAcknowledge = client->demo.botReliableAcknowledge; - } else { + } else +#endif + { reliableAcknowledge = client->reliableAcknowledge; } @@ -504,6 +531,7 @@ static void SV_AddEntitiesVisibleFromPoint( vec3_t origin, clientSnapshot_t *fra continue; } +#ifdef DEDICATED if (sv_autoDemo->integer == 2) //How find out how to only add all entities for the bot named RECORDER, not all bots? what entities can we still exclude? { sharedEntity_t *ent2; @@ -513,6 +541,7 @@ static void SV_AddEntitiesVisibleFromPoint( vec3_t origin, clientSnapshot_t *fra continue; } } +#endif // ignore if not touching a PV leaf // check area @@ -784,6 +813,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageSent = (sv_pingFix->integer ? Sys_Milliseconds() : svs.time); client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageAcked = -1; +#ifdef DEDICATED if (sv_demoPreRecord->integer) { // If pre record demo message buffering is enabled, we write this message to the buffer. static bufferedMessageContainer_t bmt; // I make these static so they don't sit on the stack. Com_Memset(&bmt, 0, sizeof(bufferedMessageContainer_t)); @@ -824,6 +854,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { bmt.time = sv.time; bmt.isKeyframe = qtrue; // This is a keyframe (gamestate that will be followed by non-delta frames) demoPreRecordBuffer[client - svs.clients].push_back(bmt); + client->demo.preRecord.minDeltaFrame = 0; client->demo.preRecord.keyframeWaiting = qtrue; client->demo.preRecord.lastKeyframeTime = sv.time; } @@ -847,11 +878,9 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { } } else { // Pre-recording disabled. Clear buffer to prevent unexpected behavior if it is turned back on. - demoPreRecordBuffer[client - svs.clients].clear(); - client->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer*2; // Make sure that turning pre-recording on again will immediately cause creation of a keyframe + SV_ClearClientDemoPreRecord(client); } - // bots need to have their snapshots built, but // they query them directly without needing to be sent if ( client->demo.isBot ) { @@ -859,6 +888,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { client->demo.botReliableAcknowledge = client->reliableSent; return; } +#endif // send the datagram SV_Netchan_Transmit( client, msg ); //msg->cursize, msg->data ); @@ -956,6 +986,7 @@ void SV_SendClientSnapshot( client_t *client ) { // build the snapshot SV_BuildClientSnapshot( client ); +#ifdef DEDICATED if ( !client->demo.demorecording ) { //dont think this needs to be done with singledemo option if (sv_autoDemo->integer == 2) { if (client->netchan.remoteAddress.type == NA_BOT && !Q_stricmp(client->name, "RECORDER")) { @@ -968,10 +999,15 @@ void SV_SendClientSnapshot( client_t *client ) { } } } +#endif // bots need to have their snapshots built, but // they query them directly without needing to be sent - if ( client->netchan.remoteAddress.type == NA_BOT && !client->demo.demorecording ) { + if ( client->netchan.remoteAddress.type == NA_BOT +#ifdef DEDICATED + && !client->demo.demorecording +#endif + ) { return; } From 4371f956a49edbb99e48b4f24d90698006754608 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 14 Jun 2023 22:49:27 +0200 Subject: [PATCH 09/12] - sv_demoPreRecord is now a simple 1 or 0 switch. - Server pre record time is now controlled via sv_demoPreRecordTime. - sv_demoPreRecordTime and sv_demoPreRecordKeyframeDistance are both in seconds instead of milliseconds now. --- codemp/server/server.h | 1 + codemp/server/sv_ccmds.cpp | 2 +- codemp/server/sv_init.cpp | 5 +++-- codemp/server/sv_main.cpp | 5 +++-- codemp/server/sv_snapshot.cpp | 8 ++++---- japro_docs.md | 5 +++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/codemp/server/server.h b/codemp/server/server.h index 7ba629a1b8..654e319bb3 100644 --- a/codemp/server/server.h +++ b/codemp/server/server.h @@ -319,6 +319,7 @@ extern cvar_t *sv_maxOOBRateIP; extern cvar_t *sv_autoWhitelist; extern cvar_t *sv_demoPreRecord; +extern cvar_t *sv_demoPreRecordTime; extern cvar_t *sv_demoPreRecordKeyframeDistance; extern cvar_t *sv_demoWriteMeta; diff --git a/codemp/server/sv_ccmds.cpp b/codemp/server/sv_ccmds.cpp index 113f5202a6..397660bda9 100755 --- a/codemp/server/sv_ccmds.cpp +++ b/codemp/server/sv_ccmds.cpp @@ -1644,7 +1644,7 @@ void SV_ClearClientDemoPreRecord( client_t *cl ) { demoPreRecordBuffer[cl - svs.clients].clear(); Com_Memset(&cl->demo.preRecord,0,sizeof(cl->demo.preRecord)); - cl->demo.preRecord.lastKeyframeTime = -sv_demoPreRecordKeyframeDistance->integer * 2; // Make sure that restarting recording will immediately create a keyframe. + cl->demo.preRecord.lastKeyframeTime = -(1000*sv_demoPreRecordKeyframeDistance->integer) * 2; // Make sure that restarting recording will immediately create a keyframe. } void SV_ClearAllDemoPreRecord( ) { diff --git a/codemp/server/sv_init.cpp b/codemp/server/sv_init.cpp index 60e38e8086..14981762b2 100644 --- a/codemp/server/sv_init.cpp +++ b/codemp/server/sv_init.cpp @@ -1030,8 +1030,9 @@ void SV_Init (void) { sv_autoWhitelist = Cvar_Get("sv_autoWhitelist", "1", CVAR_ARCHIVE, "Save player IPs to allow them using server during DOS attack" ); #ifdef DEDICATED - sv_demoPreRecord = Cvar_Get("sv_demoPreRecord", "15000", CVAR_ARCHIVE, "If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration?"); - sv_demoPreRecordKeyframeDistance = Cvar_Get("sv_demoPreRecordKeyframeDistance", "5000", CVAR_ARCHIVE, "A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients."); + sv_demoPreRecord = Cvar_Get("sv_demoPreRecord", "0", CVAR_ARCHIVE, "Activate server demo pre-recording so demos can be retroactively recorded for duration sv_demoPreRecordTime (seconds)"); + sv_demoPreRecordTime = Cvar_Get("sv_demoPreRecordTime", "15", CVAR_ARCHIVE, "How many seconds of past packets should be stored for server demo pre-recording?"); + sv_demoPreRecordKeyframeDistance = Cvar_Get("sv_demoPreRecordKeyframeDistance", "5", CVAR_ARCHIVE, "A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients."); sv_demoWriteMeta = Cvar_Get("sv_demoWriteMeta", "1", CVAR_ARCHIVE, "Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on."); #endif diff --git a/codemp/server/sv_main.cpp b/codemp/server/sv_main.cpp index 43e2c0c7d8..16044c2e2a 100644 --- a/codemp/server/sv_main.cpp +++ b/codemp/server/sv_main.cpp @@ -75,8 +75,9 @@ cvar_t *sv_maxOOBRate; cvar_t *sv_maxOOBRateIP; cvar_t *sv_autoWhitelist; -cvar_t *sv_demoPreRecord; // If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration? -cvar_t *sv_demoPreRecordKeyframeDistance; // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept. +cvar_t *sv_demoPreRecord; // Activate demo pre-recording +cvar_t *sv_demoPreRecordTime; // How many seconds of past packets should be stored so demos can be retroactively recorded for that duration? +cvar_t *sv_demoPreRecordKeyframeDistance; // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message (in seconds)? The shorter the distance, the more precisely the pre-record duration will be kept. cvar_t *sv_demoWriteMeta; // Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on. cvar_t *sv_snapShotDuelCull; diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index a0b557f422..cfc1402c9b 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -834,7 +834,7 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { // Check for whether a new keyframe must be written in pre recording, and if so, do it. if (sv_demoPreRecord->integer) { - if (client->demo.preRecord.lastKeyframeTime + sv_demoPreRecordKeyframeDistance->integer < sv.time) { + if (client->demo.preRecord.lastKeyframeTime + (1000*sv_demoPreRecordKeyframeDistance->integer) < sv.time) { // Save a keyframe. static byte keyframeBufData[MAX_MSGLEN]; // I make these static so they don't sit on the stack. static msg_t keyframeMsg; @@ -861,12 +861,12 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { // Clean up pre-record buffer // - // The goal is to always maintain *at least* sv_demoPreRecord milliseconds of buffer. Rather more than less. - // So we find the last keyframe that is older than sv_demoPreRecord milliseconds (or just that old) and then delete everything *before* it. + // The goal is to always maintain *at least* sv_demoPreRecordTime seconds of buffer. Rather more than less. + // So we find the last keyframe that is older than sv_demoPreRecordTime seconds (or just that old) and then delete everything *before* it. demoPreRecordBufferIt lastTooOldKeyframe; qboolean lastTooOldKeyframeFound = qfalse; for (demoPreRecordBufferIt it = demoPreRecordBuffer[client - svs.clients].begin(); it != demoPreRecordBuffer[client - svs.clients].end(); it++) { - if (it->isKeyframe && (it->time + sv_demoPreRecord->integer) < sv.time) { + if (it->isKeyframe && (it->time + (1000*sv_demoPreRecordTime->integer)) < sv.time) { lastTooOldKeyframe = it; lastTooOldKeyframeFound = qtrue; } diff --git a/japro_docs.md b/japro_docs.md index 521ccce176..518e163944 100644 --- a/japro_docs.md +++ b/japro_docs.md @@ -129,8 +129,9 @@ g_raceLog 0//Log to races.log (incase database gets messed up). g_playerLog 0//Used by /amlookup sv_autoRaceDemo 0 //Requires custom server executable with "svrecord" command. - sv_demoPreRecord 15000 // If not 0, how many milliseconds of past packets should be stored so demos can be retroactively recorded for that duration? - sv_demoPreRecordKeyframeDistance 5000 // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients. + sv_demoPreRecord 0 // Activate server demo pre-recording so demos can be retroactively recorded for duration sv_demoPreRecordTime (seconds) + sv_demoPreRecordTime 15 // How many seconds of past packets should be stored for pre-recording + sv_demoPreRecordKeyframeDistance 5 // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message (in seconds)? The shorter the distance, the more precisely the pre-record duration will be kept, but also the higher the RAM usage and regularity of non-delta frames being sent to the clients. sv_demoWriteMeta 1 // Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on. #### Bots From 10e7dff6645f03af2a2635626e287cbd9d65edac Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 15 Jun 2023 00:50:02 +0200 Subject: [PATCH 10/12] Forgot to put two ifdefs. Nothing critical, just cleaning up. --- codemp/server/server.h | 3 ++- codemp/server/sv_main.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/codemp/server/server.h b/codemp/server/server.h index 654e319bb3..f94916ffa6 100644 --- a/codemp/server/server.h +++ b/codemp/server/server.h @@ -317,11 +317,12 @@ extern cvar_t *sv_banFile; extern cvar_t *sv_maxOOBRate; extern cvar_t *sv_maxOOBRateIP; extern cvar_t *sv_autoWhitelist; - +#ifdef DEDICATED extern cvar_t *sv_demoPreRecord; extern cvar_t *sv_demoPreRecordTime; extern cvar_t *sv_demoPreRecordKeyframeDistance; extern cvar_t *sv_demoWriteMeta; +#endif extern cvar_t *sv_snapShotDuelCull; diff --git a/codemp/server/sv_main.cpp b/codemp/server/sv_main.cpp index 16044c2e2a..a20a041b12 100644 --- a/codemp/server/sv_main.cpp +++ b/codemp/server/sv_main.cpp @@ -75,10 +75,12 @@ cvar_t *sv_maxOOBRate; cvar_t *sv_maxOOBRateIP; cvar_t *sv_autoWhitelist; +#ifdef DEDICATED cvar_t *sv_demoPreRecord; // Activate demo pre-recording cvar_t *sv_demoPreRecordTime; // How many seconds of past packets should be stored so demos can be retroactively recorded for that duration? cvar_t *sv_demoPreRecordKeyframeDistance; // A demo can only start with a gamestate and full non-delta snapshot. How often should we save such a gamestate message (in seconds)? The shorter the distance, the more precisely the pre-record duration will be kept. cvar_t *sv_demoWriteMeta; // Enables writing metadata to demos, which can be set by the server/game. This is invisible to normal clients and can be used for storing information about when the demo was recorded, start of the recording, and so on. +#endif cvar_t *sv_snapShotDuelCull; From 1b611fe8f63d0966a66c28eb48078a96f2b1cf86 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 16 Jun 2023 14:39:18 +0200 Subject: [PATCH 11/12] - Fixed a comment - Some extra safety checks for demo pre-recording, just in case - Clean up possibly mysteriously leftover packages that for whatever reason weren't cleared yet before adding new messages. - Always clear demo pre-record buffer on client disconnect, even if demo wasn't being recorded. --- codemp/server/server.h | 2 +- codemp/server/sv_ccmds.cpp | 2 +- codemp/server/sv_client.cpp | 4 ++-- codemp/server/sv_snapshot.cpp | 23 +++++++++++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/codemp/server/server.h b/codemp/server/server.h index f94916ffa6..56eadd7e6c 100644 --- a/codemp/server/server.h +++ b/codemp/server/server.h @@ -134,7 +134,7 @@ typedef struct { qboolean isBot; int botReliableAcknowledge; // for bots, need to maintain a separate reliableAcknowledge to record server messages into the demo file struct { - // this is basically the equivalent of the demowaiting and minDeltaFrame values above, except it's for the demo pre-record feature and will be done every sv_demoPreRecordKeyframeDistance milliseconds. + // this is basically the equivalent of the demowaiting and minDeltaFrame values above, except it's for the demo pre-record feature and will be done every sv_demoPreRecordKeyframeDistance seconds. qboolean keyframeWaiting; int minDeltaFrame; diff --git a/codemp/server/sv_ccmds.cpp b/codemp/server/sv_ccmds.cpp index 397660bda9..f2dc61c5d8 100755 --- a/codemp/server/sv_ccmds.cpp +++ b/codemp/server/sv_ccmds.cpp @@ -1848,7 +1848,7 @@ void SV_RecordDemo( client_t *cl, char *demoName ) { static byte preRecordBufData[MAX_MSGLEN]; // I make these static so they don't sit on the stack. static msg_t preRecordMsg; - if (!it->isKeyframe || index == 0) { + if ((!it->isKeyframe || index == 0) && it->msgNum <= cl->netchan.outgoingSequence && it->time <= sv.time) { // Check against outgoing sequence and server time too, *just in case* we ended up with some old messages // We only want a keyframe at the beginning of the demo, none after. Com_Memset(&preRecordMsg, 0, sizeof(msg_t)); Com_Memset(&preRecordBufData, 0, sizeof(preRecordBufData)); diff --git a/codemp/server/sv_client.cpp b/codemp/server/sv_client.cpp index 2ababde4fd..e5215ebb92 100644 --- a/codemp/server/sv_client.cpp +++ b/codemp/server/sv_client.cpp @@ -416,9 +416,9 @@ void SV_DropClient( client_t *drop, const char *reason ) { #ifdef DEDICATED if ( drop->demo.demorecording ) { SV_StopRecordDemo( drop ); - SV_ClearClientDemoPreRecord( drop ); - SV_ClearClientDemoMeta( drop ); } + SV_ClearClientDemoPreRecord(drop); // Happens on (re)connect too but let's be safe/clean :) + SV_ClearClientDemoMeta(drop); #endif // if this was the last client on the server, send a heartbeat diff --git a/codemp/server/sv_snapshot.cpp b/codemp/server/sv_snapshot.cpp index cfc1402c9b..568b4dc026 100644 --- a/codemp/server/sv_snapshot.cpp +++ b/codemp/server/sv_snapshot.cpp @@ -815,6 +815,29 @@ void SV_SendMessageToClient( msg_t *msg, client_t *client ) { #ifdef DEDICATED if (sv_demoPreRecord->integer) { // If pre record demo message buffering is enabled, we write this message to the buffer. + + // But first, Do a quick cleanup of possible old packages in the buffer that have msgNum > client->netchan.outgoingSequence + // This shouldn't really happen as we clear the buffer on disconnects/connects and map_restarts but let's be safe. + demoPreRecordBufferIt lastEvilPackage; + qboolean evilPackagesFound = qfalse; + for (demoPreRecordBufferIt it = demoPreRecordBuffer[client - svs.clients].begin(); it != demoPreRecordBuffer[client - svs.clients].end(); it++) { + if (it->msgNum > client->netchan.outgoingSequence || it->time > sv.time) { + lastEvilPackage = it; + evilPackagesFound = qtrue; + } + else { + break; + } + } + if (evilPackagesFound) { + // The lastTooOldKeyframe itself won't be erased because .erase()'s second parameter is not inclusive, + // aka it deletes up to that element, but not that element itself. + Com_Printf("Found evil old messages in demoPreRecordBuffer. This shouldn't happen.\n"); + lastEvilPackage++; // .erase() function excludes the last element, but we want to delete the last evil package too. + demoPreRecordBuffer[client - svs.clients].erase(demoPreRecordBuffer[client - svs.clients].begin(), lastEvilPackage); + } + + // Now put the current messsage in the buffer. static bufferedMessageContainer_t bmt; // I make these static so they don't sit on the stack. Com_Memset(&bmt, 0, sizeof(bufferedMessageContainer_t)); MSG_ToBuffered(msg,&bmt.msg); From c192d97d229c6577ff5e5a7e2ee47a4f4416315b Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 16 Jun 2023 18:53:35 +0200 Subject: [PATCH 12/12] Fixed small mistake. --- codemp/server/sv_bot.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codemp/server/sv_bot.cpp b/codemp/server/sv_bot.cpp index 4df182fe5d..13d264f00e 100644 --- a/codemp/server/sv_bot.cpp +++ b/codemp/server/sv_bot.cpp @@ -233,9 +233,9 @@ void SV_BotFreeClient( int clientNum ) { #ifdef DEDICATED if ( cl->demo.demorecording ) { SV_StopRecordDemo( cl ); - SV_ClearClientDemoPreRecord( cl ); - SV_ClearClientDemoMeta(cl); } + SV_ClearClientDemoPreRecord(cl); + SV_ClearClientDemoMeta(cl); #endif }