Code3Arena

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 31 | Next >>

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    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:

    1. 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!).

    2. 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).

    PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 31 | Next >>