Skip to content

Creating your first Cathode script

Matt Filer edited this page May 2, 2023 · 15 revisions

CathodeLib makes it easy to develop custom scripts for Alien: Isolation in C#, by allowing access to the game's internal "Cathode scripting system".

Getting started

Setting up a project with CathodeLib is simple:

  • Create a new C# .NET Framework project in Visual Studio (if you haven't already)
  • Right click on the project and select "Manage NuGet Packages"
  • Click "Browse" and search for "CathodeLib", then click "Install"
  • Navigate to your CS file, and at the top, add using CATHODE.Scripting

Making your first script

Lets write some code! In this example, we'll create a brand new commands file, and a script within it to show an objective when our level has loaded.

  1. Create your Commands file:

    • Commands commands = new Commands("COMMANDS.PAK");
  2. Create your first script (called a Composite):

    • Composite composite = commands.AddComposite("My Cool Script", true);
      • The first parameter names the script "My Cool Script".
      • The second parameter sets this script as the one we'll run first when we load our level (the root).
  3. You can now add entities to your composite to perform logic in-game:

    • For this example, we want to show an objective when the level starts up - so let's add some Function entities to our composite:
      • FunctionEntity checkpoint = composite.AddFunction(FunctionType.Checkpoint);
        • The Checkpoint Function entity will allow us to trigger other entities when it itself is triggered.
      • FunctionEntity objective = composite.AddFunction(FunctionType.SetPrimaryObjective);
        • The SetPrimaryObjective function will show an objective when triggered.
  4. With your entities added, you can now give them parameters to customise them:

    • Continuing with the objective example, let's add parameters to our Checkpoint Function entity:
      • checkpoint.AddParameter("is_first_checkpoint", new cBool(true));
        • The parameter is_first_checkpoint tells the game that our Checkpoint should be triggered when the level starts up.
        • The datatype of the parameter is boolean, which we define as a cBool - various other types are available, such as cString, which we'll use next!
      • checkpoint.AddParameter("section", new cString("Entry"));
        • The parameter section gives our Checkpoint a name - in this case, we're calling it "Entry".
    • Now, let's customise our objective by giving our SetPrimaryObjective Function some parameters:
      • objective.AddParameter("title", new cString("Do Something!"));
        • Here we're giving our objective a title - this can be a regular string, or a localisation ID.
      • objective.AddParameter("additional_info", new cString("Hey, you should go and do something!"));
        • Now, we give our objective a description, which players can view when they open the TAB menu.
  5. Add links between the entities:

    • Entities can be linked via their parameters, just like how nodes are linked within Blueprint in Unreal Engine. This allows entities to share data, or simply just trigger eachother when events occur. For our example here, we'll trigger our objective when our Checkpoint has loaded.
      • checkpoint.AddParameterLink("finished_loading", objective, "trigger");
        • finished_loading is the parameter our Checkpoint entity activates when it has loaded, and trigger is the parameter on our SetPrimaryObjective entity which causes it to activate, setting our objective and displaying the popup in-game.
  6. Save your Commands file:

    • commands.Save();

Our code is complete! Run your application and a "COMMANDS.PAK" file should be generated in its working directory.

To run our commands in-game, copy the generated PAK file into an Alien: Isolation level's "WORLD" folder (I'm gonna use BSP_TORRENS) and launch the game to that level - you can do this easily by using OpenCAGE's "Launch Game" functionality. You should see your objective pop up when the level loads!

Full code:

//Create our Commands file to contain our scripts
Commands commands = new Commands("COMMANDS.PAK");

//Create our first script (a "Composite") and give it a name
Composite composite = commands.AddComposite("My Cool Script", true);

//Add a "Checkpoint" function to our script
FunctionEntity checkpoint = composite.AddFunction(FunctionType.Checkpoint);

//Let the game know we want to load in to our checkpoint
checkpoint.AddParameter("is_first_checkpoint", new cBool(true));

//Give our checkpoint a name
checkpoint.AddParameter("section", new cString("Entry"));

//Add a "SetPrimaryObjective" function to our script
FunctionEntity objective = composite.AddFunction(FunctionType.SetPrimaryObjective);

//Give our objective a title (visible in the initial popup, and tab menu)
objective.AddParameter("title", new cString("Do Something!"));

//Give our objective a description (visible in the tab menu)
objective.AddParameter("additional_info", new cString("Hey, you should go and do something!"));

//Trigger our objective when our checkpoint has finished loading
checkpoint.AddParameterLink("finished_loading", objective, "trigger");

//Save the Commands file
commands.Save();

Modifying existing scripts

Starting from scratch is cool, but why not utilise existing scripts, written by the game's developers?

CathodeLib supports loading existing commands files, so instead of creating our own, lets open an existing one and add our own script to it!

  1. Load the commands file:

    • Commands commands = new Commands("Alien Isolation/DATA/ENV/PRODUCTION/ENG_ALIEN_NEST/WORLD/COMMANDS.PAK");
      • Just like before, we create our Commands object, however this time we're loading from ENG_ALIEN_NEST.
  2. We'll make a new composite again to write our script in, but we could just as easily edit an existing composite:

    • Composite composite = commands.AddComposite("My Cool Script", true);
      • Just like before, we specify true on the second parameter to set this composite as the one that runs when the level first starts up. This is especially important in this example, as we'll want to override the composite that is already set to run on startup. It didn't matter so much before when there were no others in the commands file.
  3. Because we're loading a commands file with existing Composites, we can take advantage of that, and instance them:

    • For this example, lets spawn a player, using the pre-made composite which provides that functionality:
      • FunctionEntity checkpoint = composite.AddFunction(FunctionType.Checkpoint);
        • Just like before, we'll use the Checkpoint function to trigger our logic on level load.
      • FunctionEntity playerSpawn = composite.AddFunction(commands.GetComposite("ARCHETYPES\\SCRIPT\\MISSION\\SPAWNPOSITIONSELECT"));
        • Now, we instance the pre-defined SPAWNPOSITIONSELECT script which is already in the commands file, to use as a function.
  4. Just like before, lets add parameters to our Checkpoint function so it knows to execute on load:

    • checkpoint.AddParameter("is_first_checkpoint", new cBool(true));
    • checkpoint.AddParameter("section", new cString("Entry"));
  5. Now, lets add a link to trigger our player spawner composite instance when the checkpoint is called:

    • checkpoint.AddParameterLink("finished_loading", playerSpawn, "SpawnPlayer");
  6. Save your Commands file:

    • commands.Save();

Now, run your application again and your ENG_ALIEN_NEST commands file should be updated.

Launch the level, just like you did before, and see the player spawn! You should have no weapons and no environment. You may still have some models floating around, as levels are populated by a mixture of commands scripts and "mover" data, the latter we'll get on to in another tutorial. You should also still have collision, as collision for the level is baked to a Havok collision file. You can instance your own CollisionBarrier function entities to add custom colliders.

Full code:

//This time, we load an existing Commands file, from the game's ENG_ALIEN_NEST level
Commands commands = new Commands("Alien Isolation/DATA/ENV/PRODUCTION/ENG_ALIEN_NEST/WORLD/COMMANDS.PAK");

//Create our new composite and set it as the one that loads first in the level's Commands
Composite composite = commands.AddComposite("My Cool Script", true);

//Create our checkpoint just like last time to act on the level entry, and apply its parameters
FunctionEntity checkpoint = composite.AddFunction(FunctionType.Checkpoint);
checkpoint.AddParameter("is_first_checkpoint", new cBool(true));
checkpoint.AddParameter("section", new cString("Entry"));

//Since we loaded in our Commands file, we can grab composites that already exist within it 
Composite spawnPositionSelect = commands.GetComposite("ARCHETYPES\\SCRIPT\\MISSION\\SPAWNPOSITIONSELECT");

//We can then create function entities that instance these composites to execute their functionality
FunctionEntity playerSpawn = composite.AddFunction(spawnPositionSelect);

//This particular composite implements a public variable called SpawnPlayer which spawns the player
//Lets link to that public variable off of our checkpoint when it finishes loading
checkpoint.AddParameterLink("finished_loading", playerSpawn, "SpawnPlayer");

//Save the Commands out back to ENG_ALIEN_NEST
commands.Save();

You can now continue on to build up more advanced scripts, for example - spawning objects and moving them around, spawning particle systems, and giving weapons/items to the player. Have fun!

Further reading & useful tips

If you'd like to understand more about the basics of the Cathode scripting system, check out this Wiki page!

As mentioned above, for easy testing going forward, you can use OpenCAGE's "Launch Game" functionality, which allows you to load directly into a level, with support for custom levels outside of the ones that ship with the game. This also optionally supports "hot reloading", which allows you to press the "INSERT" key at runtime to reload a level - super useful for testing script changes without restarting the game!

You can check out the OpenCAGE Wiki to view a list of all entities and parameters that you can utilise through Commands.

Entities can optionally be created with all their known parameters (not including inherited parameters from interfaces, listed in the above links). Be aware though, this will add ALL parameter defaults (including link placeholders) which may not be ideal. You may want to remove parameters with ParameterVariant values that do not match ParameterVariant.PARAMETER after creation (when using the autopopulate option) to avoid inadvertently triggering events.

As the game is built in release mode, debugging is limited, and as such any incorrect configurations in your scripts may, on occasion, hard-crash the game (although the scripting system is very forgiving). Be patient!