Creating New Sexpressions in the FS2 Source Code
As told to Dynamo by Dave Baranec by: Dynamo - May 03, 2002 for Freespace Watch
DaveB passed me this little article on how to write in your own sexps using the FS2 Source Code. I'm sure you'll all find it quite useful. -Dynamo
The sexpression system in FS2 is the heart and soul of the game. All mission scripting hinges on this code. It is a Lisp-like interpreted pseudo-language. This doc is intended as a jumping point for programmers to create custom new sexpressions using the public source code.
A key point to remember is that both Fred2 and FS2 share the sexpression code. FS2 needs it to evalulate sexpressions when running a mission and Fred2 needs it to visually show mission designers what’s going to happen in the mission. As such, when adding or changing sexpressions you need to change code in both FS2 and Fred2.
The body of this doc contains the fine details of adding a new sexpression and there’s a section at the end giving a quick checklist of the steps.
The majority of the code that needs to be changed is in sexp.h and sexp.cpp in the code tree. Both FS2 and Fred2 reference this. Looking in sexp.h we see a bunch of #defines describing all the known sexpressions.
The first thing to note is the OPF_* defines. These define the types of arguments that may be passed to an sexpression. For example, when ordering a ship to fire a beam weapon at a turret on another ship, one of the arguments you can specify is a subsystem, represented by the OPF_SUBSYSTEM #define. The sexpression code uses these values to verify that sexpressions it is evaluating are legal, and Fred uses them in its interface code.
Evaluated sexpressions can return values. The next set of #defines we see are the OPR_* return types. OPR_NUMBER, OPR_BOOL, OPR_POSITIVE, etc. For example, the “and” sexpression (OP_AND) returns a boolean yes/no answer of type OPR_BOOL. More on OPR_* stuff later.
The next set of #defines are the categories. Sadly, the word “catagory” (misspelled!) is used and managed to become a permanent fixture of the code. In any case, the OP_CATAGORY_* defines are used by Fred to break down sexpressions into manageable categories for ease of use by mission designers. It is useful to note the OP_CATAGORY_CHANGE category. This is used for sexpressions that cause things to happen in a mission, as opposed to being parts of larger sexpressions (like OP_AND). Most custom sexpressions fall into this category.
After this we come to the sexpression #defines themselves. The sexpression is the bitwise OR of an (8 bit) identifier supplied by the programmer, an OP_CATAGORY_* and one or more of the optional sexpression flags (located just above the OP_CATAGORY_* #defines). Its important to note that the low 8 bits of the 2 byte sexpression value is used as a unique identifier within a given category. When combined with an OP_CATAGORY_* (bits 8-11) you get a completely unique sexpression value. So, with any given category there are a maximum of 256 (0x0 – 0xff) different sexpressions.
Step 1 is to create a new #define for our sexpression. When adding a new sexpression be absolutely sure to assign it into its proper OP_CATAGORY_ and give it its own unique identifier within the category. You’ll note that all sexpressions are organized by category – to preserve programmer sanity you should always try to maintain this organization when adding your new sexpression.
Step 2. Now that we’ve created our custom #define for our new sexpression, we need to add some more code in sexp.cpp.
Near the top of sexp.cpp there’s an array defined as
In here you need to add some info for your newly created sexpression. Add an entry and fill in the text string that will represent the sexpression in the editor (e.g “has-docked”), fill in the value of the sexpression created in the previous section (e.g. OP_HAS_DOCKED), and fill in the minimum and maximum number of arguments. For example, has-docked takes a minimum of 3 and a maximum of 3 arguments (i.e., it takes exactly three arguments). There is a special value INT_MAX which can be used in the maximum arguments field when you want to indicated an arbitrary number of arguments. You do not need to have your sexpression entry appear here in the same order as your #define does, but it doesn’t hurt to try and preserve some semblance of order.
The next step is to add evaluation code for your sexpression inside of the eval_sexp() function. Inside of this function you’ll see a big switch statement with a case: statement for every sexpression. Add a case: statement for your own sexpression and handle your sexpression as appropriate. For example, if you were creating an sexpression “walk-the-plank”, you might write a function called sexp_walk_the_plank() and you would call it from the case statement. By general convention, sexp evaluation functions are typically located in sexp.cpp.
Its important to understand the recursive nature of the sexpression system. If you are creating an sexpression “has-player-eaten-his-sandwich” chances are someone is going to use the return value of this sexpression. So your sexp_player_has_eaten_his_sandwich() function needs to return a value appropriate value based on its OPR_* define. By general convention, 1 is returned as “yes” and 0 is returned as “no”. However some sexpressions return numeric values, such as OP_SHIELDS_LEFT, and some return no value at all such as OP_SUPERNOVA_START (in this no-return-value case the appropriate value to return is 1). So, you need to make sure you return yes/no from your sexpression evaluation function so that the mission designer’s:
if player-has-eaten-his-sandwich self-destruct alpha1
evaluates correctly. I don’t want to go into too much more detail here as they should be fairly obvious when looking through the already-existing sexpressions for examples.
The next step is to fill in code for our sexpression inside of query_operator_return_type(). This is where the OPR_* defines come in. This function is a simple switch statement. Stick a case: statement in for your sexpression in the appropriate place to define its return type.
The next step is to fill in code for our sexpression inside of query_operator_argument_type(). This is similar to the sexpression return type but is used to define the types of the incoming arguments to the sexpression itself. Again we’re dealing with a switch statement. Put in a case: statement for your sexpression and be sure to return the proper type of each argument the sexpression takes, based on argnum (zero based index).
So for example, the OP_AND sexpression takes two arguments A and B and returns the logical AND of the two. So, the type of argument for both argnum 0 and 1 is OPF_BOOL. On the other hand, the sexpression OP_IS_DISARMED_DELAY takes two different argument types. Argument 0 is positive number, and argument 1 is a ship. If you look at the case statement for OP_IS_DISARMED_DELAY you’ll see how this is handle appropriately.
The final step is to add help text for the sexpression in Fred. Inside the file sexp_tree.cpp (in the Fred2 subproject) you’ll see a big array
Add an entry for your own sexpression here. It’s a pretty straightforward step.
Now, your new sexpression is ready to go! Fire up Fred2 and give it a shot. If you create any missions using this custom sexpression, anyone who wants to use your sexpression needs to be running an executable based on your code. Same thing goes for Fred2 for anyone who wants to edit your mission.
New Sexpression Checklist
- Add a properly constructed #define for your new sexpression to sexp.h
- Add a properly constructed entry to the Operators array in sexp.cpp
- Write your code to handle the sexpression and hook it properly into eval_sexp()
- Fill in the appropriate code in query_operator_return_type()
- Fill in the appropriate code in query_operator_argument_type()
- Fill in a help text entry in sexp_tree.cpp
- Test your code, sucka!