Writing a Mutator

Mutators are special AngelScript files that differ from level scripts (.j2as files) in three key ways:

  1. Instead of being connected to individual .j2l files, any mutator can be run in any level at all. (This is not always a good idea—for example, a mutator script designed solely for Treasure Hunt games wouldn't be much help run in a Race level—but it is allowed.) Mutators are enabled or disabled using the /mutators command, and clients joining a server with one or more mutators running will download those scripts and run them just like they would an ordinary script file.
  2. To signify this and to make for easier organization, mutators use the file extension .mut instead of .j2as.
  3. Mutators may not define the onFunction# or onPlayerTimerEnd hook functions, because they are too closely tied to specific level designs. Mutators may define functions that happen to use those names, but they will not be called by JJ2 under normal circumstances. (Mutators can however use the jjPLAYER method timerFunction to make a player use a timer end function defined inside a mutator script, as well as access the global array jjEnabledASFunctions or the global function jjEnableEachASFunction.)

Otherwise, mutators have all the same options available to them that a level's primary script does, are run concurrently by both clients and servers, and may be written for any number of purposes. You could write a mutator that acts like an IRC bot and reacts to certain chat phrases, or a mutator to replace JJ2's normal health system with a new one of your devising, or a mutator to draw a minimap of players to some corner of the screen, or plenty of other things. Since any number of different mutators may be loaded at once, you should try to make each one do as few distinct tasks as possible.

To be specific about how exactly mutators work: each one is loaded by JJ2+'s AngelScript engine as a script module, meaning each has access to the same set of JJ2+-defined global variables, functions, classes, etc., but cannot access the other modules' locally defined variables or functions. Two mutators may each define a function void foo(), but since they live in separate modules, there will be no naming conflict. (The primary .j2as script of a level is also loaded as a distinct script module.)

Because mutators do not need to worry about naming conflicts with other mutators (or the primary script), individual hooks may be multiply defined across different modules. Three different mutators may all define a void onMain() function, for example, and JJ2+ will run all three of them every tick. Specifically, for any multiply defined hook, the version (if any) in the primary script will be run first, followed by the versions (if any) in each active mutator, in alphabetical order. It becomes the job of the server host (or remote admins) to pick a list of mutators that do not run code at cross purposes with one another.

For example, suppose you are playing foo.j2l and running three scripts: foo.j2as, bar.mut, and baz.mut, with the following hooks:

void onPlayer(jjPLAYER@ p) { p.coins = 5; } //foo.j2as
void onPlayer(jjPLAYER@ p) { p.coins += 1; } //bar.mut
void onPlayer(jjPLAYER@ p) { jjDrawString(p.xPos, p.yPos - 40, "" + p.coins); } //baz.mut

The net result of the three script modules will be that the number 6 (5+1) will always be drawn above each local player's head.

Certain hooks—e.g. onLocalChat and onDrawHealth—have bool return values, where "true" means roughly "this script has performed all necessary actions relevant to this event," e.g. if onDrawHealth returns true then JJ2 will not run its normal health-drawing code but instead assumes the script is somehow doing the job of presenting that information to the player. If two different mutators each define onDrawHealth, and the first mutator's version returns true, the second mutator's version will still be run, but JJ2 will not draw the normal hearts no matter what value the second mutator's version returns. The return values are ORed together and it only matters that at least one of them (no matter which one) returns true.

Besides onFunction# and onPlayerTimerEnd, the only exception to the above rule is onReceive, which is only run at most once per jjSTREAM packet received. The global function jjSendPacket has an argument toScriptModuleID specifying which script module should receive the packet, defaulting to jjScriptModuleID, which is the index of the script module calling the function. The values of jjScriptModuleID match up with the order of modules that a multiply defined hook will be called in, so for example, in a server running foo.j2as and baz.mut and bar.mut, jjScriptModuleID will equal 0 within foo (primary script is always 0), 1 within bar, and 2 within baz (alphabetically later than bar). Therefore if jjScriptModuleID is passed as the value for toScriptModuleID, the packet will be received by the onReceive hook defined in the same module as jjSendPacket was called in—but if toScriptModuleID equals 0, the packet will be received by onReceive defined in the primary script instead. This is rarely a good idea but could be used by mutators designed to extend specific scripts.

Mutators can use all the same preprocessor directives that a primary script can, including or requiring or offering any supplementary files relevant to their execution. In fact, they are (currently) the only kind of script that can use #pragma name.

Even if you are not writing a mutator, it is important to be aware that they exist and might be run in parallel with your level's primary script (assuming you are creating a multiplayer level). For example, it might be tempting to repurpose jjPLAYER::coins to keep track of some other player-related property in a level with no coins in it, but it's better to declare your own non-member variable for that, in order to avoid potential conflicts with a mutator using jjPLAYER::coins for something else. Certainly there will always be scripts that simply cannot functionally coexist with each other (a primary script where everyone has airboards; a mutator that disables all flight), but you might as well do your best to restrict such cases to times when the basic purposes of the scripts are in conflict, instead of just when one of them isn't coded carefully enough.