Vault-Tec Labs

Another scripting tutorial

400pages on
this wiki
Add New Page
Add New Page Talk4

Another tutorial by Agrajag of Fan Made Fallout, found [here]

I first thought about going through everything from the very beginning (including the syntax and all that jazz), but I abandoned the idea, given that there already are a number of [tutorial]s for that. Instead, I'll try to cover some of the aspects that I haven't been able to find in any tutorials elsewhere, that might be a little tricky (though by no means impossible) to figure out yourself.

In this tutorial, I will, to the best of my ability, try to explain the following:

  • Variable types
  • Procedure types, and how to make your own procedures
  • A more detailed explanation on how to read the information in commands.doc.
  • An explanation on how some of the less intuitive so called p_proc procedures work.
  • How to make new Global variables.
  • How to make new Map variables.
  • How to make new imported/exported variables.
  • A couple of tips and tricks that I've learned and come to appreciate at a few occasions when scripting.

I will not attempt to explain the basic syntax, what macros are, how to compile scripts or where to look for further information on your own. While this tutorial is for anybody with a rudimentary scripting/programming knowledge, those things are all covered elsewhere. If you want to learn how to get started with the compiler and all that, you can check out my [Scripting - Getting started tutorial].

Variable typesEdit

A variable, as you should know, is a way of easily storing, accessing and modifying a piece of data for a script. It is important to realise, however, that while the data ultimately consists of bits of ones and zeroes, the compiler will read them in different ways depending on the nature of the data. For example, one variable might store a number, while another stores a letter, or a string of letters. It is common to separate between the following variable types:

  • int - the variable stores an integer, such as "1", "-15" or "42".
  • boolean - the variable is either 0 (false) or 1 (true).
  • string - the variable is a series of letters, such as "hello world!", "Error." or foo".
  • float - the variable is a real number, such as "3.14", "-15" or "42.0001".

However, in fallout scripting, and this is where things starts to become a little confusing, there are only variables of the type "variable". When you make a new variable, the engine detects what type of variable it is, and treats it like so. For example, if you write

 variable x := 5;
 x += 2;

you create a variable x. When you set it to equal 5, the engine will treat it as an integer, and so when you add 2 to it, it will become 7. If, on the other hand, you write this:

 variable x := "5";
 x += 2;

you will create a variable x that is interpreted by the engine as a string. The "+" operator works differently for strings and integers: integers will be added together with the base ten (common mathematical addition), whereas strings will simply be concatenated. Hence, in the last example, 5+2 will become 52, because the variable x is read as a string.

Normally, the fallout engine's "dynamic" approach to variable types is pretty handy, and the scripter doesn't have to think about it at all. It's easier to learn and easier to handle. The downside is that there's no easy way of changing a variable from one type to another. Most of the time, you won't need to do that, but there are cases where having a more specific handling of different variable types allows for better scripting solutions. I will touch more on that in the tips and tricks section of this tutorial.

In fallout scripting, there is one more variable type to be aware of, that will play a common role when scripting. I'm talking about the "object pointer". The object pointer is a "pointer" to another "object". Now that you know what that is, let's move on.


Just kidding. Let's think of all the items, critters, scenery, walls, tiles and so on as fallout objects. Each such object can have a script attached to it (actually, I'm not so sure about the tiles, but whatever). It is common for one script to want to refer to another script. Say for example you want an NPC to start dialogue with the PC if the PC gets too close. There are several different ways of doing that, but they all rely on one script being able to communicate with another. We can think of each instance of an object as having a specific "value" that is unique to that object. This is the object pointer. My understanding of what an object pointer really is in terms of ones and zeroes is next to non-existent (I admit to having forgotten most about variable pointers in C++, when I read about that some five years ago), but knowing it is not really relevant for us when we work with fallout scripting. An abstract knowledge of what it is will suffice.

There are several different commands (all listed in commands.doc) that will return an object pointer for an object that fulfill different criterias. The most common such command is self_obj, which returns an object pointer to the object to which the script that uses the command is attached. So if I have an NPC called Bub, and in Bub's script write self_obj, I will receive a pointer to Bub. Very useful. The second most common command to return an object pointer is dude_obj. It returns an object pointer to the dude, which is to say the PC, the player character.

NOTE: An object pointer should not be confused with an object PID number. A PID number is a prototype identification number, and all prototypes in fallout has one. You can think of a prototype as object, and it is either a piece of scenery, a critter or an item (I'm actually not sure how tiles and walls and roofs are treated, but I don't think they are prototypes as such. When scripting, though, you only really need scenery, critters and items). Each fallout prototype stores things like the type of object, if it can be picked up, if it's openable, if it can be destroyed, what art should be used for the object, how many hitpoints it's got (if it's a critter), what the default script is, and some other. The prototype identification number - the PID - is simply a number that represents each different prototype there is. The pid number can be useful in certain contexts, such as when creating new objects with a script, or when checking to see an NPC is carrying a certain kind of object in it's inventory.

The difference between a PID number and an object pointer may not be obvious at the first glance. The most important thing to know is that several objects on a map can have the same prototype, thus the same PID number, while every single object on a map always have a unique object pointer. Two super cattle prods may have the same PID number (which by the way is defined by the macro PID_SUPER_CATTLE_PROD, which is 399 - this can be found in the ITEMPID.H header that comes with the mapper), but they will always have different object pointers. In the same way, you can have ten identical dwarfs placed next to each other, all using the same script and the same PID number, but still have each one of them behave differently from each other.

To further illustrate the importance of knowing the difference between a PID number and an object pointer, consider this code:

 //add a beer to the dude's inventory
 //implement the effects of beer on the dude

It will cause the game to crash. The reason for this is that the arguments of add_obj_to_inven and use_obj_on_obj are both of the type object pointer, and PID_BEER is a PID number, not an object pointer. We either need to possess the object pointer of a specific beer in order to move it to the PC's inventory (for example if the PC buys a beer from a bartender, we can use the object pointer of a beer in the bartender's inventory), or we can create a new beer and put it directly in the PC's inventory (removing a beer from the bartender's inventory if necessary). The latter is usually sufficient, and easier to do than the former. Likewise, to use a beer on the PC, we need to have a specific beer that we can use - we need an object pointer. In this example, the following code would probably do what we were looking for:


Note that the first argument of create_obj is of the type PID - it creates a new instance of an existing prototype and returns the object pointer to it.

Now that you know a little about different variable types, let's have a closer look at the different types of procedures.

READERS'S NOTE: The use of pointers is related to the inability of a function to modify directly the value of a variable outside its scope. Instead, they can modify "what a value points to", to effectively alter a value outside of their scope. And they're called pointers because they point to what we want to change, instead of being that object.

Procedure typesEdit

A procedure, or a function (I won't distinguish between the two - whenever I say function, I mean procedure, and vice versa. They are the same to me.) is like a variable, except instead of storing a specific piece of data, it stores a whole piece of code. You've probably already figured out how to make them, but if you haven't, here follows a short description on that.

First, you need to define the procedure at the beginning of the code. This is done by writing

 procedure my_procedure;

After that, somewhere further down the script, you need to write the following:

 procedure my_procedure begin

After begin and before end is (obviously) where your own code goes. It can be virtually anything. Note that the procedures doesn't have to be in the same order as the procedure defines in the beginning of the code.

A procedure can be of different types, just like the variables. The type of the procedure is determined by what it returns. This is where the procedure differs most from a macro. If you recall, a macro simply replaces the macro name with a piece of code (or a variable, or anything, really). When the script is compiled, all macros are in fact replaced by the compiler to whatever they are macros for. Where a macro is like an abridgement that is later expanded again by the compiler, a procedure call causes the engine to make a jump in the code, from wherever the procedure was called from, to the beginning of the procedure code, and after the code in the procedure is run, the engine jumps back to where it was, and brings a value back with it.

The most common procedure types are these:

  • void - the procedure returns nothing.
  • int - an integer is returned.
  • string - a string is returned.
  • boolean - either true (1) or false (0) is returned.
  • object pointer - an object pointer is returned. The commands self_obj and dude_obj are procedures of the type "object pointer".

To call a procedure of the type void, you have to type call, followed by a blank step, followed by the procedure name, followed by a semicolon. A procedure that returns a value, which is to say all procedures except those of the type void, must not use the call operator. Instead, the value that is returned must go somewhere - into a variable, as the argument in another function, as part of an expression in an if check, and so on. If you just make the procedure call without storing the returned value anywhere, it will be tantamount of just writing that value without any context, and so the code will not be compiled.

You are best advised to make sure that the variable in which you store the returned value is of the same type as the value returned. There will be no compilation errors if this is not the case, but the variables might not be converted into the right type. Here's an example:

 variable x := 5;
 procedure foo begin
    return "2";

We make an integer variable called x, and a procedure called foo, which will return "2". However, the value returned is of the type string, which is indicated by the quotation marks.

 variable y;
 y := x+foo;

What do you think will be displayed? Will y be treated as an integer or as a string? The latter is the case - y will become the string "52", and that is also what will be displayed. I don't know exactly how the engine treats the different variable types in situations like these, so the only way you can really find out is by testing yourself. If a variable appears to "break", it might be the result of the variable types not translating the way you thought they were going to do. Might be worth keeping in mind, although the occasions where it might be a problem are not that frequent.

As you saw from the above example, in order to make a procedure return something, you need to add return <value> at the end of the procedure. It doesn't strictly have to be at the end, but whenever the engine reaches return, the rest of the procedure is cancelled.

As you might have noticed, a procedure may take arguments. This is incredibly useful for optimising your code, and strictly necessary to accomplish certain tasks. What happens is pretty much that you create a new variable for the procedure to use within the code of the procedure, and when you make the function call, you can assign the starting values of those variables - that's the arguments of the procedure. I'll explain how to make functions like that.

First you need to define it at the top of the script:

 procedure my_procedure(variable arg1, variable arg2, ... , variable argN);

You can have as many arguments as you want (not sure if there's a technical limit, but you probably won't need that many), as hinted by the argN in the above code.

The actual procedure looks like this:

 procedure my_procedure(variable arg1, variable arg2, ... , variable argN) begin

The variables arg1, arg2, ... , argN can be used as normal variables, but only within the procedure. This means you can have several procedures with argument variables of the same name, if you like. To call the above procedure, you write this:

 call my_procedure(<value1>,<value2>, ... , <valueN>);

Note that you only use call when it's a procedure of the type void. <value1> through <valueN> are any values. Just as before, you should keep in mind what type of variable you expect them to be. A function call that uses the wrong variable types as arguments can screw the procedure over.

Now, I think I've covered all you really need to know about procedures. There seems to be some limitations on what you can use as arguments in function calls - IIRC you can't have your own procedures as arguments, even though they are of the right type. There may be other limitations, but I don't quite remember what they were. Any way, now you've learned the hardest part in understanding how to read the commands.doc - your number one source of available hard-coded procedures in fallout 2.

Reading commands.docEdit

It is very important to know how to learn about the functions calls you can make. These are listed in commands.doc, and so it is very important to know how to read that document properly.

An entry in commands.doc may look like this:

 obj_close                  Attempts to close a given object (what) if it is of an openable type.
 void              Object

obj_close is the name of the function. void is the type of the value returned by the procedure, as covered above. Next to the name of the function, to the right, is a short description of what the function does. Sometimes it's a bit hard to follow, but usually it gives a pretty good idea of what happens when you use the function.

what(ObjectPtr) is the argument taken by the procedure. ObjectPtr tells us that the argument (called "what", although that's not necessary other than to understand the description) needs to be of the type "object pointer" to work. In this case, obj_close tells the engine to close an object that is closable, which is to say a door. We need to tell the engine what door to close, and this is where we use the function's parameter.

I can't say exactly what Object refers to in the above example, but it seems to be a general hint at what the function is doing, or what part of the engine is being used in the function. It can be one of the following: Object, Critter, Script, Anim, Inven, Meta, Sound, Party, Map, Skill, Dialog, Time, Combat, Debug.

Hopefully this explains how to read the commands.doc, and consequently, how to use the "core functions".

And now for something completely different.

The p_proc proceduresEdit

As you may or may not know, different type of objects (critters, scenery, items, etc.) can make use of a number of different built in procedures that are more or less essential for fallout scripting. The name of these procedures all end with "_p_proc". You probably already know what most of them does, as they are briefly described both in commands.doc and in the script templates that comes with the mapper. A few of them aren't as intuitive as the others though, and raises a few questions. I'll go through them now, and explain more specifically how the game handles them and how they can be used.

* start

This procedure is only supposed to be called by the engine when the script is first run, then never again, unless you specifically call it from somewhere else. However, in my experimenting, it seems to be run each time the map is entered from another map, even if you've been there before. I haven't tested this in game yet, so it might only be some setting in the mapper (but I doubt it).

* map_enter_p_proc

This procedure is called once by the engine each time the map is entered from another map. It is not run when the elevation is changed.

* map_update_p_proc

This procedure is called roughly once every 30 seconds. It is also called each time you exit a dialogue, each time you exit the pipboy screen and each time you use an item. In addition to that, it is run once when you enter the map, and, seemingly, twice when you change elevation. If you enter the map from another map, it would seem like you are first moved to the first elevation of the map, and then immediately teleported to the right elevation, causing map_update_p_proc to be run three times in a row when you enter any other elevation but the first one, from another map: once because you enter the map, and twice because you changed location. Possibly, map_update_p_proc is run once when you leave an elevation and once when you enter a new elevation (but not when you leave the map!).

* critter_p_proc

This procedure is called every "heartbeat", which means it's run ten times each second, except during combat.

* timed_event_p_proc

This procedure is called by the function add_timer_event(pointer:obj, int:time, int:fixed_param). obj is a pointer to the object whose script's timed_event_p_proc procedure you want to access. time is the number of game ticks (1 game tick = 1/10 seconds) you want to pass before the timed_event_p_proc is called. fixed_param is an variable you can set to separate between different timed events. In timed_event_p_proc, you can check the value of fixed_param against whatever number you used when calling add_timer_event. This allows for multiple uses of timed_event_p_proc in the same script.

Here's an example of how you can use timed_event_p_proc:

Suppose we want to happen regularly every 10-15 seconds. At an appropriate place in the script (which is to say, where we want the <code> to start occuring), we write something like this:


This means that the function timed_event_p_proc will be called in 5 to 10 seconds (or 50 to 100 game ticks).

  procedure timed_event_p_proc begin
     if(fixed_param == 1) then begin
     if(fixed_param == 2) then begin

The first time we add the timer event, the fixed_param variable is set to 1. After 5-10 seconds, the timed_event_p_proc is called, and rather than performing all things that are in that procedure (maybe we need to use timers for other things as well), it will pick only the things that happens when the fixed_param variable equals to one (that's the first if-clause, as you can see). To exemplify that, I've made this example so that it will only take 5-10 seconds from the time where the timer is started until <code> is ran for the first time. After that however, a new timer is added, this time with a delay of 10-15 seconds. To separate the two, I've used a different fixed_param. After <code> is run for the second time, <somethingelse> will also run, just for the heck of it, and then the timer is added again. And when that is run, it will add itself again, creating an infinite loop. Had <code> been a floater for an NPC, it would have floated a text message once every 10-15 seconds, for all eternity.

How to make global variablesEdit

Global variables are variables that can be accessed and altered by any fallout script. They are very useful for interacting between scripts.

To make one, you need to add a line to global.h and vault13.gam. In vault13.gam (you need to extract this file from master.dat - it is found in the data folder. As with most files, it can easily be altered by any text editor), scroll down to the bottom of the list. You'll find the following:

 GVAR_NEW_RENO_FLAG_4                    :=0;    //      (694)

To add a new global, simply write a new line just like that, with the variable name you prefer. Note that it must begin with "GVAR_". The ":=0;" part sets the default value of the variable. The "(694)" indicates the number of the variable. Obviously, you need to increase by one for each variable you add, so the next variable is 695. It's not strictly necessary to add the last part ("// (694)"), as it's a comment, but it's recommended, as that number corresponds to the define number you use in the header. The order of the entries determine which number it is, and if you screw up the order, the variables won't work properly.

Now, in global.h, you need to add the following:

 #define GVAR_YOUR_VARIABLE                  (695)

Obviously, "YOUR_VARIABLE" needs to be whatever the name of your variable was. The entry can be anywhere in global.h, but the number must be the same as that in vault13.gam.

Finally, make sure that global.h is #included in your script (normally define.h includes all other useful headers, so if you have included define.h, you have also included global.h). Now you should be ready to use your new variable. To access the variable, use the command global_var(GVAR_YOUR_VARIABLE);, and to give the variable a new value, use set_global_var(GVAR_YOUR_VALUE,NEW_VALUE); (needless to say (yet I say it anyway), NEW_VALUE is whatever integer you want the global to be). That's it, really. Globals can only take the value of integers, so you can't store strings or object pointers or anything like that in them. They're still useful, though.

How to make map variablesEdit

Map variables are variables specific to a map, and they can be accessed and altered by any fallout script on that map.

To make a map variable, you need to add a line to the header file and the gam file for the map. If you have no map header or gam file for your map, you need to make one. Just open a new text document and save it as a .h and .gam file. The .gam file needs to have the same name as the map, so if your map is called, your .gam file must be named imarket.gam. The .gam file must be placed in the same folder as the .map file, which is normally the fallout2\data\maps folder.

The map header has the following syntax, as can be derived from any of the other map headers:

 #ifndef IMARKET_H
 #define IMARKET_H
 #define MVAR_YOUR_VARIABLE                  (0)

For each map variable you want, you have to add a define line like the one above. "YOUR_VARIABLE" can be replaced by anything you want, but all map variables must begin with "MVAR_". As usual, the number in brackets indicate the number of the map variable, and it needs to correspond to the place of the variable in the list in the .gam file.

Which brings me to the .gam file. The syntax looks like this:

 MVAR_PC_WORKING                          :=0;     //       (0)

As with the globals, the numer in brackets is only a comment, not strictly necessary, but useful in the sense that you don't have to count all the entries to know which number the variable corresponds to. The ":=0;" part sets the default value of the variable to 0. The default value can be anything you like.

Finally, you have to #include the map header to your script. The address will be slightly different depending on where the header file is in reference to the script file. If you have your scripts in the folder scripts\cotc and your headers in the folder scripts\headers, you have to type the following to #include a header:

 #include "..\headers\header.h"

Of course, header.h is to be replaced with the actual name of the header. So, to include a header called imarket.h, you write #include "..\headers\imarket.h". After that, you're ready to use your map variable. To access the variable, use the command map_var(MVAR_YOUR_VARIABLE);. To change the value of a map variable, type set_map_var(MVAR_YOUR_VARIABLE,NEW_VALUE);, where "NEW_VALUE" is any integer you want the variable to be. Just like globals, map variables can only be integers.

How to make imported/exported variablesEdit

If you want a variable from one script to be used in another script, you can export that variable and import it in the other script. It can be useful in a number of occasions, but you need to take care in using it, as you obviously can't import a variable that haven't been exported yet. You must be sure that the variable you want to import is already exported.

It's fairly straight forward to use, really. All you need to do in order to export a variable is write "export variable <variable name>" instead of just "variable <variable name>" when you define the variable in the beginning of the script. Similarly, to import a variable, you write "import variable <variable name>" at the beginning of the script, and the variable is imported.

If a variable is exported from script A and imported in script B, it is virtually shared by the two scripts, like a global variable. The difference between exporting and importing a variable is that the variable must be exported before it can be imported. Problems are bound to occur if you try to import a variable from a script that doesn't yet exist.

What's good with imported/exported variables is that they can be of any type. So you can export an object pointer, for instance, which is useful in some circumstances.

Tips & TricksEdit

One thing I really miss in fallout scripting is the ability to make variable arrays. Arrays of variables makes optimising code a lot easier.

A variable array is basically a series of variables in one. It usually takes the following form:

 variable foobar[N] := {value0,value1,...,valueN};

N is the number of variables in the array. To access the n'th variable in the array, you just type foobar[n]. This is particularly useful in loops. For example, you can have a hundred variables with different values in them, and display them all with just two lines of code, by looping through the array.

A far fetched work-around for that can sometimes be to make use of the fact that we can retrieve information from .msg files. Instead of storing the values of the variables, we can store them in an .msg file. That way, instead of typing foobar[N], we type mstr(N) for the same effect. The problem is that the message_str procedure is of the type string, meaning that everything it returns will be treated as text, rather than integers, or whatever else we wanted. This is of course all well if we wanted it to be strings, but if we want integers, what then? Here's a little trick I figured out...

The idea comes from the discovery that the condition n == "n" will be considered true when n is a number. So 7 == "7" is regarded by the compiler as true, even though the left side of the expression is a number, and the right side is (technically) a string.

Suppose we have a .msg file filled with (positive) numbers that we need to access as integer values. We can save those values by using the following (either very beautiful or very ugly, depending on your taste) procedure:

  procedure msg2int(variable msg) begin
     variable int := 0;
     while(int != msg) do begin
        int += 1;
     return int;

All you need to do is input the (string) value from the .msg file as the argument in the function, and the function will keep increasing the (integer) variable int by one until it is equal to the value received from the .msg file. When it is equal, the int variable will be returned - the msg2int procedure returns a value of the integer type.

This might all seem abstract - what can you use this stuff for? Well, here's an example of how to use it:

  procedure Perks begin
     variable temp;
     //the_perk is the number of the _listed_ perk (note: the number of that perk
     //might be different in define.h, due to unused perks etc.).
     //message_str(NAME,the_perk) returns the name of the perk with that number,
     //as stored in Statman.msg
     the_perk := 5*page + 101;
     if(page == 0) then begin
        the_perk := 100;
   //perk0-perk5 are the numbers of the perks in define.h, retrieved from the
   //Statman.msg (in which the numbers have been filled in by hand). When they
   //are retrieved from the msg file however, they are of the type "string",
   //meaning they can't be used as proper integers, meaning they can't be used
   //by the message_str command. To fix that, the procedure msg_2_int gives the
   //perk0-perk5 variables their proper values, as integers. Took a while to
   //figure that one out ;)

   temp := message_str(NAME,the_perk+100);
   perk0 := msg_2_int(temp);
   temp := message_str(NAME,the_perk+101);
   perk1 := msg_2_int(temp);
   temp := message_str(NAME,the_perk+102);
   perk2 := msg_2_int(temp);
   temp := message_str(NAME,the_perk+103);
   perk3 := msg_2_int(temp);
   temp := message_str(NAME,the_perk+104);
   perk4 := msg_2_int(temp);
   temp := message_str(NAME,the_perk+105);
   perk5 := msg_2_int(temp);

   say("Please select the perk that you wish to add/remove.");
   //only display the [previous] message if there's previous page...
   if(page > 0) then begin
   //these five will be shown in all "pages". The player options display the
   //name of the perk (message_str(NAME,the_perk), followed by an integer
   //(the has_trait-bit) - 1 means you have the perk, 0 means you don't. The
   //option leads to the perk_edit# function, which will add or remove the perk.






   //There's room for one more player option if there's no [previous] or [next]
   //button, as is the case if the page is 0 (first) or 14 (last).
   if(page == 0) then begin

   //always display the [back] button to take you back
   //don't display the [next] button if you're already displaying the last one.
   if(page != 14) then begin

The above code will allow you to add/remove any of the perks in fallout 2, through dialogue. Each perk in fallout 2 corresponds to a number. However, out of the 119 spots, only 93 are usable, and so there are gaps in the number sequence. I filled in the working numbers by hand in an .msg file, and used the above trick to show all the perks without having to write 15 procedures that look virtually the same. Note, by the way, that I had to have five different procedures to link to in the dialogue option, as it's appearantly not possible to make a function call if one of the arguments in that call is a function that takes an argument.

Another useful trick that I've noticed is that if you try to retrieve an entry that doesn't exist from an .msg file, the message_str procedure will return "Error", rather than crashing or causing other problems. Say you want to get the text that corresponds to number 104 in an .msg file, and there is no entry 104 in that file, you get "Error" instead. This is very useful if you want a more dynamic procedure for floating dialogue, for example. Suppose you have a critter that tells a number of jokes, and each joke consists of anything between 1 and 100 lines. Then you can have joke 1 begin at line 100, going through 101, 102, etc., and joke to begins at 200, and so on. Let the critter float as usual, starting at joke number*100, and for each line floated, you increase a variable by one. Now you can check if the joke is done by checking if joke number*100 + variable counter equals "Error". Here's the code for a script you can try out, if you want to (haven't tested it myself, as I just wrote it, but it should work. You'll need to write the jokes yourself, though Wink):

  #include "..\headers\define.h"

  #define NAME               SCRIPT_JOKER

  #include "..\headers\command.h"

  procedure talk_p_proc;
  procedure timed_event_p_proc;

  #define timer_jokes        (0)

  variable counter := 0;
  variable joke := 0;

  procedure talk_p_proc begin

     if(mstr(joke*100+counter) == "Error") then begin
        counter := 0;
        joke := random(1,40);


  procedure timed_event_p_proc begin
     if(fixed_param == timer_jokes) then begin
        counter += 1;
        if(mstr(joke*100+counter) != "Error") then begin

Without using this trick, you'd need a set of variables or something to keep track of how long each joke is, alternatively have a unique procedure for each joke. Needless to say, if you have many jokes, you'd need many procedures. The above trick will allow you to solve the problem with a minimal amount of code. I'm sure the fact that message_str returns "Error" can be used in other situations as well, so keep it in mind.

Also on Fandom

Random Wiki