CARRION Guide

Basic Scripting Tutorial for CARRION

Basic Scripting Tutorial

Overview

Have you ever thought “I want to make my own map in Carrion but I have no idea how to script!”?Do you want to know how objects work without digging through the game’s code?Then this is the guide for you!This guide will teach you how to get started with scripting, how to troubleshoot problems and give you a detailed list of most scriptable objects and how to use them.

Introduction

Getting started with Carrion map making

If you’re completely new to Carrion map making, check out the official workshop tutorial first:
[link]
Subscribe to this workshop item, then start it via the Carrion Mod Loader. It should teach you how to create a map with the UGCToolkit, how to find that map’s file location and how to open its level and script files.
Note: you might need to enable the beta version to see it.

This tutorial’s script “workshop.cgs” contains examples for everything that will be explained in this guide.

Requirements
  • Basic mapping tools (see above)
  • A good text editor (I highly recommend Notepad++[notepad-plus-plus.org]!)
  • A test map

In Case Everything Crashes and Burns

At any point, your scripts might stop working or the game crashes altogether. Don’t worry if that happens, it’s natural. Move to the Chapter “Troubleshooting” to get help with figuring out the problem. If you can reliably analyze and fix sudden problems then you’re well on your way to becoming a good map maker. It might seem daunting at first but with enough practice you’ll be able to fix most problems within seconds.

Basic Scripting

Scripting in carrion works by sending a signal from one object to another via “input -> output;”.
In the following example we have a switch and a door:

Let’s open the map’s script and enter the following line of code:

test_switch -> test_door;

When the player pulls test_switch it will open test_door:

What is happening here?

The above line of code establishes a connection between test_switch and test_door. When the player pulls the switch, the switch’s state will change from 0 to 1 and transmit that new state to all connected objects. The receiving test_door will then also change its state from 0 to 1 and open itself.


Relaying Signals

Simple objects like switches, doors and lamps will relay any signal they receive. To test this, let’s add a lamp to our test level:
Make sure it starts turned off by going to its properties and setting the “InitState” to 0.

Now if we want test_lamp to turn on when we pull the switch, we have two options.

Option 1: Second connection

We can establish another connection originating from test_switch:

test_switch -> test_door; test_switch -> test_lamp;
Option 2: Relay

Upon receiving a signal, the door can relay it to another object:

test_switch -> test_door; test_door -> test_lamp;
Option 2b: Compact Relay/Chain

The last example code can also be turned into a chain like this:

test_switch -> test_door -> test_lamp;

These two code examples behave in the exact same way. That means if anything sends a signal to test_door then it will relay that signal in both cases, even if it’s in the middle of the line.


Camera Sweeps

Whenever a far away door opens, you always want the player to immediately know it. We use sweeps to show the player where to go next or to show them that they are making progress.
To setup a camera sweep, we need a marker, something affected (e.g. a door) and something to trigger the sweep (e.g. a switch):

First of all, we want the camera to move to the door when we use the switch:

test_sweep = (SWEEP()) -> [test_marker, duration 0] -> [test_marker, duration 2]; test_switch -> test_sweep;

Hold on, why do we use test_marker twice? you might ask. That is because, despite looking the same, both have different purposes. The first “[test_marker, duration 0]” will make the camera move to test_marker as quickly as possible. If we instead wrote “(SWEEP()) -> [test_marker, duration 12]” then the camera would need roughly 12 seconds to arrive at the specified marker. Unless you’re setting up some sort of cinematic cutscene, you should always use duration 0 for the first block.
The second block [test_marker, duration 2] specifies that the camera should stay there for 2 seconds before returning to the player. How long to set this duration is up to personal preference, but you usually don’t want to make the player wait too long and there are some other things to keep in mind which will be explained below.

Next up, we want to know when the camera arrives at the door, so we know when to open it. For this, we will create a sweep event:

test_event = (SWEEP_EVENT()); test_sweep = (SWEEP()) -> [test_marker, duration 0] -> [test_event, duration 0] -> [test_marker, duration 2]; test_switch -> test_sweep; (WAIT_FOR_EVENT(test_event)) -> test_door;

When we trigger the sweep and the camera arrives at test_marker then [test_event, duration 0] will trigger two parts of the code to run at the same time:
“-> [test_marker, duration 2]” will make the camera, as we already know, stay at test_marker’s position for two seconds.
Meanwhile “(WAIT_FOR_EVENT(test_event)) -> test_door;” will respond to test_event and send a 1 signal to test_door, thus opening it.

Offscreen Behavior To Keep In Mind

Objects like doors only move when the camera is in the same room as them. Let’s look at this level where the door is in another chamber:
If we try to open the door via “test_switch -> test_door;” then the door won’t open yet because it is not in the same chamber as the player and we can’t move to that chamber because the door is blocking it. There are generally three ways to solve this issue:

Solution 1: Observe The Door Until It Opens

With the code example above, we can move the camera to the other chamber and then give the door the signal to open. The observation duration has to be long enough for the door to (almost) completely open. If we return the camera too early, then the door is no longer observed and will be stuck in a half-opened state, potentially still blocking the player.

Solution 2: Signal Then Observe

If the door gets a 1 signal before the camera sweep then it will immediately open upon the camera entering the same room. This solution is easier to implement, but should be avoided because the opening animation of the door helps the player to figure out what is going on. They might not even realize that the now open door was previously closed.

test_sweep = (SWEEP()) -> [test_marker, duration 0] -> [test_marker, duration 1]; test_switch -> test_sweep; test_switch -> test_door;
Move The Door As Well

If we have the space, we can just move the door further away from the chamber border. You should still do a sweep to show the player where to move next, but you can leave the camera duration at 1-3 seconds. The potentially half-open door is no problem because the player can still move into the room and once that happens, the door will be immediately open because it already got a signal.

Special Methods and Logical Operators

You can modify the the relaying of signals by introducing special methods like (DELAY(1)) or (PASS(0)) or logical operators like (NOT()), (OR()) and (AND()).

Note: There are also the special methods (SWITCH(0)), (THRESHOLD(0.01)), SRLATCH(0), (AMP(2)) and (SUM()) but they lie beyond basic scripting and are therefore not included in this guide. If you are interested in learning more about them, please tell me and I’ll make an advanced guide or you can look them up in the official tutorial’s script “workshop.cgs”.

Special Methods
(DELAY(x))

This delays the signal by x seconds.

test_switch -> (DELAY(1)) -> test_door;

The door opens one second after the switch is pulled.

test_switch -> (DELAY(3.5)) -> test_lamp;

The lamp turns on 3.5 seconds after the switch is pulled.

All special methods can be used multiple times per chain:

test_switch -> (DELAY(1)) -> test_door -> (DELAY(2.5)) -> test_lamp;

(PASS(x))

Only the specified signal can be passed via this method.
Note: To test this more easily, enable the switch’s property “IsToggle”.

test_switch -> (PASS(1)) -> test_door;

The switch can only open the door.

test_switch -> (PASS(0)) -> test_lamp;

The switch can only turn off the lamp.

Logical Operators

A logical operator will take one or more inputs (depending on the operator) and send a corresponding output. This guide will explain the fundamentals but there’s countless tutorials on the internet if you want to learn about them in detail.

To test them, we will be using the following setup:
Both switches’ “IsToggle” is enabled and the lamp’s InitState is 0.

(NOT())

This is a logical operator which negates a signal before relaying it. That means if it gets a 0 input it will send a 1 output and if it gets a 1 input it will send a 0 output.

input_switch_1 -> (NOT()) -> output_lamp;

Pulling the switch down or on will turn the lamp off.
Pulling it back up will turn the lamp on.

(AND())

This takes any number of inputs. The output is only 1 when all inputs are 1. In any other case this operator’s output is 0.

and_operator = (AND()); input_switch_1 -> and_operator; input_switch_2 -> and_operator; and_operator -> output_lamp;

Note: Even if both switches are initially on, the operator won’t output anything. It specifically needs to receive a 1 signal from all inputs before it sends a 1 output.

(OR())

This takes any number of inputs. The output is 1 if any of its inputs is 1. The output is 0 if all inputs are 0.

or_operator = (OR()); input_switch_1 -> or_operator; input_switch_2 -> or_operator; or_operator -> output_lamp;

Note: Similar to the (AND()) operator, (OR()) won’t initially output anything until it receives at least one 1 signal.

Objects

Note: To prevent this guide from bloating up, I won’t explain every single property. However, what might be obvious for me might not be for someone who’s new to map making. Therefore, don’t hesitate to tell in the comment section if you think I should explain something in more detail.

Simple Relaying Objects

These objects serve a simple purpose and will relay any signal they receive. That means if any of them receive a 0 or 1 signal, they will act accordingly (e.g. a door opens/closes) and send that same signal to any connected object. They will relay a signal even if their own state doesn’t change (e.g. an off switch will still relay a 0 signal).
These simple objects include:

  • lamp
  • door
  • floodgate
  • block_door
  • elevator
  • growing_tentacle
  • sentry_drone
  • turret
  • sticky_bomb
  • missile_tank
  • morph

Switch

Allows the player to turn it on/off and thus interact with the world around them.
Getting an input from another object or being pulled by the player will set the switch’s “IsOn” property.
Relays: yes

IsEnabled

This property determines whether it’s possible to pull the switch. Can be set via the following code:

input_switch_1 -> (input_switch_2.EnabledState);
IsOn

Determines whether the switch is on/down/left/1 or off/up/right/0

Variant

SLIDE: vertical switch
LEVER: horizontal switch


Elevator

The elevator requires two markers that specify where floor 0 and floor 1 is.
A 0 input will send it to floor 0, and 1 will send it to floor 1.
Relays: yes

Implementing elevators is tricky and making them realistic even more so. Beginners should decide whether they want to implement them decoratively or as part of exploration.

Elevators as decoration

This type is purely there to make your level look more realistic. The elevator can be used multiple times and has synchronized switches for both floors. For this implementation, use the script provided by the official workshop tutorial:

#elevators elevator_switch1 -> (PASS(0)) -> (NOT()) -> elevator_switch2; elevator_switch1 -> (PASS(1)) -> (NOT()) -> elevator_switch2; elevator_switch2 -> (DELAY(0)) -> (PASS(0)) -> (NOT()) -> elevator_switch1; elevator_switch2 -> (DELAY(0)) -> (PASS(1)) -> (NOT()) -> elevator_switch1; elevator_switch1 -> elevator;

Note: This code is very fragile and from what I can tell it shouldn’t even work, but somehow it does. Only change the names “elevator_switch1”, “elevator_switch2” and “elevator” to the object names in your level. If you change anything else then using one of the switches will cause your game to freeze and crash.

Elevators as roadblocks

These elevators are there to prevent the player from passing through the elevator shaft. Once they get to the other side via another section, they can use a switch to make the elevator move away to make the shaft accessible from both sides. This is purely there to make backtracking easier for the player.
Implementing this can be as easy as the following:

mines_elevator_switch -> mines_elevator;

Laser

The laser detects whether the player is currently obstructing it.
It will output a 1 signal if it is turned on.
It will output a 0 signal if it is turned off or obstructed.
Because the laser doesn’t relay a signal (or only once upon being turned off), it can be disabled as soon as the player touches it with the following line:

test_laser -> (PASS(0)) -> test_laser;

Using a laser with a reset switch and a master shutdown switch like you see it in the main game is a bit more complicated. Here’s an example on how I implement in in a room I call “A01”:
For both switches “IsEnabled” is true and “IsOn” is false. The laser and block door have “IsOn” and “InitState” respectively checked (although the door init state doesn’t matter; the laser will open it either way).

# A01 laser door a01_laser_shutdown_event = (SWEEP_EVENT()); a01_laser_shutdown_sweep = (SWEEP()) -> [a01_laser_marker, duration 0] -> [a01_laser_shutdown_event, duration 0] -> [a01_laser_marker, duration 1]; a01_laser_door_or = (OR()); a01_laser_master_switch -> (DELAY(1)) -> a01_laser_shutdown_sweep; a01_laser_master_switch -> a01_laser_door_or; a01_laser_master_switch -> (NOT()) -> (a01_laser_reset_switch.EnabledState); (WAIT_FOR_EVENT(a01_laser_shutdown_event)) -> (NOT()) -> a01_laser; a01_laser_reset_switch -> a01_laser -> a01_laser_door_or; a01_laser -> a01_laser_reset_switch; a01_laser_door_or -> a01_laser_door;

Coming into contact with a01_laser will disable the laser, close a01_laser_door and enable a01_laser_reset_switch. a01_laser_reset_switch will re-enable the laser and open a01_laser_door.
If you successfully bypass the laser and pull a01_laser_master_switch then a sweep will get triggered, the laser is disabled and the door opened.
This script is a bit convoluted and bigger than it really needs to be but in return it is very robust and can be used in almost any situation.


Jammer Receiver

The jammer receiver can observe any number of people/drones and if they all die, send a signal (usually to a door/floodgate).
If you want to observe a human/soldier, make sure to check their “HasAntenna” property.

a01_jammer_receiver:Observe(a01_human_1); a01_jammer_receiver:Observe(a01_human_2); a01_jammer_receiver:Observe(a01_soldier_1); a01_jammer_receiver:Observe(a01_drone_tank_1); a01_jammer_receiver:Observe((a01_drone_1.Drone)); a01_jammer_receiver -> a01_jammed_door;

Note: Yes, the extra brackets around the drone are necessary and mandatory.


Sentry Drone

A single drone with a shield and gun. Sending it a 1 signal will release it.
Relays: yes

a01_drone_release_laser -> (PASS(0)) -> a01_drone_release_laser; a01_drone_release_laser -> (NOT()) -> a01_drone_1 -> a01_drone_2;

Note: The laser is not necessary. This is just an example on how to combine lasers and drones.
A drone can also be in proximity state and release itself if the player gets too close:

a01_drone_release_laser -> (PASS(0)) -> a01_drone_release_laser; a01_drone_release_laser -> (NOT()) -> (a01_drone_1.ProximityState) -> (a01_drone_2.ProximityState);

To lock a drone in its container and prevent it from being released (even if it get’s a 1 signal or proximity gets triggered) use the following:

a01_drone_lock_switch -> (a01_drone_1.LockCircuitNode) -> (a01_drone_2.LockCircuitNode);

Upgrade Jar

Upgrade Jars/Containers will send a 0 signal after the player exits it. This is useful if you want to disable a lamp marking the entrance to a secret jar:

a01_energy_jar -> a01_secret_lamp;

Tooltip

Shows a message at the bottom of the screen while the player is inside it and fulfills the activation condition. Make sure to enable either IsTriggeredByImpersonation or IsTriggeredByPhysics but not both.
The tooltip sends a 0 signal when it is disabled (triggered if DisableAfterUse is checked and the player leaves the tooltip area). This can be useful if you want to e.g. trigger a sweep once the player reaches a certain area.

a01_tooltip -> (NOT()) -> a01_sweep;

Morph

Tears open a large solid area when triggered. Needs a mask (object type “morph_mask”) for each entrance to the morphed area. I recommend putting the morph and its masks in layer “Objects+” as they are very bulky and likely to cover other objects.
First, prepare the morph openings:

morph_airvent:SetMask(mask_airvent1); morph_airvent:SetMask(mask_airvent2); morph_airvent:BuildBowels();

Then trigger it with a normal 1 signal:

hive_airvent -> morph_airvent;

Troubleshooting

Gamelog

If your scripts don’t work or your game crashes, you should first check the gamelog. You can find it via the in-game console or by opening it with a text editor.

Locating the Gamelog File

For Windows users the gamelog file can be found under:

C:Users[your_username]AppDataLocalLowPhobiaCarrion

If you can’t see the AppData folder, then you need to configure your Windows settings to view hidden files[support.microsoft.com].
If you found the file but can’t open it then close Carrion and try again.

To find the cause of the error, scroll all the way down. If the game crashed then you should see the error message followed by a stack trace. Ignore the latter, the error message is all you need.
Alternatively the log will just stop after e.g. ” |- Filling up tile layers…”. The potential errors and their solutions will be explained below.

Opening The Console

For people with English keyboards, you can open the console with the Tilde key (between Esc and Tab). Everyone else needs to change their keyboard layout or preferably change their console key. To do the latter, open your Carrion settings file, which is found near the gamelog:

C:Users[your_username]AppDataLocalLowPhobiaCarrion_steam_xxxxxxxxxxxxxxxxxsettings.json

In this file, change the setting “console_hotkey”. I personally use F1 because it’s close to the game’s other debug keys:

“console_hotkey”: “F1”,

Now, from within the game, we can open the console with the key we specified:
The console displays the gamelog and therefore also the error. If you can’t see what you’re looking for, use the “Page Up” and “Page Down” keys to scroll in the console, although I recommend that you restart the level with F2 and then immediately check the console again to see the error.


Fixing The Problem

Note: This section was largely taken from the CARRION Modding Discord[discord.com] (credits to cuni). You can find this guide and more in #mapping-tips or by going to e.g. #mapping and typing the following:

.c maphelp

Errors: causes and possible fixes
ERROR: Level doesn’t exist: X
With X being the name of the level you typed in; see if you misspelled it. To make sure check the files again.

Object reference not set to an instance of an object.
This error can occur when certain objects are placed outside the map boundaries or a chamber.

One or more errors occured. (Object reference not set to an instance of an object.)
This one is slightly different as it’s most likely the chambers’ fault. Check if the Chamber has the IsSubChamber (bool) ticked, if it does; untick it. Also make sure OverrideFgGenerator and OverrideBgGenerator are ticked. Another possible cause can be that the chambers’ Texture is misspelled or the chamber is simply way too big.
-When this error occurs it usually crashes the game.

Unsupported object type for layer Objects.
This one is most likely just the Type of an object being misspelled or changed instead of it’s Name, so check if all the recently placed/edited objects have the correct types.
-When this error occurs it usually crashes the game.
Personal addition by Encreedem: If the object types are all correct, make sure the objects are in the correct layer:

Arithmetic operation resulted in an overflow. / Attempted to divide by zero.
These two usually only occur when obstacles like the gratings and wooden walls are smaller than 1×1 tiles (16×16 px).
-When this error occurs it usually crashes the game.

Index was outside the bounds of the array.
This error usually happens when a human falls outside the map boundaries/out of bounds and can also be caused if the Elevators’ custom property DefaultFloor is a number greater than 1.
-When this error occurs it usually crashes the game.

No Error in the Gamelog and the game just outright crashes on level-load or when entering a chamber
There can be several causes for an outright crash. First, check if the chamber is set up properly, make sure the IsSubChamber (bool) is NOT ticked. Also make sure your level has a monster in it, else it can’t load obviously. The most common issue is that the chamber has a lack of tiles on the border, why this happens is unknown but let’s explain it this way:
The game automatically places a couple dots inside the chamber which shoot lines in every direction, these lines can only be stopped by a solid Tile. So if there are no tiles they reach length=infinity and so the game crashes.

Concave shapes are not supported yet.
This is a typical error when using Tilemap Morphs incorrectly, if you read it: Concave shapes arent supported yet, so that means the opposite (Convex Shapes) are supported. For this one I recommend googling the difference of Concave and Convex shapes.
-When this error occurs parts of the script may stop working.

Script stops working
Check the ingame console or gamelog. Below you’ll find errors that have to do with the script.
[Line X]: Variable not found: X.
This is most likely just a misspelled variable name. Compare your variables/names from Tiled with the Script ones.
-When this error occurs parts of the script may stop working.

Error at [Line]]: Unexpected character: ‘X’
This error can be as simple as just having misspelled a variable or having forgotten a semicolon.
-When this error occurs parts of the script may stop working.

Editing The Level Files

While it might be risky, sometimes we have to edit the level file directly if we want to fix a problem in a reasonable amount of time. But first, make a backup of your map before you open it with a text editor! Making changes to the map might cause irreparable damage if you’re not careful!


Wrong template path: ../../../Content/Templates/flow/elevator_right_closed.json
This error occurs if you copy an object with a template (check its properties to see if it has one) from another map to yours. The easiest solution is to delete that object and replace it with one from your level’s Templates folder.
To directly fix the template path, open your level with a good text editor and look for the following:

../..


Replace the incorrect path to the “Templates” folder:

../Templates/

Game crashes after ” |- Filling up tile layers…” or tiles don’ appear
This is usually caused by a wrong tileset. Open your level with a good text editor and look for the following:

../..


In this example only the tileset path “../TileSets/tiles.json” is correct. We need to either delete the other references or change their paths to “../TileSets/tiles.json”.

If you delete them, only select the curly brackets {} and what is within them as well as the comma before this block.

Afterwards, replace the now marked invalid tiles with the correct ones:

If you fix the paths

“source”:”../TileSets/tiles.json”


… then go to Tiled and save your map again. If the tileset paths have been set correctly, saving via Tiled will fix the formatting and consolidate the tileset references into a single block.


Which One Is The Correct Tileset?

You might be wondering how your map even got the wrong tileset. Most likely it was somehow selected because you have opened levels from multiple maps in Tiled (e.g. two different levels of yours as well as cuni’s debug map).
To see which one is correct, right click one and choose “Open Containing Folder…”.

Repeat this until you find the one in the same custom map as the level you’re working on.

Questions and Feedback

Should you have questions regarding mapping or scripting in general, please go to the CARRION Modding Discord[discord.com]. Specifically the channels #mapping and #mapping-tips contain lot’s of useful information for new and veteran mappers alike.
I frequently lurk there and there are many other people who might be able to help you with all aspects of mapping.

If you have any questions or feedback regarding this guide, feel free to post them in the comment section. I’m happy to clarify and elaborate on any of the previously mentioned points.
If anyone is interested, I might make a more advanced scripting guide (e.g. for synchronized elevator + door controls, complex interlocking logic gates for advanced machinery or to explain more special methods).

Lastly, if you want practical examples for some of the previously mentioned topics, check out my maps:
[link]
I just learned scripting when I started making this map, therefore the code further up in the script might be less optimized and polished than the code below.

[link]
Very small but it shows you how to (mis-)use triggers, jammer receivers and containers.

[link]
Only enter if you’re brave enough.

SteamSolo.com