Surasia's Random Stuff Blog


Cats and reverse engineering.


Havokscript Thumbnail Image showcasing armor from Halo Infinite next to a block of Havokscript code.



Intro: Why Havokscript?

Halo has had a long history of utilizing a custom scripting language for game logic, spanning back to the original Halo: Combat Evolved which used a lisp-inspired haloscript. As the “Blam!” engine expanded and became hard to maintain, 343 Industries chose to switch to HavokScript for their next project, Halo 5: Guardians. Haloscript was a collection of simple bindings to C code, the switch to the new HavokScript retained a large part of engine functions.


Havokscript is a now-discontinued part of the Havok Game SDK, built upon Lua 5.1. It integrates structures, prototypes, a debugger, external hooks and additional type checking. There is no public documentation available for this language, however a GDC Presentation by Malcolm Tyrrell expands on the major changes made in Havokscript.


Targeting Our Code

Before writing any code, it’s important to define what will run on the server or the client. In HavokScript, the server can interface with the client to run client-specific commands, such as os.execute(). This is done via the RunClientScript() function, which also allows for specific code to be executed for specific players.


The target for a section of our code is set using preprocessor directives, which are a custom extension to Havokscript added by 343 Industries.

  • --## CLIENT: Client-side code
  • --## SERVER: Server-side code
  • --## COMMON: Code that is interpreted on both client and server.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  --## SERVER

  function HelloWorld():void -- optional type check: returns "void" type.
      print("Hello World!"); -- Prints "Hello World" to internal console.
  end

  --## CLIENT

  function remoteClient.HelloWorld():void
  -- Every function needs to be defined under "remoteClient".
      print("Hello World!");
  end

  --## SERVER

  RunClientScript("HelloWorld"); -- Runs "Hello World" on client.

  --## COMMON

  function HelloWorld():void -- Code gets interpreted on both server and client.
      print("Hello World!");
  end


The Parcel System

Parcels in HavokScript, in the simplest sense, are special classes that are initialized and run by the game mode at startup. These parcels are created using Parcel.MakeParcel, which creates an instance of a Parcel object which can then be initialized. They serve the main purpose of allowing the hooking of event handlers and registering callbacks, which in a practical sense means that the game can execute specific parts of the Parcel with arguments from the engine. For example, by hooking the g_eventTypes.weaponPickUpEvent event, we can run our custom code which will have access to variables such as weaponPosition locally.


To create a Parcel, we first need to create a global table with an instance name. Afterwards, we create a new instance of the parcel using the New() method, which will return a table of the newly created instance. For example, the code below takes in the arguments of a Slayer gamemode, which can be used globally in the parcel.


1
2
3
4
5
6
7
8
9
10
11
12
  --## SERVER

  global exampleParcel:table = Parcel.MakeParcel
  {
      instanceName = "exampleParcel",
  }

  function exampleParcel:New(initArgs:SlayerInitArgs):table
      local exampleParcelInstance = self:CreateParcelInstance();
      exampleParcelInstance.instanceName = initArgs.instanceName;
      return exampleParcelInstance;
  end


We can now define various methods for our Parcel, some of which will be run by the engine at startup:

  • Run(): Runs at gamemode intro.
  • Initialize(): Runs before gameplay.


1
2
3
4
5
6
7
8
9
  --## SERVER

  function exampleParcel:Initialize():void
      print("Hello from Init!");
  end

  function exampleParcel:Run():void
      print("Hello from Run!");
  end


The Initialize() function in particular allows us to register events, which will run a custom function that is called when an event is received. Gamemode settings can also be changed using this method.

1
2
3
4
5
6
7
8
9
10
  --## SERVER

  function exampleParcel:Initialize():void
      self:RegisterGlobalEventOnSelf(g_eventTypes.weaponPickupEvent, self.HandleWeaponPickedUp)
      -- Creates a global event callback to a method of our parcel.
  end

  function exampleParcel:HandleWeaponPickedUp():void
      print("This code runs when a player picks up a weapon!");
  end


C++ to Lua API: Functions, Definitions

If you’ve been following on so far, you might’ve noticed that we are calling functions such as RegisterGlobalEventOnSelf which are seemingly not declared in any of our code. This is because HavokScript offers a set of bindings to functions in the actual engine, initialized at runtime.


The list of built-in functions and enum definitions is quite large, and is mostly undocumented. Most functions however are clearly named and can be used in parts of your code. Some examples include:

  • Variant_GetEngineName(): Returns engine name string (ex: “Slayer”).
  • Toggle_TaaEnabled(): Toggles Temporal Anti Aliasing.
  • Player_GetXuid(Player): Gets Xbox User ID of player.


The full list of functions and enums (such as g_eventTypes) can be found in the lua environment dump created using Soupstream’s debug mode. Unfortunately though, due to no documentation being available, most of these functions do not have known argument types and return types and requires guesswork to implement.


… to be continued!