> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/scripting/syntax.md).

# Getting Started with CSL

CSL is All Out's custom programming language built to make writing multiplayer games as easy as creating single player games.

CSL is All Out’s custom scripting language. It’s statically typed and feels most similar to Go/Odin — except **gameplay state is automatically synced from the server to clients**.

{% hint style="info" %}
You don’t need to write RPCs, SyncVars, or custom replication. We handle these for you!
{% endhint %}

## Your first script (`main.csl`)

When you create a new project, All Out generates a `main.csl` in your project’s `scripts/` folder.

```go
import "core:ao"

// ============================================================================
// Global lifecycle
// ============================================================================

ao_before_scene_load :: proc() {
    // Register item definitions, currencies, etc.
    // Runs after the empty Scene exists, before scene content is loaded.
}

ao_start :: proc() {
    // Called once when the scene starts.
}

ao_update :: proc(dt: float) {
    // Called every frame.
}

ao_late_update :: proc(dt: float) {
    // Called every frame after ao_update.
}

ao_draw :: proc(dt: float) {
    // Cosmetic-only. Interactive UI belongs in ao_update/ao_late_update.
}

// ============================================================================
// Player lifecycle
// ============================================================================

Player :: class : Player_Base {
    ao_start :: method() {
    }

    ao_update :: method(dt: float) {
    }

    ao_late_update :: method(dt: float) {
    }

    ao_draw :: method(dt: float) {
        // Cosmetic-only. Never interactive UI.
    }

    ao_end :: method() {
    }
}
```

For example, log a message when each player joins:

```go
Player :: class : Player_Base {
    ao_start :: method() {
        log_info("hello %", {this.get_username()});
    }
}
```

If you want a deeper explanation of when these functions run, see [Game/Frame Lifecycle](/scripting/game-frame-lifecycle.md).

## Imports

Your `main.csl` should import `core:ao` and any folders you create (like `ui/`, `abilities/`, etc).

```go
import "core:ao"
import "ui"
```

Imports point at folders, not individual files. A folder import includes the `.csl` files in that folder. Imports inside that folder are resolved relative to it:

```go
// main.csl
import "core:ao"
import "abilities"

// abilities/projectiles.csl
import "helpers"
```

{% hint style="warning" %}
In most projects, **import in `main.csl` only**. Don’t sprinkle imports across lots of files — you’ll eventually end up with confusing order/visibility issues.
{% endhint %}

## Declarations (variables and constants)

Declarations bind a name to a value.

### Variables

```go
// General form
<name>: <type> = <expression>;
```

Either `<type>` or `<expression>` can be omitted:

```go
// Explicit type
my_value: int = 42;

// Type inference
my_value := 42; // inferred to int

// Zero-init (same as my_value := 0;)
my_value: int;
```

### Constants

Constants use `::` and must be compile-time constants. They can be scalars, strings, types, procedure values, arrays, or compound literals:

```go
MAX_PLAYERS :: 12;

Spawn_Desc :: struct {
    name: string;
    pos: v2;
}

DEFAULT_SPAWNS: []Spawn_Desc : {
    {"Blue", {-4, 0}},
    {"Red", {4, 0}},
};
```

This is invalid (because `a` is not compile-time constant):

```go
a := 123;
b :: a; // compile error
```

Global variable initializers must also be compile-time constants. Use `ao_before_scene_load` or `ao_start` for runtime initialization.

### Global variables

Globals use the same declaration syntax as locals. They can be left zero-initialized or initialized with compile-time constants, including structs, arrays, procedure values, and `typeid` values:

```go
score_total: int;
spawn_names: []string = {"Blue", "Red"};
default_type: typeid = int;

start_round :: proc() {
}

on_match_start: proc() = start_round;
```

Globals are mutable and persist for the lifetime of the script instance. A hotload keeps their current values, so an edited initializer applies only after a game restart; hotload does apply edits to `::` constants, so declare read-only data tables as constants. Avoid using globals for per-player gameplay state.

## Types

### Primitive types

* Signed integers: `s8`, `s16`, `s32`, `s64`
* Unsigned integers: `u8`, `u16`, `u32`, `u64`
* Booleans: `bool`
* Floats: `f32`, `f64`
* Aliases:
  * `int` == `s64`
  * `uint` == `u64`
  * `float` == `f32`
* Vectors: `v2`, `v3`, `v4`
* `string`
* `typeid`
* `any`

### Numeric literals

Fractional literals can include or omit the zero before the decimal point. For example, `.52` and `-.52` are equivalent to `0.52` and `-0.52`:

```go
opacity := .75;
offset := v2{-.52, 1.22};
```

### Vector types

`v2` has `.x`, `.y`; `v3` adds `.z`; `v4` adds `.w` — all float fields:

```go
pos: v2 = {10, 20};        // x=10, y=20
color: v4 = {1, 0, 0, 1};  // red with alpha=1
offset := v3{1, 4, 9};     // type inference
```

## Structs and classes

Structs are **value types** (copies on assignment). Classes are **reference types** (you allocate them with `new`).

### Structs (value types)

```go
Food_Definition :: struct {
    name: string;
    food_value: int;
}

food: Food_Definition;
food.name = "Apple";
food.food_value = 10;
```

### Classes (reference types)

```go
Foo :: class {
    value: int = 10;
    position: v2 = {12, 34};
}

foo := new(Foo);
```

Class fields can have default values. A derived class can override inherited defaults without redeclaring the field:

```go
Enemy :: class {
    health: int = 100;
    speed: float = 3.0;
}

Boss :: class : Enemy {
    health = 500;
    speed = 1.5;
}
```

### Inheritance

Structs/classes can inherit from other structs/classes:

```go
Animal :: class {
    name: string;
    age: int;
}

Dog :: class : Animal {
    breed: string;
}

dog := new(Dog);
dog.name = "Buddy";
dog.age = 5;
dog.breed = "Labrador";
```

Derived methods with the same name shadow inherited methods. Calls are not virtual: method lookup uses the receiver's static type, so calling through a base-typed value never selects a same-named method from the derived type.

Derived classes expose a read-only `base` field that views the same object as its immediate parent type. It lets method lookup start from the parent type explicitly:

```go
Animal :: class {
    speak :: method() {
        log_info("animal");
    }
}

Dog :: class : Animal {
    speak :: method() {
        base.speak();
        log_info("dog");
    }
}

dog := new(Dog);
dog.speak();        // Dog.speak
dog.base.speak();   // Animal.speak

animal: Animal = dog;
animal.speak();     // Animal.speak, not Dog.speak
```

`base` is not a reserved word. A local declaration named `base` shadows the inherited field normally.

## Procedures and methods

### Procedures (`proc`)

```go
add :: proc(a: int, b: int) -> int {
    return a + b;
}

result := add(2, 4); // 6
```

Procedures returning multiple values must wrap the return types in parentheses:

```go
get_thing :: proc() -> (Thing, bool) {
    return g_thing, true;
}

thing, ok := get_thing();
```

Use the same comma-separated form with `=` to assign results to existing destinations. An assignment can consume either one multi-return expression or one expression per destination:

```go
thing: Thing;
ok := false;
thing, ok = get_thing();
a, b = b, a;
```

The number of destinations must match the number of values. Right-hand expressions are evaluated from left to right before any destination is resolved or written. This makes swaps safe, and `_` can ignore a value:

```go
a, b = make_pair(b, a);
_, ok = get_thing();
```

Comma-separated right-hand expressions are supported specifically by assignment statements; they do not create a tuple value that can be used in another expression.

Procedures are normal values and can be assigned/stored like any other value:

```go
op := proc "op_add" (a: int, b: int) -> int { return a + b; };
op = proc "op_mul" (a: int, b: int) -> int { return a * b; };
```

Every procedure needs a unique identity so hotloading can match old and new code. Named declarations (`add :: proc(...)`) get this automatically. A `:=`/`=` proc value stays anonymous — one anonymous proc per scope is fine, but if a scope holds more than one, give each a unique link name with `proc "name" (...)` or the compiler reports a link ID collision.

### Methods (`method`)

Use `method()` inside a struct/class. Methods have an implicit `this` reference parameter.

```go
Dog :: class {
    name: string;

    bark :: method() {
        log_info("% says bark!", {name});
    }
}

dog := new(Dog);
dog.name = "Buddy";
dog.bark();
```

### Field access vs method calls

Use `.` for both fields and methods:

```go
hp := player.health;        // field access
player.respawn();          // method call
entity.set_local_scale({2, 2});
```

{% hint style="info" %}
Any procedure can be called as a “method” if its first parameter matches the receiver type. Real methods and procedure-valued fields on the type win before CSL falls back to a matching free procedure.
{% endhint %}

## Arrays

CSL has a few “array-like” types you’ll use constantly:

* **Fixed arrays**: `[4]int`
* **Slices / managed arrays**: `[]T` (often used as “read-only view” into an array)
* **Dynamic arrays**: `[..]T` (resizable list)
* **Unmanaged arrays**: `[^]T` (used in built-in API signatures like `format_string`, `log_info`, etc — pass values as `{a, b, c}`)

Dynamic arrays can be initialized with a compound literal. They expose `.data`, `.count`, and `.capacity`, and use method-call syntax for operations:

```go
numbers: [..]int = {10, 20};
numbers.append(30);

log_info("count: %", {numbers.count});
log_info("first: %", {numbers[0]});
```

For a full guide (including removal patterns), see [Arrays and Collections](/scripting/arrays-and-collections.md).

## Control flow

### If / else

```go
if hp <= 0 {
    die();
} else if hp < 25 {
    Notifier.notify(player, "Low health!");
} else {
    // all good
}
```

### Switch

Use `default:` for the default clause. Cases support **multiple values** (comma-separated) and **ranges**. No C-style fallthrough.

```go
Item_Tier :: enum {
    COMMON;
    UNCOMMON;
    RARE;
    EPIC;
    LEGENDARY;
}

get_tier_color :: proc(tier: Item_Tier) -> v4 {
    switch tier {
        case .COMMON:              return {0.7, 0.7, 0.7, 1.0};
        case .UNCOMMON:            return {0.3, 0.8, 0.3, 1.0};
        case .RARE, .EPIC:         return {0.3, 0.5, 1.0, 1.0};
        case .LEGENDARY:           return {1.0, 0.8, 0.2, 1.0};
        default:                   return {1, 1, 1, 1};
    }
}

// Range cases
get_difficulty :: proc(level: int) -> string {
    switch level {
        case 1..5:          return "Easy";
        case 6..<11:        return "Medium";
        case 11..20, 25:    return "Hard";
        default:            return "Unknown";
    }
}
```

`..` includes both endpoints. `..<` excludes the upper endpoint.

### While / for

```go
while condition {
    // ...
}

// Inclusive range: 0..9 includes 9
for i: 0..9 {
    log_info("i=%", {i});
}

// Half-open range: 0..<count excludes count
for i: 0..<items.count {
    log_info("item %", {items[i]});
}

// Reverse iteration
for i: 0..<items.count #reverse {
    log_info("reverse item %", {items[i]});
}

// Iterate array/slice elements
for item: my_items {
    // ...
}

// With index variable
for item, i: my_items {
    log_info("item % at index %", {item, i});
}

// Iterate custom iterators (common for entities/components)
for enemy: component_iterator(Enemy) {
    enemy.update_ai();
}
```

Custom iterator-based `for` loops require a `next :: method() -> bool` and a `current` field.

## Casting

Use `expr.(T)` or `cast(T)expr` to cast:

```go
a := 123.4;
b := a.(int);   // 123
c := cast(float)b; // 123.0
```

When the target type is already known, you can let CSL infer it:

```go
i: int = a.();
j: int = cast a;
```

Procedure values cannot be cast to another procedure signature or to/from non-procedure values. For deliberate low-level signature reinterpretation, use an unsafe declaration: `#unsafe retyped := (&callback).(*Other_Callback).*`.

Class casts must stay within one inheritance hierarchy. Upcasts do no runtime work; downcasts verify the concrete object type and throw on a mismatch. Null class values remain null when downcast.

## Passing by reference: `ref` (preferred)

When you need to modify a parameter, **prefer `ref`** over raw pointers. Taking a raw address with `&` requires an `#unsafe` context. A single unbraced unsafe declaration remains in the surrounding scope.

```go
update_position :: proc(pos: ref v2, velocity: v2, dt: float) {
    pos.x += velocity.x * dt;
    pos.y += velocity.y * dt;
}

my_pos := v2{0, 0};
update_position(ref my_pos, {10, 5}, 0.16);
```

## Closures and callbacks

Procedure literals can capture enclosing parameters and local variables:

```go
invoke_twice :: proc(callback: proc()) {
    callback();
    callback();
}

count_twice :: proc(initial: int) -> int {
    value := initial;
    invoke_twice(proc() {
        value += 1;
    });
    return value;
}
```

Capturing procedure literals are scoped callback expressions. They may only be invoked immediately or passed directly to a procedure parameter that accepts captures. That parameter may invoke the callback or forward it directly to another capture-accepting parameter. Capturing procedures cannot be assigned to variables, returned, or placed in fields, globals, collections, or other storage.

An unannotated procedure parameter might receive a capturing procedure, so it has the same invoke-or-forward restriction. Mark the parameter `#no_captures` when the procedure needs to store, return, or otherwise manipulate it. Callers may then pass only non-capturing procedures; keep persistent state in explicit userdata:

```go
Callback_State :: class {
    value: int;
}

Callback :: proc(userdata: Object);
saved: Callback;
saved_userdata: Object;

install :: proc(userdata: Object, #no_captures callback: Callback) {
    saved_userdata = userdata;
    saved = callback;
}

increment :: proc(userdata: Object) {
    state := userdata.(Callback_State);
    state.value += 1;
}

state := new(Callback_State);
install(state, increment);
```

The annotation is part of the receiving procedure's contract and may also appear in a procedure type, such as `Retainer :: proc(userdata: Object, #no_captures callback: Callback)`. A procedure with captures and a plain procedure still have the same two-word procedure/environment representation; the environment is zero for a plain procedure. Calls fail if low-level reinterpretation is used to pass a procedure with captures to a `#no_captures` parameter.

## Type info (types as values)

`typeid` values can be passed to polymorphic procs:

```go
default_of :: proc($T: typeid) -> T {
    t: T;
    return t;
}

a := default_of(int);      // 0
b := default_of(string);   // ""
c := default_of([4]int);   // {0, 0, 0, 0}
```

`#type_info(T)` returns reflection data for a type. Use its `id` field when logging or formatting the type's name:

```go
ti := #type_info(Thing);
log_info("type: %", {ti.id});
```

## Best practices (CSL in All Out)

* **Avoid global gameplay state.** Multiple players connect — store per-player state on `Player` instead.
* **Draw all player UI from `Player.ao_late_update`.** Wrap it in `is_local_or_server()`.
* **Use `is_local()` only for player-specific visual overrides.** Do not change gameplay or UI state inside it.
* **Mobile-first defaults.** Don’t rely on keyboard/mouse input unless your game is explicitly PC-focused.
* **If you’re unsure about syntax or APIs**, open the matching `.csl_engine` file under `api_references/` in your project.
