For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

You don’t need to write RPCs, SyncVars, or custom replication. We handle these for you!

Your first script (main.csl)

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

import "core:ao"

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

ao_before_scene_load :: proc() {
    // Register item definitions, currencies, etc.
    // Runs before the scene is created.
}

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.
}

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

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

    ao_update :: method(dt: float) {
    }

    ao_late_update :: method(dt: float) {
    }

    ao_end :: method() {
    }
}

For example, log a message when each player joins:

If you want a deeper explanation of when these functions run, see Game/Frame Lifecycle.

Imports

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

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:

Declarations (variables and constants)

Declarations bind a name to a value.

Variables

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

Constants

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

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

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:

Globals are mutable and persist for the lifetime of the script instance. Avoid using them 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

Vector types

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

Structs and classes

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

Structs (value types)

Classes (reference types)

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

Inheritance

Structs/classes can inherit from other structs/classes:

Procedures and methods

Procedures (proc)

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

Methods (method)

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

Field access vs method calls

Use . for both fields and methods:

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.

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 expose .data, .count, and .capacity, and use method-call syntax for operations:

For a full guide (including removal patterns), see Arrays and Collections.

Control flow

If / else

Switch

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

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

While / for

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

Casting

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

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

Passing by reference: ref (preferred)

When you need to modify a parameter, prefer ref over raw pointers.

Callbacks: function pointers + userdata (no closures)

CSL does not have closures. Inline proc(...) { ... } cannot capture surrounding variables.

To carry context, pair callbacks with a userdata: Object field:

Type info (types as values)

typeid values can be passed to polymorphic procs:

Best practices (CSL in All Out)

  • Avoid global gameplay state. Multiple players connect — store per-player state on Player instead.

  • Separate cosmetic vs gameplay logic. Use is_local() for local-only UI/particles, and is_local_or_server() for gameplay inputs that must run on server + local client.

  • 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, check the api_reference/ folder generated in your project (it contains the latest core.csl surface).

Last updated