Structured Legacy Scripts
Contents
Writing a structured script.plist
Most of the legacy scripts for Oolite are written without a structure. This includes the legacy native missions inside the program itself. The script just follows the timeline of the story and the majority the condition tests are put in the main level witch is evaluated on every update. This makes the scripts easy to read for a programmer, but this way the program must check a lot of conditions. A lot of these condition tests could be avoided by using a nested condition structure. This way part of the script can be excluded from evaluation and the fastest code is code that is not evaluated. But to understand how code can be improved one must first know when the code is evaluated.
Code evaluation
On startup Oolite puts all script code in one big file and whenever needed it runs this complete script file. It starts from the first line and only stops when it reaches the last line. It will never pause during a script. After completion it just waits until the code is started again. Oolite can not select a certain part of the code, or code from a certain script, it just runs them all, every time again.
Evaluation moments
Evaluation of all scripts happens every 10 game seconds. Beside this there are 3 extra moments the script is evaluated: When the player launches, when he enters a system from a witchspace jump and when he docks at a station. While docked the script is only evaluated when one of the next screens is active: GUI_SCREEN_CONTRACTS, GUI_SCREEN_EQUIP_SHIP, GUI_SCREEN_LONG_RANGE_CHART, GUI_SCREEN_MANIFEST, GUI_SCREEN_SHIPYARD, GUI_SCREEN_SHORT_RANGE_CHART, GUI_SCREEN_STATUS, GUI_SCREEN_SYSTEM_DATA. When an other screen is shown, evaluation is postponed but the timer runs further and script evaluation immediately starts when an evaluated screen is entered after the 10 seconds time-period.
There is also an extra script evaluation when a ships AI calls the function: "scriptActionOnTarget:". This however is rare, but makes that there could be extra evaluations besides the one that schedules ever 10 seconds and the 3 special ones.
Code is very fast. Even long scripts run within a fraction of a second. While jumping, launching or being docked this pause will not be noticed. But in flight one could experience a short game freezing during execution. With only a few installed OXP's bad written code will run fast enough, but the more code you add, the more important it becomes that the whole set of installed scripts will be executed as fast as possible.
The new introduced Java Script is activated on a quite different way. The code contains at about 20 well-defined places within the code a triggering moment for script starting, comparable with the legacy evaluation moments. The main difference is that it only runs that part of the javascript that is assigned to that moment and not the whole of all scripts.
Saving program time
Because Oolite just runs the whole script in its total, it is important that as much as possible unnecessary code is excluded from checking. This can be achieved by grouping pieces of program together behind one single condition. Is such a condition not met the legacy script can skip the whole group of underlying conditions. One such a main condition can be a galaxy number when the script plays only in one galaxy.
Other good groups are based on the status-string: STATUS_DOCKED, STATUS_LAUNCHING, STATUS_EXITTING_WITCHSPACE and STATUS IN_FLIGHT.
By using these groups the code is easily split in a few big pieces. It also makes sure that while flying, when code evaluation visibly stops the game, very little code is executed.
One other possibility to speeding things up is not to check everything in one condition set. When something happens only at a special occasion, you should only check the most rare event. Only when that condition is met, the remainder of the conditions is checked. See the next example where in the first case always 3 conditions are checked and in the second case the last two are only checked when the first is true. In both cases "My_Stuff" will only be executed when all tree conditions are met.
{ conditions = ( "mission_thargplans equal RUNNING", "planet_number equal 36", "d100_number lessthan 15" ); do = (My_Stuff); }
{ conditions = ( "mission_thargplans equal RUNNING" ); do = ( { conditions = ( "planet_number equal 36", "d100_number lessthan 5" ); do = (My_Stuff); } ); }
When the first condition is true, this nested set will take more time to execute, but in the majority of cases the fist condition is not true and time is saved by skipping the remainder.
Examples
A good example is the OXP Deposed until version 1.2. In the main level of the script there are 115 condition sets that have to be evaluated every time. Every condition set contains a condition where it tests for galaxy 3 or 4. That means 115 checks. By grouping them in two main sets, one for each system, it saves 113 checks (=115 minus 2). But it also does not check the galaxy 4 stuff while in galaxy 3 and the galaxy 3 stuff while in galaxy 4. This grouping happened now in the newer version 1.2.1
Within each system one then can make groups based on the status-string.
Other examples of bad time management are the legacy missions inside Oolite itself. All the game conditions keep being checked, even when a mission is completed. Such type of missions should start with a single check for completion of the mission. If yes, the code can then skip the whole remainder of the script instead of useless keep checking everything after completion of the mission. e.g The thargoid_plans mission does always, on every update, 18 condition checks, even when finished. When placed in a shell like:
{ conditions = ( "mission_thargplans equal MISSION_COMPLETE" ); do = (nothing); else = (Original_Stuff); }
there is only one condition to be evaluated after completion.
In javascript this goes even better. There a script can remove itself completely from the evaluation list so that even the first check can be avoided.
STATUS_DOCKED
Only STATUS_DOCKED is a special case. This should never be used as a main check to avoid that it runs on every station. In a normal system there is only a main station and sometimes a rock hermit. But there could be other OXP's that add stations. This status has to be subdivided into a "dockedAtMainStation_bool equal yes" group and a "stationName_string equal my_name" group. And when there is only a check for one type of station, this can replace the STATUS_DOCKED check.
Which tools to use
The more structure you apply the more difficult it becomes to write it with a simple text editor. To keep a right overview, indentation has to be used and matching brackets should be placed in line. To make this easier an editor that understands this structures should be used.
On the mac this can be achieved by using the plist editor that is present on every OSX installation disc. The complete name of this thing is: "property list editor". It is part of the developers package (filename: "developer.mpkg") that is shipped with every OSX installation disk. The program is quite simple. It allows to save your file in ascii plist or XML plist format. It also does a structure check on opening a file. But for copying pieces of code you still need a text editor.
There are probably a lot of better tools, but this one is readily available.
For example mac users can use the shareware program PlistEdit Pro. Available at: http://www.fatcatsoftware.com/plisteditpro/
See also ScriptTimer.oxp - described here (2008)