TUTORIAL 31 - Alt Weapon Fire
by
HypoThermia
This tutorial has grown out of a long e-mail discussion with CAIRATH
about an alternative weapon fire mode. Although it only covers a small
amount of the work needed to implement such a thing, it should get you off
to the best possible start.
Along the way we're going to take a look at how the player state is
managed by the server and predicted by the client in the bg_pmove.c source code.
This was crucial to the problems we were tackling, and hopefully
you'll come away from this tutorial with a better understanding of the
purpose, subtlety, and fragility, of this section of code.
Although the solution to the problem (once identified) was pretty trivial,
I've not seen it linked to coding problems or solutions before. There are also
limitations: you can only use this idea twice before running out of resources.
So choose the things that you want to be "predictable" carefully.
To show that everything is working we'll implement a trivial weapon
modification as an alternative fire mode: a double firing rate. When we're
done I'll also give a method for testing your mod without having a network
card or setting up an Internet connection.
Finally, we'll take a look at how to reproduce the problem that needed to be
solved in the first place. Any feedback on a solution to this would be
appreciated.
This tutorial was written for 1.17 code, but can be applied to 1.27 as well.
Comments on how to update the code are at
the end of the tutorial.
1. The role of bg_pmove.c
The code in bg_pmove.c is used in both the client and the server. It
takes as input the player state, and the most recent player mouse/keyboard
input, and extrapolates to produce a new player state as output.
The input from the client is provided through the usercmd_t data
structure, and is marked with a time index serverTime. This time index
acts to sort the commands into order, and indicate their duration or influence.
typedef struct usercmd_s {
int serverTime;
byte buttons;
byte weapon;
int angles[3];
signed char forwardmove, rightmove, upmove;
} usercmd_t;
This usercmd_t originates in the client executable and can only be read
(not modified) in the client QVM code. You'll realize the importance of this when you
understand that the usercmd_t data contains information on: when
you fire, how much movement you've requested, how much you've
changed your orientation. This is all under the control of the executable, so
you can't intercept or process commands like +attack. The result of issuing
a command like +attack is stored in the usercmd_t instead, and can't be
tampered with in a VM.
The usercmd_t is then sent to the server (possibly across an
Internet connection) with a time delay related to your game ping. When it
arrives at the server, its used to bring your movement and position up to date
by the code in bg_pmove.c.
How does the server stop some players running
ahead of each other? The answer lies in the use of "snapshots", which
the server generates 40 times a second. If a
usercmd_t arrives that goes beyond the current snapshot, then its held
back and processed in the next correct snapshot.
For the server this gives the "true" position, orientation,
weapon state, and action of that player. I use the word
"true" to mean the state that is used to decide what damage is
taken, whether a shot from another player hits or misses, whether the player
lands on a platform etc. This true position is then sent back to the client, which
updates the player state accordingly.
Back in the client things are a little more interesting, and possibly more
difficult to follow. The usercmd_t data is generated in the
client, forwarded to the server, and then sent back. There will be a time
delay between the command being issued and the response to that command being
received from the server. In order to mask this "round trip" delay as
much as possible, the client uses bg_pmove.c for prediction.
Prediction takes the last valid player state issued by the server,
and starts applying usercmd_t data to it as they're generated
in the client, completely bypassing the connection to the server. So
as you move through the map - provided you have a clean connection -
you'll have smooth movement. Other players will see your
movement based only on the true state maintained by the server.
As "true" player states are returned from the server they
are merged with the client predicted state. The merging process
"decays" the predicted state away so the player state returns
to the true state provided by the server. If, however, your position is
too far away for a decay to be viable, you immediately jump sharply to
the new position.
1.1 Tapping into usercmd_t
Because our client and server can only read the data stored in
usercmd_t (generated by the clients executable) we can't directly
modify these values and return them to the executable. If we want to make use
of usercmd_t in bg_pmove.c then we're going to have to look
outside the QVM code.
Fortunately we don't have to look far: Id have very kindly left some
unused but functional data in usercmd_t. If you take a look in
q_shared.h you'll find a family of constants BUTTON_* that
refer to the possible states of the usercmd_t->buttons bits:
Defined constant
|
Value of constant
|
Bind command
|
BUTTON_ATTACK
|
1
|
+attack
|
BUTTON_TALK
|
2
|
messagemode, toggleconsole, togglemenu
|
BUTTON_USE_HOLDABLE
|
4
|
+button2
|
BUTTON_GESTURE
|
8
|
+button3
|
BUTTON_WALKING
|
16
|
+speed
|
BUTTON_ANY
|
128
|
See below
|
All of these commands are intercepted and managed by the client executable
(not the client VM!) The gap in between BUTTON_WALKING and
BUTTON_ANY shows that there are two unallocated values: 32 and 64. Can we
make use of these?
Inspiration came from the Q3
console command list
maintained by JakFrost here
at PlanetQuake. The commands +button5 and +button6 are available
but not allocated for use. They map directly to the values 32 and 64
respectively. To make use of this we just need to add the appropriate constant
to q_shared.h and use it in bg_pmove.c. These commands can
then be bound to a key or mouse button, ready for use.
Unfortunately we're limited to having only two "predictable"
commands, so you need to make sure that you really do need to use them!
The framework that I've provided here gives an obvious example:
adding an alternative fire mode. I've left it as a framework so you can
decide how and what you want to implement: a double shotgun effect, a dual
mode rocket launcher/grenade thrower... you decide!
Finally, the BUTTON_ANY flag isn't used within the VM source code, so
there's no obvious use for it.
KnetterGek has sent in a good
explanation of what it's for: as part of the connection "keep alive"
network code. Transmitting a usercmd_t between the client and
the server executables includes a time stamp from the client. The server can
gauge the quality of connection and decide to kick someone if it gets nothing for
the allowed time-out period. This allows a spectator (and, annoyingly, a player)
to stay connected even if they're not using the keyboard or mouse.
2. The framework code changes
There are only a few lines of code we need to add to give us the
alt-fire mode for all weapons. With this in place you can then make the
changes you need to implement your ideas of what this alt-fire function
should actually do in your mod.
Files modified:
- bg_public.h
- q_shared.h
- bg_pmove.c
- g_active.c
- cg_event.c
In bg_public.h we need an additional event flag for the alt-fire
mode. This event flag will then be implemented in both the client and server.
Inside the entity_event_t enumeration at about line 308:
typedef enum {
// code snipped
EV_FIRE_WEAPON,
EV_ALTFIRE_WEAPON,
EV_USE_ITEM0,
// code snipped
} entity_event_t;
The second change we need to make is the flag that identifies
when button5 has been pressed. This takes on the value 32 (2^5),
leaving 64 (2^6) for the use of button6.
In q_shared.h at about line 895:
#define BUTTON_ATTACK 1
#define BUTTON_TALK 2
#define BUTTON_USE_HOLDABLE 4
#define BUTTON_GESTURE 8
#define BUTTON_WALKING 16
#define BUTTON_ALT_ATTACK 32 // button5
#define BUTTON_ANY 128
With the flags that we need introduced, we now add the code in
bg_pmove.c that handles when button5 has been pressed. This
replicates the behaviour of BUTTON_ATTACK, and generates out new event,
EV_ALTFIRE_WEAPON.
Working in bg_pmove.c in PM_Weapon() at about line 1497 we
check for a successful fire:
// check for fire
if ( ! (pm->cmd.buttons & (BUTTON_ATTACK | BUTTON_ALT_ATTACK)) ) {
pm->ps->weaponTime = 0;
pm->ps->weaponstate = WEAPON_READY;
return;
}
and then send out the event:
// take an ammo away if not infinite
if ( pm->ps->ammo[ pm->ps->weapon ] != -1 ) {
pm->ps->ammo[ pm->ps->weapon ]--;
}
// fire weapon
if (pm->cmd.buttons & BUTTON_ALT_ATTACK)
PM_AddEvent( EV_ALTFIRE_WEAPON );
else
PM_AddEvent( EV_FIRE_WEAPON );
Note that the code we've replaced used the numerical value of 1 (one).
This was the value of BUTTON_ATTACK, and we've now made it explicit.
All that remains is to place the handling of the event in the client and
server code. Our changes here are part of the framework, and default to the
fire mode already implemented.
When you include this in your mod you'll want
to separate out EV_ALTFIRE_WEAPON from EV_FIRE_WEAPON in some
way, and give your own firing code. In the client you'll be handling
all of the animations displayed on screen, while in the server you'll be
concerned with the game physics, collision detection, damage inflicted,
and so on. For the client think presentation, for the server think implementation
(at least all those things that don't or can't go into bg_pmove.c).
For the client event handling, add the following to CG_EntityEvent()
in cg_event.c at about line 608:
case EV_CHANGE_WEAPON:
DEBUGNAME("EV_CHANGE_WEAPON");
trap_S_StartSound (NULL, es->number,
CHAN_AUTO, cgs.media.selectSound );
break;
case EV_FIRE_WEAPON:
case EV_ALTFIRE_WEAPON:
if (event == EV_ALTFIRE_WEAPON)
DEBUGNAME("EV_ALTFIRE_WEAPON")
else
DEBUGNAME("EV_FIRE_WEAPON");
CG_FireWeapon( cent );
break;
and for the server event handling, add the following to ClientEvents()
in g_active.c around line 476:
G_Damage (ent, NULL, NULL, NULL, NULL, damage, 0, MOD_FALLING);
break;
case EV_ALTFIRE_WEAPON:
case EV_FIRE_WEAPON:
FireWeapon( ent );
break;
case EV_USE_ITEM1: // teleporter
// drop flags in CTF
item = NULL;
Compile these changes and then bind a key or mouse button to button5
using the console, like this:
bind mouse3 +button5
When you use the alt-fire mode you'll see that the weapon fires as normal.
Not much to show, so we'll make a small example to show that we do actually have
an alternative fire mode.
Told you the framework changes were trivial :)
3. A simple example
At the moment our alt-fire mode behaves identically to the existing weapon
fire. Just to show that we are actually using the new code, we'll introduce
a trivial example: double fire rate.
Open up bg_pmove.c and, at the end of PM_Weapon(), about
line 1575, insert the following code:
if ( pm->ps->powerups[PW_HASTE] ) {
addTime /= 1.3;
}
// Hypo: simple alt-fire example
if (pm->cmd.buttons & BUTTON_ALT_ATTACK)
addTime /= 2.0;
pm->ps->weaponTime += addTime;
Re-compile both the client and server code, and try it out.
Lethal!
4. A more thorough test
You might want to give your mod a more thorough work out, testing the
behaviour of your mod as a server. If you've got a local network available
then you're already sorted... but what if you've only got the machine you're
working on?
The answer is to run Q3 as a dedicated server, then start up a normal game
and connect to this dedicated server. Here are the steps:
- Run a dedicated server with the following command line:
quake3.exe +set fs_game source +set sv_pure 0 +set dedicated 1
This assumes that you're working in the mod directory quake3\source.
You need the +set sv_pure 0 to test a DLL build, or a VM file outside of a
PK3 package.
You should see a window with a server console appear, you then
need to start a map by typing "map q3dm1" (without the quotes!).
- Start up another copy of Q3 as your "client". When it gets
to the main menu, pull down the console and type:
\connect 127.0.0.1
You should see a connection screen and you'll soon join the dedicated
server that you started in step 1. You can now test your mod
across a high bandwidth connection.
You might find play is a little jerky on a Windows box
if you've only got a single CPU, and you'll definitely have CPU related problems
if you add bots. This is the competition between the OpenGL frame renderer
and the server as a separate process, as managed by the OS.
5. A problem you should be aware of...
This part of the tutorial isn't part of the framework. It's a
distilled version of the problem that CAIRATH needed to solve - a lag
between the server and client on a network connection in the code he was
using to implement an alt-fire mode. After covering a lot of ground, we
were both surprised at the simplicity of the solution I eventually
arrived at.
The lag problem appears to be caused by sending two commands from the
client to the server in quick succession, and only appears when the
server is running on a separate machine or process (as described in
section 4, above). For about a second the client and server appear to be
disconnected, the lagometer screen capture below shows what this looks
like:
The effect of sending two commands to the server very quickly
(bottom half = ping)
Remembering that the bottom part of the lagometer shows a height proportional
to the ping to the server, and green means the packets are getting through as
properly received snapshots, it look like something weird is going on here.
When this "blip" is over your position jerks, showing that you've
been moving around on predicted movement from the client
bg_pmove.c.
You can enable the lagometer by giving the console command
cg_lagometer 1.
To test this: you need to set up a dedicated server as in section 4
(one across a LAN/Internet should have the same effect), and connect
to it. Bind a mouse button or key to the command where like this:
\bind mouse3 where
If you trigger this command twice in succession, quickly, you get the
ping blip shown above. Repeatedly and quickly sending this command will
even cause the "Connection interrupted" message. The fact that the
server command where prints a value on the console is irrelevant, you can
set up your own command in the server that does nothing and the effect
is the same.
You can also create this effect by the use of
trap_SendConsoleCommand() in client code. In the absence of any
other information, I can only assume this is a bug in the Q3 networking
code. It's possible that this is actually a feature of the game code: to
stop a client from flooding the server with commands and saturating bandwidth.
Several of you have indeed e-mailed to say that this is a feature of the
game code. It can be turned off by setting sv_FloodProtect 0, but this
introduces problems with rogue and abusive players flooding the server. The most
annoying part of this method of flood protection is the blocking of the stream of
game data from the server. With 1.25 about to come out, I hope this is fixed!
|
Finally, you don't get this problem if you use the prediction methods
from usercmd_t described in this tutorial . That makes the two
free "slots" for button5 and button6 an
especially precious resource.
6. Using this tutorial with 1.27 code
The use of these BUTTON_* in the 1.27 source now differs slightly from
the old 1.17 way of doing things that this tutorial describes. First of all there
are now quite a few more of them:
#define BUTTON_ATTACK 1
#define BUTTON_TALK 2
#define BUTTON_USE_HOLDABLE 4
#define BUTTON_GESTURE 8
#define BUTTON_WALKING 16
#define BUTTON_AFFIRMATIVE 32
#define BUTTON_NEGATIVE 64
#define BUTTON_GETFLAG 128
#define BUTTON_GUARDBASE 256
#define BUTTON_PATROL 512
#define BUTTON_FOLLOWME 1024
#define BUTTON_ANY 2048
These new flags from BUTTON_AFFIRMATIVE to
BUTTON_FOLLOWME are a part of the Team Arena MISSIONPACK code,
and animate visible commands. If you're compiling for the original Q3
then these values aren't used to generate model animation.
However, the bot code does use these values to add model animation,
constructing a usercmd_t that "fakes" the movement and
keypress requests. If you want to reuse these BUTTON_* values and
have bot support (and you aren't planning on doing a TA version of your
mod) then you'll have to remove the bot usage for them as well. A quick
search through the code shows where they're used.
There are, however, still values that you can use: they're above
BUTTON_ANY. The values 4096, 8192, and 16384 (corresponding to
button12, button13, and button14 respectively).
It's possible that higher values might also work, but sometimes the
network code clips a 32-bit value (the int that these flags are stored
in) down to 16-bits to save space.
Lastly, the flooding problem described in section 5 has been reduced
considerably. Commands that are issued no longer create a flood induced
lag while they're held up. However, I've noticed that sometimes a
command fails to get through to the server (two say_team binds pressed
in quick succession often does it).
|