This page lists the various limits and gotchas (hidden dangers) you should be aware of when writing scripts.

Gotchas

Random functions in startup scripts

The script functions begin_random, random_range, and real_random_range use a pseudorandom number generator which is seeded with a fixed value on the same tick that startup scripts run. This means the results of these random functions will always be the same if used at startup.

(script startup mission
  (begin_random
    (begin (wake objective_a) (sleep_until (= 0 (ai_living_count obj_a_cov))))
    (begin (wake objective_b) (sleep_until (= 0 (ai_living_count obj_b_cov))))
    (begin (wake objective_c) (sleep_until (= 0 (ai_living_count obj_c_cov))))
  )
)

In the above example, the first expression selected by this begin_random will be the same. However, since each expression sleeps for a variable amount of time the subsequent expressions will appear more random.

Limits

Syntax nodes

ConstantHEKH1A
MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO1900132767

Syntax nodes, also called syntax data, are how scripts are represented within the scenario tag after they are compiled. Expressions and their values in the source script get compiled into compact nodes, but there is an upper limit on how many can be stored in the scenario.

If you exceed this limit, scripts will not compile. This limit is cumulative across both individual scenarios and child scenarios. So, even if the main scenario compiles, it might not work once you add its children (if its children have scripts of their own). If you hit this limit then your only choice is to remove some scripting from the level.

Script declarations

ConstantHEKH1A
MAXIMUM_HS_SCRIPTS_PER_SCENARIO5121024

The total number of scripts (startup, dormant, etc...) is also limited. If you find yourself hitting this limit then you'll need to combine together multiple scripts or remove unnecessary ones. This could include "inlining" some static scripts.

Globals

ConstantHEKH1A
MAXIMUM_HS_GLOBALS_PER_SCENARIO128512

There is a limit to the total number of declared globals in your merged scenario, either 128 (HEK) or 512 (H1A). At runtime, the values of all scenario globals and external globals are stored in the script globals datum array. Since the datum array can hold up to 1024 globals and there are nearly 500 external globals (depending on the game), H1A was able to safely expand the limit on scenario globals to 512.

Source file size

ConstantHEKH1A
MAXIMUM_HS_SOURCE_DATA_PER_FILE256kb1mb

The tools will not compile scripts for a source file above a certain size. Most projects will not be big enough to encounter this limit. If a single source file is getting too large, you can simply move some scripts to another source file as is common with the stock scenarios which separate cinematics from mission scripts.

As a last resort, you can remove comments and whitespace since they are not functional parts of the script but this will hurt the readability of your scripts. The Halo Script Preprocessor can help you strip comments from a source file.

Number of source files

ConstantHEKH1A
MAXIMUM_HS_SOURCE_FILES_PER_SCENARIO816

A scenario's scripts can be split into multiple files, but the number of files you can have is limited.

Sapien silently fails to load all of the files if there are more than the limit. It loads the first 8/16 in some order ("asciibetical"?), then excludes the rest. No errors are thrown unless scripts fail to compile because of the excluded files.

Tag references

ConstantHEKH1A
MAXIMUM_HS_REFERENCES_PER_SCENARIO256512

Various HS functions take tag paths as arguments, for example spawning an effect at a cutscene flag:

(effect_new "effects\coop teleport" teleporting_flag)

These tag paths are not stored as strings once compiled, but rather as compact tag references in the scenario. This also helps Tool determine which tags the scenario depends on and need to be included in the built map.

String data

ConstantHEKH1A
MAXIMUM_HS_STRING_DATA_PER_SCENARIO256kb800kb

Strings used in scripts are also stored in the scenario. This could include parameters like marker names:

(objects_attach chief "right hand" ar1 "")

The total amount of string data from all the scenario's scripts must remain below the limit.

Object lists

When you generate object lists by calling functions like ai_actors, the list and its referenced objects are stored in the game state. The number of lists is limited to 48 while references total 128. If you request too many object lists per tick you may see the warning no more script object lists; something bad will probably happen in the console. You can also hit this limit if you're storing too many object lists in globals. Objects which are removed from the game world are automatically removed from script object lists, but you can force this to happen earlier for non-visible objects with garbage_collect_now.

Threads

Although you can declare 512 or 1024 scripts, the game state supports at most 256 running threads, which static scripts wouldn't contribute to.

Stack space

If you've never heard of a stack in the context of computer programming before, skim through this. Halo allocates 1280 bytes for each scripting thread in a scenario, called the "stack". Stack memory is used to hold results of invoking functions, parameters for script functions, and so on.

Notably, nesting function calls will consume additional stack memory. It is very, very easy to exceed the limits of this memory if you have enough nested statements. The maximum number of nested statements is somewhere between 10 and 16 levels deep, depending on if you're invoking static scripts, if you're invoking methods with parameters, and other things.

Avoid unnecessary expressions; for example (+ 1 (+ 2 3)) can be simplified to (+ 1 2 3) and begin isn't necessary to wrap a single expression or at the top-level of a script.

; Nested statements are statements like these, where many
; things happen that are "nested" within one another

(if ;; some condition
  (begin
    (if ;; some condition
      (begin
        ;; ... do something
      )
    )
    ;; ... do something else
  )
)

WARNING: The game DOES NOT guard against exceeding stack memory in release builds!! If you exceed a script's stack memory, it will overflow into other scripts' stack memory!

This means that one script can COMPLETELY break another script if it nests too deeply. If another script's memory is clobbered, it can end up doing arbitrary things. It might wake up when it's supposed to be asleep. It might switch to a new BSP for no reason. It might crash the game. It might make objects flicker randomly.

There is not currently a reliable way to exactly tell when stack memory has been exceeded in release builds, but play and lower optimization levels will crash with a problem occurred while executing the script <script name>: corrupted stack. (valid_thread(thread)). You can use the H1A standalone build or Sapien to detect overflows.

Console scripts

Things manually entered into the console ingame also share script space with the scenario's baked-in scripts. In rare circumstances (e.g. you're just on the cusp of using too much memory), a console script's memory can overflow into a scenario script's memory, causing the above mentioned issues.