diff --git a/code/server/sv_netdemo.c b/code/server/sv_netdemo.c index 81f1b2e..10b0532 100644 --- a/code/server/sv_netdemo.c +++ b/code/server/sv_netdemo.c @@ -57,11 +57,23 @@ snapshot pipeline delivers them to a spectator client. // State // --------------------------------------------------------------- +// per-entity data stored for delta compression (entityState + PVS fields) +typedef struct { + entityState_t es; + int svFlags; + qboolean linked; + vec3_t currentOrigin; + vec3_t absmin; + vec3_t absmax; + qboolean active; // was this entity present last frame? +} svdEntityState_t; + typedef struct { // recording fileHandle_t recordFile; qboolean recording; char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection + svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta // playback fileHandle_t playFile; @@ -74,6 +86,7 @@ typedef struct { int nextFrameTime; // serverTime of next frame to read qboolean endOfDemo; qboolean needConfigstrings; // apply saved configstrings on first frame + svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read } svDemo_t; static svDemo_t demo; @@ -150,38 +163,85 @@ static void SVD_WriteHeader( fileHandle_t f ) { // --------------------------------------------------------------- static void SVD_WriteFrame( fileHandle_t f ) { - int i, count; + int i; sharedEntity_t *ent; short numChanges; + msg_t msg; + byte msgBuf[MAX_GENTITIES * 64]; // delta-compressed entities are small + int msgLen; SVD_WriteInt( f, svs.time ); + SVD_WriteShort( f, (short)sv.num_entities ); + + // delta-compress all entities into a message buffer + MSG_Init( &msg, msgBuf, sizeof(msgBuf) ); - // count active entities - count = 0; for ( i = 0; i < sv.num_entities; i++ ) { + qboolean active; ent = SV_GentityNum( i ); - if ( ent->r.linked || ent->s.eType != 0 ) { - count++; - } - } - SVD_WriteShort( f, (short)count ); + active = ( ent->r.linked || ent->s.eType != 0 ); - // write each active entity - for ( i = 0; i < sv.num_entities; i++ ) { - ent = SV_GentityNum( i ); - if ( !ent->r.linked && ent->s.eType == 0 ) { - continue; - } + if ( active ) { + entityState_t baseline; + entityState_t *from; + if ( demo.prevEntities[i].active ) { + from = &demo.prevEntities[i].es; + } else { + Com_Memset( &baseline, 0, sizeof(baseline) ); + baseline.number = i; + from = &baseline; + } + MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue ); - SVD_WriteShort( f, (short)i ); - FS_Write( &ent->s, sizeof(entityState_t), f ); - SVD_WriteInt( f, ent->r.svFlags ); - SVD_WriteInt( f, ent->r.linked ); - FS_Write( ent->r.currentOrigin, 12, f ); - FS_Write( ent->r.absmin, 12, f ); - FS_Write( ent->r.absmax, 12, f ); + // write PVS fields only if changed from previous frame + { + qboolean pvsChanged = !demo.prevEntities[i].active + || ent->r.svFlags != demo.prevEntities[i].svFlags + || ent->r.linked != demo.prevEntities[i].linked + || !VectorCompare( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin ) + || !VectorCompare( ent->r.absmin, demo.prevEntities[i].absmin ) + || !VectorCompare( ent->r.absmax, demo.prevEntities[i].absmax ); + + MSG_WriteBits( &msg, pvsChanged, 1 ); + if ( pvsChanged ) { + MSG_WriteLong( &msg, ent->r.svFlags ); + MSG_WriteLong( &msg, ent->r.linked ); + MSG_WriteFloat( &msg, ent->r.currentOrigin[0] ); + MSG_WriteFloat( &msg, ent->r.currentOrigin[1] ); + MSG_WriteFloat( &msg, ent->r.currentOrigin[2] ); + MSG_WriteFloat( &msg, ent->r.absmin[0] ); + MSG_WriteFloat( &msg, ent->r.absmin[1] ); + MSG_WriteFloat( &msg, ent->r.absmin[2] ); + MSG_WriteFloat( &msg, ent->r.absmax[0] ); + MSG_WriteFloat( &msg, ent->r.absmax[1] ); + MSG_WriteFloat( &msg, ent->r.absmax[2] ); + } + } + + // update prev state + demo.prevEntities[i].es = ent->s; + demo.prevEntities[i].svFlags = ent->r.svFlags; + demo.prevEntities[i].linked = ent->r.linked; + VectorCopy( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin ); + VectorCopy( ent->r.absmin, demo.prevEntities[i].absmin ); + VectorCopy( ent->r.absmax, demo.prevEntities[i].absmax ); + demo.prevEntities[i].active = qtrue; + } else if ( demo.prevEntities[i].active ) { + // entity was removed — write a remove marker + MSG_WriteDeltaEntity( &msg, &demo.prevEntities[i].es, NULL, qtrue ); + demo.prevEntities[i].active = qfalse; + } + // else: entity was inactive and still inactive — write nothing } + // end of entities marker + MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS ); + + // write compressed message to file + msgLen = msg.cursize; + SVD_WriteInt( f, msgLen ); + FS_Write( msg.data, msgLen, f ); + // configstring changes numChanges = 0; for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) { @@ -247,6 +307,9 @@ void SVD_Record_f( void ) { Com_Printf( "Recording server demo to %s\n", name ); demo.recording = qtrue; + // clear delta state for fresh recording + Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) ); + SVD_WriteHeader( demo.recordFile ); } @@ -361,8 +424,11 @@ static void SVD_ApplyConfigstrings( void ) { static qboolean SVD_ReadFrame( fileHandle_t f ) { int serverTime; short numEnts, numChanges; - int i; + int i, entNum, msgLen; sharedEntity_t *ent; + msg_t msg; + byte msgBuf[MAX_GENTITIES * 64]; + entityState_t newEs; serverTime = SVD_ReadInt( f ); if ( serverTime == -1 ) { @@ -370,54 +436,100 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) { } svs.time = serverTime; + numEnts = SVD_ReadShort( f ); - // clear all entities except the spectator slot + // read compressed message + msgLen = SVD_ReadInt( f ); + if ( msgLen <= 0 || msgLen > (int)sizeof(msgBuf) ) { + return qfalse; + } + FS_Read( msgBuf, msgLen, f ); + MSG_Init( &msg, msgBuf, sizeof(msgBuf) ); + msg.cursize = msgLen; + + // clear all non-spectator entities for ( i = 0; i < sv.num_entities; i++ ) { if ( i == demo.spectatorClientNum ) { continue; } ent = SV_GentityNum( i ); - ent->r.linked = qfalse; + if ( ent->r.linked ) { + SV_UnlinkEntity( ent ); + } ent->s.eType = 0; ent->s.number = i; } - numEnts = SVD_ReadShort( f ); + // parse delta-compressed entities + while ( 1 ) { + entNum = MSG_ReadBits( &msg, GENTITYNUM_BITS ); + if ( entNum == MAX_GENTITIES - 1 ) { + break; // end of entities marker + } + if ( msg.readcount > msg.cursize ) { + break; + } - for ( i = 0; i < numEnts; i++ ) { - short entNum; - entityState_t es; - int svFlags, linked; - vec3_t currentOrigin, absmin, absmax; + { + entityState_t baseline; + entityState_t *from; + Com_Memset( &newEs, 0, sizeof(newEs) ); + if ( demo.playPrevEntities[entNum].active ) { + from = &demo.playPrevEntities[entNum].es; + } else { + Com_Memset( &baseline, 0, sizeof(baseline) ); + baseline.number = entNum; + from = &baseline; + } + MSG_ReadDeltaEntity( &msg, from, &newEs, entNum ); + } - entNum = SVD_ReadShort( f ); - FS_Read( &es, sizeof(entityState_t), f ); - svFlags = SVD_ReadInt( f ); - linked = SVD_ReadInt( f ); - FS_Read( currentOrigin, 12, f ); - FS_Read( absmin, 12, f ); - FS_Read( absmax, 12, f ); + if ( newEs.number == MAX_GENTITIES - 1 ) { + // entity was removed + demo.playPrevEntities[entNum].active = qfalse; + continue; + } + + // read PVS fields + { + qboolean pvsChanged = MSG_ReadBits( &msg, 1 ); + if ( pvsChanged ) { + demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg ); + demo.playPrevEntities[entNum].linked = MSG_ReadLong( &msg ); + demo.playPrevEntities[entNum].currentOrigin[0] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].currentOrigin[1] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].currentOrigin[2] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].absmin[0] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].absmin[1] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].absmin[2] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].absmax[0] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].absmax[1] = MSG_ReadFloat( &msg ); + demo.playPrevEntities[entNum].absmax[2] = MSG_ReadFloat( &msg ); + } + } + + demo.playPrevEntities[entNum].es = newEs; + demo.playPrevEntities[entNum].active = qtrue; // skip the spectator's slot if ( entNum == demo.spectatorClientNum ) { continue; } + // apply to server entity ent = SV_GentityNum( entNum ); - ent->s = es; + ent->s = newEs; ent->s.number = entNum; - ent->r.svFlags = svFlags; - ent->r.linked = linked; - VectorCopy( currentOrigin, ent->r.currentOrigin ); - VectorCopy( absmin, ent->r.absmin ); - VectorCopy( absmax, ent->r.absmax ); + ent->r.svFlags = demo.playPrevEntities[entNum].svFlags; + ent->r.linked = demo.playPrevEntities[entNum].linked; + VectorCopy( demo.playPrevEntities[entNum].currentOrigin, ent->r.currentOrigin ); + VectorCopy( demo.playPrevEntities[entNum].absmin, ent->r.absmin ); + VectorCopy( demo.playPrevEntities[entNum].absmax, ent->r.absmax ); - // link into the world so PVS can find this entity - if ( linked ) { + if ( ent->r.linked ) { SV_LinkEntity( ent ); } - // update num_entities high water mark if ( entNum + 1 > sv.num_entities ) { sv.num_entities = entNum + 1; }