If a server is left running for close to 24 hours, certain undesired behaviors begin to manifest - shaders get stuck, bobbing items start to get choppy, explosion graphics go out of sync with their timers, etc. The problem is caused by a variable the server depends on to increment time. I will explain how to fix this problem. This little tutorial assumes you have the full Q3 engine source. Line numbers will be approximations, and I'll provide the surrounding contextual code so you should be able to find it without trouble, and I'll show changed code remarked out instead of removed.
The problem is in serverStatic_t, the time value (referred to svs.time) is used for all server-to-client transactions. This not only includes timestamps on packets, but also snapshots. Since it always increments, this is bad because animated shaders, etc, need to multiply the provided timer by some value in their functions. This causes the resultant value to exceed the maximum positive value for a signed 32 bit integer, which results in a negative value, causing animapped shaders to freeze on frame 0, and bobbing items to get jerky. What we need to do is create a new timer value that does not constantly increment, but rather resets every time the level changes so our integers stay sane.
First up, we need to define our timer. While serverStatic_t does not clear, server_t is reset every level change, so we want to use that. Go into server/server.h and look at Ln 81. You should see this:
playerState_t *gameClients;
int gameClientSize; // will be > sizeof(playerState_t) due to game private data
int restartTime;
} server_t;
Add a line to make it look like this:
playerState_t *gameClients;
int gameClientSize; // will be > sizeof(playerState_t) due to game private data
int restartTime;
// Phoenix - bugfix
int time;
} server_t;
Henceforth this will be referred to as sv.time. Now we're going to replace all instances of svs.time that can affect gameplay, but we
don't want to mess with packet timestamps or connection-specific code.
Open up
server/sv_ccmds.c Head down to
Ln 276. You should be in a function called
SV_MapRestart_f, and it should look something like this:
SV_RestartGameProgs();
// run a few frames to allow everything to settle
for ( i = 0;i < 3; i++ ) {
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
svs.time += 100;
}
We want to add our sv.time integer, but we want to increment it alongside svs.time, not replace the svs.time value. However, we want to pass sv.time - not svs.time - into the vm. Make your code look like this:
SV_RestartGameProgs();
// run a few frames to allow everything to settle
for ( i = 0;i < 3; i++ ) {
// Phoenix - bugfix
VM_Call( gvm, GAME_RUN_FRAME, sv.time );
sv.time += 100;
//VM_Call( gvm, GAME_RUN_FRAME, svs.time );
svs.time += 100;
}
In the same function, around
Ln 321 at the end of the function, look for this:
// run another frame to allow things to look at all the players
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
svs.time += 100;
}
Add these lines so it looks like this:
// run another frame to allow things to look at all the players
// Phoenix - bugfix
VM_Call( gvm, GAME_RUN_FRAME, sv.time );
sv.time += 100;
//VM_Call( gvm, GAME_RUN_FRAME, svs.time );
svs.time += 100;
}
Now we want to look at
server/sv_game.c, Head to
Ln 909. You should see the following, at the end of SV_InitGameVM:
VM_Call( gvm, GAME_INIT, svs.time, Com_Milliseconds(), restart );
}
Again we want to pass sv.time, not svs.time, into the vm:
// Phoenix - bugfix
VM_Call( gvm, GAME_INIT, sv.time, Com_Milliseconds(), restart );
//VM_Call( gvm, GAME_INIT, svs.time, Com_Milliseconds(), restart );
}
Next, open up
server/sv_init.c, and look for
Ln 442. Look at
SV_SpawnServer, and find this segment:
// run a few frames to allow everything to settle
for ( i = 0;i < 3; i++ ) {
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
SV_BotFrame( svs.time );
svs.time += 100;
}
We're going to use sv.time for both the vm call and the botframe run. Change it to look like this:
// run a few frames to allow everything to settle
for ( i = 0;i < 3; i++ ) {
// Phoenix - bugfix
VM_Call( gvm, GAME_RUN_FRAME, sv.time );
SV_BotFrame( sv.time );
sv.time += 100;
//VM_Call( gvm, GAME_RUN_FRAME, svs.time );
//SV_BotFrame( svs.time );
svs.time += 100;
}
Again, in the same function, head to
Ln 502 and locate this:
// run another frame to allow things to look at all the players
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
SV_BotFrame( svs.time );
svs.time += 100;
Give it the same treatment as follows:
// run another frame to allow things to look at all the players
// Phoenix - bugfix
VM_Call( gvm, GAME_RUN_FRAME, sv.time );
SV_BotFrame( sv.time );
sv.time += 100;
// VM_Call( gvm, GAME_RUN_FRAME, svs.time );
// SV_BotFrame( svs.time );
svs.time += 100;
Let's head over to
server/sv_main.c and jump to
Ln 779. You should be in
SV_Frame. sv.timeResidual += msec;
if (!com_dedicated->integer) SV_BotFrame( svs.time + sv.timeResidual );
This needs to be changed to this:
sv.timeResidual += msec;
// Phoenix - bugfix?
if (!com_dedicated->integer) SV_BotFrame( sv.time + sv.timeResidual );
//if (!com_dedicated->integer) SV_BotFrame( svs.time + sv.timeResidual );
Now in the same function, locate this portion:
// update ping based on the all received frames
SV_CalcPings();
if (com_dedicated->integer) SV_BotFrame( svs.time );
// run the game simulation in chunks
while ( sv.timeResidual >= frameMsec ) {
sv.timeResidual -= frameMsec;
svs.time += frameMsec;
// let everything in the world think and move
VM_Call( gvm, GAME_RUN_FRAME, svs.time );
}
We need to pass sv.time into SV_BotFrame, increment sv.time by frameMsec, and pass it into the VM. Here's how it should look when we're done:
// update ping based on the all received frames
SV_CalcPings();
// Phoenix - bugfix?
if (com_dedicated->integer) SV_BotFrame( sv.time );
//if (com_dedicated->integer) SV_BotFrame( svs.time );
// run the game simulation in chunks
while ( sv.timeResidual >= frameMsec ) {
sv.timeResidual -= frameMsec;
svs.time += frameMsec;
// Phoenix - bugfix?
sv.time += frameMsec;
// let everything in the world think and move
// Phoenix - bugfix?
VM_Call( gvm, GAME_RUN_FRAME, sv.time );
//VM_Call( gvm, GAME_RUN_FRAME, svs.time );
}
Our last change is in
server/sv_snapshot.c, on
Ln 164. Look at
SV_WriteSnapshotToClient and find this:
MSG_WriteLong (msg, sv.time);
That's not good, since this is what creates cg.time on the client. We need to do this instead:
// Phoenix - bugfix
MSG_WriteLong (msg, sv.time);
//MSG_WriteLong (msg, svs.time);
This way cg.time only increments from level start. That's fine since the client's vm is shut down every level change anyway.
These are the ONLY changes that should be done in reference to svs.time. Other references (like client->nextSnapshotTime) have to do with the client's connection to the server, and should never run backwards or be cleared or bad things might happen (like booting the client off the server).
Note that this fix is 100% server-side. There are no changes required to the client, nor to the .qvm's or renderer. I've already tested this with a server that's been running over 60 hours and I've had no detrimental effects. Gameplay is smooth, no shaders are malfunctioning, level changes work fine and bots remain connected when they should and behave properly. Demos also record properly.
LimitationsIf a map has been running on a server for over 24 hours, sv.time will cause the same problem behavior. This is unavoidable. To correct this, just change maps. Most Q3 servers would be sitting at a scoreboard at this point anyway, so unless someone's having a 48 hour frag-a-thon on the same map it will be self-correcting after a level change.
That's it. If you use this bugfix credit is always nice, but I won't insist on it. I just want this problem GONE from Q3 servers, so by all means implement this fix if you can![/color]