What Next?

Alright, so you've put all this nifty code in. How do you know that it works? Have you got ideas for other features based on this new stuff?

Test, Test, Test!

This might seem obvious to a lot of you, but it has to be said: if it's not working as designed, there's no point in having it. That goes for all of it.

There are ways I've found for testing many of these features that work very well, so in this section I'll share them. There's no point in all of us discovering these things independently.

Turn on the Lag-o-meter for local games

This one's great for testing anything at all that has to do with cl_timenudge. Locate CG_DrawLagometer() in cg_draw.c, and change the first few lines to this:

if ( !cg_lagometer.integer /* || cgs.localServer */) {

You'll also need to know how to read the thing. Luckily, it isn't that difficult. Each column in the top line represents a client frame, and each column in the bottom line represents a server snapshot.

The middle of the top line represents when the client is at the beginning of a snapshot. Values above (always yellow) represent extrapolation (cg.nextSnap is NULL), and values below (always blue) represent interpolation (both cg.snap and cg.nextSnap are defined). Playing locally, you'll see yellow every once in a while. Setting cl_timenudge to a negative value will show more yellow, and setting it positive will show more blue.

The height of a column on the lower line represents the time it took for a round trip. Red columns are missed snapshots, and yellow columns are suppressed snapshots.

Use the lag simulation code

This is as good a place as any to describe exactly what these options do.

cg_latentSnaps: specifies how far back the client game will go for snapshots. If you set this to 1, your client will effectively receive every snapshot one snapshot late. This will probably add 50ms to your ping. (It's actually 1000 / sv_fps that gets added.)

cg_latentCmds: specifies how far back the server game will go for commands. If you set this to 1, the server will effectively receive every command one command late. How much this affects your ping depends on what your framerate is. If you run at 125 FPS, each increment of one will add about 8ms to your ping.

If you want to simulate lag as exactly as possible, set cg_latentSnaps to some number, and then change cg_latentCmds to add just as much latency. If you want finer control than multiples of 100ms (which is what you'd get with that method), you can regard cg_latentSnaps as your major control and cg_latentCmds as your minor control. They don't actually have to be perfectly balanced – in fact, it hardly matters at all, since the effects are almost exactly the same no matter where your latency comes from.

If you have a high framerate, you may have problems simulating latency without freezing up altogether. The most you can get if you do 125 FPS, for example, is about 400-450. (Set cg_showmiss to 1 to see why.) Try lowering your maximum framerate if you freeze up.

cg_plOut: specifies what percentage of commands to drop. For example, setting it to 50 will drop half of all your commands.

All of these settings are compatible with cl_timenudge, cg_cmdTimeNudge, pmove_fixed, and all server-side backward reconciliation settings. The only thing that suffers is cg_optimizePrediction when you use cg_latentCmds – it has to be switched off.

Use cg_debugDelag

I must reiterate that this option probably the most important Unlagged debugging tool there is.

Here's a typical dump:

Int: time: 68360, j: 68350, k: 68400, origin: 974.00 1873.60 264.00
frac: 0.2000, origin1: 976.00 1875.00 264.00, origin2: 966.00 1868.00 264.00
Rec: time: 68360, j: 68350, k: 68400, origin: 974.00 1873.60 264.00
frac: 0.2000, origin1: 976.00 1875.00 264.00, origin2: 966.00 1868.00 264.00
level.time: 68400, est time: 68415, level.time delta: 40, est real ping: 55

The first two lines (yellow and cyan) are dumped by the client. This is when it does its own client-side hit test. The next two are dumped by the server, and the data corresponds with the data shown by the client. (In fact, for an un-tweaked Unlagged client, when the client is interpolating, the numbers should all be exactly the same.) The remaining line is also dumped by the server.

The fields in the first line are the client clock, the server times of the two snapshots that were used for interpolation, and the origin of the target that was hit. The fields in the second line are the interpolation fraction, and the two origins used for interpolation. The third and fourth lines of course are the same data, but they're the data that the server uses for backward reconciliation, where the first and second are the data the client uses for rendering.

The fields in the last line are the server clock, the estimated actual server clock, the difference between the command time and the server clock, and the difference between the command time and the estimated actual server clock.

Here's another dump:

Ext: time: 80868, j: 80850, k: 80850, origin: 1694.77 1174.93 510.53
frac: 0.0000, origin1: 1688.00 1175.00 516.00, origin2: 1688.00 1175.00 516.00
No rec: time: 80868, j: 80850, k: 80850, origin: 1688.91 1175.39 516.14
frac: 0.0000, origin1: 1688.91 1175.39 516.14, origin2: 1688.91 1175.39 516.14
level.time: 80850, est time: 80889, level.time delta: -18, est real ping: 21

The big difference here is that cl_timenudge was -30 when it was dumped. That's reflected in the prefix to the yellow line: the “Ext:” means extrapolated. (Yes, and you bright ones have already figured out that “Int:” means interpolated.) It's also reflected in the red line: the “No rec” means no backward reconciliation took place.

cl_timenudge is added to the command time, so the command time in this instance was too early for the server to backward reconcile with. There wasn't any two previous states to interpolate between in the history.

Online, when you play with a negative cl_timenudge and lag compensation enabled, you'll get “Rec:” lines from the server instead. (Unless you play with a larg cg_cmdTimeNudge, of course.) The client and server origins will also match up much more closely.

Use cg_drawBBox

This is my favorite thing for convincing people that it's mostly perception causing the false positive and false negative hit tests. It's amazing how much BS is produced by player models not fitting well into their bounding boxes. I'll talk about that later in the FAQ. Anyway, testing with this set to 1 will ease your mind about a lot of things that wouldn't look right otherwise.

Whether or not you want to fix up the visuals is up to you.

Attack prediction console dumps

Console dumps are very useful for testing attack prediction. There are numerous commented-out printf statements all around the code marked by “//unlagged - attack prediction”, which you can use for making sure random seeds match and making sure that predicted events are suppressed properly.

Simulating player “skip”

A player skips when a server frame goes by without a client command. A client command is sent every client frame. That means you can set com_maxfps to some number under sv_fps to make yourself skip – simulate a player who has routing problems or is dropping outgoing packets. I like 5 and 10, personally. If your testing buddy sees you moving in a reasonably smooth manner when com_maxfps is set to one of those values, the skip correction is working.

You can also set cg_plOut to some number near 100 and see how it handles that.

Projectile nudge and early transitioning tests

The 50ms projectile nudge visual fix-up is easy, if you're adding the cg_projectileNudge feature. Set it to some insane number and watch a bot shoot a rocket at you. You'll know right away if it's working.

Early transitioning is a little harder. Projectile nudge by itself will make rockets seem to stick in the floor a bit before blowing up, and make fast projectiles (like plasma bolts) first appear far away. If the early transitioning is working, neither of these things will happen. Also, if you're the sensitive type, your rockets will feel a lot more responsive.

For concrete evidence, you can dump to the console a projectile's base time and the client clock when the projectile is first rendered. (You'll have to add a member to centity_t to do that, but it's just a little qboolean value.) If early transitioning is working right, the numbers will be closer together.

cl_timenudge extrapolation testing

Unfortunately, there's not much by way of empirical evidence you can collect on this. There are a few things you can do to verify it perceptually, though.

First of all, if it's working, other players will move much, much more smoothly when you use a negative cl_timenudge setting than they did before.

You can bind one key to set cl_timenudge to -30, and another to set it to 0. Watch another player moving, and switch between the two settings quickly. When cl_timenudge changes to -30, he should jump ahead a little.

You can also turn off the 50ms lag correction and the full lag compensation, set cl_timenudge to -30, set cg_drawBBox to 1, and rail bots. Your aim should feel almost as if you had the 50ms lag correction turned on.

Player prediction optimization testing

Bind a key to “toggle cg_optimizePrediction”. Do different actions – such as standing in one place, backing into a corner, strafing along a curve, and just running around – on both settings. Notice the framerate differences. Make sure you try it with different pings, because the higher you ping the more the optimization matters. Also keep in mind that the optimization is more effective on lower-end CPUs, with low detail settings, and in other instances where the graphics card is doing a comparatively small amount of work.

There's also some code marked by “// debug code” in CG_PredictPlayerState(). The comments in the code will show you how to use it.

Turn on cg_showmiss to see when IsUnacceptableError() returns nonzero. It'll also let you know when an event was missed or duplicated.

Test with cg_predictItems both on and off. Have some busy FFA games. Listen for item pickup sounds to make sure they happen. (Keep in mind that if you use pmove_fixed and your framerate hovers around the fixed framerate, the client may drop sounds if you haven't fixed that bug.)

Possible Extensions

Here are some things I've thought up that may be of use to some of you, and general descriptions of how they would work.

Backward reconciliation limit

This one's probably the most obvious: how would you implement a limit to how much backward reconciliation will take place?

First of all, realize that with NUM_CLIENT_HISTORY defined as 17, the upper limit is 850ms if the server's sv_fps is set to the default, 20. That'll cover pings up to 800. (There are definite problems when pings climb too high – such as not having enough backup commands to do a full predict, which will screw over your prediction when you ping above 450 or so if you get 125 FPS – and 800 is sort of regarded as the upper limit.)

The function you'll need to change is G_DoTimeShiftFor() in g_unlagged.c. First, calculate the maximum backward reconciliation time: level.previousTime + ent->client->frameOffset - g_maxDelagPing.integer. (You'd use level.previousTime instead of level.time to account for the built-in 50ms lag.) Then calculate the normal backward reconciliation time: ent->client->attackTime + ent->client->pers.cmdTimeNudge. Use the greater of the two values in the call to G_TimeShiftAllClients() and you're done.

100ms lag compensation

This one is definitely out for any game like Quake 3. The rail trail and shotgun effects would get totally screwed. The lightning effects would be very difficult to fix. The rail trail would appear behind players you hit. The shotgun pellets, since every strike isn't dictated by the server (they're reconstructed on the client using a random seed), will also be screwed up.

However, for games in which every strike is dictated by the server and no weapon has a trail, this would work very well.

Calculate the backward reconciliation time as level.previousTime + ent->client->frameOffset - 50 instead of level.previousTime + ent->client->frameOffset. Use the greater of this and ent->client->attackTime in your call to G_TimeShiftAllClients().

Players who ping over 50 could also use a negative cl_timenudge to knock some lag off. Players who ping under 50 wouldn't have to use cl_timenudge, since they'd effectively be fully compensated for anyway.

Adding a little lag

It sounds funny, but it's true: a lot of people have asked for a feature which would “lag” their instant-hit weapons, at least a little bit. The reason is that the lightning gun is perceived as much too powerful (indeed, it just might be) if all you need is a straight shot. Id most certainly did balance the weapons after the netcode was complete, 50ms built-in lag and all.

If you want to do this, calculate the backward reconciliation time as ent->client->attackTime + ent->client->pers.cmdTimeNudge + 50. Then make sure the time doesn't exceed the 50ms backward reconciliation time. You'd need to do this even if it galls you to let players pinging under 50 have a straighter shaft, because at least 50ms of backward reconciliation is necessary for other features like skip correction.

One nice thing is that you wouldn't really have to fix up the visuals. As it is, in vanilla Quake 3 netcode, events happen 50ms before player states in the same snapshot are used for interpolation anyway. Nothing will have changed in the relative timing.

Dealing with spikes

As it is, with full lag compensation enabled, firing before a huge lag spike can still get you a hit. This may or may not be desirable, depending on how you look at things.

If you decide it isn't, you can constrain the backward reconciliation time using techniques similar to what I've already described. You could use the client's ping (plus 50ms of course) +/-100ms as your upper and lower limit.

Backward reconciling movers

One thing I haven't paid much attention to is how movers can get in the way of the backward reconciliation. They are left in their true states while everything else that might get hit with an instant-hit attack is backward reconcilied or stationary. This can lead to some obvious false positive and false negative hit tests.

On the other hand, it may not be so desirable from the target's point of view: getting railed “through a closed door” is probably worse than getting railed “around corners.” On yet the other hand (I suppose we have three), situations in which it would be a question at all are very rare, even on maps with a lot of doors. On our fourth hand, platforms may be a real problem in your mod, because a player riding a platform up may very well be backward reconciled to the inside of it.

It shouldn't be that difficult to keep a state history for every mover, just like for players, and backward reconcile them at the same time as the players. If you do end up implementing this, I would be very interested to see what problems you run into, or conversely how easy it was if it ends up that way.

Point-and-click spectating

I'm looking forward to getting this working in Ultra Freeze Tag.

In SpectatorThink() in g_active.c, you can backward reconcile all the players when there's a button press. Run a trace, and see if it hits any players. If it does, follow that player instead of cycling through the clients.

On the client, draw a crosshair for the spectator, and change it to a huge one that communicates “Follow This Player!” to the spectator when a client-side trace hits a player.

That would be seriously cool. You wouldn't even have to worry about prediction error getting in the way.

A little more than a simple hit test may be in order, though, because following someone would require you to more or less rail them. It would probably be better to constrain the players you'd follow to a cone (using the dot product of your forward vector and a normalized vector that points from you to the potential target), and choose the closest. Run a trace, and if it misses (the client is not visible), try the next closest.

When I get it working, I'll release the code.

Next: Frequently-asked Questions