Hacking on Word War vi
The best way to get the code if you want to hack on it is to use cvs.
Do the following at the shell prompt:
$ mkdir wwvi $ cd wwvi $ export CVSROOT=:pserver:anonymous@wordwarvi.cvs.sourceforge.net:/cvsroot/wordwarvi $ cvs loginAt this point, it will ask you for a password. Just press return.
$ cvs -z3 co wordwarvi
This will check the code out into a directory called wordwarvi
Every day or so you should probably do "cvs update" to pick up
any changes that might have been made in the repository (depending
on how active things are. Over the last year and a half, I averaged
about 0.8 commits per day, but it fluctuates.)
First a bit about how the code is organized.
Mostly, the code is in one big file, wordwarvi.c, simply because this all evolved from something which began as a way for me to kill time while I was bored. I didn't exactly set out to make a game so much as a game just kind of happened. You might think, "Oh jeez, the code's probably a mess," and you'd be partly right, but only partly. It's held up pretty well and has needed no major reorganizations. I've been programming for a very long time now, about 25 years, so I can sometimes get away with just winging it without producing a mess, esp. if it's a fairly straightforward, mostly singlethreaded app like this (yes the audio code is somewhat multithreaded, but, not in a complicated way.) With computers being as fast and flush with memory as they are these days, compiling and editing such a file is not a big deal. There's no need to wonder too much about where something is defined, it's likely in wordwarvi.c. Just hit slash and search what you're looking for (you are using vi or vim for editing right? Surely no heretics would dare to besmirch the source code by touching it with Emacs.)
The code is in C (definitely not C++, I'm not a big fan of C++ and the STL at all -- have you ever profiled an STL-using C++ program? Try it sometime.).
The style is very K&R-ish, it is pretty much along the lines of The Linux kernel coding style.
Ok, enough of that, on to the code. Let's start with an overview of the source files, and then take a look at main(), which is at the bottom of wordwarvi.c.
[scameron@zuul wordwarvi]$ ls -l *.[ch] -rw-rw-r-- 1 scameron scameron 3040 2008-12-03 18:13 joystick.c -rw-rw-r-- 1 scameron scameron 1679 2008-07-19 15:53 joystick.h -rw-rw-r-- 1 scameron scameron 19321 2008-12-13 07:13 levels.h -rw-rw-r-- 1 scameron scameron 4023 2008-12-06 00:30 ogg_to_pcm.c -rw-rw-r-- 1 scameron scameron 928 2008-12-03 17:55 ogg_to_pcm.h -rw-rw-r-- 1 scameron scameron 8399 2008-12-15 19:11 rumble.c -rw-rw-r-- 1 scameron scameron 1258 2008-07-19 17:48 rumble.h -rw-rw-r-- 1 scameron scameron 305 2008-12-03 17:57 stamp.c -rw-rw-r-- 1 scameron scameron 932 2008-12-11 20:22 version.h -rw-rw-r-- 1 scameron scameron 386951 2008-12-17 17:40 wordwarvi.c -rw-rw-r-- 1 scameron scameron 12267 2008-12-08 17:25 wwviaudio.c -rw-rw-r-- 1 scameron scameron 5360 2008-11-22 11:44 wwviaudio.h [scameron@zuul wordwarvi]$
joystick.c and joystick.h contain code for dealing with the linux input layer to talk to the joysticks and get joystick motion and button press events.
levels.h defines many constants and data structures which are used to control how each level in the game appears, how rough or smooth the terrain is, what kind and how many of each enemy are present in the level, how some of those enemies behave, etc.
ogg_to_pcm.c and ogg_to_pcm.h are derived from the source to oggdec, which is an ogg decoder. It's for decoding ogg data to PCM data that the audio hardware is wanting. It just decodes, it doesn't playback.
rumble.c and rumble.h deal with the xbox 360 game controller's rumble feature.
stamp.c is generated by the Makefile for a strange little joke in the game (to make it give you a million points per level if you build the code from source within the last hour. Hey, the first thing the game tells you is "Uuuuuse the Soooource!!!" It's not kidding.)
version.h just contains the version number of the game.
wwviaudio.c and wwviaudio.h define the audio playback engine the game uses, as a layer on top of portaudio.
Finally, wordwarvi.c contains the meat of the game.
A first look at the code: main() in wordwarvi.c
(You should open up wordwarvi.c in another window, and
search for 'main(' while reading what's below. You
can
browse the word war vi source code via your
browser as well.).
At first, there's just a bunch of initializing of variables, parsing commandline options and the .exrc config file, setting upt portaudio, reading in audio data, a bunch of gtk/gdk stuff to set up the main window, colors, callbacks, opening up joystick devices, the rumble device, etc.k An important line of code is this one:
timer_tag = g_timeout_add(1000 / frame_rate_hz, advance_game, NULL);That sets up a timer which calls the function advance_game 30 times a second (or at some other rate if frame_rate_hz is changed from its default value.) The advance_game() function is the heart of Word War vi, it is the main game loop. Shortly after this timer is set up, this line of code occurs:
gtk_main ();That is gtk's main loop. It gathers events from the keyboard and mouse, and dispatches them to whatever callbacks are registered. It also dispatches the timer to advance_game 30 times a second. It doesn't return right away, but only when the program is exiting. Next, we'd want to look at advance_game(). But before we can understand that, we need to know about a few data structures in the game.
The most important data structure in the game is called "go" (for game object) and it is an array of struct game_obj_t's.
There is one struct game_obj_t, an element of "go", for every single object in the game, every enemy, every bullet, every spark, every hunk of debris, in the game. (Some exceptions: some text displayed on the screen, the terrain, and the background stars). Pretty much every other thing in the game has a game_obj_t associated with it.
So what's in a struct game_obj_t?
struct game_obj_t { int number; /* offset into the go[] (game object) array */ obj_move_func *move; obj_draw_func *draw; obj_destroy_func *destroy; struct my_vect_obj *v; /* drawing instructions */ int x, y; /* current position, in game coords */ int vx, vy; /* velocity */ int above_target_y; int below_target_y; int color; /* initial color */ int alive; /* alive? Or dead? */ int otype; /* object type */ struct game_obj_t *bullseye; /* point to object this object is chasing */ int last_xi; /* the last x index into the terrain array which */ /* corresponds to the segment directly underneath */ /* this object -- used for detecting when an object */ /* smacks into the ground. */ int counter; /* a counter various object types used for var. purposes */ union type_specific_data tsd; /* the Type Specific Data for this object */ int missile_timer; /* to keep missiles from firing excessively rapidly */ int radar_image; /* Does this object show up on radar? */ int uses_health; struct health_data health; struct game_obj_t *next; /* These pointers, next, prev, are used to construct the */ struct game_obj_t *prev; /* target list, the list of things which may be hit by other things */ int ontargetlist; /* this list keeps of from having to scan the entire object list. */ get_coords_func gun_location; /* for attaching guns to objects */ struct game_obj_t *attached_gun;/* For attaching guns to objects */ struct my_vect_obj *debris_form;/* How to draw it when destroyed. */ };
Some of the more important of the above structure members are:
Member | What it's for |
move | function called every frame (30 times per second) to move the object. These functions are, by convention, named xxxx_move, where xxxx is the type of object being moved. So, for instance, if you want to know how the octopus enemies in the game move, you would look for octopus_move(). |
draw | function called to draw the object (when it's on screen) for each frame (30 times per second.) These functions are, by convention, called xxxx_draw, where xxxx is the type of object to be drawn. However, if the object is simple, and can be draw just by a series of lines, it may use draw_generic(). and provide instructions for draw_generic in the "v" element. (non generic draw functions may explicitly call draw_generic as a base function.) |
x,y | Coordinates of the object in game world |
vx, vy | Velocity of the object in the game world. |
v | Pointer to instructions on how to draw an object (mostly this is just an array of relative coordinates of connected vertices in a line drawing, with some line break and color change instructions in the mix.) | next, prev | pointers to the "next" and "previous" elements in the "target list", which is the subset of "shootable" things in the game. The "go" array contains every object, but when a laser or bomb is moving through the air it is nice not to have to check its coordinates against every object in go[], as that is a lot of objects, and most (e.g. sparks) cannot be hit by a laser or bomb. The "target list" concept lets us only check against that subset of objects which may actually be hit.) |
There are a couple of pointers, "player", and "player_target." Usually (except for xmas mode) these point to the same thing, to go[0], which represents the player's ship. (In xmas mode, player points to rudolph, and player_target points to the sleigh.)
You will often see that in the "move" function of an enemy, it will refer to player_target to find out how near to the player, and in what direction the player lies in order to determine what action it should take.
To control which elements of the go[] array are used and which are free, seeing as how they dynamically are made active and inactive during the course of the game, there is another variable, free_obj_bitmap[], which is a big array of 32 bit ints. One bit in one of the ints in this array indicates whether the corresponding element of go[] is free or allocated. There are a couple of functions to find a free slot and allocate it, and to free an object. find_free_obj() allocates an object, setting a bit in free_obj_bitmap, and free_obj() deallocates an object, clearing the corresponding bit in free_obj_bitmap. Though I use the term allocate and free, this is not malloc()/free(), nor new/delete or anything like that. The go[] array is statically allocated. I am just doing my own internal allocation. My allocator, knowing that all elements of go[] are the same size, can do some optimizations which make it faster than allocating with malloc as objects are needed. (This is the kind of place where C++ and the STL die a horrid death.) Realize I'm doing my own "allocation" individually for every single spark in an explosion, for example. It must be fast.
There is a game_state variable, which is a structure that is a mish mash of various global game state. Things like viewport coordinates and velocity (arguably these should be separated out into their own structure), current score, whether sound effects are on or off, whether music is on or off, some timer variables to control the rates of the player firing lasers and bombs, and various other global state. This is probably one of the messier areas of the code, in terms of aesthetics.
There is a "terrain" variable:
struct terrain_t { /* x,y points of the ground, in game coords */ int npoints; int x[TERRAIN_LENGTH]; int y[TERRAIN_LENGTH]; int slope[TERRAIN_LENGTH]; } terrain;This "terrain" variable contains the x,y coords of one endpoint of each line segment making up the terrain, along with the slope of that line segment. (The slope is used for figuring out how chunks of debris bounce.)
Together, the game_state, terrain, and go[] arrays pretty much encompass the state of the game (there are no doubt some minor details not included in those three though.)
So, how are these three things initialized at the beginning of a game? The terrain array is filled in by a fairly simple fractal algorithm at the beginngin of each level. The function which does this is called generate_terrain(). Have a look at it if you're curious.
The game_state is mostly initialized by initialize_game_state_new_level and start_level. (Various things have to be initialized at various times, so it's a bit scattered around.)
The most interesting one is the game object array, go[]. This is also initialized by start_level(). This start_level() function calls many functions named add_xxxx, where xxxx is some kind of object, and it is responsible for adding objects of that type into the go[] array. (I use the word "type" loosely here, for all elements of go[] are of the same type: struct game_obj_t.)
For example, here's a section of start_level():
generate_terrain(&terrain); add_buildings(&terrain);/* Some FreeBSD users report that */ /*add_buildings() causes crashes. */ /* Commenting this out on FreeBSD */ /* may help, but, no buildings. */ /* I've looked at the code, but */ /* don't see anything wrong with it. */ add_humanoids(&terrain); add_bridges(&terrain);That obviously generates the terrain, adds buildings, humanoids (the guys you pick up), and bridges to whatever level is about to start. Then, there is an array defined in levels.h, called leveld[], which contains a specification of what objects are in a level, and how they are distributed in that level. This specification is examined and for each kind of object another add_xxxx function does the work of adding that type of object into the go[] array. Most of those add_xxxx functions loop through however many of the type of object they are to create, and call another function, add_generic_object, which adds an object with some things specified, and then this returned object is further customized. add_generic_object() is worth having a look at, It allocates an object with find_free_obj, then initializes position, velocity, move and draw functions, color, vector of points (for drawing), what type of object it is, whether it should be on the target list, whether (and maybe how long) it should live, etc.
Now we're ready to look at the advance_game() function, which gets called
by the timer we set up 30 times a second.
advance_game(), called once per frame.
Have a look at the code.
There are a few sections I'm going to ignore. There is a section for handline when the game is paused. when the help screen is active, if the user is in the process of quitting, and so on. Those are really detours that don't come into play while the game is actively being played, and it's a kind of messy state machine that flips between these modes, but these special modes themselves do more or less the same thing as the regular game-in-play mode: they check for user input, and draw the screen. They skip the step of moving the objects, but I'm getting ahead of myself. The gist of this advance_game function is as follows:
This function gets called whenever an "expose" event to the drawing area in the main window comes in. That "expose" event gets triggered 30 times a second, because it's triggered at the end of advance_game by an explicit call to gtk_widget_queue_draw().
This function draws the terrain, and the "boundaries" on the left, top, and right sides of the game area (if they're on screen) then draws the star field (calls draw_stars), and then calls draw_objs to draw all the objects. Then it calls draw_radar (which only draws the borders of the radar). Then, it draws the help screen or quit screen if either of those are active.
The draw_objs function loops through all the objects in the game, and if the object is alive, it draws it on the radar if it's the type of thing that shows up on the radar, checks if it's on screen, and if so, either draws it directly if the objects draw function is NULL, or calls the object's draw function. How to add a new kind of enemy
Ok, that may be enough exposition about how the game works in general. Suppose you want to add a new type of enemy into the game, how would you do it? What would be the steps?
Let's take as an example the "big" rockets which were added to the game rather lately. i'm taking this one as an example because it is rather simple, and because I know the change went in as a single commit. Here is the diff which represnets all the changes needed to add this new enemy type to the game.
First let's have a look at the changes in levels.h, and see what's going on there.
#define OBJ_TYPE_SHIP 'w' /* Bill Gates's state of the art warship. */
#define OBJ_TYPE_GUN 'g' /* ground based laser gun */
#define OBJ_TYPE_ROCKET 'r' /* ground based rockets */
+#define OBJ_TYPE_BIG_ROCKET 'I' /* ground based rockets */
#define OBJ_TYPE_SAM_STATION 'S' /* ground based missile launching station */
#define OBJ_TYPE_GDB 'd' /* GDB enemy */
#define OBJ_TYPE_OCTOPUS 'o' /* a big ol' octopus */
Here, the line:
#define OBJ_TYPE_BIG_ROCKET 'I' /* ground based rockets */get added to the code.
For each kind of object there is a constant, OBJ_TYPE_XXXX for that object which defines a unique character by which that type is recognized (crude, sure, but it works.) So we add a new one for our new one in with the others.
Next, there are a series of changes like this:
{ OBJ_TYPE_SAM_STATION, 8, DO_IT_RANDOMLY, 0 }, { OBJ_TYPE_GUN, 18, DO_IT_RANDOMLY, 0 }, { OBJ_TYPE_KGUN, 25, DO_IT_RANDOMLY, 0 }, - { OBJ_TYPE_AIRSHIP, 1, 90, 0 }, + { OBJ_TYPE_AIRSHIP, 4, 90, 0 }, { OBJ_TYPE_WORM, 1, DO_IT_RANDOMLY, 0 }, { OBJ_TYPE_BALLOON, 1, DO_IT_RANDOMLY, 0 }, { OBJ_TYPE_GDB, 9, DO_IT_RANDOMLY, 0 }, { OBJ_TYPE_OCTOPUS, 1, 75, 1 }, + { OBJ_TYPE_BIG_ROCKET, 15, DO_IT_RANDOMLY, 0 }, // { OBJ_TYPE_TENTACLE, 0, DO_IT_RANDOMLY, 0 },(It seems I increased the number of airships as well, but this is unrelated. Oops.) These are just adding a specified number of the new object type OBJ_TYPE_BIG_ROCKET into each level of the game. There are extensive comments in levels.h which explain what those structures are. Read them.)
Then we get into the changes to wordwarvi.c
#define NROCKETS 20 /* Number of rockets sprinkled into the terrain */
#define NJETS 15 /* Number of jets sprinkled into the terrain */
#define LAUNCH_DIST 1200 /* How close player can get in x dimension before rocket launches */
+#define BIG_ROCKET_LAUNCH_DIST 200 /* How close player can get in x dimension before rocket launches */
#define MAX_ROCKET_SPEED -32 /* max vertical speed of rocket */
#define SAM_LAUNCH_DIST 400 /* How close player can get in x deminsion before SAM might launch */
The above change is just defining a constant which gets used by the big rocket's
move function to know when to launch.
@@ -381,6 +382,7 @@
score_table[OBJ_TYPE_MISSILE] = 50;
score_table[OBJ_TYPE_HARPOON] = 50;
score_table[OBJ_TYPE_ROCKET] = 100;
+ score_table[OBJ_TYPE_BIG_ROCKET] = 400;
score_table[OBJ_TYPE_SAM_STATION] = 400;
score_table[OBJ_TYPE_BRIDGE] = 10;
score_table[OBJ_TYPE_GDB] = 400;
The above is adding to the score table the number of
points that are awarded for shooting down a big rocket.
If the type of object you're trying to add to the game
isn't shootable or bombable or destroyable,
(say, it's strictly scenery, like a rock,
or a tree or something) then you can skip this step.
+struct my_point_t big_rocket_points[] = {
+ { 0, -35 },
+ { -5, -25 },
+ { -5, 5 },
+ { 0, 15 },
+ { 5, 5 },
+ { 5, -25 },
+ { 0, -35 },
+ { LINE_BREAK, LINE_BREAK },
+ { -5, 5 },
+ { -10, 15 },
+ { 10, 15 },
+ { 5, 5 },
+};
+
|
Image represented by code to the left. Each square is 5 units |
struct my_vect_obj sleigh_vect;
struct my_vect_obj left_sleigh_vect;
struct my_vect_obj rocket_vect;
+struct my_vect_obj big_rocket_vect;
struct my_vect_obj jet_vect;
struct my_vect_obj spark_vect;
struct my_vect_obj right_laser_vect;
Once you've made the points array, you've got to
make a vector thing to hold them. Just the way this thing works.
It's got a pointer to the array, and a count of the number of
elements. It gets initialized elsewhere.
Now we get to some interesting code, the big rocket's move function:
+static void add_bullet(int x, int y, int vx, int vy,
+ int time, int color, struct game_obj_t *bullseye);
+void big_rocket_move(struct game_obj_t *o)
+{
+ int xdist, ydist, gl, i;
+ if (!o->alive)
+ return;
+
+ /* Should probably make a rocket-station, which launches rockets */
+ /* instead of just having bare rockets sitting on the ground which */
+ /* launch once, blow up, then that's the end of them. */
+
+ /* see if rocket should launch... */
+ xdist = abs(o->x - player_target->x);
+ if (xdist < BIG_ROCKET_LAUNCH_DIST && o->alive != 2 &&randomn(100) < 20) {
+ ydist = o->y - player_target->y;
+ // if (((xdist<<2) <= ydist && ydist > 0)) {
+ if (o->vy == 0) { /* only add the sound once. */
+ wwviaudio_add_sound(ROCKET_LAUNCH_SOUND);
+ o->alive = 2; /* launched. */
+ o->vy = -6; /* give them a little boost. */
+ }
+ // }
+ }
+ if (o->alive == 2) {
+ if (o->vy > MAX_ROCKET_SPEED - (o->number % 5))
+ o->vy--;
+
+ /* let the rockets veer slightly left or right. */
+ if (player_target->x < o->x && player_target->vx < 0)
+ o->vx = -2;
+ else if (player_target->x > o->x && player_target->vx > 0)
+ o->vx = 2;
+ else
+ o->vx = 0;
+
+ /* It's possible a gravity bomb smashes the rocket */
+ /* into the ground. */
+ gl = find_ground_level(o, NULL);
+ if (o->y > gl) {
+ wwviaudio_add_sound(ROCKET_EXPLOSION_SOUND);
+ explosion(o->x, o->y, o->vx, 1, 70, 150, 20);
+ remove_target(o);
+ kill_object(o);
+ return;
+ }
+
+ ydist = o->y - player_target->y;
+ if ((ydist*ydist + xdist*xdist) < 16000) { /* hit the player? */
+ wwviaudio_add_sound(ROCKET_EXPLOSION_SOUND);
+ do_strong_rumble();
+ explosion(o->x, o->y, o->vx, 1, 70, 150, 20);
+ // game_state.health -= 10;
+ remove_target(o);
+ kill_object(o);
+
+ for (i=0;i<15;i++) {
+ add_bullet(o->x, o->y,
+ randomn(40)-20, randomn(40)-20,
+ 30, YELLOW, player_target);
+ }
+
+ return;
+ }
+ }
+
+
+ /* move the rocket... */
+ o->x += o->vx;
+ o->y += o->vy;
+ if (o->vy != 0) {
+ explode(o->x, o->y+15, 0, 9, 8, 7, 13); /* spray out some exhaust */
+ /* a gravity bomb might pick up the rocket... this prevents */
+ /* it from being left stranded in space, not moving. */
+ if (o->alive == 1)
+ o->alive = 2;
+ }
+ if (o->y - player->y < -1000 && o->vy != 0) {
+ /* if the rocket is way off the top of the screen, just forget about it. */
+ remove_target(o);
+ kill_object(o);
+ o->destroy(o);
+ }
+}
+
Now, the above was largely copied from rocket_move, and modified for big_rocket_move.
(arguably the common code should be factored out of rocket_move and big_rocket_move).
But, what's going on here? The move functions all get passed the game_obj_t pointer of the object that's being moved. So typically they adjust o->x and o->y by o->vx and o->vy in the trivial case. But they also do other things, like try to figure ot where the player is, and what action to take, if any, based on this information.
Going through the above function step by step (keep in mind this gets called 30x per second):
The next series of changes to the code which look like:
@@ -4530,6 +4632,7 @@
case OBJ_TYPE_KGUN:
case OBJ_TYPE_TRUSS:
case OBJ_TYPE_ROCKET:
+ case OBJ_TYPE_BIG_ROCKET:
case OBJ_TYPE_JET:
case OBJ_TYPE_MISSILE:
case OBJ_TYPE_HARPOON:
are adding the new object type into switch statements in laser_move()
and bomb_move() and gravity_bomb_move() to make them behave as they
do with other shootable objects.
This change:
@@ -7254,6 +7360,8 @@
rocket_vect.p = rocket_points;
rocket_vect.npoints = sizeof(rocket_points) / sizeof(rocket_points[0]);
+ big_rocket_vect.p = big_rocket_points;
+ big_rocket_vect.npoints = sizeof(big_rocket_points) / sizeof(big_rocket_points[0]);
jetpilot_vect_left.p = jetpilot_points_left;
jetpilot_vect_left.npoints = sizeof(jetpilot_points_left) / sizeof(jetpilot_points_left[0]);
jetpilot_vect_right.p = jetpilot_points_right;
is setting up the points for the rocket drawing into the big_rocket_vect structure.
Basically just assigning a pointer to the beginning of the array containing the
points and a count of the number of elements in the array.
Next is a function to add the big rockets into the levels at the beginning
of each level. This is probably largely copied from add_rockets().
+static void add_big_rockets(struct terrain_t *t, struct level_obj_descriptor_entry *entry)
+{
+ int i, xi;
+ struct game_obj_t *o;
+ for (i=0;i<entry->nobjs;i++) {
+ xi = initial_x_location(entry, i);
+ o = add_generic_object(t->x[xi], t->y[xi] - 15, 0, 0,
+ big_rocket_move, NULL, WHITE, &big_rocket_vect, 1, OBJ_TYPE_BIG_ROCKET, 1);
+ if (o != NULL) {
+ o->above_target_y = -35;
+ o->below_target_y = 15;
+ level.nbigrockets++;
+ }
+ }
+}
+
The initial_x_location function chooses an x location based on the specification
in entry (which is ultimately coming from levels.h). Notice the call to add_generic_object
to do most of the work, then the object is slightly customized. The above_target_y and
below_target_y adjust the vertical "hit zone' for the laser for this object.
The remainder of the changes are added to start_level() to make it interpret the instructions in levels.h for the new object type:
@@ -10647,6 +10771,7 @@ add_socket(&terrain); level.nrockets = 0; + level.nbigrockets = 0; level.njets = 0; level.nflak = 0; level.nfueltanks = 0; @@ -10669,6 +10794,9 @@ case OBJ_TYPE_ROCKET: add_rockets(&terrain, &objdesc[i]); break; + case OBJ_TYPE_BIG_ROCKET: + add_big_rockets(&terrain, &objdesc[i]); + break; case OBJ_TYPE_JET: add_jets(&terrain, &objdesc[i]); break;
For each sound you add, add 1 to the NCLIPS macro, and define a new number for your new sound. In this case, two sounds were added, so NCLIPS was changed from 56 to 58, and the new new numbers were:
#define RADAR_FAIL 55 #define RADAR_READY 56Then, in the function init_clips, add a call to read_ogg_clip for each sound you want to add. For the two new radar sounds, these lines were added:
wwviaudio_read_ogg_clip(RADAR_READY, "sounds/radar_ready.ogg"); wwviaudio_read_ogg_clip(RADAR_FAIL, "sounds/radar_fail.ogg");
Then, wherever in the code you want to play your new sound, just add a line like:
wwviaudio_add_sound(RADAR_FAIL);
That's it! Well, of course, you've got to have the sound, which should be 44100
samples per second, mono, encoded as an ogg, via oggenc, for example.
Making and submitting patches
And here I'm being optimistic, and supposing someone will go to the trouble to modify my game, and make something worthwhile and send it to me to be included in the game. One can hope.
If you've checked the code out as described at the top of this page, and modified it to your liking, then to make a patch, all you've got to do is execute the following command:
cvs diff -u > mypatch.patch
You can then send me the patch. You can find my email address in the file called AUTHORS.