Handling OXP Dependencies with JavaScript

From Elite Wiki

Loading/Initialisation Dependencies

The Issue

One of the common issues with OXP development is that many OXPs are dependent on the existence of other OXPs. If they only need to exist side by side this isn't a big problem and the player only has to make sure all necessary OXPs are installed. If you need another OXP to be initialised first (have it's startUp function called first) you will run into a problem. An example of this would be if say your OXP added a station above some moons in a range of systems. If your OXP ran before the OXP(s) that created the moons then it may place the stations differently or not at all compared to if it ran later or last.

This problem arises because Oolite does not guarantee the order in which the OXPs are loaded. Some version do load in alphabetical order but that isn't true across the board. There have been solutions to this problem by either running the initialisation code from a timer or from a screen change event instead of the startUp function but these methods are not good for multiple dependency chain issues and in the case of screen change events do not always trigger. Since version 1.74 of Oolite there has been a much simpler solution.

Solution

Oolite 1.74 removed the automatic calling of OXPs' reset functions (making this.reset redundant) and reloads all functions from the cache when the player dies before calling all the OXPs' startUp functions.

This allows for an OXP to call a dependency OXP's startUp function before it does its own initialising, provided it hasn't been run yet. Then have that startUp function deleted so it can't be called again until the player dies. (Please note that a JavaScript function deleted whilst it is in the middle of being called, that includes subfunctions, will still be fully executed.) This method then works through the dependency chain until all dependencies are initialised.

As an example if OXP_A depends on OXP_B which depends on OXP_C with this method it doesn't matter what order the OXPs get called in:


Oolite's Load Order: OXP_A, OXP_C, OXP_B.

  • OXP_A's startUp is called first.
  • It deletes itself and then looks for the existence of OXP_B's startUp function and as it exists calls it.
  • OXP_B's startUp first deletes itself and then looks for the existence of OXP_C's startUp. As it exists it is called.
  • OXP_C startUp first deletes itself and as it doesn't have any dependencies then executes its initialisation code and returns execution back to OXP_B's startUp.
  • OXP_B startUp now executes its initialisation code and returns execution back to OXP_A's startUp where it gets to execute its initialisation code.

All the above was within the initial call to OXP_A's startUp.

  • Now Oolite will want to call OXP_C's startUp as it is next in its list. As it has already been called and deleted it won't find it and will skip it.
  • Finally Oolite will try to call OXP_B's startUp function which also has been deleted and will also therefore be skipped.


The result is that the three OXP's have been initialised in the right order. If you tried this with a different OXP load order the results will be the same.

If the dependency OXP is not using this method you must delete that's startUp function yourself after you have called its startUp function. This may have a subtle knock on effect if that dependency OXP has it own dependencies that are handled in some other way. If that is the case you may find that you need to include those additional OXPs as dependencies of your OXP and call them first.

Well that's the theory, so here is what a OXP template using this idea should look like in practice:

this.name           = "Oxp_A_Script";
this.author         = "Author";
this.copyright      = "Copyright Info"
this.licence        = "License";
this.description    = "Some Description";
this.version        = "1.0";

this.startUp = function()
{

      delete this.startUp; // Must be done straight away to prevent loops. It doesn't stop this function from working.


      // function to handle the initialising of other scripts
      function runOtherStartUp(s) {
	if (worldScripts[s].startUp) worldScripts[s].startUp(); // Call the other startUp.
	if (worldScripts[s].startUp) delete worldScripts[s].startUp; // Make sure the other startUp is deleted as it may not be written using
                                                                     // this template.
      };

      // calling each dependency:
      runOtherStartUp("Oxp_B_Script") // Repeat for each OXP script that this OXP script depends on.


      // Test for existence of an OXP that isn't required but effects the way this OXP works.
      this.Oxp_X_Script_Exists = !!worldScripts["Oxp_X_Script"];  // Repeat for other similar OXP script checks.
      // Please note that the two not (!) operators here work by have one convert the existence of the script object into a boolean which has
      // its true/false status the wrong way round for our needs and then the other swaps the boolean's status to way we need it.


      // Do Oxp_A_Script's initialisation stuff here as required.

      log(this.name + " " + this.version +" loaded."); // This goes last so the load messages appear in a sensible order.

}

// The rest of OXP script goes here

Replace the OXP names, repeat the sections and fill the rest of the code as needed.

Event Dependencies

The Issue

There are also times when you will want for a particular event the code from your OXP to run after the code from another OXP. The method chosen above for loading can not be used for events as deleting the function will mean the event code will work once and the next time the event is triggered there is no code to call. This issue therefore requires another solution.

Solution

The current solution for this is to use the timer mechanism. In essence this involves using a timer started from your script's event to trigger code to be called later after the code from the dependency OXP's event. This method does suffer from the fact that you can not handle dependency chains very well (if at all) and you have to set the timer to trigger long enough afterwards to guarantee that the code your code depends on has already been run.

This could be done using this code:

this.eventName = function {
  this.eventTimer = new Timer(this, this.funcToRun, secondsToWait);
}

this.funcToRun = function {
  // Put your event code here.
}

Choose the appropriate names for the eventName (see Ship Script Event Handlers or World Script Event Handlers), eventTimer and funcToRun, and now many seconds to wait (secondsToWait) before it launches (usually about 1 second).

See Also