Pre-Initialization

For a normal (non-debug) build, after you compile your program, but before the final "image file" containing the compiled program is created, TADS performs a process called "pre-initialization."  During this process, you can set up the initial state of your objects.  After pre-initialization is completed, the compiler creates the final image file, storing all of the changes you made during pre-initialization.

 

Note that the compiler does not perform pre-initialization when you compile for debugging (by specifying the "-d" option to t3make, for example).  Pre-initialization will still happen when you compile for debugging, so you don't have to worry about taking into account the debug or non-debug mode when writing your program; it's just that the pre-initialization step is deferred in debug mode until you actually run the program with the interpreter.  This is important because it gives you a chance to trace through your pre-initialization code using the debugger, which wouldn't be possible if this step were completed during compilation.

 

The advantage of performing initializations during the pre-initialization step is that pre-initialization happens only once, immediately after you compile your program.  This step is not repeated each time a user runs your program.  If you have to perform any complicated initialization steps that take a noticeable amount of time to run, you can hide this delay from the user by performing the steps during pre-initialization.

 

The default start-up code performs the pre-initialization step.  If you're not using the default start-up code, you must provide your own pre-initialization mechanism.

PreinitObject

During pre-initialization, the library scans the program for all instances of class PreinitObject, which is a simple class the library defines; PreinitObject is a "mix-in" class, which means that any class can inherit from PreinitObject as well as other classes without complications.

 

For each PreinitObject in the program, the library calls the object's execute() method.  Each class that inherits from PreinitObject should override execute() to specify its initialization code.

 

By default, the order in which the library initializes PreinitObject instances is arbitrary.  However, an object's initialization code might in some cases depend upon another object having been initialized first.  In these cases, an object can define the execBeforeMe property to a list of the objects that must be initialized before it is; the library will ensure that all of the listed objects are initialized first.  In addition, an object can define the execAfterMe property list to a list of the objects that must be initialized after it is.

 

For example, to define an object that cannot be initialized until after another object, called "myLibInit", has been initialized, we would write this:

 

myInitObj: PreinitObject
  execute() { /* my initialization code */ }
  execBeforeMe = [myLibInit]
;

InitObject

The library also defines a class called InitObject, which works the same way as PreinitObject, but is used during normal start-up rather than pre-initialization.  Just before the default start-up code calls your main() routine, it invokes the execute() method on each instance of InitObject.

 

As with PreinitObject's, you can use the execBeforeMe and execAfterMe properties in your InitObject instances to control the order of initialization.

Add-in Libraries

The PreinitObject mechanism makes it easy to perform special initializations for add-in library modules.  Because the library scans all instances of PreinitObject, an add-in module will be automatically included in the pre-initialization process simply by virtue of being included in a build.

 

Note that the best way for a library module to use PreinitObject is to define a special object whose only purpose is to be the main initialization object for the entire module.  This object's execute() method can perform the library-wide initialization.  The advantage of using a single initialization object for the library is that user code (as well as other add-in libraries) can more easily create dependencies on the library, where the order of initialization is important: you simply document the name of your library's initialization object so that users can add that object to their execBeforeMe and execAfterMe lists as needed.

 

Note that add-in libraries are free to perform global object scans themselves in the course of their own execute() implementations.  This allows an add-in library to add global functionality to all objects in the program, such as by setting up library-specific networks of derived information.

Derived Information

The most common type of task to perform during pre-initialization is setting up "derived" property settings.  A derived setting is one that can be determined entirely from some other information; the new setting thus doesn't actually contain any new information, but just stores the original information in a different format.

 

Derived settings might seem pointless: why would you want to store the same information twice?  Indeed, storing derived information is the source of a great many programming errors, since related pieces of information can get "out of sync" with one other if one is not careful to update all of the pieces every time any of the pieces changes.  Despite this, it's often difficult to find another way of doing something, so derived data show up all the time in practical programming situations.

 

A good text-adventure example of derived information is the way object location information is stored.  The typical game programmer assigns a location to each object in the game; we might do this with a "location" property:

 

book: Item
  location = bookcase
;

 

Clearly, if some code in your program were handed a reference to this "book" object, and you wanted to know the object that contained the book, you'd just evaluate the object's location property:

 

  objCont = obj.location;

 

Now, what if you were handed a reference to the "bookcase" object, and you wanted to find out what objects the bookcase contains?

 

One solution would be to check each object that you think might appear in the bookcase:

 

  booklist = [];
  if (book.location == bookcase) booklist += book;
  if (bookEnd.location == bookcase) booklist += bookEnd;

 

Apart from being incredibly tedious, this is a terribly inflexible way to program.  If you added one more object to your game, you'd have to add a line to this code.  And just imagine how bad it would be if you wanted to find out what the bookcase contains in more than one place in your program.

 

A more flexible and less tedious solution would be to use the firstObj() and nextObj() functions to iterate over all of the objects in the game:

 

  booklist = [];
  for (local obj = firstObj() ; obj != nil ; obj = nextObj(obj))
  {
    if (obj.location == bookcase)
      booklist += obj.location;
  }

 

You clearly wouldn't want to write that too many times, but it's not so bad to write it once and put it in a function.  Better yet, you could make this a method of a low-level class, and rather than asking whether the location is "bookcase" you could ask if it's "self", and thus you'd have a method you could call on any object to produce a list of its contents.

 

The only problem with this approach is performance.  If we had to run through all of the objects in the game every time we wanted to know what's in a container, our game would become slower and slower as it grows.  It is reasonable to expect that we would want to know the contents of an object frequently, too, so this code would be a good candidate for optimization.

 

Performance is probably the primary reason that derived information shows up so frequently in real-world programming.  We will often construct a data structure that represents some information in a manner that is very efficient for some particular use, but is terrible for other uses – our "location" property above is a good example.  When we encounter another way that we will frequently use the same data, and this other way can't make efficient use of the original data structure, we often find that the best approach is to create another parallel data structure that contains the same information in a different form.

 

So, how could we make it more efficient to find the contents of a container?  The easiest way would be to store a "contents" list for each container.  So, if asked to list the contents of an object, we'd simply get its "contents" property, and we'd be done.  There'd be no need to look at any object's "location" property, since the "contents" property would give us all of the information we need.

 

One problem with this approach is that it would make a lot of extra work if we had to type in both the "location" and the "contents" properties for every object:

 

book: Item
  location = bookcase
;
 
bookcase: Container, FixedItem
  location = library
  contents = [book, bookEnd]
;

 

Not only would it be tedious to type in a "contents" list for every object, but it would be prone to errors, especially as we added objects to the game.

 

The solution we're coming to is probably obvious by this point.  Rather than forcing the programmer to type in both a "location" and a "contents" property, we could observe that both properties actually contain the same information in different forms, and hence we could automatically derive one from the other, and store the results.  The programmer would only have to type in one of them.  The easier of the two, for the programmer, would seem to be "location", so let's make "contents" be the derived property.  During program initialization, we'd go through all of the objects in the game and construct each objects "contents" list, using the same code we wrote earlier.

 

The difference between what we were doing earlier and what we're proposing now is that, this time, we plan to store the derived information.  Each time we construct a list of objects that an object contains, we'll store the list in the "contents" property for the object.  So, without adding any typing, we'll end up with the same "contents" declaration that we made manually for "bookcase" above.  We'll construct this contents list once, during program initialization, for every object in the game.

 

There's another detail we must attend to: each time we change an object's location, we must at the same time change the "contents" property of its original container and of its new container.  The best way to do this is to define a method that moves an object to a new container, and updates all of the necessary properties.  We could call this method "moveInto":

 

  moveInto(newLocation)
  {
    /* remove myself from the old location's contents */
    if (location != nil)
      location.contents -= self;
 
    /* add myself to the new location's contents */
    if (newLocation != nil)
      newLocation.contents += self;
 
    /* update my location property */
    location = newLocation;
  }

 

As long as we're always careful to move objects by calling their moveInto() method, and we never update any object's "location" or "contents" properties directly, all of the properties will remain coordinated.

 

Many programmers who work with object-oriented languages develop a habit of using "accessor" and "mutator" methods when accessing an object's properties, rather than evaluating the object's properties directly.  An "accessor" is simply a method that returns the value of a property, and a "mutator" is a method that assigns a new value to a property.  The advantage of using accessors and mutators as a matter of course becomes clear when we consider our moveInto() example above: these methods help keep all of the internal book-keeping code for an object in one place, so that outside code doesn't have to worry about it.