# Welcome to All Out!

We are a group of Minecraft, Among Us, and Roblox community members building the fastest way to make watchable multiplayer games for YouTubers, streamers, and fans!

There are two ways to build games on All Out. Locally on your PC using our Windows editor, or in the cloud using our AI-native Cloud Build tools. You can switch between them at any time!

## Building Locally

**To make a game locally, you'll need to bring:**

* A Windows PC
* A killer idea
* An eye for game design
* Some programming experience (or a lot of patience with AI models😅)

**We'll take care of the rest:**

* 250,000 high quality textures, animations, and sound effects
* World building tools
* A custom scripting language built for multiplayer game creation
* One-click multiplayer on iOS, Android, and PC
* A community of hundreds-of-thousands of players & YouTubers eager to try your game

[Get started with the Windows PC editor](/getting-started/project-layout)

## Cloud Build

Cloud Build enables you to build games on any device, anywhere, any time.

When you use Cloud Build, we spin up a virtual machine running the All Out editor, and pair it with the latest AI models, our asset catalog, and MCP tools to bring your game to life.

Cloud build has limited free usage since we pay for the VM time and AI models that power it. To get more usage, we offer several subscription tiers to cover extra usage.

[Get started with Cloud Build](/using-cloud-build/introduction-to-cloud-build)


# Creating Your First Project

Creation starts on the All Out website in the Creator portal, then you'll download the All Out editor to make your game world!

Once you have access to the creator tools, the Create tab on the All Out website will have an option to create a "New Game".

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-adf8618776aa15d750e6f2c028c4fcada487d6e8%2Fimage.png?alt=media" alt="New Game option on the Creator Portal Create page"><figcaption></figcaption></figure>

You'll be asked to pick a game name (you can always change it later!) and then you'll be taken to the management page for your game!

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2FQ4RGbkCJQOGOG3BSKBzz%2Fimage.png?alt=media&amp;token=fca30bb2-8a4b-4ddc-b296-5d9ec72f44d7" alt="Choices for local development and Cloud Build"><figcaption><p>Choosing build locally will prompt you to install the Windows editor and is great for professional users. Cloud Agent is a good way to get started. See <a href="/using-cloud-build/introduction-to-cloud-build">Introduction to Cloud Build</a> for more information.</p></figcaption></figure>

After you've created your game, you'll be taken to the "Manage" section which will allow you to fill in extra information and change game settings. You can come back to this page at any time.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2FCIjgMXd7gNAm9q1YDAcb%2Fimage.png?alt=media&amp;token=ff604935-5740-43b2-9f8d-7c7f71066b33" alt="Manage section for a game in the Creator Portal"><figcaption></figcaption></figure>

Next, you'll [**download and launch the editor**](/getting-started/first-game)!

## Local project files

A new local project starts with this structure:

```
your-game/
├── ao.project
├── scripts/
│   └── main.csl
├── scene/
├── res/
└── .gitignore
```

`ao.project` identifies the project. Put CSL source files in `scripts`, scene data in `scene`, and imported textures, audio, and other assets in `res`. Keep all of these files in version control except paths already excluded by the generated `.gitignore`.


# Launching The Editor

The Editor is a downloadable program that allows you to create and upload your game world to All Out.

### Downloading/Opening the Editor

Now that you've created your first project, you'll see a green "Edit" button at the top of the page.

![Edit button on a game's Creator Portal page](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-46c96c817779f8f251d86dd024b340429cfe545e%2Fimage.png?alt=media)

When clicked:

* If you've already played All Out, the launcher will already be installed and open the editor for you!
* If you haven't played All Out, you'll be prompted to download and install the launcher.

{% hint style="info" %}
Use the Creator Portal's **Edit** button for the first launch so the editor receives the correct project and platform version. After the project exists locally, you can also open it from the editor's project picker.
{% endhint %}

### Setting your games directory

When you open the editor for the first time, you'll be asked to select a game directory. You can select any folder on your PC, although **we recommend creating a dedicated folder to store your games**.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-a87e8b8060028a8fb9f05127cf987f679f3ab4d5%2Fimage.png?alt=media" alt="Games directory picker in the All Out editor"><figcaption></figcaption></figure>

After you've selected a games folder you'll be notified that the project you launched isn't found on your PC already (since this is a new game you're making!)

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-f3502568e7b58d105ad392f268db6122245a9244%2Fimage.png?alt=media" alt="Prompt to create a fresh local copy of a new project"><figcaption></figcaption></figure>

Select the "Start Fresh" option to pick/create a folder inside your games directory to store a local copy of your new project.

After creating the folder for your game, you'll be launched into the editor and [**you're ready to start building your first game!**](/getting-started/testing-and-publishing)

{% hint style="info" %}
If Windows Defender causes a measurable slowdown while files are generated, see [Windows Defender exclusions](/reference/windows-defender-exclusion). An exclusion is not normally required.
{% endhint %}


# Building a Basic Game

To get you started, this guide walks you through building an apple collection game!

### Apple Collection

{% hint style="info" %}
This example game skims over details that are covered in-depth in other guides. If you need any help, check the left sidebar or use the search feature at the top for extra information!
{% endhint %}

#### Building the map

We'll start by creating a basic world with an island to walk on and an apple to collect later!

1. Search for an Apple and an Island in the [Asset Catalog](/using-the-editor/asset-catalog) and drag them into your game's scene view.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-acecdae4bfa75a93ca72c27b79de79fa55dbdb1c%2Fimage.png?alt=media" alt="Apple and island assets placed in the scene"><figcaption></figcaption></figure>

2. That island is pretty small! In the [Inspector](/using-the-editor/inspector) set the `Scale` to 10x10 to make it bigger

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-c4ca715b0b60f8691738c5198f00d957c236775e%2Fimage.png?alt=media" alt="Island scale set in the Inspector"><figcaption></figcaption></figure>

3. Now the island is a good size, but the apple is behind the island! Set the island's `Layer` to -10 to push it behind everything else.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-126a998b78f427e923e730b4971f600cb124eb91%2Fimage.png?alt=media" alt="Island rendering layer set in the Inspector"><figcaption></figcaption></figure>

#### Adding Scripts

In your game project's `scripts` directory, create a new file called `pickup_apple.csl` and open it in a text editor or use an [AI based IDE](/scripting/cursor-claude-antigravity-setup).

Paste the following example script and save the file:

```go
// This creates the component that makes the apple interactable,
// we'll add it to the Apple in the next step of the tutorial.
Pickup_Apple :: class : Interactable {
    is_picked_up: bool;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Pick up");
    }

    can_use :: method(player: Player) -> bool {
        if is_picked_up return false;
        return true;
    }

    on_interact :: method(player: Player) {
        if is_picked_up return;
        is_picked_up = true;

        // Start the pop effect
        effect := new(Apple_Pop_Effect);
        entity.set_active_effect(effect);
    }
}

// A cool animation for when you eat it :D
Apple_Pop_Effect :: class : Effect_Base {
    sprite: Sprite_Renderer;
    start_scale: v2;
    start_pos: v2;

    effect_start :: method() {
        sprite = entity.get_component(Sprite_Renderer);
        start_scale = entity.local_scale;
        start_pos = entity.local_position;
        set_duration(0.4);
    }

    effect_update :: method(dt: float) {
        t := get_elapsed_time() / 0.4;

        // Scale up then quickly shrink to nothing
        scale_curve: float;
        if t < 0.3 {
            // Quick pop up (scale to 1.3x)
            scale_curve = lerp(1.0, 1.3, Ease.out_back(t / 0.3));
        } else {
            // Shrink to nothing
            scale_curve = lerp(1.3, 0.0, Ease.in_back((t - 0.3) / 0.7));
        }

        entity.set_local_scale(start_scale * scale_curve);

        // Float upward slightly
        rise := Ease.out_quad(t) * 0.5;
        entity.set_local_position({start_pos.x, start_pos.y + rise});

        // Fade out near the end
        if sprite != null {
            alpha := 1.0 - Ease.in_quad(max(0.0, (t - 0.5) / 0.5));
            sprite.color.w = alpha;
        }
    }

    effect_end :: method(interrupt: bool) {
        entity.destroy();
    }
}
```

> Saving a CSL file is detected and compiled automatically. In a running test, press **Alt+F9** to manually hot-load the compiled scripts. A hot-load keeps current values of globals and of existing objects' fields, so restart to apply a changed initializer. Stop and restart for scene changes. **Ctrl+R** reloads the whole editor project and is not required for normal script compilation.

#### Adding the Apple component

Now that you've created your apple script, we'll need to apply it to the apple in the scene. To do that, we'll add the new `Pickup_Apple` component from the script by using the Inspector.

1. Select your Apple in the scene.
2. Click the "Add Component" button in the Inspector.
3. Search for the "Pickup\_Apple" component and click it to add it to the apple.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-40e3c8234f5c0af781a1c92e85748280e0aa8d97%2Fimage.png?alt=media" alt="Adding the Pickup Apple component in the Inspector"><figcaption></figcaption></figure>

Now you'll see the component has been added to the apple! You can adjust some fields from the Apple by changing the values in the inspector:

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-04d42907e5796fe34eb094810c5666a4b4426d80%2Fimage.png?alt=media" alt="Pickup Apple component fields in the Inspector"><figcaption></figcaption></figure>

#### Playing your game

When you're ready, hit the fun green triangle and try your game!

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-e26510caa7307e5538830ed5ffd985a6ee920879%2Fimage.png?alt=media" alt="Apple collection game running in a local test"><figcaption></figcaption></figure>

### Next Steps

In the next sections, you'll learn how to playtest your game with multiple players, create your own scripts, and build your dream world.

**Here are some challenges to try:**

* Make many apples for the player to eat
* Play sound effects when eaten
* **Advanced:** try writing a script to respawn more apples after they're eaten

\--

{% hint style="info" %}
SIDE NOTE: If your island is low resolution/pixelated, click the island's png asset in the "Assets" section and set the Resample Multiplier to 1 and click Apply

<img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-d564ee004b2d5a196dd984b18137a8680b09c04c%2Fimage.png?alt=media" alt="Texture settings with Resample Multiplier set to 1" data-size="original">
{% endhint %}


# Playtesting Your Game

We make it super simple to try your game locally, with friends, and on mobile!

### Testing your game

Once you're ready to play your game, hit the green play button at the top of the editor. This will launch the server and client on your local PC to play!

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-54b3b10afbb9559a16389212163403d2dde0e514%2Fimage.png?alt=media" alt="Play button in the All Out editor toolbar"><figcaption></figcaption></figure>

### Testing with multiple local clients

After each feature you build, it's important to launch multiple clients at the same time to make sure that the game functions even when multiple players interact with your game world. This is a common source of bugs, so we make it easy to do this testing.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-5ab9444ae561ae865cf92db492da293514de3275%2Fimage.png?alt=media" alt="Local client-count controls beside the play button"><figcaption></figcaption></figure>

> Use the +/- buttons next to the play button to determine how many instances of the client to start at a time. You can also use the "Add Client" button while testing to add additional clients any time you need.

### Testing with friends

To test your game with friends, publish the game with the upload button in the top right of the editor and set the game's visibility in the creator portal to "Unlisted". Then send the link to the "View Public Page" to your friends!

### Testing on mobile

Since nearly 90% of All Out's player base is on mobile, it's super important to make sure your game plays well on phones!

We make it easy to test on mobile by automatically exposing your local editor server to devices on your local network.

To join, launch your game from the editor with the play button, then open your mobile device. **It must be connected to the same Wi-Fi/network as your PC**.

At the top of the All Out mobile app you'll see a yellow "Join Editor Game" prompt after refreshing.

### Making changes while playing/hot reloading

Saving a CSL file is detected and compiled automatically. While a test is running, press **Alt+F9** to manually hot-load the compiled scripts into the game. A hot-load keeps the game's current state: globals, `#static` variables and the fields of existing objects keep their values and `ao_start` does not run again, so restart the test to apply a changed initializer or startup code. The game, including clients that join later, keeps its previous scripts until a hot-load succeeds; if it refuses one (for example, after changing the fields of a registered item type), restart the test. Stop and restart the test after changing the scene or other non-script project data. **Ctrl+R** reloads the whole editor project; it is not required for normal script compilation.

The editor prepares and publishes changed assets in the background. Start Game waits for that work to finish so it cannot launch with a stale asset package. If publishing fails, use **Try Again** beside the play controls after correcting the reported asset error.

For unusual cache or compiler problems, the **File** menu has two recovery actions:

* **Force Recompile Scripts** clears the C# assembly reuse cache and recompiles the project scripts.
* **Clear Project Caches** integrity-checks the current scene and compile caches, asks for confirmation, and rebuilds them from project sources.


# Introduction/Scene View

### Basic Layout

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-bc234bbc1c42999d963f3d6860412cbbea6d8f7a%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

### Selecting Entities

#### Basic Selection

To select any entity in your scene, just click on it once. You can also double click any entity in your [Hierarchy](/using-the-editor/hierarchy) to find it in the scene quickly.

#### Group Selection

To select many entities at once, drag your mouse across the scene to create a box that will select all entities within it.

#### Lasso Tool

Hold alt and drag your mouse around the scene to select only the items that cross your mouse's path!

#### Overlapping Entities

To choose between entities that overlap or select an item behind another one, click the item multiple times to swap between them.

### Editing Tools

#### Position, Rotation, Scaling, and Depth Offset

You can use the tools in the top left of the scene view to switch between positioning, rotating, scaling and changing the depth offset of entities in your world.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-8c952fa7411e298b8146af2546f2301934f51ac9%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

When you select each tool, it will show an overlay over the entity in the scene you can use to adjust its properties.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-8a74f8f44ba764edda5f87c77ea0061f81fc96f0%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

If you click and drag the center circle, this will freely move the tree anywhere you want.

You can click and drag the red or green arrows to lock your movement left/right or up/down for more precise adjustments.


# Asset Catalog

All Out provides 250,000+ assets for you to use freely in your games made on the platform!

At the bottom of the editor are two windows. The **Asset Catalog** and the **Assets** view

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-b180d5fca48f128bc52102021e32b1cce4eb9f49%2Fimage.png?alt=media" alt="Asset Catalog and Assets panels"><figcaption></figcaption></figure>

#### Adding Assets From the Asset Catalog

To add textures, sound effects, and animations to your project: search for what you're looking for, then click the cloud icon to download it. You can also drag a texture or Spine animation into the scene to download and place it.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-6c099e530e12c479254c4e1328b913254f558ed2%2Fimage.png?alt=media" alt="Searching the Asset Catalog"><figcaption></figcaption></figure>

The **assets** pane on the right shows all the files and folders inside of your game's `res` directory. You can pull these assets directly into your scene by dragging them in, or reference them in your scripts.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-53c498a8f58f93ac0e0448526953aabb17660ba8%2Fimage.png?alt=media" alt="Project files in the Assets panel"><figcaption></figcaption></figure>

#### Adding Assets From Your Local PC

You can add textures (.png), sound effects (.wav) or [Spine Animations](/core-engine-concepts/spine) from your local computer to the `res` folder to use them in your game.

Sound effects must be PCM-encoded `.wav` files. Spine rigs need a matching `.spine`, `.atlas`, and every image file named by the atlas.

See [Spine Animations](/core-engine-concepts/spine) for the Spine bundle layout.


# Hierarchy

### Entities

Entities are the building blocks of worlds in All Out. Every player and object in games is an "Entity" and the hierarchy is where you can see all of the entities in your game world.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-156fbe43d388a1f890b1e6bdab5fdc1746f3ba57%2Fimage.png?alt=media" alt="Scene entities in the hierarchy"><figcaption></figcaption></figure>

You can click on any entity in your game to highlight it in the hierarchy.

When an entity is selected in the hierarchy, its properties (like size, position, and layering) can be changed in the [Inspector](/using-the-editor/inspector)

Double-click an entity in the hierarchy to center the scene view on it.

#### Creating New Entities

There are 3 ways to make new entities in your scene

* Press the "+" button at the top of the hierarchy
* Right click empty space on the hierarchy and click "New Entity"
* Drag an [Asset Catalog](/using-the-editor/asset-catalog) asset into the scene

You can also drag textures, Spine rigs, and prefabs from the Assets pane into the scene.

### "Folders"

To make the equivalent of a folder, just create a new entity and drag other entities on top of it to group them.

This is especially useful if you have a set of entities you want to duplicate because you can hit ctrl+d with the "folder" entity selected and it will also duplicate all of the child entities grouped under it.


# Inspector

The inspector pane allows you to make changes to any entity in your scene like changing its size or layer and adding components

At the top of the inspector, you have basic properties about the entity like its position and scale.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-660a5ccc906e033b525e2a0873d975202c200906%2Fimage.png?alt=media" alt="Entity transform and components in the inspector"><figcaption></figcaption></figure>

Below the entity properties are all of the components you have currently added to that entity.

Each component can provide "fields" for you to edit. For example, Interactable components allow you to change their `Text` or `Radius` fields.

Your own custom components made with scripts can expose fields to the inspector as well. For example, you could make a tycoon purchase interactable with a field to change its price in the editor.

```go
Tycoon_Unlock_Button :: class : Interactable {
    @ao_serialize price: s64;
    is_unlocked: bool;

    ao_start :: method() {
        set_listener(this);
        set_text("Unlock");
    }

    can_use :: method(player: Player) -> bool {
        return !is_unlocked;
    }

    on_interact :: method(player: Player) {
        if !Economy.can_withdraw_currency(player, "Coins", price) return;

        Economy.withdraw_currency(player, "Coins", price);
        is_unlocked = true;
        set_text("Unlocked");
    }
}
```

Now when you add the `Tycoon_Unlock_Button` component to any entity, you can set a price for that entity to unlock.

Runtime-only fields such as `is_unlocked` are not shown unless they also use `@ao_serialize`.


# Prefabs

Prefabs allow you to create groups of entities that can be spawned in dynamically or reused in the world and easily changed in the editor.

### Creating Prefabs

Prefabs must be created in the editor by right clicking in the [Hierarchy](/using-the-editor/hierarchy) and selecting "create prefab".

When you do this, a new asset will be created in your asset explorer window that can be dragged into the scene.

To edit a prefab, right click on it and click "Edit Prefab" which will open a new window to make changes to those entities.

When you save a prefab, all instances of that prefab in your scene will automatically be updated.

### Limitations

{% hint style="info" %}
The All Out prefab system is currently very early in development and doesn't allow for dynamic component fields on each instance of a prefab. Each linked instance does preserve its root name, position, rotation, scale, enabled state, network flags, and sibling order when the prefab is updated.
{% endhint %}

### Spawning Prefabs From CSL

```go
enemy_prefab := get_asset(Prefab_Asset, "prefabs/enemy.prefab");
enemy := Scene.instantiate(enemy_prefab, spawn_position);
```

`Scene.instantiate` returns the prefab's root entity. Use its optional callback when fields must be set before component `ao_start` methods run.

### Unlinking Prefabs

You can change an instance of a prefab in your scene back into its individual entities by right clicking it in the hierarchy and selecting "Unlink Prefab". This can be useful if you need to add dynamic fields.


# Building Worlds

Now that you have the basics of using the editor down, here are some tips on building immersive, high-quality worlds in All Out:

### Layering and Depth Offset

Sprite\_Renderers and Spine\_Animators have two unique properties that determine which is on top as they move up and down in the scene.

**Layer** is the broad and simple way of deciding what goes on top. Elements with a low layer will be behind everything. Some recommended values might be:

* Map background textures: -100 layer
* Players, NPCs, Cars: 0 layer
* Overlays, clouds, particles: 100 layer

**Depth Offset** is more precise and lets you determine when something will go behind something else if it's **on the same layer**.

* Adjust the depth offset easily using the depth offset tool in the top left of the scene view.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-d52a427f09cf69b672825e2b16ee9dd1ed1a378a%2Fimage.png?alt=media" alt="Depth-offset line across a world object"><figcaption><p>The player will appear behind the object when it is above the red line, and in front of the object when it's below.</p></figcaption></figure>

### Schleem for Scale

To easily check the sizing and layering of elements in your game, you can preview a player's character by holding `Alt+Space` and moving your mouse around the scene window.

Hold **Ctrl** as well to snap the preview to the nearest point on the selected navmesh.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-d3fb810cffeafad8129616fd79acc26721fb5e5d%2Fimage.png?alt=media" alt="Player scale preview in the scene view"><figcaption></figcaption></figure>


# Terrain

The terrain system lets you paint large, seamless ground surfaces using up to four blended texture layers and an optional water overlay.

## Adding a Terrain

Add a `Terrain_Component` to any entity. The terrain is centered on the entity's position and spans a grid of chunks, each covering 32×32 world units.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-787aebcb261f07bfe955e3089d4e9c85171dab46%2FAllOutEditor_gLy0UWMR3i.png?alt=media" alt="Terrain component fields in the inspector"><figcaption></figcaption></figure>

## Terrain Properties

| Property                     | Description                                                                                               |
| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Width / Height in Chunks** | Size of the terrain grid. Each chunk is 32×32 world units, so a 4×4 terrain covers 128×128 units.         |
| **Detail Tiling**            | How many times each ground texture repeats across a single chunk. Higher values = more detail repetition. |
| **Render Layer**             | Controls draw order relative to other sprites and terrain. Lower values draw behind.                      |
| **Chunk Load Margin**        | Extra distance (in world units) beyond the camera view at which chunks are loaded.                        |
| **Texture 0–3**              | The four ground texture layers. Each channel of the splat map (R, G, B, A) corresponds to one texture.    |

## Editing Mode

Select the terrain entity and click the **Edit** button in the inspector to enter terrain editing mode. Click **Cancel** to exit.

While editing, you can paint directly in the scene view using the brush settings below.

## Brush Settings

| Setting              | Description                                                                                                                       |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Brush Radius**     | Size of the circular brush in world units.                                                                                        |
| **Brush Hardness**   | Controls falloff. At 1.0 the brush is a hard circle with no feathering. Lower values produce a soft gradient from center to edge. |
| **Brush Strength**   | How strongly a soft texture stroke applies. At full hardness, texture paint overwrites the pixel and ignores strength.            |
| **Selected Channel** | Which texture layer to paint (0–3 corresponding to Texture 0–3, or 4 for water).                                                  |
| **Erase Mode**       | When enabled, painting removes the selected texture layer instead of applying it.                                                 |

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-df4b2e908a2a6cf52c7339fee36696fbf52a5acd%2FAllOutEditor_vAqo4ea6ss.png?alt=media" alt="Terrain brush controls and texture channels"><figcaption></figcaption></figure>

## Painting Terrain Textures

Painting works by modifying a per-chunk 256×256 splat map. Each pixel stores four channel weights (RGBA) that blend between Texture 0–3.

* **Click and drag** to paint along a line. The brush stamps at regular intervals along your stroke.
* **Soft blending** gradually increases the target channel while proportionally reducing others, preserving smooth transitions.
* **Hard overwrite** (at full hardness) sets the target channel to 100% and all others to 0%.
* **Erase** reduces the selected channel, letting underlying textures show through.

Chunks are **allocated lazily** — empty chunks consume no memory until you first paint on them.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-baa5a9f479b5ff98042b45fde1f77f04412095b5%2FAllOutEditor_NXvAzbpNOh.png?alt=media" alt="Blended terrain textures"><figcaption></figcaption></figure>

## Water Layer

The terrain also supports a paintable water overlay with its own set of properties:

| Property                | Description                                                  |
| ----------------------- | ------------------------------------------------------------ |
| **Water Body Texture**  | The main scrolling water texture.                            |
| **Water Shore Texture** | Texture applied at the edges of water areas.                 |
| **Water Body Tiling**   | Repetition of the water texture.                             |
| **Water Scroll Speed**  | How fast the water texture scrolls.                          |
| **Water Shore Width**   | Width of the shore transition band.                          |
| **Water Alpha**         | Opacity of the water overlay.                                |
| **Water Render Layer**  | Draw order for the water layer (default: one above terrain). |

Water uses a single-channel mask instead of the four texture channels. Select channel 4 to paint it. The water brush uses a fixed soft falloff and partial opacity; hardness and strength are hidden. Box fill writes fully opaque water. Erase targets zero opacity.

## Box Fill

In addition to the circular brush, terrain and water can be filled using an axis-aligned box selection. Box fill always uses hard overwrite — useful for quickly covering large rectangular regions with a single texture or filling/clearing water areas.

## Memory & Performance

The inspector shows a debug readout at the bottom of the terrain section:

* **Chunks**: How many chunks have allocated splat/water data out of the total grid.
* **Memory**: Estimated CPU and GPU memory usage.

Only chunks you've painted on consume memory. Splat and water data are LZ4-compressed for serialization, so file sizes stay small even for large terrains.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-9fa1a392ee54602eefb49075b5a87f5480671b53%2FAllOutEditor_eQNOpvv3cj.png?alt=media" alt="Terrain chunk and memory readout"><figcaption></figcaption></figure>

## Tips

* Start with a low brush strength (0.1–0.3) for natural-looking blends between texture layers.
* Use a large, soft brush for broad base coverage, then switch to a smaller, harder brush for detail edges.
* Use `1`–`4` to select a texture channel and `5` for water.
* Use `[` and `]` to change brush radius.
* Hold **Shift** and click to paint a straight line from the previous stroke.
* Hold **Ctrl** and drag for box fill.
* Keep chunk counts reasonable. A fully painted 16×16 terrain uses about 64 MiB of GPU splat-map memory. A fully allocated water mask adds about 16 MiB.


# Tilemaps

The tilemap system lets you paint tile-based worlds using auto-tiling masks, multiple layers, and wall extrusion with built-in collision generation.

## Adding a Tilemap

Add a `Tilemap_Component` to any entity. The tilemap is centered on the entity's position and spans a grid of chunks (each 32×32 tiles). The total size in tiles is `width_in_chunks × 32` by `height_in_chunks × 32`.

## Tilemap Properties

| Property                     | Description                                                                            |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| **Width / Height in Chunks** | Size of the tile grid. Each chunk is 32×32 tiles. A 10×10 tilemap gives 320×320 tiles. |

Layers are configured individually once you enter editing mode (see below).

New ground layers have transparent outlines. New wall layers have opaque black body and top outlines and no top texture, so the body texture fills the top row. Both masks default to `$AO/tile_mask.png`. Choose the ground or wall-body fill textures separately. These creation defaults apply in the editor, MCP, CSL, and C#; loading an existing layer preserves its saved settings.

For black wall tops, add a 1×1 opaque black PNG to the project as `res/ao_black.png` if it does not exist, then set it as each wall layer's top texture. The engine no longer ships `$AO/black.png`: layers saved with it load with no top texture, and scripts should load `ao_black.png` instead.

Changing layer kind switches a default ground/body outline to match the new kind. Custom outline colors are preserved.

For a building, use floor and wall layers on the same tilemap. Extend flooring underneath every wall tile as well as the interior; stopping at the inner wall edge leaves a gap. Door openings remove wall tiles while retaining the floor.

## MCP Authoring

Agents must keep tile size at its default 1, including in runtime code, and keep structural entities at unit scale. Tile size is not an MCP option. Change the footprint through tile counts and the material pattern through texture scale.

* `tilemap_inspect` reads layer settings, occupied bounds, paginated horizontal occupancy runs, and tile/world conversions.
* `asset_prepare_tileable` trims transparent border padding from downloaded fill textures through asset crop settings and enables repeating. It preserves existing crop and opaque artwork; check `opaqueEdges` for remaining edge transparency. `apply:false` previews the crop. Supply `crop` to replace the current insets when a known feathered border also needs removal. Crops use `[left,bottom,right,top]` source-pixel insets and affect all uses of the asset without changing the source image.
* `tilemap_configure` resizes the grid and adds, patches, removes, or clears layers. New layers require fill textures; providing `textures` replaces the entire list. Project fill textures are prepared for repeating without automatically cropping them. Masks are left unchanged.
* `tilemap_paint` sets or erases rectangles, connected brush strokes, and explicit cells.
* `tilemap_stamp` copies occupancy through explicit layer mappings, optionally across maps. Rotations are counterclockwise in the Y-up grid. Merge preserves destination cells beneath source holes; replace clears them.

Use `modify_scene` to create the entity and `Tilemap_Component`. Tools target an entity by AOID or name, with `componentId` when needed. Tile coordinates and layer indices are zero-based; tile rectangle endpoints are inclusive, and a world rectangle covers the cells whose centers lie in \[min,max). Use `tilemap_inspect`'s `worldCenter` when placing objects at tile centers: native tile-to-world conversion returns the tile corner. Position and scale are respected; entity rotation is ignored.

Paint and stamp reject out-of-bounds destinations unless `clip: true`. Ordered operations validate before applying each edit and stop at the first error, retaining earlier successes. MCP limits each request to 128 operations, 1,000,000 cell visits and 1,000,000 expanded tile instances, with grids from 2 to 100 chunks per axis.

Inspect collision footprints with `editor_scene_screenshot(colliderDebug: true)`. Only wall layers with collision enabled generate these shapes; verify openings and solid boundaries with the player in game.

## Runtime Scripting

`Tilemap_Component` exposes the same persistent layer data used by the editor. Layers are addressed by zero-based index. Removing a layer shifts every later index down by one, so do not keep an index across layer removal unless you update it.

### CSL

```csl
tilemap := entity.get_component(Tilemap_Component);
ground := tilemap.add_layer(.GROUND);

tilemap.set_layer_tint(ground, v4{0.8, 0.9, 1, 1});
grass := tilemap.add_layer_texture(ground, get_asset(Texture_Asset, "tiles/grass.png"), 1);
tilemap.set_layer_texture_scale(ground, grass, {2, 1});
tilemap.set_layer_texture_offset(ground, grass, {0.5, 0});
tilemap.set_layer_mask(ground, get_asset(Texture_Asset, "$AO/tile_mask.png"));
tilemap.set_tile(ground, 10, 12);

world_position := tilemap.tile_to_world(ground, 10, 12);
tile_x, tile_y := tilemap.world_to_tile(ground, world_position);
```

CSL tilemap mutations are ordinary scene state. They participate in prediction, rollback, and server reconciliation like other CSL gameplay state.

### C\#

```csharp
Tilemap_Component tilemap = Entity.GetComponent<Tilemap_Component>();
int ground = tilemap.AddLayer(TilemapLayerKind.Ground);

tilemap.SetLayerTint(ground, new Vector4(0.8f, 0.9f, 1.0f, 1.0f));
int grass = tilemap.AddLayerTexture(ground, Assets.GetAsset<Texture>("tiles/grass.png"));
tilemap.SetLayerTextureScale(ground, grass, new Vector2(2.0f, 1.0f));
tilemap.SetLayerTextureOffset(ground, grass, new Vector2(0.5f, 0.0f));
tilemap.SetLayerMask(ground, Assets.GetAsset<Texture>("$AO/tile_mask.png"));
tilemap.SetTile(ground, 10, 12);

Vector2 worldPosition = tilemap.TileToWorld(ground, 10, 12);
tilemap.WorldToTile(ground, worldPosition, out int tileX, out int tileY);
```

C# calls mutate the current simulation only. For a multiplayer runtime change, use the game's existing RPC flow to invoke the same mutation on every peer; the tilemap API does not create a new replication channel.

The runtime API includes:

* `Resize`, chunk/tile dimensions, and layer-aware local/world coordinate conversion.
* Layer add, remove, clearing, kind, render mode/order, tint, masks, outlines, wall settings, and collision settings.
* Weighted texture add, replace, reweight, remove, lookup, and per-texture scale/offset.
* Individual tile query, set, and clear operations.

Tilemaps are at least 2×2 chunks. Resizing preserves the map center, moves existing tile coordinates by half the size difference, and crops tiles outside the new bounds. Out-of-bounds tile writes do nothing and queries return false. Texture weights must be non-negative, and each layer supports at most 256 textures.

Invalid layer or texture indices and invalid layer settings are rejected at the scripting boundary. Texture scale must be positive on both axes. C# throws an `ArgumentOutOfRangeException` (or `ArgumentException` for the texture-count limit), while CSL reports a script error containing the invalid value and valid range.

Layer tint multiplies the complete layer, including outlines and wall tops. Tint changes are applied as a render uniform and do not rebake tiles or rebuild collision. Multiple tile and collision changes made before the tilemap's next update are coalesced into one geometry/collision rebuild.

## Editing Mode

Select the tilemap entity and click the **Edit** button in the inspector to enter tilemap editing mode. Click **Cancel** to exit. Editing mode deactivates automatically if you deselect the entity.

While editing, a red border outlines the tilemap bounds and a grid overlay is drawn in the scene view.

## Tools

The tilemap editor has three tool modes, switched via hotkeys:

| Tool       | Hotkey | Description                                       |
| ---------- | ------ | ------------------------------------------------- |
| **Brush**  | `B/W`  | Paint tiles by clicking and dragging.             |
| **Eraser** | `E`    | Remove tiles by clicking and dragging.            |
| **Stamp**  | `S/R`  | Select a region to copy, then paste it elsewhere. |

You can also **hold Shift** while in Brush mode to temporarily erase.

## Brush Painting

Click and drag to paint tiles on the selected layer. The brush uses Bresenham line interpolation between frames so you never skip tiles even when moving fast.

**Brush size** is adjusted with `[` and `]` (range 1–32). The brush is circular — tiles within the radius are affected.

**Alt+click** draws a straight line from your last click position to the current tile, useful for long straight edges.

**Ctrl+drag** switches to rectangle fill mode — drag to define a rectangle, and all tiles in the region are filled (or erased if Shift is also held) when you release.

## Layers

Each tilemap supports multiple layers. Click a layer in the inspector to select it for painting. The selected layer is highlighted in blue.

### Adding and Removing Layers

* Click **Add Layer** at the bottom to create a new layer.
* Click **Remove Layer** inside a layer's panel to delete it.

### Layer Properties

| Property         | Description                                                                                                                                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Kind**         | `Ground` or `Wall`. Ground tiles are flat; Wall tiles extrude vertically.                                                                                                                                                                        |
| **Layer Mode**   | `Default` uses automatic layering (ground behind player, walls Y-sorted). `Manual` lets you set a fixed render layer value.                                                                                                                      |
| **Manual Layer** | Only shown in Manual mode. The explicit render layer value.                                                                                                                                                                                      |
| **Tile Size**    | Visual size of each tile in world units; defaults to 1. Agents must leave it unchanged.                                                                                                                                                          |
| **Tint**         | Color multiplier for the complete layer, including wall tops and outlines.                                                                                                                                                                       |
| **Textures**     | One or more textures with weights for randomized variety, plus independent scale and offset controls for each texture's repeating pattern. The engine hashes tile coordinates to deterministically pick a texture per tile based on the weights. |
| **Mask**         | An 8×8 grid texture used for auto-tiling. Each cell corresponds to a different adjacency configuration (edges, corners, isolated, etc.). A default map is included `$AO/tile_mask.png`                                                           |
| **Outline**      | Color for tile outlines (alpha 0 = no outline).                                                                                                                                                                                                  |

### Multi-Texture Variety

Each layer can have multiple textures with weights. Tiles deterministically select a texture variant based on a hash of their grid coordinates, so the pattern is stable across saves and doesn't shift when you add/remove tiles.

Each texture is shown as a card with a full-width asset picker and separate **Weight**, **Scale (Tiles)**, and **Offset (Tiles)** rows. Weight controls how frequently the texture appears relative to the others.

Scale controls the size of one repeat in tile units. For example, a Y scale of `2` makes the texture repeat every two vertically stacked tiles. Scale must stay positive. Offset shifts the repeat origin in local tile units; a positive X or Y value moves the origin in that positive tilemap direction. These controls affect only the texture pattern, not tile geometry, masks, collision, or texture selection.

## Auto-Tiling

Tiles automatically select the correct mask cell based on their 8-directional neighbor configuration. The system:

1. Checks all 8 neighbors (N, NE, E, SE, S, SW, W, NW).
2. Diagonal neighbors only count if both adjacent cardinal neighbors are present (prevents visual artifacts at corners).
3. Maps the resulting 8-bit adjacency mask to one of 64 cells in the 8×8 mask texture.

This means you only need to paint tile presence — the correct edge, corner, and interior visuals are computed automatically.

## Wall Layers

Setting a layer's kind to **Wall** enables vertical extrusion. Wall tiles are placed on the grid like ground tiles, but each placed tile generates a column of body tiles extending upward by the **Wall Height**.

### Wall Properties

| Property        | Description                                                                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Wall Height** | Number of tiles tall each wall column is (minimum 2).                                                                                                                                      |
| **Top Texture** | Separate texture for the top row of the wall (the "cap"), with the same scale and offset controls as body textures. Its card is always visible rather than nested in the Textures section. |
| **Top Mask**    | Mask texture for the wall top (uses the same 8-neighbor auto-tiling as ground).                                                                                                            |
| **Top Outline** | Outline color for the wall top tiles.                                                                                                                                                      |

Wall bodies use the layer's main texture. Wall tops use the base row's neighbors, which keeps cap edges clean.

Walls are Y-sorted by their base row, so players walk behind walls above them and in front of walls below.

The tilemap follows its entity's X/Y scale. Rendering, scene-view painting and selection, coordinate conversion, wall collision, and navmesh input all use the scaled tile geometry. Negative scale mirrors the tilemap. A zero scale axis collapses the geometry and cannot be used for world-to-tile editing or conversion.

### Wall Collision

| Property       | Description                                                                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Collision**  | Generate obstacle edges for navmesh rebuilding.                                                                                                              |
| **Inset**      | Shrinks collision edges inward from tile boundaries for tighter-fitting collision. Values are in unscaled tilemap-local world units and follow entity scale. |
| **Offset**     | Shifts all collision edges in unscaled tilemap-local world units, then follows entity scale.                                                                 |
| **Debug Draw** | Draws green collision edges in the scene view for debugging.                                                                                                 |

When collision is enabled, the engine builds obstacle edges from exposed tile boundaries. Navmeshes with automatic rebuilding enabled pick up those edges. In CSL, the edges block movement only for agents locked to such a navmesh; otherwise add colliders.

## Stamp Tool (Copy & Paste)

Press `S` to switch to Stamp mode. This enables a region selection workflow:

1. **Select**: Click and drag to highlight a rectangular region. The selected tiles are copied to the clipboard. A "Copied" popup confirms the action.
2. **Paste**: Press `Ctrl+V` to enter ghost mode. A translucent preview of the clipboard follows your cursor.
3. **Rotate**: Scroll the mouse wheel to rotate the ghost preview in 90-degree increments.
4. **Place**: Click to stamp the tiles. Hold Shift while clicking to place multiple copies without exiting ghost mode.
5. **Cancel**: Press `Escape` to exit ghost mode without placing.

Hold **Ctrl** while starting a stamp selection to copy tiles from **all layers** at once, preserving multi-layer structures.

## Keyboard Shortcuts Summary

| Shortcut       | Action                                              |
| -------------- | --------------------------------------------------- |
| `B` or `W`     | Switch to Brush tool                                |
| `E`            | Switch to Eraser tool                               |
| `S` or `R`     | Switch to Stamp tool                                |
| `Shift` (held) | Temporarily erase while in Brush mode               |
| `[` / `]`      | Decrease / increase brush size                      |
| `Ctrl+drag`    | Rectangle fill with the active Brush or Eraser tool |
| `Alt+click`    | Draw straight line from last click                  |
| `Ctrl+V`       | Enter paste/ghost mode                              |
| `Scroll wheel` | Rotate ghost preview (in ghost mode)                |
| `Shift+click`  | Place multiple copies (in ghost mode)               |
| `Escape`       | Cancel ghost mode                                   |


# Introduction to Cloud Build

Cloud Build enables you to build multiplayer games on any device, anywhere, any time.

When you use Cloud Build, we spin up a virtual machine running the All Out editor and pair it with the latest AI models, our asset catalog, and MCP tools to bring your game to life.

Cloud build has limited free usage since we pay for the compute time and AI models that power it. To get more usage, we offer several subscription tiers to cover extra usage.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2FanjBeMtdMxFuVkwqqxG0%2Fimage.png?alt=media&amp;token=e1883219-004d-4b73-a550-2fa9e16fb4b3" alt="Cloud Build workspace in the Creator Portal" width="521"><figcaption></figcaption></figure>

To get started, select "Cloud Agent" when creating a new game, or select "New Cloud Build" on an existing game from the sidebar.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2FtZobvZWs5se4hEHlGmEW%2Fimage.png?alt=media&amp;token=75bdd337-352c-4b94-8655-a970d28e449c" alt="New Cloud Build control for an existing game"><figcaption></figcaption></figure>

Depending on your subscription tier, you'll have a variety of models and options to pick from:

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fb5C56HLV8Yz5xxviYSXQ%2Fimage.png?alt=media&amp;token=4238e946-b944-4fe8-a1c2-0d194c22ea4b" alt="Cloud Build model and option-count choices"><figcaption></figcaption></figure>

If you select "2 Options", for every prompt you submit we'll create two independent versions of that prompt and you'll be able to pick your favorite each time.

### Sessions, Rounds, and Options

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2FCYQgYdCrt6fbaZfAYwMn%2Fimage.png?alt=media&amp;token=66adc052-7a2a-4395-af2a-58fc4cb38a7e" alt="Cloud Build session showing rounds and job options"><figcaption></figcaption></figure>

**Sessions**

When you create a new cloud build session, behind the scenes we create a temporary copy of your game that you can work on without fear of messing up your current game. Its accepted work does not change the active game until you finish the publishing flow.

You can see your session name at the top and in the sidebar.

**Rounds**

Each session is made up of "Rounds", which are your prompts that make a version of the game. When a round is done, you can "Accept" it which allows you to submit a new prompt to continue iterating.

**Jobs**

Jobs are the actual options you see working after you submit a round. Jobs are the All Out editor running inside a virtual machine paired up with the agent/model you selected earlier.

With a linked Git repository, each job uses its own branch and opens a pull request into the session's draft branch. Accepting a job selects it, but you must still verify that its pull request merged successfully. See [Git Integration](/using-cloud-build/git-integration).

**Finishing a Session**

When you're done and happy with your session, you can click "Apply to live" at the top of the page which will publish the latest accepted round to your main game. Confirm the active version and test the published build afterward.

{% hint style="info" %}
Cloud Build runs can take hours to run, but you can leave them and come back at any time. If you have the app we'll also send you a notification when they're done!
{% endhint %}


# Git Integration

Git is a tool that makes it much easier to work with multiple people. Git allows you and your editor team to run several cloud builds at the same time and merge your results using an industry standard system.

When you [link a Git repo to your project on the collaborate page](/going-all-out/collaborating-and-version-control), Cloud Build keeps concurrent work separate:

1. A session has a draft branch.
2. Each job attempt runs on its own branch.
3. A completed attempt opens a pull request from its attempt branch into the session's draft branch.
4. Accepting an option selects that job for the session to continue from, but it does not guarantee that a conflicted or failed pull request merged.
5. Before finishing the session, verify that the selected pull request merged and that the session draft branch contains the expected commit.

If two jobs or sessions change the same lines or assets, review the pull request and resolve any conflict in the linked Git host. Always review a job's diff before accepting it.


# Cloud Build Tips

Write simple, specific prompts.

#### Creating a fishing game

{% hint style="warning" %}
**Bad prompt**

```
Make Fishermon but better
```

{% endhint %}

{% hint style="success" %}
**Better prompt**

```
Create a simple fishing game with a giant island surrounded by water.

You can see fish swimming around in the water outside the island. 

Add an aimed fishing ability and if it lands on a fish, add it to my inventory 
with an epic animation.

Add a shop NPC in the center of the island that I can sell my fish to. 

Add a rod NPC next to them where I can buy a better rod. The better rods make
the fish I catch more rare. 

Add lots of sound effects and simple animations to make the game tactile. 
```

{% endhint %}

#### Adding a gameplay feature

{% hint style="warning" %}
**Bad prompt**

```
make combat better
```

{% endhint %}

{% hint style="success" %}
**Better prompt**

```
Add a sword ability with a 0.5s cooldown. It does 20 damage to nearby players. 

Add a dash ability with a 4 second cooldown. 
Play a short trail effect as they dash and a cool sound effect.

They are invicible while dashing
```

{% endhint %}

**Pick the right model for your needs.**

Low quality models are cheap and fast, but they struggle with full game builds. Use stronger reasoning models for scripting and structured changes. Use visually strong models for map building and layout ideas. Experiment to find what works best for your use case!&#x20;

High quality models are pretty good at most things!


# Cursor/Claude/Antigravity Setup

You can write CSL with any text editor, but using an editor with CSL tooling makes a huge difference: syntax highlighting, inline errors, autocomplete, and quick navigation.

It's also even easier than ever to get started thanks to tools that integrate AI agents which can help you learn and write CSL.

You’ll write `.csl` files inside your project’s `scripts/` directory.

## 1. Using the Agent Panel

If you want the simplest setup, use the AI Agent panel inside the All Out editor.

Using the agent panel only requires having one of the industry standard AI subscriptions. Today we support:

* OpenAI/ChatGPT Codex
* Claude Code
* OpenCode

Whether you use the built-in agent panel or a third-party agent directly, the editor adds a few project files to your game folder to give the agent better context:

* `AGENTS.md` with information about All Out and CSL
* `.agents/skills/` and `.claude/skills/` with engine-specific workflows
* `api_references/` with the current CSL API as `.csl_engine` files
* `.mcp.json`, `.cursor/mcp.json`, `.codex/config.toml`, and `opencode.json` so supported tools can connect to the editor for compiling and fetching assets

## 2. Third Party Agents

{% tabs %}
{% tab title="Installing Cursor (programmers)" %}
We recommend Cursor (or VS Code) to develop All Out games. Our editor extension provides:

* Syntax highlighting support
* Inline compile error diagnostics
* Autocomplete

1. [Download and install Cursor](https://cursor.com/)
2. [Install the All Out CSL extension](https://marketplace.cursorapi.com/items/?itemName=AllOutGames.csl-language-tools)
3. Open your game project folder in Cursor (the folder that contains `scripts/` and `res/`)

{% hint style="info" %}
All Out automatically adds some files to your project to help give AI context:

* `AGENTS.md` with information about All Out and CSL
* `.agents/skills/` and `.claude/skills/` with engine-specific workflows
* `api_references/` with the current CSL API as `.csl_engine` files
* `.mcp.json`, `.cursor/mcp.json`, `.codex/config.toml`, and `opencode.json` for editor access
  {% endhint %}
  {% endtab %}

{% tab title="Installing Claude Code (non-programmers)" %}
If you don't have much experience programming, or prefer using Claude Code, we also have native support for it.

Claude Code is an AI tool that can write and debug CSL scripts for you.

1. [Download and install Claude Code](https://code.claude.com/docs/en/overview)
2. Open a terminal inside your game project folder and type `claude` to login with your Claude account.

{% hint style="warning" %}
Even if you use an AI model, it helps a **ton** to spend some time getting familiar with CSL scripting at a basic level. You'll be able to direct the AI more efficiently and make quick adjustments. Plus, it's fun!
{% endhint %}

{% hint style="info" %}
All Out automatically adds some files to your project to help give AI context:

* `AGENTS.md` with information about All Out and CSL
* `.agents/skills/` and `.claude/skills/` with engine-specific workflows
* `api_references/` with the current CSL API as `.csl_engine` files
* `.mcp.json`, `.cursor/mcp.json`, `.codex/config.toml`, and `opencode.json` for editor access
  {% endhint %}
  {% endtab %}

{% tab title="Antigravity" %}
Antigravity is Google's version of Cursor and can be a nice alternative if you prefer the Google ecosystem. It does require some additional setup to get the MCP server working.

1. [Download and install Antigravity](https://antigravity.google/download)
2. Install the All Out extension to Antigravity
3. Manually configure the All Out MCP server

**MCP Server Setup**

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-e65bd3002a4ad4ee6a5eb82f518ef1e21b1333cc%2Fimage.webp?alt=media" alt="Antigravity MCP settings"><figcaption></figcaption></figure>

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-f3877c346ada587652cacd923c96df714c75f1e7%2Fimage.webp?alt=media" alt="Antigravity raw MCP configuration"><figcaption></figcaption></figure>

Then paste this into the raw config. Use the port written to the project's `.mcp.json`:

```json
{
    "mcpServers": {
        "allout": {
            "serverUrl": "http://localhost:3333/mcp"
        }
    }
}
```

If the editor selected a different port, replace `3333` with that value.

{% hint style="info" %}
All Out automatically adds some files to your project to help give AI context:

* `AGENTS.md` with information about All Out and CSL
* `.agents/skills/` and `.claude/skills/` with engine-specific workflows
* `api_references/` with the current CSL API as `.csl_engine` files
* `.mcp.json`, `.cursor/mcp.json`, `.codex/config.toml`, and `opencode.json` for editor access
  {% endhint %}
  {% endtab %}
  {% endtabs %}

{% hint style="warning" %}
If Windows builds are slow, measure before changing antivirus settings. Exclude only the narrowest trusted project or generated path that resolves the issue. See [Windows Defender Exclusion](/reference/windows-defender-exclusion).
{% endhint %}


# 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).

## 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).

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


# Language Basics

CSL primitives, enums, structs, classes, references, and polymorphism.

This page is the “data modeling” side of CSL: primitives, structs/classes, and the patterns you’ll use to represent game state cleanly.

{% hint style="info" %}
If you’re brand new, start with [Getting Started with CSL](/scripting/syntax). This page assumes you already know how to declare variables and write `proc`/`method`.
{% endhint %}

## Primitive types

### Numbers and bools

* Signed integers: `s8`, `s16`, `s32`, `s64` (alias: `int` == `s64`)
* Unsigned integers: `u8`, `u16`, `u32`, `u64` (alias: `uint` == `u64`)
* Floats: `f32` (alias: `float`), `f64`
* Booleans: `bool`

```go
health: int = 100;
speed: float = 6.5;
is_dead: bool = false;
opacity := .75; // same as 0.75
```

CSL widens numbers when that is safe: smaller signed integers can become wider signed integers, smaller unsigned integers can become wider unsigned integers, unsigned integers can become wider signed integers when the target can hold the full range, and `f32` can become `f64`.

### Strings

`string` is used constantly for IDs, names, and formatting.

```go
item_id := "sword";
log_info("picked up %", {item_id});

first_name := "Ada";
last_name := "Lovelace";
full_name := first_name + " " + last_name;
full_name += "!";
```

Concatenation works in both runtime expressions and compile-time constants.

### Vectors

`v2`, `v3`, `v4` are built-in math/data types:

```go
pos: v2 = {10, 20};
color: v4 = {1, 0, 0, 1};
```

## Enums

Enums are great for readable “mode/state” fields:

```go
Game_Phase :: enum {
    LOBBY;
    RUNNING;
    POST_GAME;
}

phase := Game_Phase.LOBBY;

if phase == .RUNNING {
    // ...
}
```

{% hint style="info" %}
Enum values are commonly referenced with the shorthand `.VALUE` when the compiler can infer the enum type.
{% endhint %}

## Structs (value types)

Use structs for “plain data” you want to copy around cheaply (config, small state blobs, descriptors).

```go
Damage_Desc :: struct {
    amount: int;
    knockback: v2;
}

hit := Damage_Desc{amount=10, knockback={2, 1}};
```

Structs can inherit from other structs. Compound literals include inherited fields first:

```go
Base_Stats :: struct {
    health: int;
}

Enemy_Stats :: struct : Base_Stats {
    speed: float;
}

stats: Enemy_Stats = {100, 4.5};
```

### When to use a struct

* A descriptor/config object (`*_Desc`)
* Small, self-contained state you want to copy by value
* Data you want to embed inside another type

### When not to use a struct

* Large mutable state shared across multiple systems (use a class)
* “Identity” objects (players, items, long-lived objects) (use a class)

## Classes (reference types)

Classes are reference types. Create instances with `new(...)`.

```go
Enemy :: class {
    health: int = 50;
    target: v2 = {0, 0};
}

e := new(Enemy);
```

### Inheritance

You can inherit from other structs/classes:

```go
Weapon_Definition :: class {
    id: string;
    name: string;
}

Gun_Definition :: class : Weapon_Definition {
    fire_rate: float;
}
```

Derived classes inherit default field values. They can override inherited defaults with `field = value` in the class body:

```go
Weapon_Definition :: class {
    damage: int = 10;
}

Rocket_Definition :: class : Weapon_Definition {
    damage = 100;
    splash_radius: float = 3.0;
}
```

Every derived class has a read-only `base` field that views the same object as its immediate parent type. Use it when a derived method shadows a same-named parent method and needs to select the parent method explicitly:

```go
Weapon :: class {
    fire :: method() {
        // Base behavior.
    }
}

Burst_Weapon :: class : Weapon {
    fire :: method() {
        base.fire();
        // Additional burst behavior.
    }
}
```

CSL methods are not virtual. The receiver's static type determines where method lookup begins; a base-typed value never calls a same-named method declared only on its derived runtime type.

{% hint style="warning" %}
Use inheritance when there’s a real “is-a” relationship. For most gameplay data, composition (fields) is simpler and easier to maintain.
{% endhint %}

## Arrays, slices, and dynamic arrays

You’ll see three common shapes:

* **Fixed arrays**: `[N]T` (compile-time size)
* **Slices / managed arrays**: `[]T` (a view of an array)
* **Dynamic arrays**: `[..]T` (resizable list)

Example:

```go
spawn_points: [4]v2 = {{0,0}, {5,0}, {0,5}, {5,5}};

alive_enemy_ids: [..]int = {123, 456};
```

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

## Passing by reference (`ref`)

If a procedure needs to modify a value, use `ref`:

```go
apply_damage :: proc(health: ref int, amount: int) {
    health -= amount;
}

hp := 100;
apply_damage(ref hp, 10);
```

This is especially useful for “layout-style” APIs that progressively cut/shrink a rect:

```go
draw_panel :: proc(rect: ref Rect) {
    header := rect.cut_top(50);
    draw_header(header);
    // rect is now the remaining area for content
}
```

## Casting

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

```go
f := 12.7;
i := f.(int);       // 12
f2 := cast(float)i; // 12.0
```

When the target type is already known, `expr.()` and `cast expr` infer the target type.

## Type polymorphism (generic-style procs)

CSL supports polymorphic procedures using `$` to deduce types at the callsite:

```go
min :: proc(a: $T, b: T) -> T {
    if a < b return a;
    return b;
}

a := min(10, 20);       // T == int
b := min(1.25, 3.5);    // T == float
```

## Types as values (`typeid`)

`typeid` is a value that represents a type at runtime and/or for polymorphic calls:

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

zero := default_of(int);
empty := default_of(string);
```

Class casts are only valid between related types. Derived-to-base casts are free; base-to-derived casts validate the object's concrete runtime type and throw if it is not the requested type or one of its subclasses. Casting a null class value produces null. Use `value.#type` when selecting behavior among several possible derived types instead of using a failing cast as a type test.

## Practical modeling guidelines

* **Per-player state lives on `Player`.** If it should differ between players, it shouldn’t be global.
* **Prefer small structs for descriptors** and store runtime state in classes/components.
* **Run gameplay in the shared predicted path.** Do not compute gameplay state only under `is_local()` or guard it with `Game.is_server()`.

For player-specific patterns, see [Adding Player Logic](/scripting/player-model). For the ECS side of the world, see [Entities and Components](/scripting/entities-and-components).


# Game/Frame Lifecycle

In contrast to event driven scripting like Roblox, games on All Out use a frame lifecycle model that allows you to run code when the game starts, each frame, and at the end of a session.

All Out games run on a simple lifecycle: some callbacks happen once at startup, and some happen every frame.

You’ll implement these callbacks either:

* **Globally** (top-level `ao_*` procs in `main.csl`), or
* **On components** (methods like `ao_update` inside `Player` or your own `Component` subclasses)

{% hint style="info" %}
If you’re looking for the “template” `main.csl`, see [Getting Started with CSL](/scripting/syntax).
{% endhint %}

## Global lifecycle (scene-wide)

These are top-level procs in `main.csl`:

### `ao_before_scene_load`

Runs after the empty `Scene` is created but before its serialized entities and components are loaded. This is where you typically register definitions needed while the scene loads:

* Item definitions (inventory system)
* Currencies (economy system)
* Global config values/constants

### `ao_start`

Runs once when the scene starts.

Common uses:

* Spawn runtime entities/prefabs
* Initialize global systems
* Read game-wide save data

### `ao_update(dt)`

Runs every frame.

Common uses:

* Game timers, wave managers
* Spawning logic that depends on time
* Gameplay rules

### `ao_late_update(dt)`

Runs every frame after `ao_update`.

Common uses:

* Scene-wide work that depends on final positions/state for the frame
* “Cleanup” work after updates

Player UI belongs in `Player.ao_late_update`, not this global callback.

### `ao_draw(dt)`

Cosmetic-only; runs on submissions that may render and skips resim. `dt` is the submitted sim delta; use `get_time()` for absolute time.

Never put gameplay or interactive UI here. Use `ao_update`/`ao_late_update` (usually `Player.ao_late_update` for UI).

## Component lifecycle (per-entity)

Components (including `Player`) can implement:

* `ao_start()`
* `ao_update(dt)`
* `ao_late_update(dt)`
* `ao_draw(dt)` (cosmetic-only; renderable frames)
* `ao_end()`
* `ao_on_state_sync()` (called when network state is synchronized)

{% hint style="warning" %}
`ao_start()` is not replayed for every client. When a client joins after an object has already started on the server, the synchronized component can arrive with its start lifecycle already complete. Do not use `ao_start()` for client-local presentation setup that every joining client must run; rebuild that presentation from synchronized state in `ao_on_state_sync()` instead.
{% endhint %}

Example:

```go
Orbiter :: class : Component {
    ao_start :: method() {
        // init
    }

    ao_update :: method(dt: float) {
        // per-frame behavior
    }

    ao_end :: method() {
        // cleanup (when destroyed)
    }
}
```

## Where to put code: a rule of thumb

* **Per-player state/logic**: put it on `Player` (see [Adding Player Logic](/scripting/player-model))
* **Reusable entity behaviors**: put them on custom components (see [Entities and Components](/scripting/entities-and-components))
* **Global coordination / rules**: put it in the global lifecycle (`ao_*` procs)

## Server vs local

Global and component gameplay callbacks run on both the predicting client and the server. Do not guard gameplay with `Game.is_server()`.

Draw all player UI from that player's `ao_late_update` call stack, wrapped in `is_local_or_server()`. Use `is_local()` only for player-specific visual overrides. State changed only under `is_local()` is replaced by the next server sync.

These are methods on `Player_Base`. Call them on a player reference, or as `is_local_or_server()` / `is_local()` inside a `Player` method (implicit `this`).


# Entities and Components

All Out games are built around entities (things in the world) and components (behavior/data attached to entities).

If you’ve used Unity before: think “GameObject + Components”. If you’ve used Roblox: think “Instance + Scripts/Components”. The core idea is the same: **you compose gameplay by adding components to entities.**

## Entities

Entities exist in two common ways:

* **Placed in the editor**: they’re already in the scene at startup.
* **Created at runtime**: you spawn them from script.

### Creating and destroying entities

```go
entity := Scene.create_entity();
entity.set_local_position({10, 20});
entity.set_local_scale({2.5, 2.5});
entity.set_local_rotation(0);

// When you're done:
entity.destroy();
```

{% hint style="warning" %}
Destroying an entity destroys its components too. Don’t keep references to components after you destroy their entity.
{% endhint %}

### Parenting runtime-created entities

Use `keep_world_transform=true` to preserve the child's world position, rotation, and scale when changing its parent:

```go
child.set_parent(parent, keep_world_transform=true);
```

Pass `false` to keep the child's local transform instead. Pass `null` as the parent to detach it.

### Iterating entities

```go
for e: entity_iterator() {
    // ...
}
```

## Components

Components are the units of behavior. A component “lives on” an entity and can read/modify that entity.

### Getting and adding components

```go
sprite := entity.get_component(Sprite_Renderer);

player := entity.get_component(Player);

my_comp := entity.add_component(My_Component);
```

`add_component` calls `ao_start()` before returning. To initialize fields that `ao_start()` reads, pass a callback; it may capture locals from the caller:

```go
health := 100;
enemy := entity.add_component(Enemy, proc(enemy: Enemy) {
    enemy.health = health;
});
```

### Iterating components

```go
for enemy: component_iterator(Enemy) {
    enemy.tick_ai();
}
```

## Built-in components you’ll use a lot

### `Sprite_Renderer`

```go
texture := get_asset(Texture_Asset, "ui/button.png");

sprite := entity.get_component(Sprite_Renderer);
sprite.set_texture(texture);
sprite.color = {1, 1, 1, 1}; // RGBA
sprite.depth_offset = 0.5;
sprite.layer = 10;

// Optional: use a custom material. Pass true if the renderer should own it.
// sprite.set_material(material, true);
```

### Prefabs (`Prefab_Asset`)

Prefabs must be created in the editor. In scripts, you instantiate them.

```go
prefab := get_asset(Prefab_Asset, "MyPrefab.prefab");
spawned := Scene.instantiate(prefab);
spawned.set_local_position({1, 3});
```

The initialization callback runs before components on the prefab are started. It can be passed with either the default or an explicit position:

```go
target := v2.{1, 3};
spawned := Scene.instantiate(prefab, target, proc(entity: Entity) {
    entity.set_local_scale({2, 2});
});
```

### Spine (`Spine_Animator`)

If you’re using 2D animated characters, you’ll often work with `Spine_Animator`. See [Spine](/core-engine-concepts/spine).

## Writing a custom component

Make new components in dedicated files (e.g. `orbiter.csl`) and attach them to entities either:

* **Manually in the editor**, or
* **At runtime** with `entity.add_component(...)`

Components can implement lifecycle callbacks:

* `ao_start()`
* `ao_update(dt)`
* `ao_late_update(dt)` (after all updates)
* `ao_draw(dt)` (cosmetic-only; renderable frames)
* `ao_end()` (when destroyed)

`ao_draw` skips resim. Gameplay and interactive UI must use `ao_update`/`ao_late_update`, never `ao_draw`.

Example:

```go
Orbiter :: class : Component {
    center: v2;
    radius: float;
    speed: float;
    angle: float;

    ao_start :: method() {
        center = entity.local_position;
        radius = 2;
        speed = 1;
        angle = 0;
    }

    ao_update :: method(dt: float) {
        angle += speed * dt;

        offset_x := cos(angle) * radius;
        offset_y := sin(angle) * radius;

        entity.set_local_position({center.x + offset_x, center.y + offset_y});
    }
}
```

{% hint style="info" %}
Lifecycle methods for global scripts (`ao_start`, `ao_update`, ...) are covered in [Game/Frame Lifecycle](/scripting/game-frame-lifecycle).
{% endhint %}

## Serialized fields (`@ao_serialize`)

Use `@ao_serialize` to expose a field in the Inspector and include it in scene and JSON serialization.

```go
Chest :: class : Component {
    @ao_serialize capacity: int;
}
```

## Triggers and proximity queries

Trigger colliders expose `on_trigger_start`, `on_trigger_stay`, and `on_trigger_end` callbacks. See [Navmesh and Collision](/core-engine-concepts/navmesh-and-collision) for setup and callback signatures.

Use proximity queries when you need every component in a radius or only the closest one.

Useful helpers:

```go
Scene.get_all_components_in_range     :: proc(position: v2, range: float, results: ref [..]$T)
Scene.get_closest_component_in_range  :: proc(position: v2, range: float, $T: typeid) -> (T, bool)
```

Example:

```go
nearby: [..]Enemy;
Scene.get_all_components_in_range(player.entity.world_position, 5.0, ref nearby);

for e: nearby {
    // ...
}

closest_pickup, found := Scene.get_closest_component_in_range(player.entity.world_position, 2.0, Pickup);
if found {
    // ...
}
```

## Best practices

* **Per-player state belongs on `Player`.** Avoid global variables that would break with multiple players.
* **Prefer components for gameplay behaviors.** You’ll end up with reusable pieces you can attach to different entities.
* **Draw 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.


# Adding Player Logic

In All Out, the player is a first-class gameplay object. Your `Player :: class : Player_Base` is where you put **per-player state** (health, loadouts, cooldowns, UI toggles, progression, etc).

{% hint style="warning" %}
Assume multiple players are connected. Avoid global state that would break when there’s more than one player.
{% endhint %}

## Per-player state

Store gameplay state on the player or objects owned by the player.

```go
Player :: class : Player_Base {
    health: int;
    inventory_open: bool;
}
```

## Server + client: what runs where?

All Out automatically syncs gameplay state from the server to clients. Gameplay methods run on both the predicting client and the server, so do not guard them with `Game.is_server()`.

Two common checks:

* `is_local_or_server()`: all player UI and the input it produces
* `is_local()`: player-specific visual overrides only

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if is_local_or_server() {
            // Draw all player UI and handle its input here.
        }

        if is_local() {
            // Apply player-specific visibility here, but do not change fields.
        }
    }
}
```

{% hint style="info" %}
Draw all player UI from `Player.ao_late_update` under `is_local_or_server()`. Fields changed only inside `is_local()` are replaced during reconciliation.
{% endhint %}

## Player identity and profile data

`Player_Base` exposes identity fields you’ll use often:

* `p.get_username() -> string`
* `p.get_user_id() -> string`
* `p.avatar_color`
* `p.device_kind` (`.PHONE`, `.TABLET`, `.PC`)
* `p.is_game_owner()`, `p.is_game_editor()`, `p.is_owner_or_editor()` (the game's owner and their team), `p.is_vip()`, `p.is_moderator()`, `p.is_youtuber()`
* `p.is_chat_open()` and UI rect helpers such as `p.get_chat_rect()`

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

## Temporary state reasons

Player states such as freezing, invisibility, name hiding, ghosting, movement-input blocking, and joystick blocking use named reasons. Reasons are counted: every call to an `add_*_reason` method requires one matching `remove_*_reason` call.

Use the corresponding `has_*_reason` method when you need set-like behavior or need to avoid adding the same reason repeatedly:

```go
if !player.has_freeze_reason("cutscene") {
    player.add_freeze_reason("cutscene");
}

if player.has_freeze_reason("cutscene") {
    player.remove_freeze_reason("cutscene");
}
```

The same pattern is available for `invisibility`, `name_invisibility`, `name_offset`, `ghost`, `disable_movement_input`, and `joystick_disable` reasons. Each family also has `has_any_*_reason()` when only the presence of any reason matters. Do not call an add method every frame: matching by name does not make it idempotent.

## Persistence: where to store player progress

* **Economy**: currencies (coins/gems/xp) with automatic persistence + creator portal editing\
  See [Economy](/data-and-persistence/economy).
* **Save**: general key/value persistence (settings, quest state, unlock lists, etc)\
  See [Save System](/data-and-persistence/save).
* **Inventory**: item stacks/instances in a player inventory (optionally auto-saved)\
  See [Inventory](/core-engine-concepts/inventory).

## Common pattern: load saved values in `ao_start`

```go
Player :: class : Player_Base {
    xp: s64;
    selected_skin: string;

    ao_start :: method() {
        xp = Save.get_int(this, "xp", 0);
        selected_skin = Save.get_string(this, "selected_skin", "default");
    }
}
```

## Best practices

* **Keep per-player state on `Player`.** Avoid globals for anything player-specific.
* **Run gameplay in the shared predicted path.** Draw player UI under `is_local_or_server()` and reserve `is_local()` for player-specific visual overrides.
* **Prefer the built-in persistence APIs** instead of rolling your own (Economy/Save/Inventory).

## Player\_Base reference

See `api_references/core/ao/core.csl_engine` in the project folder for the complete `Player_Base` API.


# Networking Fundamentals

All Out is multiplayer-first, but CSL is designed so you can write gameplay almost like it’s singleplayer.

CSL runs that gameplay on the client for immediate feedback and on the server for authority. The client is regularly reconciled to the server's authoritative scene state. You do not write RPCs or separate network-spawn code.

## Write one gameplay path

Damage, rewards, spawning, movement, random choices, and sound calls should run in the shared gameplay path.

{% hint style="warning" %}
Do not guard gameplay with `Game.is_server()`. It disables prediction and can make client and server state diverge.
{% endhint %}

Keep per-player state on `Player` or another object owned by that player. Use scene-wide state only when it is truly shared.

## Player execution checks

`Player_Base` has two execution checks:

| Check                  | Use                                                                              |
| ---------------------- | -------------------------------------------------------------------------------- |
| `is_local_or_server()` | Player UI and the input it produces. Use it from that player's `ao_late_update`. |
| `is_local()`           | Player-specific visual overrides, such as hiding an object only from its owner.  |

All player UI, including cosmetic UI, belongs under `is_local_or_server()`. Never store gameplay or UI state only under `is_local()`; the next server sync replaces it.

```go
Player :: class : Player_Base {
    inventory_open: bool;

    ao_late_update :: method(dt: float) {
        if is_local_or_server() {
            rect := UI.get_safe_screen_rect()
                .top_right_rect()
                .grow(35, 90, 35, 90)
                .offset(-100, -45);
            bs := UI.default_button_settings();
            ts := UI.default_text_settings();

            if UI.button(rect, bs, ts, "Items").clicked {
                inventory_open = !inventory_open;
            }
        }

        if is_local() {
            // Apply player-specific visibility here, but do not change fields.
        }
    }
}
```

If a local visual override is changed by reconciliation, recompute it from synchronized state in `ao_on_state_sync()` or apply it every frame.

## Sound

Call `SFX.play` from the same shared gameplay path as the event. The engine deduplicates predicted playback when the server result arrives. To play a sound for one player, set `SFX_Desc.specific_to_player`; do not wrap the call in `is_local()`.

## Avoid per-frame state churn

Every gameplay field that changes can contribute to a network state diff. Do not store a countdown by subtracting `dt` from it every frame when the same state can be represented by a fixed time.

Store the transition time once and derive the remaining duration:

```go
Round_Timer :: class : Component {
    round_ends_at: float;

    begin_round :: method(duration: float) {
        round_ends_at = get_time() + duration;
    }

    remaining :: method() -> float {
        return max(0.0, round_ends_at - get_time());
    }

    ao_update :: method(dt: float) {
        if round_ends_at != 0.0 && get_time() >= round_ends_at {
            round_ends_at = 0.0;
            finish_round();
        }
    }
}
```

This synchronizes the deadline when it changes instead of synchronizing a new timer value every frame. The same pattern works for cooldowns, temporary effects, and scheduled state changes.

## Diagnosing mismatches

Check for:

* Gameplay hidden behind `is_local()` or `Game.is_server()`.
* Per-player values stored in globals.
* Local-only data used to seed gameplay randomness.
* State changed every frame when a fixed timestamp would represent it.

## Related docs

* [Adding Player Logic](/scripting/player-model)
* [Game/Frame Lifecycle](/scripting/game-frame-lifecycle)


# Random, Math, & More

This page is a grab-bag of “you’ll use these constantly” utilities: RNG, formatting, time, and a few core subsystems that show up in most games.

{% hint style="info" %}
If you’re looking for arrays/slices, see [Arrays and Collections](/scripting/arrays-and-collections).
{% endhint %}

## Random numbers (RNG)

Random uses an explicit `u64` seed. Pass the seed by `ref` so it updates.

```go
rng: u64 = rng_root_seed();
// or deterministic per-entity:
// rng: u64 = rng_seed(entity.id);

// Range values are inclusive
roll := rng_range_int(ref rng, 1, 10);
chance := rng_range_float(ref rng, 0, 1);

// Random points and shuffling
offset := rng_disk(ref rng, 0.5, 2.0);
spawn_points: [..]v2;
spawn_points.append({0, 0});
spawn_points.append({4, 0});
rng_shuffle(ref rng, spawn_points);

// Mix a value into a seed without drawing a random number.
rng_mix(ref rng, entity.id);
independent_seed := mix_u64(rng, 123); // returns the mixed seed; rng is unchanged
```

{% hint style="warning" %}
Gameplay randomness must run in the shared predicted gameplay path. Start from `rng_root_seed()` or another stable seed; do not seed gameplay from local-only data.
{% endhint %}

`mix_u64(a: u64, b: u64) -> u64` and `rng_mix(rng: ref u64, value: u64)` are intrinsics with the same deterministic mixing algorithm. `rng_mix` stores the mixed result back into `rng`; the order of repeated mixes matters.

## Math functions

These functions are declared in `core:basic`, which `core:ao` also imports. In the tables, `F` means separate `f32` and `f64` overloads; `float` is `f32`, and `int` is `s64`. Arguments in an overload use the same type. `T` denotes a generic numeric type, and `V` denotes separate `v2`, `v3`, and `v4` overloads.

### Scalar math reference

| Function signature                                                             | Behavior                                                                                                                         |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `ceil(x: F) -> F`                                                              | Smallest integral value greater than or equal to `x`: `ceil(1.25)` is `2.0`, `ceil(-1.25)` is `-1.0`.                            |
| `floor(x: F) -> F`                                                             | Largest integral value less than or equal to `x`: `floor(1.25)` is `1.0`, `floor(-1.25)` is `-2.0`.                              |
| `round(x: F) -> F`                                                             | Nearest integral value, with halfway cases away from zero: `round(2.5)` is `3.0`, `round(-2.5)` is `-3.0`.                       |
| `sin(v: F) -> F`                                                               | Sine of an angle in radians.                                                                                                     |
| `cos(v: F) -> F`                                                               | Cosine of an angle in radians.                                                                                                   |
| `atan2(y: F, x: F) -> F`                                                       | Angle in radians from an `(x, y)` direction. Pass **y first**.                                                                   |
| `to_degrees(radians: F) -> F`                                                  | Convert radians to degrees, preserving precision.                                                                                |
| `to_radians(degrees: F) -> F`                                                  | Convert degrees to radians, preserving precision.                                                                                |
| `pow(b: F, e: F) -> F`                                                         | Raise base `b` to exponent `e`.                                                                                                  |
| `sqrt(x: F) -> F`                                                              | Square root.                                                                                                                     |
| `Math.exp(v: F) -> F`                                                          | Natural exponential, e raised to `v`.                                                                                            |
| `Math.log(v: F) -> F`                                                          | Natural logarithm. The global `log` function is a logging alias.                                                                 |
| `abs(a: T) -> T`                                                               | Absolute value.                                                                                                                  |
| `sign(v: T) -> T`                                                              | `-1` for negative values; `1` for zero and positive values.                                                                      |
| `min(a: F, b: F) -> F`; `min(a: int, b: int) -> int`                           | Smaller of two values.                                                                                                           |
| `max(a: F, b: F) -> F`; `max(a: int, b: int) -> int`                           | Larger of two values.                                                                                                            |
| `clamp(a: F, min: F, max: F) -> F`; `clamp(a: int, min: int, max: int) -> int` | Constrain a value to inclusive bounds, checking the lower bound first.                                                           |
| `lerp(a: F, b: F, t: F) -> F`                                                  | Linear interpolation; `t` is not clamped.                                                                                        |
| `linear_step(start: float, end: float, time: float) -> float`                  | Normalize `time` to the interval and clamp to `[0, 1]`; returns `1` if the endpoints are equal.                                  |
| `next_power_of_two(n: s64) -> s64`                                             | Smallest power of two at least `n`; returns `0` for non-positive input. Inputs above `2^62` overflow to the minimum `s64` value. |

`PI` is also available as a constant.

Scalar helpers and `dot` execute as VM math instructions. Vector `lerp` lowers directly to vector arithmetic instructions. They require no foreign calls. The composite helpers `in_range` and `normalize_vector_to_radius` remain CSL code built from these primitives.

Floating-point `min` and `max` return the second argument when the values compare equal or either is NaN. `clamp` preserves a NaN input. `abs` preserves signed zero and NaN; integer `abs` preserves the signed minimum value on overflow. These are the same behaviors as the original CSL helpers.

### Rounding

`ceil`, `floor`, and `round` return a floating-point value of the input precision. Exact integers, infinities, and signed zero are unchanged; NaN produces NaN. Rounding up means toward positive infinity, so `ceil(-1.25)` is `-1.0`. `round` uses halfway-away-from-zero rounding, rather than ties-to-even.

```go
slots := ceil(2.25).(int);       // 3
cell := floor(-2.25).(int);      // -3
nearest := round(-2.5).(int);    // -3
truncated := (-2.25).(int);      // -2: casts truncate toward zero
precise: f64 = 1.0000000000000002;
rounded := ceil(precise);        // 2.0, still f64
```

Only cast the result to `int` when it is finite and fits in `int`. There is no `trunc` function. For decimal display formatting, use `format_float(value, decimals=2)`; it does not change the numeric value.

### Vector math reference

| Function signature                | Behavior                                                 |
| --------------------------------- | -------------------------------------------------------- |
| `length(v: V) -> float`           | Vector length.                                           |
| `length_squared(v: V) -> float`   | Squared length, avoiding a square root.                  |
| `normalize(v: V) -> V`            | Normalize a vector.                                      |
| `dot(a: V, b: V) -> float`        | Dot product.                                             |
| `lerp(a: V, b: V, t: float) -> V` | Component-wise linear interpolation; `t` is not clamped. |

### Examples

```go
angle_sin := sin(x);
angle_cos := cos(x);
angle := atan2(dir.y, dir.x);
degrees := to_degrees(angle);
radians := to_radians(degrees);

result := pow(2.0, 3.0);   // 8.0
root := sqrt(16.0);        // 4.0

value := lerp(0.0, 100.0, 0.5);  // 50.0
clamped := clamp(value, 0.0, 10.0);
stepped := linear_step(0.0, 1.0, value);

absolute := abs(-5);
minimum := min(5, 10);
maximum := max(5, 10);
direction_sign := sign(-12);

len := length(v);
len_sq := length_squared(v);
normalized := normalize(v);
facing := dot(normalized, {1, 0});
```

## String formatting

Use `%` placeholders with an argument array:

```go
format_string("Value: %", {42});
format_string("health: 100%");
```

If you need an argument adjacent to a percent sign (or next to another argument), use `%0` as an alias for `%`:

```go
hp := 67;
format_string("health: %0%%", {hp}); // "health: 67%"
```

For decimal rounding, use `format_float`:

```go
value := 3.14159;
format_string("pi: %", {format_float(value, decimals=2)}); // "pi: 3.14"
```

Common string helpers:

```go
clean := string_trim("  hello  ");
parts := string_split("a,b,c", ",");
name_lower := to_lower(player.get_username());
name_upper := to_upper("ready");
short := string_substring("abcdef", 1, 3); // "bcd"
```

## Logging

Logging follows the same formatting rules:

```go
log_info("Name: %, age: %", {player.get_username(), 12});
```

## Time

```go
current_time := get_time();       // float seconds since game start
frame := get_frame_number();      // current frame number
now_ns := get_nanoseconds_since_epoch();
utc := get_utc_datetime();
```

## SFX

```go
sound := get_asset(SFX_Asset, "sfx/click.wav");

desc := SFX.default_sfx_desc();
desc.entity_to_follow = entity.id;
desc.delay = 0.25;

sound_id := SFX.play(sound, desc);
SFX.stop(sound_id);
```

{% hint style="warning" %}
Call gameplay sounds from the shared gameplay path. The engine reconciles predicted playback. For a sound that only one player should hear, set `desc.specific_to_player = player`; do not wrap `SFX.play` in `is_local()`.
{% endhint %}

## Economy quick start (currencies)

Economy currencies are **per-player** and **automatically persisted**.

```go
ao_before_scene_load :: proc() {
    coin_icon := get_asset(Texture_Asset, "ui/coin.png");
    Economy.register_currency("Coins", coin_icon);
}

Economy.deposit_currency(player, "Coins", 10);
coins := Economy.get_balance(player, "Coins");

COST :: 50;
if Economy.can_withdraw_currency(player, "Coins", COST) {
    Economy.withdraw_currency(player, "Coins", COST);
}
```

Deposit and withdrawal amounts must be non-negative.

See [Economy](/data-and-persistence/economy) for the full guide.


# Advanced Language Reference

CSL gives you a few “array-like” tools that cover most gameplay needs: fixed arrays, slices, and dynamic arrays. This page is the practical guide to using them without foot-guns.

## Quick glossary

* **Fixed array**: `[N]T` — size is known at compile time.
* **Slice / managed array**: `[]T` — a view over array data (often used for parameters).
* **Dynamic array**: `[..]T` — resizable list with `count` and `capacity`.

```go
fixed: [4]int = {1, 2, 3, 4};
view: []int = fixed; // slice view

list: [..]int = {10, 20};
```

{% hint style="info" %}
Dynamic array methods use normal dot calls (e.g. `list.append(x)`). Fields also use dot access (e.g. `list.count`).
{% endhint %}

## Dynamic arrays (`[..]T`)

Dynamic arrays are the “default list” type for gameplay code.

### Creating and appending

```go
players_seen: [..]string = {"Ada", "Grace"};
players_seen.append(player.get_user_id());
```

Dynamic-array literals may contain runtime expressions and can be returned directly when the expected type is `[..]T`:

```go
make_values :: proc(seed: int) -> [..]int {
    return {seed, seed + 1};
}
```

Dynamic-array class field defaults are also supported. Each class instance receives independent mutable storage:

```go
Loadout :: class {
    slots: [..]string = {"sword", "potion"};
}
```

### Reserving capacity (performance)

If you know you’ll add a lot of items, reserve first to avoid repeated reallocations.

```go
results: [..]Enemy;
results.reserve(128);
```

### Removing items

You generally pick between **fast removal** (order doesn’t matter) and **ordered removal** (preserve order).

```go
values: [..]int;
values.append(10);
values.append(20);
values.append(30);

values.unordered_remove_by_value(20); // swaps with last, fast
values.ordered_remove_by_value(10);   // shifts, keeps order
```

You can also remove by index:

```go
values.unordered_remove_by_index(0);
values.ordered_remove_by_index(0);
```

### Clearing

```go
values.clear(); // O(1): just sets count to 0
```

## Passing arrays into procedures

Many APIs accept `[]T` (a slice/view). Dynamic arrays can be passed where `[]T` is expected.

```go
sum :: proc(arr: []int) -> int {
    total := 0;
    for v: arr {
        total += v;
    }
    return total;
}

nums: [..]int;
nums.append(1);
nums.append(2);
nums.append(3);

total := sum(nums); // implicit [..]int -> []int
```

## Iteration patterns

### Iterate elements

```go
for v: nums {
    log_info("v=%", {v});
}
```

### Iterate indices

Use `..<` when you want a normal "0 up to, but not including, count" loop:

```go
for i: 0..<nums.count {
    log_info("nums[%]=%", {i, nums[i]});
}
```

Some engine collections provide iterators. `inventory.slots()` visits every slot in order, yields `null` for empty slots, and exposes the actual slot as the optional index:

```go
for item, slot: inventory.slots() if item != null {
    log_info("item % is in slot %", {item.get_definition().id, slot});
}
```

{% hint style="warning" %}
`..` ranges are inclusive. `..<` ranges exclude the upper bound.
{% endhint %}

Reverse loops use `#reverse`:

```go
for i: 0..<nums.count #reverse {
    log_info("nums[%]=%", {i, nums[i]});
}
```

## Hashtables

Use `Hashtable(Key_Type, Value_Type)` when you need fast lookup by key. It supports integer and string keys, along with the usual methods for lookup, insertion, overwrite, removal, and iteration.

```go
scores: Hashtable(string, s64);
scores.add("alice", 10);
scores.add_or_overwrite("bob", 25);

score, ok := scores.find("alice");
if ok {
    log_info("alice score: %", {score});
}

for value, key: scores {
    log_info("% = %", {key, value});
}
```

Use `add` when duplicate keys should assert. Use `add_or_overwrite` when updating an existing key is expected. `try_add` returns `true` if the key already existed and leaves the old value unchanged.

## Closures and callbacks

Procedure literals may capture surrounding values, but capturing procedures are callback expressions and cannot be stored in collections or other locations. A procedure parameter must be marked `#no_captures` before it can be stored, and callers may then pass only non-capturing procedures; see [Closures and callbacks](/scripting/syntax#closures-and-callbacks) for the usage rules and explicit-userdata pattern.

## API reference

Each project includes an `api_references/` folder containing the current core API. Open the matching `.csl_engine` file to inspect a type or procedure.


# Textures

Files under `res` are addressed relative to that folder:

```go
icon := get_asset(Texture_Asset, "ui/icon.png");
```

`get_asset` returns null when the path is missing. Keep paths case-correct and check optional assets before using them.

## Texture information

```go
Texture_Asset :: class : Asset {
    get_width           :: method() -> s64;
    get_height          :: method() -> s64;
    get_world_size      :: method() -> v2;
    get_aspect          :: method() -> float;
    get_pivot           :: method() -> v2;
    get_pixels_per_unit :: method() -> float;
    get_uvs             :: method() -> (v2, v2);
}
```

`get_world_size()` returns the source texture dimensions divided by its pixels-per-unit setting. It does not include renderer or attachment scale.

Use `Sprite_Renderer.set_texture` for a world sprite, or pass the texture to `UI.quad` for immediate-mode UI.

## Texture settings

Select a texture in the Assets pane to edit its asset settings, then click **Apply**.

* **Pixels Per Unit** controls its world size.
* **Filter** uses linear sampling by default. Use nearest sampling for pixel art.
* **Wrap Repeat** allows the texture to tile. Also enable **Wrap** on the `Sprite_Renderer`, then adjust its scale and UV offset.
* **Pivot** changes the texture origin.
* **Crop** trims source pixels during preprocessing.

### Nine-slicing

Nine-slicing preserves the corners and borders of a texture while stretching its center. Set **Slice** in left, bottom, right, top order. **Slice Scale** changes the rendered border thickness.

For a `Sprite_Renderer`, set its **Nine Slice** values in the component. For immediate-mode UI, pass the same values through `Quad_Params`:

```go
nine_slice := Texture_Nine_Slice_Data.{
    slice = {25, 40, 25, 25},
    slice_scale = 1
};

UI.quad(rect, panel_texture, params={
    nine_slice = nine_slice
});
```

## Mip assets

Mip assets are smaller versions loaded early and retained under memory pressure.

**Auto Generate Mips** under **Edit → Game Config** includes textures and Spine rigs referenced by the project. Keep it enabled for most games. Explicitly enable **Should Mip** on an asset that is loaded only through a computed path and cannot be found by the reference scan.

When automatic generation is disabled, **Manual Mip Quality** selects a `1/8`, `1/16`, or `1/32` source-size divisor. New projects default to `1/32`.

## Dynamic canvases

`Dynamic_Canvas` is a scene-owned RGBA texture that CSL can modify:

```go
Dynamic_Canvas :: class {
    create    :: proc(width: s64, height: s64, pixels_per_unit: float = 100) -> Dynamic_Canvas;
    destroy   :: method();
    fill      :: method(color: v4);
    set_pixel :: method(x: s64, y: s64, color: v4);
    get_pixel :: method(x: s64, y: s64) -> v4;
    stamp     :: method(from: v2, to: v2, radius: float, color: v4, softness: float = 0.5, opacity: float = 1);

    width: s64 #read_only;
    height: s64 #read_only;
    pixels_per_unit: float #read_only;
}
```

Create the canvas once, assign it to a sprite, and destroy it when its owner ends:

```go
Paint_Surface :: class : Component {
    canvas: Dynamic_Canvas;

    ao_start :: method() {
        canvas = Dynamic_Canvas.create(256, 256);
        canvas.fill({1, 1, 1, 1});
        entity.get_component(Sprite_Renderer).set_canvas(canvas);
    }

    ao_end :: method() {
        canvas.destroy();
    }
}
```

Canvas pixels are synchronized scene state, not client-only state. Update them through the normal shared gameplay path. A canvas is limited to 512×512 pixels; one scene can hold up to 16 canvases and 4 MiB of canvas pixels.


# Sound and Haptics

Play sound effects and local impact feedback.

Use the SFX API to play short sound effects (UI clicks, footsteps, impacts) and simple looping audio (ambient loops, music).

{% hint style="warning" %}
Currently only **PCM-encoded `.wav`** files are supported. If you have a `.wav` that won’t import/play, convert it to PCM using Audacity (or a similar tool).
{% endhint %}

### Quick start (play a sound)

```go
click := get_asset(SFX_Asset, "sfx/click.wav");

desc := SFX.default_sfx_desc();
SFX.play(click, desc);
```

### SFX API reference

```go
SFX_Asset :: class : Asset {}

SFX_Channel :: enum {
    SFX;
    MUSIC;
}

SFX_Desc :: struct {
    specific_to_player: Player;
    positional:       bool;
    position:         v2 #read_only;
    delay:            float;
    volume:           float;
    speed:            float;
    volume_perturb:   float;
    speed_perturb:    float;
    range_multiplier: float;
    loop_timeout:     float;
    entity_to_follow: u64;
    loop:             bool;
    channel:          SFX_Channel;
    unique_id:        u64;

    set_position :: method(p: v2);
}

SFX :: struct {
    play              :: proc(asset: SFX_Asset, desc: SFX_Desc) -> u64;
    stop              :: proc(id: u64);
    fade_out_and_stop :: proc(id: u64, fade_time: float);
    default_sfx_desc :: proc() -> SFX_Desc;
}
```

### Getting an `SFX_Asset`

Sound effects live in your game’s `res` folder (often under `res/sfx/`).

* Drag sounds in from the **Asset Catalog** (editor downloads them into `res/`)
* Or add your own `.wav` file into `res/` from your local machine

Then reference them by path:

```go
pickup := get_asset(SFX_Asset, "sfx/pickup.wav");
```

{% hint style="warning" %}
Check asset paths during development. A missing asset produces a null handle; `SFX.play` logs a warning and returns `0`.
{% endhint %}

{% hint style="info" %}
Some built-in platform assets use `$AO/...` paths. For example: `get_asset(SFX_Asset, "$AO/sfx/FUI Hologram Ping Tone Echoed.wav")`.
{% endhint %}

### Positional (3D-ish) sounds

By default, SFX plays as a non-positional “2D” sound. To make it positional, set a position:

```go
play_hit_sfx :: proc(world_pos: v2) {
    hit := get_asset(SFX_Asset, "sfx/hit.wav");

    desc := SFX.default_sfx_desc();
    desc.set_position(world_pos);
    desc.range_multiplier = 1.5;

    SFX.play(hit, desc);
}
```

### Following an entity (moving sound source)

If a sound should move with an entity (engine hum, buzzing projectile, etc), set `entity_to_follow`:

```go
start_engine_loop :: proc(entity: Entity) -> u64 {
    engine := get_asset(SFX_Asset, "sfx/engine_loop.wav");

    desc := SFX.default_sfx_desc();
    desc.entity_to_follow = entity.id;
    desc.loop = true;
    desc.channel = .MUSIC; // optional: treat as music/ambient instead of SFX

    // Returns an ID that can be used to update or stop the sound.
    return SFX.play(engine, desc);
}
```

{% hint style="info" %}
If `entity_to_follow` is set, you usually don’t need to also call `set_position` (it’s fine if you do).
{% endhint %}

### Player-targeted sounds

Set `specific_to_player` when the server should track the sound but only one client should actually hear it:

```go
play_private_ping :: proc(player: Player) {
    click := get_asset(SFX_Asset, "sfx/click.wav");

    desc := SFX.default_sfx_desc();
    desc.specific_to_player = player;
    desc.volume = 0.7;

    SFX.play(click, desc);
}
```

### Looping + stopping

`SFX.play` returns an ID you can stop later:

```go
sound_id := 0.(u64);

start_loop :: proc() {
    loop := get_asset(SFX_Asset, "sfx/ambience.wav");

    desc := SFX.default_sfx_desc();
    desc.loop = true;
    desc.volume = 0.6;
    desc.loop_timeout = 60; // safety: auto-stop after ~60s if you forget

    sound_id = SFX.play(loop, desc);
}

stop_loop :: proc() {
    if sound_id != 0 {
        SFX.stop(sound_id);
        sound_id = 0;
    }
}
```

### Rapid repeated sounds and variation

For quick repetitive sounds such as footsteps or automatic weapons, give each logical event a stable `unique_id`. Scope a synchronized monotonic event index to its source entity when the sound does not follow that entity:

```go
footstep_id += 1; // a synchronized u64 field in gameplay state

desc := SFX.default_sfx_desc();
desc.set_position(entity.world_position);
desc.unique_id = mix_u64(entity.id, footstep_id);
SFX.play(get_asset(SFX_Asset, "$AO/sfx/Footsteps/footstep_01.wav"), desc);
```

The same logical event must retain the same `unique_id` on the client, server, and every resimulation. Every distinct nearby event must get a different value. Only derive it from an event index that is synchronized and stable through prediction. `entity_to_follow` participates in sound identity, so when it is assigned, use the event index directly instead of mixing the entity ID into `unique_id`.

Subtle variation can keep frequently played sounds from becoming monotonous:

```go
desc := SFX.default_sfx_desc();
desc.volume_perturb = 0.2;
desc.speed_perturb  = 0.15;
SFX.play(get_asset(SFX_Asset, "sfx/footstep.wav"), desc);
```

{% hint style="info" %}
Keep `volume_perturb` / `speed_perturb` subtle. Values above \~`0.3` usually sound wrong.
{% endhint %}

### Prediction and targeting

Call gameplay sounds from the same shared predicted path as the event that caused them. The sound system reconciles a client's predicted sound with the server result, so do not wrap `SFX.play` in `Game.is_server()` or `player.is_local()`.

```go
play_round_victory :: proc(position: v2) {
    desc := SFX.default_sfx_desc();
    desc.set_position(position);
    SFX.play(get_asset(SFX_Asset, "sfx/round_victory.wav"), desc);
}
```

For a sound that only one player should hear, set `desc.specific_to_player`. This keeps the event in shared gameplay code while targeting playback to that player's client.

## Haptics

`Haptics.play_impact` triggers impact feedback on the local device. Call it only for the local player:

```go
My_Player :: class : Player_Base {
    play_local_impact :: method() {
        if !is_local() return;

        Haptics.play_impact(.MEDIUM);
    }
}
```

Choose `.LIGHT`, `.MEDIUM`, or `.HEAVY`. Haptics currently work on iOS. Calls on Android, web, Windows, and servers do nothing.

Haptics are not synchronized, targeted, or deduplicated. Trigger them at the local interaction point rather than from scene-wide gameplay code.


# Spine Animations

You can add animated interactable objects to your game using our existing library of 50k+ Spine rigs.

Spine is how animated characters and props work in All Out (chests, animals, props, VFX rigs, and the player).

To make Spine feel simpler, keep this mental model:

* **Spine rig (`.spine`)**: the “blueprint” (bones + animation names + skin names)
* **`Spine_Animator`**: a component on an entity that renders/updates a Spine rig in the world
* **`Spine_Instance`**: a standalone (non-component) instance — used for UI animations or manual lifetime control
* **Skins**: “outfits/variants” that decide which images are visible
* **Animations**: named timelines (e.g. `"idle"`, `"walk"`, `"open"`)

`Spine_Bone_Transform` describes a bone's current affine basis in skeleton-local space. A bone-local point `{x, y}` maps to `position + x_axis*x + y_axis*y`.

### When to use `Spine_Animator` vs `Spine_Instance`

* Use **`Spine_Animator`** for anything that exists in the world (props, NPCs, interactables).
* Use **`Spine_Instance`** directly when you want to draw a Spine in UI or you need manual lifetime control.

The engine updates a `Spine_Animator` automatically. A standalone instance must be updated and destroyed by your code.

{% hint style="warning" %}
If you call `Spine_Instance.create()`, you must call `instance.destroy()` when you're done (otherwise you leak).
{% endhint %}

### Spine API reference (CSL)

```go
// Component — attached to world entities
Spine_Animator :: class : Component {
    depth_offset: float;
    layer: s32;
    mask_in_shadow: bool;
    instance: Spine_Instance #read_only;

    // Visuals
    color: v4; // RGBA multiplier; {1, 1, 1, 1} leaves the appearance unchanged.
    scale: v2;
    speed_multiplier: float;
    state_machine: State_Machine #read_only;

    set_skeleton :: method(asset: Spine_Asset) -> u64;
    get_skeleton :: method(id: u64 = 0) -> Spine_Asset;
    set_animation :: method(animation: string, loop: bool, track: s64, speed: float = 1);

    // Skins
    set_skin          :: method(skin: string);
    enable_skin       :: method(skin: string);
    disable_skin      :: method(skin: string);
    disable_all_skins :: method();
    refresh_skins     :: method();
    get_skins         :: method() -> []string;
    set_to_setup_pose :: method();

    // Bone local offsets (advanced)
    get_bone_local_position :: method(bone_name: string) -> v2;
    try_get_bone_local_transform :: method(bone_name: string) -> (Spine_Bone_Transform, bool);
    set_bone_local_position :: method(bone_name: string, position: v2);

    // Optional: drive animations via a state machine
    set_state_machine       :: method(machine: State_Machine, transfer_ownership: bool);
    set_color_replace_color :: method(color: Color_Replace_Color);
    set_material            :: method(material: Material, transfer_ownership: bool);
    get_tint                :: method() -> v4;
    set_tint                :: method(tint: v4);
    set_destroy_entity_when_done_current_animation :: method(enabled: bool);
}
```

`Spine_Animator` inherits `Component.awaken()`. When your script and animator start on the same entity, call `awaken()` before accessing the animator's instance.

### Adding animated objects to your world

There are 3 ways to add animated objects to your scene, all of which will create a Spine\_Animator component.

* Drag any animated asset from the [Asset Catalog](/using-the-editor/asset-catalog) into your scene
* Create a new entity and add the Spine\_Animator component
  * Set the `Skeleton Data Asset` field to a .spine file in your assets
* Add a spine animator to your world using scripting (covered in the examples)

{% hint style="info" %}
If your code and the `Spine_Animator` are on the same entity and start at the same time, call `spine.awaken()` before calling any animation methods.
{% endhint %}

### Examples

#### Openable Chest

An interactable chest that plays an “open” animation and then stays open:

```go
Chest :: class : Interactable {
    opened: bool;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Open");
        radius = 1.25;
        required_hold_time = 0.15;

        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_animation("idle", true, 0);
    }

    can_use :: method(player: Player) -> bool {
        return !opened;
    }

    on_interact :: method(player: Player) {
        opened = true;
        this.set_text("Opened");

        spine := entity.get_component(Spine_Animator);
        spine.set_animation("open", false, 0);
    }
}
```

Animation and skin names must match the names in the rig.

#### Walking Chicken

A simple “idle vs walk” loop based on movement:

```go
Chicken :: class : Component {
    @ao_serialize speed: float = 1.5;
    last_pos: v2;
    was_moving: bool;

    ao_start :: method() {
        last_pos = entity.world_position;

        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_animation("idle", true, 0);
    }

    ao_update :: method(dt: float) {
        // Example motion (patrol)
        entity.add_local_position({speed * dt, 0});

        moving := length_squared(entity.world_position - last_pos) > 0.0001;
        last_pos = entity.world_position;

        if moving != was_moving {
            was_moving = moving;
            spine := entity.get_component(Spine_Animator);
            spine.set_animation(moving ? "walk" : "idle", true, 0);
        }
    }
}
```

#### Driveable Car

If you want "driveable" behavior, the simplest approach is to drive the *movement* with your own gameplay logic, and drive the *animation* based on whether the car is moving.

```go
Car :: class : Component {
    @ao_serialize agent: Movement_Agent;
    @ao_serialize target: Entity;

    ao_start :: method() {
        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_animation("idle", true, 0);
    }

    ao_update :: method(dt: float) {
        if #alive(target) {
            agent.set_path_target(target.world_position, agent.movement_speed);
        }

        moving := length_squared(agent.velocity) > 0.01;
        spine := entity.get_component(Spine_Animator);
        spine.set_animation(moving ? "drive" : "idle", true, 0);
    }
}
```

{% hint style="info" %}
If you're switching between only two looping animations, direct `set_animation` calls are usually simpler than building a state machine.
{% endhint %}

### Skins (variants/outfits)

Some spines don't start with a default skin. If your entity is "invisible", you likely need to pick a skin.

```go
spine := entity.get_component(Spine_Animator);
spine.awaken();

// Option A: set a single skin
spine.set_skin("default");
spine.refresh_skins();

// Option B: combine multiple skins
spine.disable_all_skins();
spine.enable_skin("body");
spine.enable_skin("hat");
spine.refresh_skins();
```

{% hint style="warning" %}
Always call `refresh_skins()` after changing skins.
{% endhint %}

### Animation events

Use callbacks when a Spine timeline event should trigger gameplay or audio. For a `Spine_Animator` component, register callbacks on its `instance`.

```go
Spine_Event_Listener :: class : Component {
    ao_start :: method() {
        spine := entity.get_component(Spine_Animator);
        spine.awaken();

        spine.instance.set_on_event(this, proc(userdata: Object, event: Spine_Event_Data) {
            listener := userdata.(Spine_Event_Listener);
            listener.handle_spine_event(event);
        });
    }

    handle_spine_event :: method(event: Spine_Event_Data) {
        if event.event == "footstep" {
            desc := SFX.default_sfx_desc();
            desc.set_position(entity.world_position);
            SFX.play(get_asset(SFX_Asset, "sfx/footstep.wav"), desc);
        }
    }
}
```

```go
Spine_Event_Data :: struct {
    event: string;
    int_value: s64;
    float_value: float;
    string_value: string;
}
```

The event callback runs in the shared predicted path. Call gameplay and SFX directly; do not guard it with `Game.is_server()` or `is_local()`.

## Standalone instances and `UI.spine`

The standalone API provides manual lifetime and animation updates:

```go
Spine_Instance :: class {
    create  :: proc() -> Spine_Instance;
    destroy :: method();
    update  :: method(dt: float);

    set_skeleton      :: method(asset: Spine_Asset) -> u64;
    get_skeleton      :: method(id: u64 = 0) -> Spine_Asset;
    add_skeleton      :: method(asset: Spine_Asset) -> u64;
    remove_skeleton   :: method(id: u64);
    set_main_skeleton :: method(id: u64);

    set_animation :: method(animation: string, loop: bool, track: s64, speed: float = 1);
    set_skin      :: method(skin: string);
    refresh_skins :: method();

    get_bone_local_position       :: method(bone_name: string) -> v2;
    try_get_bone_local_transform  :: method(bone_name: string) -> (Spine_Bone_Transform, bool);

    set_on_event           :: method(userdata: Object, callback: proc(userdata: Object, event: Spine_Event_Data));
    set_on_animation_start :: method(userdata: Object, callback: proc(userdata: Object, animation: string));
    set_on_animation_end   :: method(userdata: Object, callback: proc(userdata: Object, animation: string));
}
```

Keep the instance in persistent player UI state. Create it once, update and draw it from `ao_late_update`, then destroy it from `ao_end`:

```go
My_Player :: class : Player_Base {
    menu_spine: Spine_Instance;

    ao_start :: method() {
        menu_spine = Spine_Instance.create();
        menu_spine.set_skeleton(get_asset(Spine_Asset, "characters/shopkeeper.spine"));
        menu_spine.set_skin("default");
        menu_spine.refresh_skins();
        menu_spine.set_animation("idle", true, 0);
    }

    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        menu_spine.update(dt);

        UI.push_screen_draw_context();
        defer UI.pop_draw_context();
        UI.spine(UI.get_screen_rect().center(), menu_spine, {0.75, 0.75});
    }

    ao_end :: method() {
        if menu_spine != null {
            menu_spine.destroy();
            menu_spine = null;
        }
    }
}
```

Do not create the instance inside `ao_late_update`; that would restart its animation every frame and leak the previous instance.

### State machines (optional, for complex animation logic)

You can keep animation logic simple by calling `set_animation` directly. If you have many states (idle/run/attack/hit/death), a `State_Machine` helps you define transitions once and then just set variables/triggers.

The key idea: **state names must match animation names** in your Spine file.

#### What a state machine does

Think of a state machine as a tiny “animation controller”:

* You define **states** (each state name should match a Spine animation name)
* You define **variables** (`bool`, `trigger`, `int`, `float`)
* You define **transitions** between states based on those variables
* At runtime, you only update variables (the state machine chooses the animation)

#### Minimal example: idle/walk + attack trigger

```go
NPC_Anim :: class : Component {
    state_machine: State_Machine;
    is_moving: bool;

    ao_start :: method() {
        // 1) Create the state machine + variables
        state_machine = State_Machine.create();
        moving_var := state_machine.create_variable("is_moving", .BOOL);
        attack_var := state_machine.create_variable("attack", .TRIGGER);

        // 2) Create a layer (maps to a Spine track, usually 0)
        layer := state_machine.create_layer("main", 0);

        // 3) Create states (names must match animations in the Spine rig)
        idle := layer.create_state("idle", true);
        walk := layer.create_state("walk", true);
        attack := layer.create_state("attack", false);
        layer.set_initial_state(idle);

        // 4) Movement transitions
        idle_to_walk := layer.create_transition(idle, walk, false);
        idle_to_walk.create_bool_condition(moving_var, true);

        walk_to_idle := layer.create_transition(walk, idle, false);
        walk_to_idle.create_bool_condition(moving_var, false);

        // 5) Attack from any state, then return to idle when complete
        to_attack := layer.create_global_transition(attack, true);
        to_attack.create_trigger_condition(attack_var);

        attack_to_idle := layer.create_transition(attack, idle, true); // require_state_complete = true

        // 6) Attach to the animator
        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_state_machine(state_machine, true); // transfer_ownership = true
    }

    ao_update :: method(dt: float) {
        state_machine.set_bool("is_moving", is_moving);
    }

    do_attack :: method() {
        state_machine.set_trigger("attack");
    }
}
```

{% hint style="info" %}
If you pass `transfer_ownership = true` to `set_state_machine`, the animator will destroy the state machine for you. Otherwise, you must destroy it yourself.
{% endhint %}

### Troubleshooting

Q: I dragged a spine asset into my scene and I don't see any object, it's just an empty entity

A: Make sure to click "Add Skin" and select a variant of the spine to apply! Some spines don't start with a default skin.

Q: My script crashes when I call animation methods on my Spine\_Animator

A: If your script and the `Spine_Animator` start at the same time, call `spine.awaken()` before calling any animation methods.

Q: I changed skins but nothing happened

A: After any skin change (`set_skin`, `enable_skin`, `disable_skin`, `disable_all_skins`), you must call `refresh_skins()`.

### Custom Player Skins/Animations

If you want to add custom animations to your player, you can do so by merging the base All Out player spine rig together with a custom rig containing more animations. We can provide many of these; just [Developer Support](/going-all-out/developer-support).

### The Spine Rig Format

Spine rigs include:

* A skeleton file (`.spine`) with the bones, skins, animations, and structure.
* An atlas file (`.atlas`) that maps attachments to images.
* Every image page named by the atlas. A one-page export usually has one `.png`.

### Creating your own animations

{% hint style="warning" %}
Currently to make your own animations or rigs, you'll need to own a copy of Esoteric Spine. However, we provide a massive library of existing spine rigs and animations as part of the [Asset Catalog](/using-the-editor/asset-catalog) for you to use and we recommend starting there!
{% endhint %}

#### Export Format

To use a custom Spine rig in All Out, export the standard Spine bundle into your `res` folder:

* `something.spine`
* `something.atlas`
* Every image file named by `something.atlas`

If you're unsure about export settings, start from an existing All Out Spine asset from the Asset Catalog and mirror its structure or reach out to us!


# Interactables

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-29d531efc3ba62fe4342df6008264afb7d552819%2Finteractable.png?alt=media" alt="An interact prompt near a world object"><figcaption></figcaption></figure>

### The Interactable Component

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-ca76aa70b048f6a2a55fc2c45d1993889341e38e%2Fimage.png?alt=media" alt="Interactable component fields in the inspector"><figcaption></figcaption></figure>

Adding an Interactable component to an entity will automatically show UI when players are within the `radius` you specify.

The interaction radius scales with the entity's world scale, so keep the interactable root at scale 1 and scale a visual child when you want the interaction range to stay fixed.

To respond to the player pressing the interactable, you'll need to write CSL code.

An entity can have more than one `Interactable`. Use `priority` when multiple prompts might be available at the same time.

{% hint style="warning" %}
Prompt selection is per-player, but prompt content is not. `can_use(player)` controls whether a player can see and use an interactable; `set_text`, `set_hold_text`, `set_subtitle`, and `subtitle_color` change the one shared component seen by every player. Do not refresh those values from player-specific quest, inventory, or progression state—Player A's last write would also be shown to Player B.

For different per-player actions such as "Accept Quest" and "Turn In", use a neutral shared label such as "Talk", or add separate fixed-label interactables and gate each one with `can_use(player)`. Each player will then select from the interactables valid for their own state.
{% endhint %}

The most common pattern is:

* Create a class that inherits from `Interactable`
* Call `this.set_listener(this)` in `ao_start`
* Implement `can_use` and `on_interact` on your class

{% hint style="info" %}
If you add a new interactable class in code, you still need to add that component to an entity in the editor (or spawn an entity and add the component at runtime).
{% endhint %}

#### Trash Example

```go
// Get coins for picking up the trash :D
Trash :: class : Interactable {
    @ao_serialize coins: s64;
    used: bool;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Pick up trash");
        this.set_hold_text("Picking up...");
        required_hold_time = 0.25;
        radius = 1.25;
    }

    can_use :: method(player: Player) -> bool {
        if used return false;
        return true;
    }

    on_interact :: method(player: Player) {
        used = true;
        Economy.deposit_currency(player, "Coins", coins);
        entity.destroy();
    }
}
```

#### Grow Example

An interactable that “grows” an entity (once):

```go
Grow_Button :: class : Interactable {
    @ao_serialize scale_multiplier: float;
    used: bool;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Grow");
        radius = 1.25;
        required_hold_time = 0;
    }

    can_use :: method(player: Player) -> bool {
        return !used;
    }

    on_interact :: method(player: Player) {
        used = true;
        new_scale := entity.local_scale * scale_multiplier;
        entity.set_local_scale(new_scale);
        this.set_text("Already used");
    }
}
```

#### Prompt Purchase Example

An interactable that prompts a product purchase:

```go
Buy_Admin_Pass :: class : Interactable {
    @ao_serialize product_id: string;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Buy Admin Pass");
        radius = 1.25;
    }

    can_use :: method(player: Player) -> bool {
        // Example gate: hide if already owned (game pass)
        return !Purchasing.owns_product(player, product_id);
    }

    on_interact :: method(player: Player) {
        Purchasing.prompt_purchase(player, product_id);
    }
}
```

### API Reference

```go
Interactable :: class : Component {
    // Shared prompt text (tap interaction); not per-player.
    get_text :: method() -> string;
    set_text :: method(text: string);

    // Shared prompt text while holding (for hold-to-interact).
    get_hold_text :: method() -> string;
    set_hold_text :: method(text: string);

    // Shared prompt subtitle.
    get_subtitle :: method() -> string;
    set_subtitle :: method(text: string);

    // Wire a listener object that implements:
    // - can_use(player: Player) -> bool
    // - on_interact(player: Player)
    // - on_holding(player: Player) [optional]
    set_listener :: method(new_listener: Component);

    // Common fields (set in editor or in code)
    offset: v2;
    prompt_offset: v2;
    radius: float;
    required_hold_time: float;
    priority: s64;
    subtitle_color: v4;
}
```

### Optional player hooks (global rules)

You can also implement these on your `Player` class to enforce game-wide rules (e.g. can't use interactables before respawning):

```go
Player :: class : Player_Base {
    ao_can_use_interactable :: method(interactable: Interactable) -> bool {
        return true;
    }

    ao_on_interactable_used :: method(interactable: Interactable) {
    }

    ao_on_holding_interactable :: method(interactable: Interactable) {
    }
}
```


# Abilities

Abilities are the standard way to implement player actions with:

* A consistent button UI that works well on mobile and PC.
* Cooldowns
* Optional aiming (tap/drag on mobile, mouse aim on PC)

Abilities are **per-player**. When a player joins, All Out creates an instance of each ability type and stores it on `player.abilities`.

### Quick start (draw ability buttons)

Draw ability buttons in `Player.ao_late_update`, inside `is_local_or_server()`:

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            draw_ability_button(this, Shoot_Ability, 0);  // big primary button
            draw_ability_button(this, Dodge_Roll, 1);     // small button
        }
    }
}
```

{% hint style="info" %}
Button indices map to fixed screen positions. Index `0` is the large primary button; indices `1-5` are smaller buttons around it.
{% endhint %}

### Ability API reference

```go
Ability_Base :: class {
    player: Player;

    name: string;
    icon: Texture_Asset;

    current_cooldown: float;
    type: typeid;
    is_aimed_ability: bool;
    mouse_position_on_press: v2;

    keybind_override: Keybind;
    disable_keybind: bool;
    draw_but_dont_use_keybind: bool; // Show the key hint, but handle its input yourself.

    #interface on_update      :: proc(ability: Ability_Base, params: ref Ability_Update_Params);
    #interface can_use        :: proc(ability: Ability_Base) -> bool;
    #interface on_draw_button :: proc(ability: Ability_Base, rect: Rect);
}

Ability_Update_Params :: struct : Interact_Result {
    can_use: bool;        // cooldown + can_use + Player.ao_can_use_ability (if present)
    drag_offset: v2;      // 0..1
    drag_direction: v2;   // unit direction
}

draw_ability_button :: proc(player: Player, $T: typeid, index: int);
```

### Creating an ability (basic click)

```go
Dash_Ability :: class : Ability_Base {
    on_init :: method() {
        name = "Dash";
        icon = get_asset(Texture_Asset, "icons/dash.png");
    }

    can_use :: method() -> bool {
        // Optional extra gating (in addition to cooldown)
        return true;
    }

    on_update :: method(params: ref Ability_Update_Params) {
        if params.clicked && params.can_use {
            // Apply gameplay here. This path runs through client prediction and on the server.
            // ...

            // IMPORTANT: set cooldown when you activate
            current_cooldown = 1.5;
        }
    }
}
```

### Holding abilities (active while held)

For abilities that are active while held (sprint, shield, beam), use `Ability_Utilities.update_holding_ability` so it works on both PC and mobile.

```go
keybind_sprint: Keybind;

ao_before_scene_load :: proc() {
    keybind_sprint = Keybinds.register("Sprint", .LEFT_SHIFT);
}

Sprint_Ability :: class : Ability_Base {
    on_init :: method() {
        name = "Sprint";
        draw_but_dont_use_keybind = true;
        keybind_override = keybind_sprint;
    }

    on_update :: method(params: ref Ability_Update_Params) {
        holding := Ability_Utilities.update_holding_ability(player, ref params, keybind_sprint);
        player.is_sprinting = holding.active;
    }
}
```

### Aimed abilities (drag to aim on mobile)

If you want “always aiming” behavior (mouse aim + click to fire on PC, press-drag-release on mobile), use `Ability_Utilities.full_update_aimed_ability`:

```go
Shoot_Ability :: class : Ability_Base {
    on_init :: method() {
        name = "Shoot";
        is_aimed_ability = true;
        disable_keybind = true;
    }

    on_update :: method(params: ref Ability_Update_Params) {
        activation := Ability_Utilities.full_update_aimed_ability(player, ref params);

        if activation.activate && params.can_use {
            // activation.direction is a unit vector
            // shoot_projectile(player.entity.world_position, activation.direction);
            current_cooldown = 0.5;
        }
    }
}
```

If you want “click to enter aim mode” (PC toggles aim mode; right-click cancels), use `Ability_Utilities.full_update_targeted_aimed_ability`:

```go
Roll_Ability :: class : Ability_Base {
    on_init :: method() {
        name = "Roll";
        is_aimed_ability = true;
    }

    on_update :: method(params: ref Ability_Update_Params) {
        activation := Ability_Utilities.full_update_targeted_aimed_ability(player, this, ref params);
        if activation.activate && params.can_use {
            current_cooldown = 1.25;
        }
    }
}
```

### Game-wide ability restrictions (optional)

Implement `ao_can_use_ability` on your `Player` class to enforce global rules (dead players can't use abilities, disabled during cutscenes, etc).

```go
Player :: class : Player_Base {
    ao_can_use_ability :: method(ability: Ability_Base) -> bool {
        if health.is_dead return false;
        return true;
    }
}
```

### Best practices

* Always draw buttons in `ao_late_update` and guard with `is_local_or_server()`
* Set `current_cooldown` when you activate (otherwise it can be spammed)
* Use `Ability_Utilities` helpers for holding/aiming so mobile + PC behave consistently
* Put global rules in `Player.ao_can_use_ability`, not duplicated per ability


# Effects

Effects temporarily take control of an entity for complex behaviors like dashes, attacks, eating animations, cutscenes, or death/respawn sequences.

Effects can be used on players, NPCs, or any entity. They’re especially useful when you want:

* Movement you control in code (dash/knockback/roll)
* “Lock out” player input for a short sequence (eat, revive, cutscene)
* A timed window of state (invincibility, slow, stun)
* UI that is tied to a temporary state (respawn countdown)

## Active vs passive

All Out supports two kinds of effects:

* **Active effects**: one at a time per entity. Setting a new active effect interrupts the current active effect.
* **Passive effects**: multiple can be attached at once (stackable buffs/debuffs/status effects).

{% hint style="info" %}
An active effect is also inserted into the entity’s effect list, so it shows up in `effect_iterator` just like passive effects.
{% endhint %}

## Quick start (a simple timed effect)

Create a class that inherits from `Effect_Base`, then attach it to an entity.

```go
Apple_Pop_Effect :: class : Effect_Base {
    sprite: Sprite_Renderer;
    start_scale: v2;
    start_pos: v2;

    effect_start :: method() {
        sprite = entity.get_component(Sprite_Renderer);
        start_scale = entity.local_scale;
        start_pos = entity.local_position;
        set_duration(0.4); // auto-removes after 0.4s
    }

    effect_update :: method(dt: float) {
        t := get_elapsed_time() / 0.4;

        // Scale up then shrink to nothing
        scale_curve: float;
        if t < 0.3 {
            scale_curve = lerp(1.0, 1.3, Ease.out_back(t / 0.3));
        } else {
            scale_curve = lerp(1.3, 0.0, Ease.in_back((t - 0.3) / 0.7));
        }

        entity.set_local_scale(start_scale * scale_curve);

        // Float upward slightly
        rise := Ease.out_quad(t) * 0.5;
        entity.set_local_position({start_pos.x, start_pos.y + rise});

        // Fade out near the end
        if sprite != null {
            alpha := 1.0 - Ease.in_quad(max(0.0, (t - 0.5) / 0.5));
            sprite.color.w = alpha;
        }
    }

    effect_end :: method(interrupt: bool) {
        // Clean up when done (or interrupted)
        entity.destroy();
    }
}
```

Attach it as an active effect:

```go
effect := new(Apple_Pop_Effect);
entity.set_active_effect(effect);
```

## Effect lifecycle

Effects are callback-based. Implement only what you need:

* `effect_start()`: called once when the effect is attached
* `effect_update(dt)`: called every frame
* `effect_late_update(dt)`: called every frame after `effect_update`
* `effect_draw(dt)`: cosmetic-only drawing on renderable frames
* `effect_end(interrupt)`: called when the effect is removed

`effect_draw` includes players and skips resim. Interactive UI must use `effect_update`/`effect_late_update`, never `effect_draw`.

## Active effects (exclusive control)

Use `set_active_effect` for effects that should be mutually exclusive (dash, attack windup, death sequence).

```go
dash := new(Dash_Effect);
dash.direction = {1, 0};
player.entity.set_active_effect(dash);
```

If the entity already has an active effect, it is ended with `interrupt = true`.

## Passive effects (stackable buffs / status)

Use `add_passive_effect` for effects that can stack (slow, poison, shield, invincibility window).

```go
slow := new(Slow_Effect);
slow.speed_multiplier = 0.5;
slow.set_duration(4.0);
enemy.entity.add_passive_effect(slow);
```

## API reference

### `Effect_Base`

```go
Effect_Base :: class {
    entity: Entity;
    player: Player; // null if this effect wasn't added to a player!

    player_specific: struct {
        freeze_player: bool;
        disable_movement_inputs: bool;
    };

    // Optional callbacks
    #interface effect_start       :: proc(c: Effect_Base);
    #interface effect_update      :: proc(c: Effect_Base, dt: float);
    #interface effect_late_update :: proc(c: Effect_Base, dt: float);
    #interface effect_draw        :: proc(c: Effect_Base, dt: float);
    #interface effect_end         :: proc(c: Effect_Base, interrupt: bool);

    // Read-only
    start_time: float;
    next_effect: Effect_Base;
    prev_effect: Effect_Base;

    get_elapsed_time :: method() -> float;
    get_duration_remaining :: method() -> float;
    set_duration     :: method(duration: float);
    remove_effect    :: method(interrupt: bool);
}
```

### Applying / removing effects

```go
set_active_effect  :: proc(entity: Entity, new_active_effect: $T);
add_passive_effect :: proc(entity: Entity, new_effect: $T);

remove_all_effects :: proc(entity: Entity);
remove_effect      :: proc(entity: Entity, type: typeid, interrupt: bool) -> bool;
```

### Checking / iterating effects

```go
get_effect :: proc(entity: Entity, $T: typeid, mode := Try_Get_Effect_Mode.EXACT_MATCH) -> (T, bool);
has_effect :: proc(entity: Entity, $T: typeid, mode := Try_Get_Effect_Mode.EXACT_MATCH) -> bool;

effect_iterator :: proc(entity: Entity) -> Effect_Iterator;
```

{% hint style="warning" %}
After calling `remove_effect(...)`, the effect is detached and no longer rooted. Its reference is invalid. **Return immediately** and do not access `this` again.
{% endhint %}

## `freeze_player` vs `disable_movement_inputs`

* **`player_specific.freeze_player = true`**: locks player position entirely.
* **`player_specific.disable_movement_inputs = true`**: ignores input, but your effect can still move the player (dash/roll).

Both only apply when `player != null` (i.e. the effect is on an entity that has a `Player` component).

## Player emotes

Player emotes use a built-in active effect. Starting another emote interrupts the current emote, while any other active effect prevents a new emote from starting. Movement interrupts emotes and restores the main animation layer, except for `Emote/T_Pose`, which allows movement.

Use block reasons when gameplay should temporarily prevent the emote wheel from starting an emote:

```go
player.add_emote_block_reason("stunned");

// Later, when the restriction ends:
player.remove_emote_block_reason("stunned");
```

Reasons are counted as entries, so matching add/remove calls are important. `remove_emote_block_reason` removes one matching entry and returns whether it found one. Use `has_emote_block_reason(reason)` to query one reason and `has_any_emote_block_reason()` to query whether any are active. You can also control the built-in effect directly:

```go
if player.has_any_emote_block_reason() {
    // At least one block reason is active.
}

started := player.try_trigger_emote("Emote/Wave");
cancelled := player.cancel_emote();
```

`try_trigger_emote` returns `false` when the animation is not equipped, emotes are blocked, or another kind of active effect owns the player. The normal emote wheel uses this same path.

## Effect examples

### Dash / roll (active effect)

```go
Roll_Effect :: class : Effect_Base {
    direction: v2;
    original_friction: float;

    effect_start :: method() {
        player_specific.disable_movement_inputs = true;
        original_friction = player.agent.friction;
        player.agent.friction = 0;
        player.animator.state_machine.set_trigger("dodge_roll");
        player.set_facing_right(direction.x > 0);
        set_duration(0.5);
    }

    effect_update :: method(dt: float) {
        player.agent.velocity = direction * 8;
    }

    effect_end :: method(interrupt: bool) {
        player.agent.friction = original_friction;
    }
}
```

### Invincibility window (passive effect)

Use a passive effect to represent the invincible state, and check for it wherever you apply damage.

```go
Invincible_Effect :: class : Effect_Base {
}

give_invincibility :: proc(player: Player, seconds: float) {
    e := new(Invincible_Effect);
    e.set_duration(seconds);
    player.entity.add_passive_effect(e);
}

// Example damage gate:
take_damage :: proc(player: Player, amount: s64) {
    if player.entity.has_effect(Invincible_Effect) {
        return;
    }
    // Assumes your Player class has a health field.
    player.health.take_damage(amount);
}
```

### 2x size for a few seconds (passive effect)

```go
Grow_Effect :: class : Effect_Base {
    scale_multiplier: float;
    start_scale: v2;

    effect_start :: method() {
        start_scale = entity.local_scale;
        entity.set_local_scale(start_scale * scale_multiplier);
    }

    effect_end :: method(interrupt: bool) {
        entity.set_local_scale(start_scale);
    }
}

apply_grow :: proc(entity: Entity) {
    if entity.has_effect(Grow_Effect) return;

    e := new(Grow_Effect);
    e.scale_multiplier = 2.0;
    e.set_duration(3.0);
    entity.add_passive_effect(e);
}
```

### Death / respawn sequence (active effect with local UI)

```go
Death_Effect :: class : Effect_Base {
    effect_start :: method() {
        player_specific.freeze_player = true;
        player.add_name_invisibility_reason("death");
        player.animator.state_machine.set_trigger("death");
    }

    effect_update :: method(dt: float) {
        time_until_respawn := 5.0 - get_elapsed_time();

        if time_until_respawn <= 0 {
            remove_effect(false);
            return;
        }
    }

    effect_end :: method(interrupt: bool) {
        player.remove_name_invisibility_reason("death");
        respawn_player(player); // user-defined: teleport player to spawn point
        player.health.reset();
        player.animator.state_machine.set_trigger("RESET");
    }
}

My_Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        death, found := entity.get_effect(Death_Effect);
        if !found return;

        time_until_respawn := max(0.0, 5.0 - death.get_elapsed_time());
        ts := UI.default_text_settings();
        ts.size = 64;
        rect := UI.get_screen_rect().bottom_center_rect().offset(0, 150);
        UI.text(rect, ts, "Respawning in %", {time_until_respawn.(int) + 1});
    }
}
```

### Cutscene lock (active effect)

```go
Cutscene_Effect :: class : Effect_Base {
    effect_start :: method() {
        player_specific.freeze_player = true;
        set_duration(2.0);
    }
}
```

## Smooth custom visuals

The simulation runs at a fixed rate while rendering can run faster. Drawing from a component callback is automatically anchored to that component's entity. For other cases:

* Wrap world-space drawing for another entity in `UI.begin_world_space_ui(entity)` and `UI.end_world_space_ui()`.
* For a moving position that does not belong to an entity, keep a persistent `Position_Interpolation_Helper`. Pass the offset from `update(position)` to `UI.push_interpolation_offset`, then pop it after drawing.
* Use a persistent `Float_Interpolation_Helper` with `UI.quad_fill` for a changing bar value.
* Call `entity.mark_teleported()` after a discontinuous position change.

```go
Smooth_Bar :: class {
    progress: float;
    interpolation: Float_Interpolation_Helper;

    draw :: method(rect: Rect) {
        fill := UI.quad_fill(interpolation.update(progress), .RIGHT);
        UI.quad(rect, core_globals.white_sprite, {0.2, 0.9, 0.3, 1}, params={fill=fill});
    }
}
```

Keep interpolation helpers in persistent component or UI state. A helper created inside the draw method has no previous frame to interpolate from.

## Best practices

* Use `set_active_effect` when the effect “owns” the entity for a short sequence (dash/attack/death/cutscene).
* Use `add_passive_effect` for stackable state (buffs/debuffs/status).
* Use `set_duration(...)` for timed effects instead of manual timers.
* Always restore any modified state in `effect_end` (friction, scale, animation triggers, etc).
* Run gameplay effects in the shared predicted path.
* `effect_draw` is cosmetic-only. Draw interactive effect UI from the player's `ao_late_update` under `is_local_or_server()`.


# Particle Systems

`Particle_System_Component` draws deterministic 2D particle effects. Particle motion is sampled analytically from a descriptor, seed, and time instead of advancing and storing a transform for every particle each frame.

## Quick start

Add the component and play one of the built-in presets:

```go
particles := entity.add_component(Particle_System_Component);
particles.play(Particle_System_Desc.preset_explosion());
```

`play` validates and copies a descriptor, resets the deterministic seed and start time, and enables the component. Call `restart()` to replay the active descriptor:

```go
particles.restart();
```

Call `stop()` to disable the effect. A bare `Particle_System_Component` with no descriptor is inert until `play()` is called, so adding one never starts an invisible default effect first.

Burst presets disable their component after the longest possible particle lifetime. Continuous presets remain enabled until you disable or remove the component.

## Presets

Every preset call returns a fresh descriptor that can be safely customized:

```go
desc := Particle_System_Desc.preset_smoke();
desc.color_over_lifetime = Particle_System_Desc.over_lifetime_v4(
    {0.4, 0.15, 0.7, 0.8},
    {0.05, 0.01, 0.1, 0}
);
desc.spawn_position_radius = 0.3;
particles.play(desc);
```

The component's active `desc` is read-only. `get_desc()` returns a value copy that can be safely customized and passed back to `play()`; running particles can therefore never observe a half-applied descriptor change.

The built-in presets are:

* `preset_default`
* `preset_shotgun`
* `preset_firework`
* `preset_explosion`
* `preset_sparks`
* `preset_smoke`
* `preset_dust`
* `preset_fountain`
* `preset_embers`
* `preset_snow`
* `preset_rain`
* `preset_confetti`
* `preset_magic_aura`

## Local and world simulation space

Set `simulation_space` according to how already-spawned particles should react when the emitter moves:

```go
desc.simulation_space = .LOCAL; // Existing particles follow the emitter.
desc.simulation_space = .WORLD; // Existing particles remain where they spawned.
```

Both modes use the same deterministic analytical sampler. They differ only in the runtime information needed to recover the particle origin:

* **Local space** keeps constant-size emitter state. A newly created local-space system allocates no per-particle simulation state.
* **World space** stores one small birth record per reusable particle slot: its emitter position at birth and life index. Position, velocity, rotation, size, and color are still computed analytically.

This makes local space O(1) state and world space O(`desc.get_particle_count()`) state. Switching an existing world-space component to local space clears the slot count, although the managed array may retain its previous capacity for reuse.

## Burst and continuous emission

Burst emission starts the requested number of particles at once:

```go
desc.emission = Particle_Emission.burst(32);
desc.prewarm = false;
desc.auto_disable_when_finished = true;
```

Continuous emission schedules the requested number of particles per second:

```go
desc.emission = Particle_Emission.continuous(20);
desc.prewarm = true;
desc.auto_disable_when_finished = false;
```

Treat `emission`, `prewarm`, and `auto_disable_when_finished` as one lifecycle configuration when changing a preset. Presets return complete descriptors, and assigning a new `emission` does not reset either of the other fields. In particular, ambient presets such as `preset_magic_aura()` already have `prewarm = true`; clear it explicitly before changing them to burst emission, or `play()` will reject the descriptor.

The engine derives the smallest safe reusable slot pool automatically:

```go
slot_count = ceil(emission.rate * lifetime.max)
```

Use `desc.get_particle_count()` to inspect the resulting burst count or continuous slot count. Continuous capacity is not an authoring control: emission rate and lifetime already determine the maximum number of simultaneously eligible particles.

`prewarm` analytically advances a continuous schedule by one complete pool cycle, giving ambient effects an already-running appearance without simulating earlier frames. A WORLD-space prewarm has no earlier emitter trajectory to inspect, so it deliberately assumes the emitter was stationary at its current position before activation.

## Common descriptor fields

Ranges and over-lifetime values use helper constructors:

```go
desc.speed = Particle_System_Desc.range_float(2, 5);
desc.lifetime = Particle_System_Desc.range_float(0.5, 1.2);
desc.size_over_lifetime = Particle_System_Desc.over_lifetime_v2(
    {0.2, 0.06},
    {0.04, 0.01}
);
```

`size_over_lifetime` contains full world-space width and height. Supplying both dimensions intentionally allows stretching for streaks and rectangular particles. To preserve a sprite's source aspect ratio, specify its visual height:

```go
desc.size_over_lifetime = Particle_System_Desc.size_from_sprite_height(
    desc.sprite,
    0.2,
    0.5
);
```

The main descriptor fields are:

* `spawn_offset`, `spawn_rect_size`, and `spawn_position_radius`: where particles originate relative to the emitter.
* `direction` and `direction_spread_degrees`: base travel direction and random angular spread. The sampler normalizes the direction.
* `speed`, `gravity`, and `friction`: analytical motion controls.
* `lifetime`, `fade_in_seconds`, and `fade_out_seconds`: lifetime and opacity envelope.
* `color_over_lifetime`: RGBA color interpolation. Fades multiply the interpolated alpha instead of replacing it.
* `initial_rotation_degrees` and `angular_velocity_degrees`: rotation range and angular velocity range.
* `face_direction`: points each particle along its current velocity instead of applying angular velocity.
* `size_over_lifetime`: independent full width/height over the particle lifetime.
* `sprite`: rendered texture.
* `seed_salt`: stable per-descriptor variation mixed into the activation seed.

`layer` and `z_offset` live on `Particle_System_Component` and control render ordering.

## Validation

`play()` and `restart()` assert on invalid descriptors so programmer errors fail at their source. Editors and other authoring tools can validate without starting the effect:

```go
error: string;
if !desc.try_validate(ref error) {
    // Present error to the author.
}
```

`validation_error()` returns the same error directly, or an empty string when the descriptor is valid. Invalid values are never silently clamped.

## Determinism and interpolation

Each sample is derived from the particle-system seed, slot index, life index, and current time. Drawing does not mutate simulation state. Current and previous analytical samples are passed to the interpolated quad renderer, including position, size, and rotation, so particles remain smooth between fixed simulation updates.

World-space birth positions are captured during the shared update path rather than during drawing. Drawing samples no later than the latest completed birth-position update, so startup and re-enable timing cannot expose a newly eligible slot before its origin exists. This keeps the effect compatible with prediction and resimulation while avoiding render-only state changes.


# Inventory

### Overview

All Out provides a player inventory API that allows you to create items, give them to players, and for players to arrange, use, and drop items in your game.

There are three core concepts:

* **Item definitions** (`Item_Definition`): what an item *is* (name/icon/stacking + your own fields)
* **Item instances** (`Item_Instance`): an actual stack in the world/in an inventory
* **Inventories** (`Inventory`): a container of slots that holds item instances

Every player has a built-in inventory at `player.default_inventory`.

### Persistence

If you'd like player inventories to be automatically saved across game sessions, enable the **Auto Save Player Inventory** box in the **Edit → Game Config** section of the editor.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-d2182c0249148fa7a8df5e6629073d431d7dedaf%2Fimage.png?alt=media" alt="Player inventory settings in Game Config"><figcaption></figcaption></figure>

{% hint style="info" %}
Configure both settings programmatically in `ao_before_scene_load`, before player inventories are created or restored:

```csl
ao_before_scene_load :: proc() {
    Scene.set_player_inventory_capacity(32);
    Scene.set_auto_save_player_inventory(true);
}
```

The configured capacity is the minimum used during a persistent restore; a larger capacity stored in the save is preserved. Changing the scene setting in `Player.ao_start` is too late for that player's restored `default_inventory` and does not resize it. Use `Items.set_capacity` when you intentionally need to resize an existing inventory. Shrinking fails if a slot being removed contains an item.
{% endhint %}

### Named player inventories

Create persistent inventories only during the player's `ao_start`. Each key belongs to that player:

```csl
bank: Inventory;

ao_start :: method() {
    bank = Items.create_player_inventory(this, "bank", capacity=48);
}
```

The call returns a restored inventory synchronously. Repeating it with the same player and key during `ao_start` returns the same inventory without resetting its contents or capacity. Calls outside that player's `ao_start` fail even if the inventory already exists; keep the returned inventory in a field for later use. Keys must contain only letters, numbers, underscores, or hyphens and cannot be empty. Keep the key stable across game versions.

Named inventories are automatically saved independently of the default inventory's auto-save setting. The engine saves and destroys them when the player leaves; do not add a matching `destroy_inventory` call. Removing a registration from your game does not erase its saved data. Use the existing item movement and inventory drawing APIs with the returned inventory.

The default inventory keeps its existing `$AO.inventory` save key, encoded format, and fetch path. Named inventories use separate `$AO.inventory_<key>` save entries. Both travel through the existing player startup path; no default-inventory migration is needed.

For a saved inventory, capacity is the greater of the requested initial capacity and saved capacity. Use `Items.set_capacity` for subsequent changes. Register item definitions before players join.

After directly modifying an item's `@ao_serialize` fields, call `Items.mark_item_dirty(item)` to save the containing inventory. Normal inventory operations already mark changes dirty. CSL networking replicates the fields automatically.

### Inventory API reference

```go
Scene :: struct {
    get_player_inventory_capacity :: proc() -> int;
    set_player_inventory_capacity :: proc(capacity: int);
    get_auto_save_player_inventory :: proc() -> bool;
    set_auto_save_player_inventory :: proc(enabled: bool);
}

Item_Definition_Desc :: struct {
    id:         string;
    name:       string;
    icon:       Texture_Asset;
    stack_size: s64;    // 1 = not stackable, -1 = infinite
    tier:       Item_Tier;
}

Items :: struct {
    // Inventories
    create_inventory  :: proc(unique_id: string, capacity: s64) -> Inventory;
    create_player_inventory :: proc(player: Player, key: string, capacity: s64) -> Inventory;
    destroy_inventory :: proc(inventory: Inventory) -> bool;
    mark_item_dirty :: proc(item: Item_Instance);
    set_capacity      :: proc(inventory: Inventory, capacity: s64);

    // Item definitions + instances
    register_item_definition :: proc(desc: Item_Definition_Desc, $Definition_Type: typeid = Item_Definition, instance_type: typeid = Item_Instance) -> Definition_Type;
    create_item_instance     :: proc(definition: Item_Definition, count: s64 = 1) -> Item_Instance;
    create_item_instance     :: proc(definition: Item_Definition, $T: typeid, count: s64 = 1) -> T;
    destroy_item_instance    :: proc(instance: Item_Instance, count: s64 = -1);

    // Moving items around
    can_move_item_to_inventory                  :: proc(instance: Item_Instance, inventory: Inventory, will_destroy_item: ref bool) -> bool;
    move_item_to_inventory                      :: proc(instance: Item_Instance, inventory: Inventory);
    move_as_many_items_as_possible_to_inventory :: proc(instance: Item_Instance, inventory: Inventory, destroyed_item: ref bool) -> s64;
    remove_item_from_inventory                  :: proc(instance: Item_Instance, inventory: Inventory);
    can_move_all_items                          :: proc(entries: []Item_Transaction_Entry) -> bool;
    move_all_items                              :: proc(entries: []Item_Transaction_Entry);
    can_swap_items                              :: proc(inventory_a: Inventory, inventory_b: Inventory, slot_a: s64, slot_b: s64) -> bool;
    swap_items                                  :: proc(inventory_a: Inventory, inventory_b: Inventory, slot_a: s64, slot_b: s64);

    // Queries
    calculate_room_in_inventory_for_item :: proc(definition: Item_Definition, inventory: Inventory) -> s64;
    destroy_all_items :: proc(inventory: Inventory);

    // UI (optional)
    draw_inventory :: proc(rect: Rect, inventory: Inventory, options: Inventory_Draw_Options) -> bool;
    draw_hotbar    :: proc(player: Player, inventory: Inventory, options: Inventory_Draw_Options) -> Draw_Hotbar_Result;
}

Inventory :: class {
    slots :: method() -> Inventory_Slot_Iterator;
    get_item :: proc(inventory: Inventory, index: s64) -> Item_Instance;
    has_item_id :: proc(inventory: Inventory, item_id: string) -> bool;
    capacity: s64 #read_only;
}

Item_Definition :: class {
    get_icon :: method() -> Texture_Asset;
    id: string #read_only;
    name: string #read_only;
    type: typeid #read_only;
    instance_type: typeid #read_only;
    tier: Item_Tier #read_only;
}

Item_Instance :: class {
    quantity:   s64       #read_only;  // stack count
    slot_index: s64       #read_only;  // slot in the parent inventory
    inventory:  Inventory #read_only;  // parent inventory (null if not in one)
    get_definition :: method() -> Item_Definition;
}

Item_Transaction_Entry :: struct {
    instance: Item_Instance;
    inventory: Inventory;
}

Inventory_Draw_Options :: struct {
    title: string;
    show_exit_button: bool;
    show_scroll_bar: bool;
    show_background: bool;
    allow_drag_drop: bool;
    drag_drop_color_multiplier: v4;
    hotbar_item_count: int;
    columns: int;
    rows: int;
    force_select_hotbar_index: int;
    hide_bag_button: bool;
    enable_selection: bool;
    scroll_item_selection: bool;
    keyboard_item_selection: bool;
    enable_use_from_hotbar: bool;
    on_before_draw: (proc(item: Item_Instance, rect: Rect));
    on_after_draw: (proc(item: Item_Instance, rect: Rect));

    inventory_default :: proc() -> Inventory_Draw_Options; // hotbar_item_count = 0
    hotbar_default :: proc() -> Inventory_Draw_Options;    // hotbar_item_count = 6
}

Draw_Hotbar_Result :: struct {
    selected_item: Item_Instance;
    selected_item_index: s64;
    dropped_item: Item_Instance;
    entire_rect: Rect;
    inventory_open: bool;
    inventory_open_t: float;
}
```

### Quick start (register + give an item)

Register item definitions once in `ao_before_scene_load`, then create instances and move them into a player’s inventory.

```go
// Optional: custom item types
Weapon_Definition :: class : Item_Definition {
    damage: s64;
}

Weapon_Item :: class : Item_Instance {
    @ao_serialize durability: s64;
}

sword_defn: Weapon_Definition;

ao_before_scene_load :: proc() {
    sword_defn = Items.register_item_definition(
        {id="sword", name="Iron Sword", icon=get_asset(Texture_Asset, "icons/sword.png"), stack_size=1, tier=.COMMON},
        Weapon_Definition,
        Weapon_Item
    );
    sword_defn.damage = 10;
}

give_sword :: proc(player: Player) {
    item := Items.create_item_instance(sword_defn, Weapon_Item);

    will_destroy: bool;
    if Items.can_move_item_to_inventory(item, player.default_inventory, ref will_destroy) {
        item.durability = 100;
        Items.move_item_to_inventory(item, player.default_inventory);
        // If will_destroy is true, the item was absorbed into an existing stack
        // and this item reference should not be used after the move.
    }
    else {
        // Inventory full → destroy the instance we created
        Items.destroy_item_instance(item);
    }
}
```

Every custom `Item_Instance` field that must survive inventory save and restore needs `@ao_serialize`. A field without it returns to its normal default after restoration. For nested custom classes, annotate both the containing field and each nested field that must persist.

Persist a selection such as an equipped weapon using `Save.save_item_reference(player, key, item)` and `Save.get_item_reference(player, key)`. The item may belong to the player's auto-saved `default_inventory` or any named player inventory. References survive slot rearrangement within that inventory and rejoining. Removing, dropping, destroying, or transferring an item to any other inventory invalidates its old references, even for the same player or if the item later returns. A stack consumed by a merge also invalidates its references. Create named inventories before resolving saved selections. Inventory remains authoritative for ownership and item fields, so do not copy those values into Save.

`item.entry_id_in_inventory` exposes the current inventory's entry number for diagnostics. It is positive while the item is owned, remains unchanged when slots are rearranged, changes on transfers, and becomes `0` when detached. Save references include the inventory key along with this number. Use `Save.save_item_reference` rather than persisting the number directly.

### Displaying the hotbar

To show the standard inventory hotbar UI on screen, call `Items.draw_hotbar` inside your Player's `ao_late_update` method. Without it, the inventory system works behind the scenes but the player won't see it!

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            draw_player_hotbar(this);
        }
    }
}

draw_player_hotbar :: proc(player: Player) {
    options := Inventory_Draw_Options.hotbar_default();
    options.hide_bag_button = false;

    result := Items.draw_hotbar(player, player.default_inventory, options);

    // result.selected_item is the currently highlighted item (or null)
    // result.inventory_open tells you if the full bag UI is showing
}
```

{% hint style="info" %}
`Items.draw_hotbar` handles the entire hotbar + bag toggle UI for you. It returns a `Draw_Hotbar_Result` with `selected_item`, `selected_item_index`, `inventory_open`, and `dropped_item` fields.
{% endhint %}

{% hint style="warning" %}
`hide_bag_button = true` prevents the player from opening the backpack. If the backpack is already open, it will close, so only use it when players still have another way to access important items.
{% endhint %}

### Checking if a player already has an item

A common pattern is to give an item only if the player doesn't already have one:

Use the definition's registered ID:

```go
give_sword_if_needed :: proc(player: Player) {
    if player.default_inventory.has_item_id("sword") return;
    give_sword(player);
}
```

You can also check how much room is left for a specific item type before creating it:

```go
room := Items.calculate_room_in_inventory_for_item(sword_defn, player.default_inventory);
if room > 0 {
    give_sword(player);
}
```

### Custom Inventories

You can create inventories that aren’t tied to a player, for things like:

* Chests / storage containers
* Fish/mob teams

```go
chest_inventory := Items.create_inventory("chest_01", 12);
```

To access items:

```go
for item, slot: chest_inventory.slots() if item != null {
    defn := item.get_definition();
    log_info("Slot %: %", {slot, defn.name});
}
```

`slots()` visits every slot in order. `item` is `null` for an empty slot, and the optional second loop variable is the actual slot index. Use `get_item(index)` when you need random access to one slot.

### Atomic item moves

Use a transaction when several item moves must either all succeed or all fail:

```go
moves: [..]Item_Transaction_Entry;
moves.append({instance = sword, inventory = player.default_inventory});
moves.append({instance = shield, inventory = player.default_inventory});

if Items.can_move_all_items(moves) {
    Items.move_all_items(moves);
}
```

Each item instance may appear only once in the transaction. Always call `can_move_all_items` first; `move_all_items` asserts if the complete transaction no longer fits. A move can merge a stack and destroy the moved instance, so do not keep using item references after the transaction.

### Dropped Items

If you want players to drop items into the world (and let other players pick them up), use the dropped items system:

```go
import "core:dropped_items"
```

#### Spawning a dropped item

```go
item := Items.create_item_instance(sword_defn, Weapon_Item);
dropped := Dropped_Item.spawn(player.entity.world_position, item);
```

#### Drop animation + snapping to navmesh

```go
gameplay_rng: u64;

dropped.do_spawn_animation(ref gameplay_rng, navmesh); // navmesh is optional
```

#### Handling drag-drop from the hotbar UI

If you use `Items.draw_hotbar`, players can drag an item out of the UI to drop it. `Dropped_Item.handle_dropped_item` removes it from the inventory and spawns a dropped item entity.

```go
result := Items.draw_hotbar(player, player.default_inventory, Inventory_Draw_Options.hotbar_default());

dropped: Dropped_Item;
if Dropped_Item.handle_dropped_item(result, player.entity.world_position, ref dropped) {
    dropped.do_spawn_animation(ref gameplay_rng, navmesh);
}
```

{% hint style="info" %}
Dropped items automatically despawn over time (with a warning bar). Holding the interact button on an item resets its despawn timer.
{% endhint %}

{% hint style="info" %}
You can make a dropped item exclusive (only visible/pickable by one player) using `dropped.set_exclusive(player)`.
{% endhint %}


# Player List

The built-in player list shows the players in the current game.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-bae2f01209040ed5bc1e6740088e5c521e1421c5%2Fplayer-list.png?alt=media" alt="The built-in player list"><figcaption></figcaption></figure>

The built-in list does not expose custom rows or fields to CSL.

If your game needs levels, teams, or room names in a list, draw a separate UI from the local player's `ao_late_update`. Iterate players with `component_iterator(Player)` and guard the UI with `is_local_or_server()`.


# Social Features

All out provides text and optional voice chat for you to use in your games

### Reading Chat

Use `core_globals.server_on_chat_message_received` when game code needs to react to normal chat messages on the server:

```go
ao_before_scene_load :: proc() {
    core_globals.server_on_chat_message_received = on_chat_message;
}

on_chat_message :: proc(player: Player, message: string) {
    log_info("% said %", {player.get_username(), message});
}
```

### Chat API reference

```go
Chat :: struct {
    Mode :: enum {
        DEFAULT;
        BUBBLE_ONLY;
    }

    set_mode :: proc(mode: Mode);
    server_send_message :: proc(message: string, player: Player = null);
    is_open :: proc() -> bool;
}
```

`Chat.server_send_message` is for server-side code such as chat command handlers. Pass `player` to target one player, or leave it `null` to send broadly. Use `Chat.set_mode(.BUBBLE_ONLY)` on the local client for games that want bubble chat without the standard chat panel.

### Chat Commands

Chat commands are mostly a **developer/admin tool** for testing and live-ops:

* Start rounds early / skip waves
* Grant test item sets or currency
* Trigger game events for debugging

To create a command, write a `proc` and annotate it with `@chat_command`.

The procedure name is the command name: `start_round` is invoked as `/start_round`. Invocation ignores surrounding whitespace and prefers an exact-case name, then falls back to a case-insensitive match.

Players can type commands into chat with a leading `/`:

* `/start_round`
* `/grant_test_loadout`
* `/trigger_event meteor_shower`

{% hint style="info" %}
Chat commands run on the server. Use `Notifier.notify(player, "...")` to send feedback back to a single player.
{% endhint %}

{% hint style="warning" %}
Not all players will have text chat enabled (parental controls and moderation mutes can disable the text box), so you shouldn't rely on chat commands for critical gameplay systems.
{% endhint %}

#### Common dev/admin commands

```go
// Start a round early (admin-only)
start_round :: proc(player: Player) {
    g_round_manager.start_round();
    Notifier.notify(player, "Round started.");
} @chat_command

// Give yourself a test loadout (admin-only)
grant_test_loadout :: proc(player: Player) {
    // Example: use your own item-granting logic here
    // item := Items.create_item_instance(sword_defn);
    // Items.move_item_to_inventory(item, player.default_inventory);
    Notifier.notify(player, "Granted test loadout.");
} @chat_command

// Trigger an event by name (admin-only, with an optional argument)
trigger_event :: proc(player: Player, event_name: string = "meteor_shower") {
    g_event_system.trigger(event_name);
    Notifier.notify(player, "Triggered event: %", {event_name});
} @chat_command
```

#### Permissions

Use permission annotations to control who can run a command:

| Annotation         | Who can use                     |
| ------------------ | ------------------------------- |
| `@any`             | All players                     |
| `@vip`             | VIP players and admins          |
| `@youtuber`        | Youtuber players and admins     |
| `@owner`           | Game owner and admins           |
| `@owner_or_editor` | Game owner, editors, and admins |
| (none)             | Admins only                     |

```go
// VIPs and admins can use this
skip_wave :: proc(player: Player) {
    g_wave_manager.skip_to_next_wave();
    Notifier.notify(player, "Skipped wave.");
} @chat_command @vip
```

{% hint style="info" %}
When launching from the editor, chat commands are allowed for faster iteration/testing.
{% endhint %}

#### Arguments & optional parameters

The first parameter must always be `Player`. After that you can add arguments (and give them default values to make them optional).

By default, commands are **admin-only**.

Supported argument types are `string`, integer types, floating-point types, `bool`, and `Player`.

A `Player` argument uses the complete space-delimited or quoted token and matches the player's display name exactly. Names containing digits or underscores work directly; quote names containing spaces.

```go
// Admin only (default)
give_currency :: proc(player: Player, amount: s64 = 100) {
    if amount < 0 {
        Notifier.notify(player, "Amount must be zero or greater.");
        return;
    }

    Economy.deposit_currency(player, "Coins", amount);
    Notifier.notify(player, "Gave you % coins.", {amount});
} @chat_command
```

Admins can call:

* `/give_currency` → gives 100 coins
* `/give_currency 500` → gives 500 coins

#### Strings with spaces

Wrap strings in quotes if they contain spaces:

```go
say :: proc(player: Player, message: string) {
    // Your own broadcast function/game message here
    log_info("% says: %", {player.get_username(), message});
} @chat_command @any
```

Example:

```
/say "Hello everyone!"
```

#### Getting command usage

Players can append `?` to a command to see parameter info:

```
/spawn_enemy?
```

### Enabling Voice Chat

Players control voice chat through their account and device settings. Games cannot force it on or bypass microphone permission, parental controls, or moderation restrictions.

Voice is positional. The default audible range is 10 world units:

```go
Voice :: struct {
    set_range :: proc(range: float);
    get_range :: proc() -> float;
}
```

Set a positive range during scene startup:

```go
ao_start :: proc() {
    Voice.set_range(14);
}
```

`Voice.set_range` changes the scene-wide range. Non-positive values are ignored.

### Communication channels

Each player has speaking and listening masks that apply to both voice and text chat. Two players can communicate when the sender's speaking mask overlaps the receiver's listening mask.

```go
TEAM_RED  :: 1.(u64) << 0;
TEAM_BLUE :: 1.(u64) << 1;

put_on_red_team :: proc(player: Player) {
    player.comms_channel_speak_mask = TEAM_RED;
    player.comms_channel_listen_mask = TEAM_RED;
}
```

Players listen and speak on all channels by default.

### Moderation

All Out automatically monitors text and voice chat for behavior that violates our [community guidelines](https://help.allout.game/hc/en-us/articles/27854798873243-Code-Of-Conduct) and will disable social features for first offenses or apply suspensions for repeat offenders.

If you see repeat bad behavior or behavior that makes it past our automated detection, please report the player to us using the in-game report system or from their profile.

If a player has been suspended accidentally, please direct them to [contact us](https://help.allout.game/) to reverse the action.


# Navmesh & Collision

Navmeshes define walkable space for pathfinding and movement constraints.

Navmeshes define the **walkable areas** in your world. Use them to:

* Snap spawned items onto reachable ground
* Lock players/NPCs to walkable regions

Colliders can also be used to carve navmeshes in the editor.

### Components (quick overview)

* **`Navmesh`**: the baked navigation mesh you query and rebuild
* **`Navmesh_Loop`**: polygon loops that define walkable boundaries (and holes)
* **Colliders**: `Box_Collider`, `Circle_Collider`, `Edge_Collider`, `Polygon_Collider`

{% hint style="info" %}
For movement/pathfinding (`Movement_Agent`) see [Movement Agents/NPCs](/core-engine-concepts/movement-agents-npcs).
{% endhint %}

### Navmesh API reference

```go
Navmesh :: class : Component {
    // Project a point onto this navmesh.
    // triangle_hint is an acceleration hint you can reuse for nearby queries.
    try_find_closest_point_on_navmesh :: method(
        to_point: v2,
        result: ref v2,
        triangle_hint: ref s64
    ) -> bool;

    // Cast through the navmesh. Returns the endpoint and whether the query succeeded.
    try_raycast :: method(position: v2, direction: v2) -> (v2, bool);

    // Queue a rebuild for the normal navmesh update.
    mark_for_rebuild :: method();

    // Rebuild now when a same-frame query needs the new geometry.
    rebuild_immediately :: method() -> bool;

    // Include only colliders on this entity and its descendants.
    child_colliders_only: bool;

    // Detect input changes and rebuild automatically.
    enable_automatic_rebuilds: bool;
}
```

### Editor setup (building a navmesh)

1. Create an entity and add a `Navmesh` component.
2. Create child entities with `Navmesh_Loop` components to define walkable polygons.
3. For each `Navmesh_Loop`:
   * Add points to define the loop shape
   * Order the outer boundary points counter-clockwise
   * To reverse an existing loop, right-click the `Navmesh_Loop` component header and select **Flip Winding Order**
   * Toggle **Flip Inside Outside** to make a hole/obstacle instead of walkable space
4. Use the navmesh debug options in the inspector to visualize the triangles.

{% hint style="info" %}
By default, a `Navmesh_Loop` defines a walkable area. Flip it to "punch holes" (unwalkable islands) into an existing navmesh.
{% endhint %}

{% hint style="warning" %}
Loop winding determines which side of every edge is treated as the inside. Counter-clockwise is the normal winding for a walkable outer boundary. Clockwise winding reverses that result. If a loop was authored backwards, right-click its component header and select **Flip Winding Order**. Use **Flip Inside Outside** deliberately for holes instead of relying on an accidentally reversed point order.
{% endhint %}

### Colliders and navmesh loops

Colliders can also contribute loops via collider inspector options (for example "Make Navmesh Loop" / "Flip Navmesh Loop").

Important caveats:

* With **Enable Automatic Rebuilds**, navmeshes detect loop, collider, tilemap, and child-navmesh input changes and queue a rebuild.
* Turn on **Child Colliders Only** to ignore unrelated colliders elsewhere in the scene.
* With automatic rebuilds disabled, call `mark_for_rebuild()` after changing an input.
* Call `rebuild_immediately()` only before a same-frame query that requires the changed geometry.

### Spawning on navmesh (clamp to reachable ground)

Use `try_find_closest_point_on_navmesh` to project a desired position onto the closest valid point on a navmesh:

```go
spawn_on_navmesh :: proc(navmesh: Navmesh, desired_position: v2) -> Entity {
    spawn_pos: v2;
    triangle_hint: s64; // 0 = not set yet

    if navmesh.try_find_closest_point_on_navmesh(desired_position, ref spawn_pos, ref triangle_hint) {
        e := Scene.create_entity();
        e.set_local_position(spawn_pos);
        return e;
    }

    return null;
}
```

{% hint style="info" %}
Reuse `triangle_hint` for repeated queries in the same area (loot spawners, wave spawns, etc). It can significantly speed up projection.
{% endhint %}

### Rebuilding navmeshes immediately

Most geometry changes are picked up automatically. Use this only when you need to force timing:

* **`rebuild_immediately()`**: rebuilds now (use only if you must query the updated mesh in the same frame)

```go
ok := navmesh.rebuild_immediately();
if !ok {
    log_info("Navmesh rebuild failed", {});
}
```

{% hint style="info" %}
Parent navmeshes refresh after child navmeshes, so stitched parent meshes automatically pick up child mesh input changes.
{% endhint %}

### Raycasting across a navmesh

`try_raycast` follows `direction` to the navmesh boundary. The second result reports whether the query succeeded.

```go
endpoint, ok := navmesh.try_raycast(start, direction);
if ok {
    // endpoint is on the navmesh boundary.
}
```

### Collider triggers

A collider with `is_trigger == true` can report when another collider enters, remains inside, or exits. Trigger callbacks work on stationary and moving colliders; a `Movement_Agent` is not required.

{% hint style="warning" %}
**Use colliders sparingly.** Only add them to gameplay-critical surfaces: map boundaries, platforms the player must land on, key obstacles, and deliberate trigger volumes. Do **not** add colliders to decorative entities (trees, bushes, background props, etc.). Most entities in a scene should have no collider.
{% endhint %}

```go
Trigger_Listener :: class : Component {
    ao_start :: method() {
        trigger_collider := entity.get_component(Circle_Collider);
        trigger_collider.is_trigger = true;

        trigger_collider.on_trigger_start = proc(self: Collider, other: Collider) {
            log("OVERLAP START: %", {other.entity.get_name()});
        };

        trigger_collider.on_trigger_stay = proc(self: Collider, other: Collider) {
            log("OVERLAP STAY: %", {other.entity.get_name()});
        };

        trigger_collider.on_trigger_end = proc(self: Collider, other: Collider) {
            log("OVERLAP END: %", {other.entity.get_name()});
        };
    }
}
```

### Proximity checks and custom hit logic

For trigger zones that use range queries instead of collider callbacks, pickups, projectile checks, or other custom hit logic, use:

* `Scene.get_all_components_in_range` / `Scene.get_closest_component_in_range`
* Simple distance checks (`in_range`) to decide "inside", "picked up", "hit", etc.

#### Scene query helpers

```go
Scene :: struct {
    get_all_components_in_range :: proc(position: v2, range: float, results: ref [..]$T);
    get_closest_component_in_range :: proc(position: v2, range: float, $T: typeid) -> (T, bool);
}
```

{% hint style="info" %}
Run damage, pickups, and scoring in the normal shared gameplay path. Do not guard predicted gameplay with `Game.is_server()`.
{% endhint %}

#### Trigger volume (enter / stay / exit)

To simulate a trigger zone, keep a list of who was inside last frame and diff it against the current frame’s results.

```go
contains_id :: proc(list: []u64, id: u64) -> bool {
    for x: list if x == id return true;
    return false;
}

Trigger_Zone :: class : Component {
    @ao_serialize radius: float;
    last_inside: [..]u64;

    ao_update :: method(dt: float) {
        center := entity.world_position;

        players: [..]Player;
        Scene.get_all_components_in_range(center, radius, ref players);

        current_inside: [..]u64;

        for p: players {
            if in_range(p.entity.world_position, center, radius) {
                current_inside.append(p.entity.id);

                if !contains_id(last_inside, p.entity.id) {
                    // on_enter
                    Notifier.notify(p, "Entered zone!");
                }
                else {
                    // on_stay
                }
            }
        }

        for id: last_inside {
            if !contains_id(current_inside, id) {
                // on_exit (you may want your own id.player lookup)
            }
        }

        last_inside.clear();
        for id: current_inside {
            last_inside.append(id);
        }
    }
}
```

{% hint style="warning" %}
This manual trigger pattern is still useful when you do not want to add a collider trigger volume. If something is teleported/despawned between updates, you may not get a clean "exit" unless you handle cleanup.
{% endhint %}

#### Pickups (closest in range)

```go
try_pickup_near_player :: proc(player: Player) {
    pos := player.entity.world_position;

    pickup, ok := Scene.get_closest_component_in_range(pos, 1.5, Pickup);
    if ok && pickup != null {
        if in_range(pickup.entity.world_position, pos, 1.5) {
            pickup.claim(player); // your own logic (grant + destroy)
        }
    }
}
```

#### Fast movement "hits" (simple sub-stepping)

If you move quickly (dash, projectile), you can miss narrow targets when checking only the final position. A simple workaround is sub-stepping: sample a few points between last and new position and run the same range queries.

```go
hit_check_move :: proc(last_pos: v2, new_pos: v2) {
    steps := 4;
    for i := 1; i <= steps; i += 1 {
        t := (i.(float)) / (steps.(float));
        p := lerp(last_pos, new_pos, t);

        enemies: [..]Enemy;
        Scene.get_all_components_in_range(p, 1.0, ref enemies);
        for e: enemies {
            if in_range(e.entity.world_position, p, 1.0) {
                // apply hit once, start cooldown, etc.
            }
        }
    }
}
```


# Movement Agents/NPCs

Use Movement\_Agent to move entities with pathfinding and navmesh constraints.

### Movement Agents

`Movement_Agent` is a movement helper component used for steering and pathfinding.

Most commonly, you use it to:

* Request a path toward a target (`set_path_target`)
* Constrain an entity to walkable space (lock to a `Navmesh`)
* Move through simple collider geometry

{% hint style="info" %}
This page covers movement and pathfinding. For building/querying navmeshes and collider trigger details, see [Navmesh & Collision](/core-engine-concepts/navmesh-and-collision).
{% endhint %}

### Movement\_Agent API reference

```go
Movement_Agent :: class : Component {
    Set_Path_Target_Result :: struct {
        success:        bool;
        next_point:     v2;
        move_direction: v2;
    }

    // Request a path toward target at a given speed.
    set_path_target :: method(target: v2, speed: float) -> Set_Path_Target_Result;

    // Constrain this agent to a navmesh (snap to it every frame).
    // Pass null to clear.
    set_navmesh_to_lock_to :: method(navmesh: Navmesh);

    // Common tuning/state
    movement_speed:   float;
    agent_radius:     float;
    friction:         float;
    velocity:         v2;
    input_this_frame: v2; // write-only from scripts; zeroed by the movement update before callbacks run (read player.input_this_frame instead)
}
```

### CSL movement physics and triggers

Movement\_Agent entities have a CSL physics path for simple ballistic movement.

Collision: the agent's enabled non-trigger colliders block against enabled non-trigger world colliders, taking `category_bits` / `mask_bits` filtering into account. Movement stops at the first hit and slides along the hit surface.

Two colliders interact only when each one's `mask_bits` contains the other's `category_bits` (both u32 bitmasks). Colliders default to `category_bits: 1`, `mask_bits: 4294967295`. The player's collider is `category_bits: 2` with `mask_bits` = everything except bit 2 (players do not collide with each other). Leave the defaults alone unless you need layers: a wall with `mask_bits: 1` or `category_bits: 2` does not block the player.

Trigger callbacks belong to the collider and do not require a `Movement_Agent`.

### Pathfinding (set a target)

Call `set_path_target` every frame while the entity should keep moving. A single call is not a persistent movement command.

```go
follow_target :: proc(agent: Movement_Agent, target_pos: v2) {
    result := agent.set_path_target(target_pos, agent.movement_speed);

    if result.success {
        // result.next_point: next waypoint
        // result.move_direction: normalized move direction (useful for facing/anim)
    }
}
```

{% hint style="info" %}
`set_path_target` is processed asynchronously with other agents. The returned result is the latest completed pathfind data, so the first frame you set a new target may not return `success == true`.
{% endhint %}

### Lock movement to a navmesh (stay on walkable space)

If an entity should never leave walkable space, lock it to a navmesh:

```go
agent.set_navmesh_to_lock_to(navmesh);
```

Call it with `null` to clear.

### Creating Moving NPCs

This is a minimal “follow the player” NPC using a movement agent:

```go
Follower_NPC :: class : Component {
    @ao_serialize agent: Movement_Agent;
    @ao_serialize target: Entity;

    ao_update :: method(dt: float) {
        if !#alive(target) return;

        result := agent.set_path_target(target.world_position, agent.movement_speed);
        if result.success {
            // Optional: face direction / set animation flags
            // dir := result.move_direction;
        }
    }
}
```


# Camera & Post Processing

### Camera

Every player starts off with a camera that follows them as they move. To adjust zoom or move the camera to other places, we provide a set of APIs.

In CSL, the camera is a per-player struct on `Player`:

* **`camera.follow_player: bool`**: when `true`, the engine keeps the camera following the player.
* **`camera.position: v2`**: the camera center in world space (used when `follow_player = false`).
* **`camera.size: float`**: zoom amount. **Larger = more zoomed out** (you see more of the world).

Player component state is synchronized, so set camera fields in the player's `ao_late_update` behind `is_local_or_server()`:

```go
import "core:ao"

My_Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if is_local_or_server() {
            // Simple "set and forget" zoom
            camera.size = 7.0;
        }
    }
}
```

#### Custom follow (offset + smoothing)

If you want camera offsets, cutscenes, or custom smoothing, turn off `follow_player` and drive `camera.position` yourself:

```go
import "core:ao"

My_Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        camera.follow_player = false;

        // Follow slightly above the player
        target_pos := entity.world_position + v2{0, 0.5};

        // Smoothly move towards the target (dt-safe)
        t := clamp(dt * 10.0, 0.0, 1.0);
        camera.position = lerp(camera.position, target_pos, t);

        // Smooth zoom
        target_size := 6.0;
        camera.size = lerp(camera.size, target_size, t);
    }
}
```

The renderer interpolates `position` and `size` between simulation frames. Their read-only previous values are available as `position_last_frame` and `size_last_frame`.

The camera also provides `is_visible` overloads for a point, `Rect`, `Sprite_Renderer`, or `Spine_Animator`. The optional `apron` argument expands the visible area.

### Post Processing

Post Processing allows you to apply visual effects to your camera like distortion, color grading, and other effects to make your game pop!

You can configure post-processing in two ways:

* **Configure in the editor**: set up the default post-processing stack via **Edit → Game Config → Post Processing**. This applies when using the default camera behavior.
* **Configure at runtime via CSL**: use the `Post_Processing` API to dynamically apply effects from your player's `ao_late_update` method.

If you enable any effect, All Out will automatically use the HDR pipeline.

#### CSL Post Processing API

Call any of the following functions each frame to apply post-processing effects. When any `Post_Processing` function is called, it **overrides** the editor defaults for that frame — so you must call every effect you want active.

| Function                               | Config Struct                 | Fields                                                     |
| -------------------------------------- | ----------------------------- | ---------------------------------------------------------- |
| `Post_Processing.bloom`                | `Bloom_Config`                | `bloom_amount: float`                                      |
| `Post_Processing.color_grade`          | `Color_Grade_Config`          | `color_filter: v3`, `saturation: float`, `contrast: float` |
| `Post_Processing.chromatic_aberration` | `Chromatic_Aberration_Config` | `channel_offsets: v3`, `focal_point: v2`                   |
| `Post_Processing.blur`                 | `Blur_Config`                 | `directions: float`, `quality: float`, `size: float`       |
| `Post_Processing.film_grain`           | `Film_Grain_Config`           | `strength: float`, `noise_scale: float`                    |
| `Post_Processing.vignette`             | `Vignette_Config`             | `radius: float`, `softness: float`                         |

Keep these calls together so multiple components do not compete over the scene-wide configuration:

```go
import "core:ao"

My_Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        // Apply bloom + vignette every frame
        Post_Processing.bloom({bloom_amount = 0.04});
        Post_Processing.vignette({radius = 0.8, softness = 0.4});
    }
}
```

To return to the editor defaults, stop calling every `Post_Processing` function.


# Projectiles

CSL does not have a built-in projectile component. Use a prefab with a script for movement and either a trigger collider or a range query for hits.

## Trigger-based projectile

Create a prefab with:

* A sprite or `Spine_Animator`
* A `Circle_Collider` with **Is Trigger** enabled
* The component below

```go
Damageable :: class : Component {
    @ao_serialize health: float = 100;

    take_damage :: method(amount: float) {
        health -= amount;
    }
}

Projectile :: class : Component {
    direction: v2;
    owner: Entity;
    @ao_serialize speed: float = 12;
    @ao_serialize damage: float = 25;
    @ao_serialize lifetime: float = 3;
    spent: bool;

    ao_start :: method() {
        collider := entity.get_component(Circle_Collider);
        collider.is_trigger = true;
        collider.on_trigger_start = proc(self: Collider, other: Collider) {
            projectile := self.entity.get_component(Projectile);
            projectile.hit(other.entity);
        };

        entity.queue_for_destruction(lifetime);
    }

    ao_update :: method(dt: float) {
        entity.add_local_position(direction * speed * dt);
    }

    hit :: method(other: Entity) {
        if spent return;
        if other == owner return;

        target := other.get_component(Damageable);
        if target == null return;

        spent = true;
        target.take_damage(damage);
        entity.destroy();
    }
}
```

Spawn the prefab with `Scene.instantiate`, then initialize its component:

```go
spawn_projectile :: proc(
    prefab: Prefab_Asset,
    owner: Entity,
    position: v2,
    direction: v2
) {
    assert(length_squared(direction) > 0, "projectile direction must be nonzero");

    projectile_entity := Scene.instantiate(prefab, position);
    projectile := projectile_entity.get_component(Projectile);
    projectile.owner = owner;
    projectile.direction = normalize(direction);
}
```

Run projectile movement and damage in the normal shared gameplay path. Prediction runs the same code on clients and server, so do not guard it with `Game.is_server()`.

## Fast projectiles

Trigger overlap checks can miss a small target when a projectile crosses it between simulation frames. For fast projectiles, query several points along the movement segment with `Scene.get_all_components_in_range`, or use a larger collision radius. See [Navmesh and Collision](/core-engine-concepts/navmesh-and-collision#fast-movement-hits-simple-sub-stepping) for a sub-stepping example.

Call `SFX.play` from the same predicted hit path. The sound system reconciles the predicted sound with the server result.


# Purchasing/Product APIs

Players can buy products (game passes + consumables) for Sparks in your game. When a purchase happens, All Out calls your **purchase handler** so you can grant the item or feature.

{% hint style="info" %}
For creating products and viewing analytics, see [In Game Products](/monetization/in-game-products).
{% endhint %}

### Product types (quick overview)

* **Game passes**: one-time, persistent unlocks (admin pass, permanent ability, house ownership)
* **Consumables**: can be bought multiple times (potion, temporary buff, one-time coin pack)

Product types are configured in the creator portal. See [In Game Products](/monetization/in-game-products).

### Purchasing API reference

```go
Product :: struct {
    id: string;
    name: string;
    description: string;
    price: s64;
    consumable: bool;
    icon: Texture_Asset;
}

Purchasing :: struct {
    prompt_purchase :: proc(player: Player, id: string);
    owns_product    :: proc(player: Player, id: string) -> bool;
    get_product     :: proc(id: string) -> Product;
}
```

### Prompting a purchase

To start the purchase flow, call `Purchasing.prompt_purchase` with a product ID.

```go
BUY_DOUBLE_JUMP_ID :: "prod_double_jump";

try_prompt_double_jump :: proc(player: Player) {
    Purchasing.prompt_purchase(player, BUY_DOUBLE_JUMP_ID);
}
```

{% hint style="info" %}
The product ID is shown in the monetization/products page in the creator portal (you can click to copy it). See [In Game Products](/monetization/in-game-products).
{% endhint %}

### Checking if a player owns a product (game passes)

For game passes, you can gate gameplay features by checking ownership.

```go
ADMIN_PASS_ID :: "prod_admin";

is_admin :: proc(player: Player) -> bool {
    return Purchasing.owns_product(player, ADMIN_PASS_ID);
}
```

{% hint style="info" %}
Consumables can be bought multiple times, owns\_product shouldn't be used to award them. Consumables are usually granted in your purchase handler (coins, items, buffs, etc).
{% endhint %}

### Reading product info (name/price/icon)

If you want to show a “Buy” UI or log product info, fetch the product definition by ID:

```go
product := Purchasing.get_product("prod_double_jump");
log_info("Product % costs % sparks", {product.name, product.price});
```

### The purchase handler (granting)

When a player buys a product in your game, All Out calls:

```go
ao_purchase_handler :: proc(player: Player, id: string) -> bool
```

If your game does not define `ao_purchase_handler`, non-consumable products are granted automatically so they work with `Purchasing.owns_product`. Consumable products still require a handler and are marked as grant failures without one.

You should:

* Switch on the product ID
* Grant the item/feature
* Return `true` if the grant succeeded
* Return `false` if the grant failed (inventory full, missing prerequisite, etc)

```go
COIN_PACK_ID :: "prod_coin_pack_small";
DOUBLE_JUMP_ID :: "prod_double_jump";

ao_purchase_handler :: proc(player: Player, id: string) -> bool {
    switch id {
        case COIN_PACK_ID: {
            // Example: grant coins (consumable)
            Economy.deposit_currency(player, "Coins", 500);
            Notifier.notify(player, "Thanks! +500 coins");
            return true;
        }
        case DOUBLE_JUMP_ID: {
            // Example: game pass unlock (persistent)
            // You typically just enable it via owns_product() checks elsewhere.
            // Still return true so the purchase is considered granted.
            Notifier.notify(player, "Double Jump unlocked!");
            return true;
        }
    }

    // Unknown ID
    return false;
}
```

### Grant failures & retries

If your purchase handler returns `false`, the purchase is marked as a **grant failure**.

* Grant failures are shown in the creator portal
* They're automatically retried when the player joins until they succeed
* You can manually re-run a purchase handler by marking a purchase as “ungranted” in the player data UI

See:

* [In Game Products](/monetization/in-game-products) (“Purchase Handlers / Grant Failures”)
* [Editing/Viewing Player Data](/data-and-persistence/editing-viewing-player-data) (“Purchases” section)

### Testing purchases

In a game launched from the editor (Start Game or `run_tests`), `Purchasing.prompt_purchase` opens a test purchase prompt marked "Test purchase: no Sparks spent". It offers **Buy** whatever your Spark balance. Buy runs your `ao_purchase_handler` through the same server path as a real purchase, and a granted game pass then passes `Purchasing.owns_product`.

* The product must already exist for your game (see [In Game Products](/monetization/in-game-products)).
* The prompt says "Purchase Complete" even when your handler returns `false`, so check the grant itself.
* Test purchases leave no purchase record. After the player rejoins, `owns_product` is false again and a failed grant is not retried. Anything your handler saved itself, such as currency, stays.
* Calling `ao_purchase_handler` yourself, for example from a chat command, runs only your grant code. It skips the server path and never sets `owns_product`.

In a test, press the prompt's buttons by name:

```go
test_buy_coin_pack :: proc() {
    Test.click_button("coin_pack_button");    // your UI that calls Purchasing.prompt_purchase
    Test.click_button("purchase modal/Buy");  // waits up to ~3s for the product to load
    Test.wait_seconds(3);                     // the handler runs 1-2s after Buy
    Test.click_button("purchase modal/Okay"); // closes "Purchase Complete"
    Test.assert_balance("coin pack granted", "Coins", 500);
}
```

### Cross-game (hub + minigames)

If you use game parenting, purchases can be shared across your games. See [Cross-Game Products/Data](/data-and-persistence/cross-game-products-data).


# Analytics

Access your game's engagement and performance metrics from the Analytics tab of the Create portal on the site or app.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-130628187e0d92b600aec5f1d86e6848b2228150%2Fanalytics-dashboard.png?alt=media" alt="Analytics tab showing engagement and performance metrics"><figcaption></figcaption></figure>

Custom analytics events are not available in CSL. Contact developer support if you need a metric that the portal does not provide.


# Matchmaking/Hub Games

We provide APIs/settings to manage how we matchmake players to servers in your game, and allow you to route players to new servers to support Hub game scenarios.

### Matchmaking Preferences

**Fill Mode:**

All Out hosts servers across the globe to deliver a low latency experience to players, however, if your game is just getting started you may want to compromise on latency to ensure players can play together in the same server. The fill modes are documented on the main page of the Creator portal and can be changed at any time.

**Instance Priority + Require High Priority Mode**

There is a special matchmaking mode that allows you to avoid sending players to specific servers with code.

Games can set a "priority" level for each instance based on round status, player count, etc using `Server.set_matchmaking_priority(priority)`.

Optionally you can request to enable "Require High Priority" mode for your game, which will **only** route players to instances with priority set to zero and will spin up **new servers** if there aren't any.

This can be useful if you have a round-based game where you don't want new players joining into already running games.

{% hint style="info" %}
This feature is available by request only since if your instances never/rarely get set to priority number 0, it will spin up a massive number of servers! Please [contact us](/going-all-out/developer-support)!
{% endhint %}

### Hub Games

Hub games are a common pattern that allows players to meet up and queue for a round-based game together. Examples would include:

* A lobby for Red Sun that lets players queue to play together and buy upgrades while waiting
* The Bed Wars hub that allows you select between gamemodes (like classes/vs classic)

### API Reference

```go
Server :: struct {
    // Transfer a player to another game (hub -> match, match -> hub, etc).
    transfer_player_to_game :: proc(player: Player, game_id: string);

    // Set matchmaking priority for this instance (0 = highest priority).
    set_matchmaking_priority :: proc(priority: int);

    // Kick a player from the server.
    kick_player :: proc(player: Player, reason: string = "Kicked.");

    // Queue system for custom matchmaking. These APIs are currently deprecated but may return. Prefer the transfer_player_to_game API.
    queue_add_player           :: proc(queue: string, player: Player);
    queue_remove_player        :: proc(queue: string, player: Player);
    queue_set_server_available :: proc(queue: string, available: bool);

    // Server identity.
    get_id :: proc() -> string;
    get_private_server_host_id :: proc() -> (string, bool);
}
```

`transfer_player_to_game`, `set_matchmaking_priority`, and `kick_player` take effect on the server. Call them from the normal shared gameplay path; the matching client call has no effect.

The two ID queries only return useful data on the server. Do not branch shared predicted gameplay on their results.

### Example: send a player to another game

```go
GAME_ID_2V2 :: "your_game_id_here";

send_to_2v2_game :: proc(player: Player) {
    Server.transfer_player_to_game(player, GAME_ID_2V2);
}
```

{% hint style="info" %}
There is currently no way to pass additional information when transferring a player to a new game, nor is there a way to dynamically configure servers at startup. If you'd like to have different modes (e.g. 1v1 vs 2v2) you'll need to create dedicated games and link them with the game parenting system.
{% endhint %}

{% hint style="info" %}
We recommend enabling [Cross-Game Products/Data](/data-and-persistence/cross-game-products-data) when using teleportation/hub APIs to ensure player purchases and save data can be easily reused between the two.
{% endhint %}


# Player Generated Assets

Runtime asset generation is not available in CSL.

CSL cannot generate or import new assets at runtime.

Add textures, sounds, and Spine rigs to the project's `res` folder before publishing. See [Asset Catalog](/using-the-editor/asset-catalog) for editor and local-file workflows.

For runtime-painted pixels, use a [Dynamic Canvas](/core-engine-concepts/assets-and-resources#dynamic-canvases). A dynamic canvas is scene state, not a new persistent asset.


# UI Fundamentals

Adding UI to your game requires writing code, but with the help of AI it's substantially easier to make sophisticated interactive UIs

***

Adding UI to your game requires writing code, but once you learn a few patterns you can build clean, scalable interfaces quickly. This guide walks you through a step-by-step tutorial that ends with a complete dialog example.

{% hint style="info" %}
All UI for a player must be drawn inside that player's `ao_late_update` call stack and wrapped with `is_local_or_server()`. This prevents UI from appearing for the wrong client. Do not draw player UI from global or non-player component callbacks.
{% endhint %}

At any time, you can jump to [UI Reference](/ui/ui-reference) for more patterns and deeper details.

### Step 1: Create a UI Entry Point

All interactive UI should be drawn from the local player's `ao_late_update`:

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            draw_ui(this);
        }
    }
}
```

### Step 2: Start From a Screen Rect

Screen UI should always be derived from `UI.get_safe_screen_rect()` or `UI.get_screen_rect()`. Remember: (0, 0) is the bottom-left and Y grows upward.

Screen-space sizes passed to the regular rect helpers are **points** on a 1080-point-tall reference canvas. The engine scales those points to the device's actual pixels, so the same layout keeps a consistent relative size across resolutions.

```go
draw_ui :: proc(player: Player) {
    screen := UI.get_safe_screen_rect();

    // A simple centered panel
    panel := screen.center_rect().grow(120, 200, 120, 200);
    UI.quad(panel, core_globals.white_sprite, {0, 0, 0, 0.7});
}
```

### Step 3: Use Cut For Layout

When laying out multiple elements, cut space out of a rect instead of positioning everything from the same origin.

```go
draw_panel_layout :: proc() {
    panel := UI.get_safe_screen_rect().center_rect().grow(140, 220, 140, 220);

    header := panel.cut_top(80);
    footer := panel.cut_bottom(70);
    body := panel;

    UI.quad(header, core_globals.white_sprite, {0.1, 0.1, 0.1, 0.8});
    UI.quad(body, core_globals.white_sprite, {0.05, 0.05, 0.05, 0.8});
    UI.quad(footer, core_globals.white_sprite, {0.1, 0.1, 0.1, 0.8});
}
```

### Step 4: Add Text

Use text settings to control size, color, and alignment.

```go
draw_header_text :: proc(rect: Rect) {
    ts := UI.default_text_settings();
    ts.size = 52;
    ts.color = {1, 1, 1, 1};
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    UI.text(rect, ts, "New Quest!");
}
```

### Step 5: Add Buttons

Buttons combine layout and interaction. Use a button sprite from the engine supplied assets (see [UI Reference](/ui/ui-reference) for more).

```go
draw_button :: proc(rect: Rect) {
    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    bs.sprite = get_asset(Texture_Asset, "$AO/new/modal/buttons_2/button_2.png");
    bs.press_scaling = 0.35;

    if UI.button(rect, bs, ts, "Accept").clicked {
        log_info("Accepted!");
    }
}
```

### Step 6: Build a Dialog

Now combine all of the steps into a reusable dialog layout.

```go
draw_dialog :: proc(title: string, body: string) {
    screen := UI.get_safe_screen_rect();
    dialog := screen.center_rect().grow(200, 300, 200, 300);

    UI.quad(dialog, core_globals.white_sprite, {0, 0, 0, 0.85});

    content := dialog.inset(30);
    header := content.cut_top(80);
    buttons := content.cut_bottom(90);
    body_rect := content;

    ts := UI.default_text_settings();
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    ts.size = 52;
    UI.text(header, ts, title);

    ts.size = 36;
    UI.text(body_rect, ts, body);

    bs := UI.default_button_settings();
    bs.sprite = get_asset(Texture_Asset, "$AO/new/modal/buttons_2/button_2.png");
    bs.press_scaling = 0.35;

    left := buttons.cut_left(210);
    buttons.cut_left(20);
    right := buttons.cut_left(210);

    if UI.button(left, bs, ts, "Cancel").clicked {
        log_info("Cancelled");
    }
    if UI.button(right, bs, ts, "Confirm").clicked {
        log_info("Confirmed");
    }
}
```


# UIDoc Quick Start

Create browser-like game UI with UIDoc HTML and CSS assets, CSL bindings, lists, and events.

***

UIDoc lets you describe game UI with HTML and CSS, then supply its changing data and handle its events in CSL. It is useful for menus, inventories, skill trees, and other structured screen-space interfaces.

UIDoc is browser-like, but it is not an embedded web browser. Use the supported subset described in [UIDoc HTML and CSS Support](/ui/uidoc-html-css-support).

## Create a UIDoc asset

Create a folder under your game's `res` directory. The folder name must end in `.uidoc` and contain `index.html` and `index.css`:

```
res/
└── UI/
    └── settings.uidoc/
        ├── index.html
        └── index.css
```

Asset paths used by CSL are relative to `res`, so this document is loaded as `UI/settings.uidoc`.

## Edit visually and generate CSL

Select a UIDoc asset in the All Out editor to open its visual authoring view. You can select and drag elements on the artboard, resize them, reorder or reparent them in Layers, and edit common HTML and CSS properties in the inspector. The preview is rendered by the UIDoc runtime itself, so it uses the same supported layout and rendering behavior as the game.

The Interface inspector infers dynamic fields, repeated lists, inputs, and actions from the document markup. Saving an asset such as `ui/shop.uidoc` also maintains `scripts/generated/uidoc/ui/shop.csl`. Its typed wrapper supplies `default_data`, `decode_event`, input readers, and `draw`, so game code does not need to repeat the low-level bindings shown later in this guide:

```csl
data := UiShop_UIDoc.default_data();
data.title = "Store";
UiShop_UIDoc.draw(data, player, shop_event);
```

For a modal, use `UiShop_UIDoc.draw(data, player, shop_event, consume=true)` to block pointer input to gameplay widgets underneath it. The optional trailing `consume` parameter defaults to `false`. Save existing UIDoc assets in the editor to regenerate wrappers with this parameter.

The generated code uses the same public `UI.uidoc_*` calls documented below; it does not add a separate runtime system or change the UIDoc runtime API.

When using the automation tools, `uidoc_create_asset` returns its template version and the exact generated `index.html`/`index.css` contents. Use `uidoc_diagnostics` for authoring and viewport checks in the editor realm. Use `uidoc_runtime_inspect` for what is drawn right now: laid-out nodes with resolved text and screen rects, click handlers and row keys, the submitted bindings, and runtime issues; it defaults to the active client while Start Game is running, otherwise requires an explicit `scope: "editor"`. Every runtime response states its realm.

## Write the document

In `index.html`:

```html
<div class="screen">
  <div class="panel">
    <span class="title">Settings</span>
    <span class="message">{{message}}</span>
    <button class="close" data-on-click="event:close">Close</button>
  </div>
</div>
```

`{{message}}` is a text binding. `data-on-click` sends a `UIDoc_Event` to CSL.

In `index.css`:

```css
.screen {
  position: fixed;
  inset: env(safe-area-inset-top) env(safe-area-inset-right)
         env(safe-area-inset-bottom) env(safe-area-inset-left);
  display: flex;
  align-items: center;
  justify-content: center;
}

.panel {
  display: flex;
  flex-direction: column;
  width: 420px;
  padding: 24px;
  gap: 16px;
  color: white;
  background: #172033;
  border: 2px solid #52627d;
  border-radius: 12px;
  box-shadow: 0 12px 28px 0 #00000066;
}

.title {
  font-size: 32px;
  text-align: center;
}

.close {
  height: 48px;
  background: #3559a8;
  border-radius: 8px;
}

.close:hover {
  background: #4770ca;
}

.close:pressed {
  background: #29447f;
}
```

Safe-area insets keep full-screen UI clear of device cutouts and the game top-bar area.

## Bind and draw it from CSL

Draw player UI from that player's `ao_late_update` call stack. Clear and rebuild the UIDoc bindings before drawing the document each frame:

```go
settings_uidoc_event :: proc(event: UIDoc_Event, userdata: Object) {
    player := userdata.(Player);
    if player == null return;

    if event.handler == "event:close" {
        player.settings_open = false;
    }
}

draw_settings :: proc(player: Player) {
    UI.uidoc_clear_bindings();
    UI.uidoc_bind_text("message", "Changes are saved automatically.");

    document := get_asset(UIDoc_Asset, "UI/settings.uidoc");
    if document == null return;

    UI.uidoc(document, false, player, settings_uidoc_event);
}

Player :: class : Player_Base {
    settings_open: bool;
    pet_name: string;

    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() && this.settings_open {
            draw_settings(this);
        }
    }
}
```

The callback receives the exact handler string from `data-on-click`. A non-repeated control can supply a static `data-key` for `event.key`; repeated controls use their resolved `data-for-key` or list-item key.

`userdata` is passed only to that callback; it does not identify the document. Each UIDoc asset may be drawn once per simulation submission. When a document stops being drawn, its current activation closes; drawing it again automatically assigns a new activation generation so stale replicated layouts cannot attach to the reopened document. At most four different UIDoc assets may be active at once.

Available top-level bindings are:

```go
bind_player_fields :: proc(player: Player) {
    UI.uidoc_bind_bool("visible", true);
    UI.uidoc_bind_text("name", player.get_username());
    UI.uidoc_bind_float("cameraSize", player.camera.size);
}
```

## Conditions, paint bindings, and lists

Use `data-if` to include a node only while a top-level boolean binding is true. An optional leading `!` inverts it. `data-if` does not resolve list-local expressions such as `item.visible`; repeated visual state should use a list-local paint binding instead, while repeated structural or interactive visibility should be decided when building the CSL list. For other dynamic visual state, keep classes static and bind a supported color or opacity:

```html
<div
  class="notice"
  data-if="showNotice"
  data-style-color="noticeColor">
  {{noticeText}}
</div>
```

Bind `noticeColor` with `UI.uidoc_bind_text` using a supported CSS color string. Class attributes are compiled as static class tokens and do not support `{{...}}` interpolation.

Use `data-for` for repeated data:

```html
<div class="inventory">
  <button
    class="item"
    data-for="item in items"
    data-for-key="item.id"
    data-key="inventory-item"
    data-style-color="item.rarityColor"
    data-on-click="event:item">
    {{item.name}}
  </button>
</div>
```

Build that list in CSL before calling `UI.uidoc`:

```go
Inventory_Row :: struct {
    id: string;
    name: string;
    rarity_color: string;
}

bind_inventory_rows :: proc(items: []Inventory_Row) {
    UI.uidoc_begin_list("items");
    for item: items {
        UI.uidoc_list_item(item.id);
        UI.uidoc_list_bind_text("id", item.id);
        UI.uidoc_list_bind_text("name", item.name);
        UI.uidoc_list_bind_text("rarityColor", item.rarity_color);
    }
    UI.uidoc_end_list();
}
```

Use a stable, unique direct expression such as `data-for-key="item.id"` for each repeated item. It is returned as `event.key`. Give `UI.uidoc_list_item(...)` the same stable value so interaction, input, and scroll identity survive list changes. A static `data-key` names the control's role; UIDoc combines that role with each enclosing list item's bound key and current index for its registered runtime identity.

For the example above, a live test name contains a suffix such as `inventory-item#potion-42:7/__widget`. `client_ui_tree` shortens it to a unique selector without the optional `/__widget`, such as `inventory-item#potion-42:7`; pass that result directly to `Test.click_button`. A role-only lookup such as `Test.click_button("inventory-item")` is ambiguous when several rows are visible. `data-on-click` and visible text are not test selectors. Nested lists append one `#key:index` pair per loop.

## Inputs

Bind an input with `data-bind-value`:

```html
<input
  id="pet-name"
  class="name-input"
  placeholder="Pet name"
  data-bind-value="petName"
  data-on-click="event:name-input">
```

Read its current value after the document has been drawn:

```go
draw_pet_name_input :: proc(player: Player, document: UIDoc_Asset) {
    UI.uidoc_bind_text("petName", player.pet_name);
    UI.uidoc(document, false, player, settings_uidoc_event);
    player.pet_name = UI.uidoc_text_value(document, "pet-name", player.pet_name);
}
```

`UI.uidoc_text_value` looks up the input by its `id` or `data-key`, then returns the current value of its `data-bind-value` binding.

## Scrolling and zooming

Scrolling is enabled with CSS overflow. Both axes can be enabled on the same viewport:

```html
<div class="viewport" data-scroll-zoom="zoom">
  <div class="canvas">
    <button
      class="node"
      data-for="node in nodes"
      data-for-key="node.id"
      data-key="canvas-node"
      data-style-transform-x="node.x"
      data-style-transform-y="node.y"
      data-on-click="event:node">
      {{node.name}}
    </button>
  </div>
</div>
```

```css
.viewport {
  width: 100%;
  height: 100%;
  overflow-x: auto;
  overflow-y: auto;
}

.canvas {
  position: relative;
  width: 1600px;
  height: 1000px;
}

.node {
  position: absolute;
  width: 160px;
  height: 64px;
}
```

Bind `zoom` with `UI.uidoc_bind_float`. `data-scroll-zoom` scales the content's positions, sizes, text, images, hit regions, and scroll extents around the viewport center. Keep fixed zoom controls outside the zoomed viewport.

{% hint style="warning" %}
Binding-expression attributes use a direct expression, such as `data-for-key="node.id"`, `data-style-transform-x="node.x"`, or `data-scroll-zoom="zoom"`. Do not put those expressions inside `{{...}}`. Mustache interpolation is for text and image `src`; classes and `data-key` values are static.
{% endhint %}

For a normal scroll panel, omit `data-scroll-zoom`. Dragging pans every enabled axis; the wheel scrolls vertically, or horizontally when only horizontal overflow is enabled.

## Common mistakes

* Load the document using its path relative to `res`, including the `.uidoc` suffix.
* Draw it from local-player UI code under `is_local_or_server()`.
* Call `UI.uidoc_clear_bindings()` and provide the current bindings before each draw.
* Draw each UIDoc asset once per update. Use `userdata` only as callback data.
* Use `data-if` only with top-level booleans. Bind list-local visual state through `data-style-opacity`/`data-style-color`, or omit structural/interactive rows from the bound list.
* Give repeated nodes a stable direct `data-for-key` expression; keep `data-key` static when naming a control role.
* Treat CSS support as property/value specific. `auto` and `none` are accepted only where the generated reference lists them, side borders such as `border-bottom` are unsupported, and `calc(...)` is limited to documented length fields with simple addition/subtraction.
* Give scroll content a real size. Transforms alone should not be used as its only layout size.
* Use UIDoc's own responsive layout and zoom behavior instead of applying a second manual UI scale in CSL.


# UIDoc HTML and CSS Support

The HTML, CSS, responsive layout, interaction, scrolling, and binding features supported by UIDoc, plus important browser features it omits.

***

UIDoc intentionally implements a practical browser-like subset for game UI. It compiles assets ahead of time and renders them through the engine; it does not run a browser, JavaScript, or a live DOM.

For a working first screen, start with [UIDoc Quick Start](/ui/uidoc-quick-start).

The generated repository reference, `docs/uidoc_supported_css.md`, is the exhaustive property/value list. It is emitted by `UIDoc::debug_supported_css_reference()` and checked by the UIDoc tests. This page explains that contract at an authoring level.

Unknown declarations, rejected values, unsupported selectors, malformed HTML, unsupported media queries, and unsupported binding attributes are compile errors. The parser may continue to collect diagnostics, but the asset is invalid until the errors are fixed. Unsupported CSS block at-rules are silently skipped; do not rely on browser at-rule behavior.

For runtime debugging, `uidoc_runtime_inspect` reports each drawn document as a tree of laid-out nodes with resolved text, screen `rect`, click handler and key, per-row keys for lists, the submitted bindings, and issues the runtime can prove (an unbound `data-if`, a list that was never submitted); branches hidden by `data-if` or `display:none` collapse to one entry. While Start Game is running, its default scope is the active client; without a running client, pass `scope: "editor"` explicitly to inspect the editor runtime. Every runtime response states its `realm`, so an editor-realm result does not describe the client. Its `rect`, `client_ui_tree`, and `client_input` `click_at` all use bottom-left-origin engine viewport coordinates; screenshot pixels use a top-left origin.

The `compile` tool preflights every current `.uidoc` source directory under `res` before compiling scripts or allowing Start Game to launch. A failed preflight reports `phase`, `file`, `line`, and `column`, plus the full structured diagnostics. `uidoc_diagnostics` reports authoring results from current project files in the editor realm; node-specific warnings include the `index.html` `line` and an `element` selector. Use the separate `uidoc_runtime_inspect` tool for live DOM details. With `viewportMatrix: true`, each asset reports `viewport_matrix.entries` (profile/scale cases); `failing`, `warning`, and grouped `issues` appear only when something was found. It audits declarations only, not clipping, overlap, or off-screen bounds. Assets without diagnostics are listed together under `clean`. Only errors fail a profile; warnings remain advisory. Verbose output puts error profiles in `failing_entries` and warning-only profiles in `warning_entries`. Compact issue groups report only `profiles_affected`; set `viewportMatrixVerbose: true` when the named viewport/scale entries are useful. Distinct source nodes are not coalesced even when they share a selector or CSS source line, so a shared-rule problem may still appear as several groups; fix the rule once and rerun diagnostics.

Two common authoring warnings need context:

* `dynamic_text_intrinsic_layout` means frequently changing text can change an intrinsically sized box and invalidate cached layout. Add `data-text-reserve` with the widest expected, localization-aware sample, or give the element a definite width and height. It is not a duplicate-text or rendering error.
* `image_aspect_drift` means an image has independent width and height with `object-fit: fill`. Use `contain`, `cover`, or `aspect-ratio` for ordinary art. Linear `image-fill-direction` fills are exempt.

## HTML and UIDoc attributes

Element names are accepted as generic layout nodes. These elements have specialized runtime behavior:

| Element  | Behavior                                   |
| -------- | ------------------------------------------ |
| `span`   | Inline text content                        |
| `img`    | Engine asset image loaded from `src`       |
| `button` | Pointer interaction and click events       |
| `input`  | Editable text bound with `data-bind-value` |

Non-empty text content creates text nodes. Use `{{name}}` for a top-level text binding and `{{item.name}}` inside a repeated list. Image `src` values also support this interpolation. Class names and `data-key` values are static.

| Attribute                          | Purpose                                                                                                   |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `id`, `class`, `style`             | CSS matching and inline style                                                                             |
| `src`, `width`, `height`           | Image source and dimensions                                                                               |
| `value`, `placeholder`, `disabled` | Input and control state                                                                                   |
| `data-if="binding"`                | Include a subtree while a top-level boolean binding is true; optional `!` inverts it                      |
| `data-for="item in items"`         | Repeat a subtree for a CSL-bound list                                                                     |
| `data-for-key="item.id"`           | Resolve the repeated click's `event.key` from a direct expression                                         |
| `data-key="role"`                  | Give a node a static interaction/input role                                                               |
| `data-bind-value="name"`           | Connect an input to a UIDoc text value                                                                    |
| `data-text-reserve="00:00"`        | Measure a stable literal sample while painting live bound text                                            |
| `data-on-click="event:name"`       | Send a click event to the CSL callback; `event.handler` receives the exact full value, including `event:` |
| `data-style-transform-x/y`         | Bind numeric translation without relayout                                                                 |
| `data-style-transform-scale`       | Bind numeric visual scale                                                                                 |
| `data-style-opacity`               | Bind numeric opacity                                                                                      |
| `data-style-color`                 | Bind a supported CSS color string without changing structure                                              |
| `data-style-image-fill-amount`     | Bind an image fill amount                                                                                 |
| `data-scroll-zoom`                 | Bind complete content zoom on a scroll viewport                                                           |

Attribute names are case-sensitive. Any other name under `data-style-`, `data-on-`, `data-bind-`, `data-for-`, `data-scroll-`, or `data-text-`, and a case variant of a supported name such as `data-style-Color`, is an `unknown_binding_attribute` compile error. Other `data-*` attributes produce a warning: UIDoc runs no scripts and has no attribute selectors, so they have no effect.

There is no `data-style-background-color`. For a state-dependent background, give a background-layer child `background-color: currentColor` and bind that child's `data-style-color`. On the element itself, `data-style-color` would also recolor its text.

Binding-expression attributes contain direct expressions, for example `data-for-key="item.id"`, `data-style-opacity="item.opacity"`, and `data-scroll-zoom="zoom"`. Do not put those expressions inside `{{...}}`. Mustache interpolation is limited to text and image `src` values.

`data-if` is narrower than the other list-aware binding attributes: it looks up a top-level boolean and does not resolve a repeated alias such as `item.visible`. For non-interactive visual state in a repeated subtree, bind `item.opacity` or `item.color`. Opacity does not disable hit testing, so filter structural or interactive rows while building the CSL list rather than leaving an invisible button in place.

### Repeated control identity and tests

`data-for-key` determines the repeated click's `event.key`. The runtime control identity is separate: it starts with static `data-key` (then HTML `id`, then an anonymous tag fallback) and appends `#key:index` for every enclosing loop. Here `key` is the value supplied to `UI.uidoc_list_item(...)`; normally it should be the same stable ID resolved by `data-for-key`. A repeated button can therefore appear in `client_ui_tree` as:

```
UI/board.uidoc:seed-cell#seed-42:7/__widget
```

`client_ui_tree` returns the shortest unique selector accepted by `Test.click_button`, without the optional `/__widget`, so a result such as `seed-cell#seed-42:7` can be passed through directly. A role-only query is ambiguous and fails when several instances are visible. Nested lists append multiple `#key:index` pairs. Handler strings and visible labels are not identity selectors.

Raw `style` and `script` blocks in the HTML are compile errors. Put authored CSS in `index.css`; there is no JavaScript runtime.

## CSS selectors and cascade

Supported selectors include:

* Element names, `.class`, `#id`, and `*`.
* Descendant and direct-child (`>`) combinators.
* Comma-separated selector lists.
* `:hover`, `:focus`, `:active`, `:pressed`, `:mouse-down`, `:dragging`, `:scroll-state`, and `:disabled`.

UIDoc uses normal id/class/type specificity and source order as the tie-breaker. Inline `style` is applied last. Paint order uses `z-index`, then document order.

Layout-affecting declarations inside interaction pseudo-state rules are compile errors. Hovering a button can safely change its color or opacity, but should not change its size or surrounding layout.

Compound selectors such as `.row.upper`, `#panel.open`, and `button.primary` are unsupported; add a dedicated class instead. Unsupported selectors produce compile errors, including sibling combinators, attribute selectors, pseudo-elements, `:not()`, `:nth-*`, `:is()`, `:where()`, and `:has()`.

## CSS layout

UIDoc supports `display: block`, `display: flex`, and `display: none`. Other display values are diagnosed and fall back to block. Its layout properties are:

* `width`, `height`, `min-*`, `max-*`, `aspect-ratio`, and `box-sizing`.
* `margin`, `padding`, `gap`, `row-gap`, and `column-gap`.
* `flex`, `flex-direction`, `flex-wrap`, `flex-grow`, `flex-shrink`, and `flex-basis`.
* `align-items`, `align-self`, `align-content`, and `justify-content`.
* `position: static`, `relative`, `absolute`, or `fixed`, with `inset`, `top`, `right`, `bottom`, and `left`.
* `overflow`, `overflow-x`, and `overflow-y` using `visible`, `hidden`, `auto`, or `scroll`.

Lengths support the relevant combinations of unitless pixels, `px`, `rem`, `em`, `%`, `vw`, `vh`, safe-area `env(...)`, and additive or subtractive `calc(...)` with up to eight terms. Percent padding and margin are diagnosed and ignored. Percent and viewport units are also invalid for gaps, but they are supported by sizes, flex basis, and the individual `top`/`right`/`bottom`/`left` offsets.

`padding`, `margin`, and `inset` accept the standard one-, two-, three-, and four-value forms. Percent values are unsupported in the `inset` shorthand; use the individual offset properties when percentages are required.

CSS keywords and math are checked per property, not accepted globally because a browser would accept them somewhere. In particular:

* `auto` is valid only for the property rows that list it, such as sizes, flex basis, offsets, `align-self`, and overflow.
* `none` is valid only where listed, such as `display`, supported paint/filter resets, `pointer-events`, and `image-fill-direction`.
* `calc(...)` is available only on documented length fields. It permits addition and subtraction of at most eight simple length terms. Multiplication, division, nested math, `min()`, `max()`, and `clamp()` are unsupported, and percent terms require a definite containing block.

When in doubt, check the exact property row in `docs/uidoc_supported_css.md`; another property's accepted values are not evidence that the same token works here.

## Text, images, and paint

Supported presentation features include:

* `color`, `background`, `background-color`, and one linear, radial, or conic `background-image` gradient.
* `border`, `border-width`, `border-color`, and a single uniform `border-radius` value. Side-specific borders such as `border-bottom`, per-corner radii, and percentage radii are unsupported. Use an explicit child divider with a fixed height/width and background color when only one edge is needed.
* Up to eight comma-separated `box-shadow` or `text-shadow` layers; box shadows may be inset.
* One `filter: blur(...)` or `filter: drop-shadow(X Y [blur] [color])`, plus `backdrop-filter: blur(...)`.
* `opacity`, `z-index`, and `pointer-events`.
* `font-family`, `font-size`, `font-style`, `font-weight`, `line-height`, `letter-spacing`, `text-align`, `white-space`, `overflow-wrap`, and `word-break`.
* Text `outline-color` and `outline-width`.
* `object-fit: cover`, `contain`, or `fill` for images.
* `image-tint`, `image-grayscale`, `image-fill-amount`, and `image-fill-direction`.
* `translate`, `scale`, and the `translate(...)`, `translateX(...)`, `translateY(...)`, and `scale(...)` transform subset. Translate percentages resolve against the element's own border box; viewport translate units remain unsupported.

Colors support common CSS forms including hex, `rgb()`/`rgba()`, `hsl()`/`hsla()`, named colors, `transparent`, and `currentColor` where applicable.

Gradients support up to eight color stops, CSS directions/angles and positions, `currentColor`, and `in oklab` or `in srgb`; Oklab is the default. Multiple background layers and CSS image URLs are not supported. Use an `img` with an engine asset path for bitmap backgrounds.

`box-shadow` follows the element's rectangular border box. `filter: drop-shadow(X Y [blur] [color])` follows the filtered element's painted alpha, including transparent image corners. Its omitted color defaults to the element's `currentColor`. Put it on the actual `img` for an irregular or nine-sliced bitmap; putting it on a container also includes the container's painted descendants in the shadow silhouette.

### Progress image fills

For a continuous progress bar, place a full-size `img` inside a fixed-size track, bind a normalized `0..1` value with `data-style-image-fill-amount`, and set `image-fill-direction: right` plus `object-fit: fill`. Use a rectangular or gradient texture with no unintended transparent gaps across the bar interior.

Do not reuse an icon or other decorative transparent image for the fill. `image-tint` is multiplicative and preserves the source alpha; it does not turn transparent pixels opaque. `object-fit: cover` also crops square art aggressively when the track is thin. Together those choices can produce empty leading space or diagonal wedges even when UIDoc's fill clipping is working correctly.

Diagnostics do not currently inspect texture alpha or flag `object-fit: cover` on progress fills. Verify progress bars visually at `0`, `0.25`, `0.5`, and `1`.

UIDoc defaults to the built-in AllIn family, with real 400, 700, and 900 faces. `@font-face` can declare project TTF or OTF Font assets by asset ID. Style and weight select the nearest declared face; missing bold or slant may be synthesized. Fonts load asynchronously, so UIDoc uses the next family or AllIn until a requested face is ready and then invalidates text layout.

Omit `line-height` for the default line height that scales with font size. Explicit `line-height: normal` is not accepted. Unitless values are multipliers; length values are fixed inherited lengths.

`letter-spacing` is inherited and accepts `normal`, unitless pixels, `px`, `rem`, or `em`. `normal` resolves to zero.

The transform subset does not include rotation, skew, 3D transforms, or multiple-token `scale` values. A filter value contains one `blur(...)` or one `drop-shadow(...)`; filter chains and other filter functions are unsupported. Filter and backdrop-filter compositor scopes can nest up to four levels.

## Responsive styles and safe areas

Media queries are supported, but only for this explicit condition allowlist:

* `(min-width: N)` and `(max-width: N)`.
* Combined minimum and maximum width queries.
* Tailwind-style `(width >= N)` and `(width <= N)`.
* `(hover: hover)`.

Width values accept unitless pixels, `px`, and `rem`. Unsupported media blocks are dropped with a diagnostic.

Use these values where safe-area lengths are accepted:

```css
env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left)
```

UIDoc's top safe-area inset also reserves the game's top-bar band.

## Scrolling and zooming

`overflow-x` and `overflow-y` are independent, so one viewport can scroll horizontally, vertically, or on both axes. A two-axis viewport pans both axes when dragged. The mouse wheel scrolls vertically when vertical scrolling is enabled; a horizontal-only viewport maps the wheel to horizontal scrolling.

The runtime currently draws a vertical scrollbar thumb. Horizontal content is still reachable by dragging even though a horizontal thumb is not drawn.

`data-scroll-zoom="zoomBinding"` adds browser-like content zoom to an interactive scroll viewport. It scales descendant geometry, text, images, transforms, hit testing, and scroll extents while the viewport and its siblings remain fixed. When the binding changes, the engine preserves the viewed area around the viewport center and reclamps the scroll position. The engine safety range is `0.05` through `20`; applications should normally use narrower limits.

## Tailwind-style classes

UIDoc assets can use supported Tailwind-style utility classes. Utilities are resolved and lowered into UIDoc CSS during asset processing; Tailwind is not running in the game. Covered areas include display, position, flex, sizing, spacing, inset, alignment, text, color, border, radius, overflow, pointer events, opacity, z-index, aspect ratio, shadows, transforms, and arbitrary image-effect properties.

The lowering step supports `hover:`, `focus:`, `active:`, `disabled:`, responsive `sm:`/`md:`/`lg:`/`xl:`/`2xl:`, bracket values such as `w-[320px]`, and CSS `@apply`. Tailwind preflight/base reset is not included. Output that cannot be represented by UIDoc fails asset baking.

## Notable browser features not supported

| Browser feature                                                          | UIDoc approach                                                                            |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| JavaScript and DOM APIs                                                  | Drive state with CSL bindings, `data-if`, `data-for`, and event callbacks.                |
| CSS Grid                                                                 | Use flexbox, block layout, or explicit positioning.                                       |
| CSS variables and custom properties                                      | Bind values from CSL or use ordinary shared classes.                                      |
| Transitions, keyframes, and CSS animations                               | Animate CSL bindings such as opacity, translation, scale, or scroll zoom.                 |
| Pseudo-elements and advanced selectors                                   | Add explicit elements and classes to the document.                                        |
| Container queries and most media features                                | Use the supported viewport-width and hover queries.                                       |
| CSS URL backgrounds and multiple background layers                       | Use a supported gradient or an `img` engine asset.                                        |
| Per-corner radii and more than eight shadows                             | Use the supported single-radius/eight-shadow limits or explicit nested elements.          |
| `min()`, `max()`, and `clamp()` CSS math functions                       | Combine `width`/`height` with the supported `min-*` and `max-*` properties.               |
| Rotate, skew, 3D transforms, filter chains, and other filter functions   | Prepare the visual as an asset or use supported translate/scale/blur/drop-shadow effects. |
| Web forms, navigation, fetch, iframes, canvas, SVG DOM, audio, and video | Use CSL and the corresponding engine systems.                                             |
| Browser semantic and accessibility behavior                              | UIDoc elements are game UI nodes; HTML tag names do not imply browser behavior.           |

UIDoc aims to make common game UI easy to author, not to reproduce every HTML and CSS feature. Keep layout within this subset so assets bake predictably and client/server UI simulation stays deterministic.


# World Space UI

Use world space UI to draw text above players heads and any other text that should feel like it's part of the game world itself.

World space UI is drawn in the 2D world instead of on the screen. Use it for nameplates, health bars, signs, and any text that should feel anchored to game objects.

{% hint style="info" %}
World space UI uses **meters** for sizing. If you use screen-space point sizes, your UI will be enormous.
{% endhint %}

### Core Rules

* Interactive UI must use `ao_update`/`ao_late_update` (player UI: `Player.ao_late_update` under `is_local_or_server()`), never `ao_draw`.
* `ao_draw` is for non-interactive cosmetic visuals only.
* For UI anchored to a moving entity, call `UI.begin_world_space_ui(entity)` and `defer UI.end_world_space_ui()` so render interpolation follows that entity.
* Use `UI.push_world_draw_context()` only for unanchored world drawing.
* Use meters for rect sizes and offsets.
* Push Z for proper depth sorting (commonly use `pos.y`).
* Use `subrect` only for percentage fills (like health bars).

### Examples

#### Displaying a player's level

```go
draw_player_level :: proc(player: Player) {
    UI.begin_world_space_ui(player.entity);
    defer UI.end_world_space_ui();

    pos := player.entity.world_position;
    UI.push_z(pos.y);
    defer UI.pop_z();

    ts := UI.default_text_settings();
    ts.size = 0.30; // World space text size
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    text_pos := pos + v2{0, 1.7};
    rect := Rect{text_pos, text_pos}.grow(0.05, 0.4, 0.05, 0.4);
    UI.text(rect, ts, "Lvl %", {player.level});
}
```

#### Player base signs

```go
draw_base_sign :: proc(sign: Entity, label: string) {
    UI.begin_world_space_ui(sign);
    defer UI.end_world_space_ui();

    pos := sign.world_position;
    UI.push_z(pos.y);
    defer UI.pop_z();

    ts := UI.default_text_settings();
    ts.size = 0.30;
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    text_pos := pos + v2{0, 2.2};
    rect := Rect{text_pos, text_pos}.grow(0.06, 0.8, 0.06, 0.8);

    // Optional dark backing for readability
    UI.quad(rect, core_globals.white_sprite, {0, 0, 0, 0.6});
    UI.text(rect, ts, label);
}
```

#### Health bars

```go
// Note: health/max_health are user-defined fields on your own class, not built-in Entity fields.
draw_world_ui :: proc(entity: My_Entity) {
    UI.begin_world_space_ui(entity.entity);
    defer UI.end_world_space_ui();

    pos := entity.entity.world_position;
    UI.push_z(pos.y);
    defer UI.pop_z();

    bar_pos := pos + v2{0, 1.5};
    bar_rect := Rect{bar_pos, bar_pos}.grow(0.1, 0.5, 0.1, 0.5);

    UI.quad(bar_rect, core_globals.white_sprite, {0, 0, 0, 0.8});

    health_pct := entity.health / entity.max_health;
    fill_rect := bar_rect.inset(0.02).subrect(0, 0, health_pct, 1);
    fill_color := lerp(v4{1, 0, 0, 1}, {0, 1, 0, 1}, health_pct);
    UI.quad(fill_rect, core_globals.white_sprite, fill_color);
}
```

### Progress bars

`World_Progress_Bar` provides a standard world-space bar:

```go
Health :: class : Component {
    current: int;
    maximum: int = 100;
}

draw_health_bar :: proc(health: Health) {
    UI.begin_world_space_ui(health.entity);
    defer UI.end_world_space_ui();

    maximum := max(1, health.maximum);
    progress := clamp(
        health.current.(float) / maximum.(float),
        0.0,
        1.0
    );

    options := World_Progress_Bar.default_options();
    options.y_bias = 1.5;
    World_Progress_Bar.draw(health.entity.world_position, progress, options);
}
```

The casts keep the division in floating point, and the clamped denominator prevents division by zero.

For a custom bar whose fill changes over time, keep one `Float_Interpolation_Helper` per bar:

```go
Smooth_Bar :: class : Component {
    progress: float;
    fill_history: Float_Interpolation_Helper;

    draw :: method() {
        UI.begin_world_space_ui(entity);
        defer UI.end_world_space_ui();

        center := entity.world_position + v2{0, 1.5};
        rect := Rect{center, center}.grow(0.1, 0.5, 0.1, 0.5);
        UI.quad(rect, core_globals.white_sprite, {0, 0, 0, 1});

        params: Quad_Params;
        params.fill = UI.quad_fill(
            fill_history.update(clamp(progress, 0.0, 1.0)),
            .RIGHT
        );
        UI.quad(rect.inset(0.02), core_globals.white_sprite, {0.1, 1, 0.1, 1}, params);
    }
}
```

Radial fills start at 12 o'clock and use signed amounts: `-1..0` fills counterclockwise and `0..1` fills clockwise. For an animated radial cooldown, pass `fill_history.update(clamp(amount, -1, 1))` to `UI.quad_fill(..., .RADIAL)`. Do not clamp to `0..1` unless clockwise-only behavior is intentional.

### Tutorial arrows

The entity overload includes the target's interpolation offset:

```go
draw_target_arrow :: proc(player: Player, target: Entity) {
    options := Tutorial_Arrow.default_options();
    Tutorial_Arrow.draw(player, target, options);
}
```

For a script-owned position that moves, store a `Position_Interpolation_Helper` and pass its offset:

```go
Player :: class : Player_Base {
    objective_position: v2;
    objective_history: Position_Interpolation_Helper;

    draw_objective_arrow :: method() {
        options := Tutorial_Arrow.default_options();
        offset := objective_history.update(objective_position);
        Tutorial_Arrow.draw(this, objective_position, options, offset);
    }

    ao_late_update :: method(dt: float) {
        if is_local_or_server() {
            draw_objective_arrow();
        }
    }
}
```

After snapping an entity to a new position, call `entity.mark_teleported()` so interpolation does not smear between the old and new positions.

### Coordinate Conversion

```go
// Convert world position to screen position
screen_pos := world_to_screen(entity.world_position);

// Convert screen position to world position
world_pos := screen_to_world(get_mouse_screen_position());
```

### Tips

* Keep world UI minimal and readable at distance. If you have a zoomable camera, make sure to adjust text size when you zoom out.
* Use `fit_aspect(texture.get_aspect())` if you draw icons in world space.
* If text flickers or overlaps, check your Z values and spacing.


# Scroll and Grid

***

Scroll views and grids make it easy to build inventories, lists, menus, and card layouts. This guide shows the correct patterns for both, including layout, IDs, and scroll bars.

{% hint style="info" %}
Follow the UI fundamentals: start from screen rects, use cut for layout, and always use `defer` for push/pop pairs.
{% endhint %}

### Scroll Views

Use scroll views for content that can exceed the available space. The scroll bar must be outside the scroll view content area.

```go
draw_scrollable_list :: proc(items: []string) {
    rect := UI.get_safe_screen_rect().inset(50);

    // Reserve space for the scroll bar first
    scroll_bar_area := rect.cut_right(8).inset(2);

    settings: Scroll_View_Settings;
    settings.vertical = true;
    settings.horizontal = false;
    settings.clip_padding = {10, 10, 10, 10};

    sv := UI.push_scroll_view(rect, "my_scroll_view", settings); {
        defer UI.pop_scroll_view();

        content := sv.content_rect;
        ts := UI.default_text_settings();
        ts.size = 24;

        for i: 0..<items.count {
            item_rect := content.cut_top(40);
            UI.text(item_rect, ts, items[i]);
        }
    }

    scroll_bar_rect := sv.compute_scroll_bar_rect(scroll_bar_area);

    UI.quad(scroll_bar_area, core_globals.white_sprite, {0.025, 0.025, 0.025, 1});
    UI.quad(scroll_bar_rect, core_globals.white_sprite, {0.1, 0.1, 0.1, 1});
}
```

**Important:** If the scroll bar is inside the scroll view content, it will break the bounds and scrolling behavior. Always cut the scroll bar area out first.

### Grid Layout

Use `Grid_Layout` for equally-sized elements like inventory slots or icon grids. Grids start at the top-left, advance to the right, then wrap to the next row.

```go
draw_inventory_grid :: proc(items: []string) {
    rect := UI.get_safe_screen_rect().inset(40);

    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    // 4 columns x 6 rows based on element count, 8 points of padding
    grid := UI.make_grid_layout(rect, 4, 6, .ELEMENT_COUNT, 8);

    for i: 0..<items.count {
        UI.push_id("slot_%", {i});
        defer UI.pop_id();

        slot_rect := grid.next();
        if UI.button(slot_rect, bs, ts, items[i]).clicked {
            log_info("selected item %", {i});
        }
    }
}
```

If you prefer fixed-size tiles, use `.ELEMENT_SIZE` instead of `.ELEMENT_COUNT`:

```go
grid := UI.make_grid_layout(rect, 100, 100, .ELEMENT_SIZE, 8);
```

The `next()` method updates grid state:

```go
slot_rect := grid.next();
grid.index; // 0, 1, 2, ...
grid.cur_x; // column
grid.cur_y; // row
```

### Scrollable Grids

`Grid_Layout` automatically expands scroll views when used inside one. This pattern is ideal for large inventories:

```go
draw_scrollable_grid :: proc(items: []string) {
    rect := UI.get_safe_screen_rect().inset(50);
    scroll_bar_area := rect.cut_right(8).inset(2);

    settings: Scroll_View_Settings;
    settings.vertical = true;
    settings.horizontal = false;
    settings.clip_padding = {10, 10, 10, 10};

    sv := UI.push_scroll_view(rect, "inventory_scroll", settings); {
        defer UI.pop_scroll_view();

        grid := UI.make_grid_layout(sv.content_rect, 4, 6, .ELEMENT_COUNT, 8);
        bs := UI.default_button_settings();
        ts := UI.default_text_settings();

        for i: 0..<items.count {
            UI.push_id("slot_%", {i});
            defer UI.pop_id();

            slot_rect := grid.next();
            if UI.button(slot_rect, bs, ts, items[i].name).clicked {
                select_item(i);
            }
        }
    }

    scroll_bar_rect := sv.compute_scroll_bar_rect(scroll_bar_area);
    UI.quad(scroll_bar_area, core_globals.white_sprite, {0.025, 0.025, 0.025, 1});
    UI.quad(scroll_bar_rect, core_globals.white_sprite, {0.1, 0.1, 0.1, 1});
}
```

### Best Practices

* Use `UI.push_id()` for repeated elements inside lists or grids.
* Cut the scroll bar area before creating a scroll view.
* Start from `UI.get_safe_screen_rect()` and derive all layout.
* Keep hover-only interactions to a minimum (mobile-first).


# UI Reference

Reference notes and patterns for CSL UI drawing.

This page collects UI rules, patterns, and examples that are helpful once you know the basics.

Read [UI Fundamentals](/ui/fundamentals) first.

### Core principles

1. **Y grows upward.** Screen coordinates use (0, 0) at the bottom-left.
2. **Start from screen rects.** Use `UI.get_safe_screen_rect()` or `UI.get_screen_rect()`, then derive everything from those rects.
3. **Push/pop pattern.** Use `defer` for every push to avoid leaking UI state.
4. **Mobile-first.** Avoid hover-only interactions.
5. **Text uses UTF-8, but character coverage is incomplete.** A character still needs a glyph in the selected font or its fallback fonts. Some characters, including emoji, are not supported yet.
6. **Use the player late-update stack.** Draw all player UI from that player's `ao_late_update` under `is_local_or_server()`.

### Quick start: a simple HUD button

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            draw_my_hud(this);
        }
    }
}

draw_my_hud :: proc(player: Player) {
    rect := UI.get_safe_screen_rect()
        .bottom_right_rect()
        .grow(40, 150, 40, 150)
        .offset(-50, 50);

    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    if UI.button(rect, bs, ts, "Action").clicked {
        log_info("action from %", {player.get_username()});
    }
}
```

### Basic drawing

#### Quads and images

```go
draw_ui :: proc() {
    rect := UI.get_screen_rect().center_rect().grow(100); // 200x200
    texture := get_asset(Texture_Asset, "my_icon.png");
    UI.quad(rect, texture);

    // With color tint (RGBA 0-1)
    UI.quad(rect, texture, {1, 0, 0, 0.5});

    // Solid color using the white sprite
    UI.quad(rect, core_globals.white_sprite, {0, 0, 0, 0.75});
}
```

#### Filled quads

Pass `Quad_Params.fill` from `UI.quad_fill` to reveal only part of a quad. The available `Fill_Direction` values are `.NONE`, `.RIGHT`, `.LEFT`, `.UP`, `.DOWN`, and `.RADIAL`.

Linear fills use amounts from `0` to `1`. Radial fills start at 12 o'clock and use a signed amount: `-1..0` fills counterclockwise and `0..1` fills clockwise. Values outside the applicable range are clamped. A zero radial amount draws nothing; `-1` and `1` both draw the complete quad.

```csl
draw_cooldown :: proc(rect: Rect, icon: Texture_Asset, amount: float) {
    params: Quad_Params;
    params.fill = UI.quad_fill(clamp(amount, -1, 1), .RADIAL);
    UI.quad(rect, icon, params=params);
}
```

For a changing fill, keep a persistent `Float_Interpolation_Helper` and pass its result to `UI.quad_fill`. Clamp before calling `update`; preserving the sign is required for counterclockwise radial fills. Use `UI.push_fill_amount` and `UI.pop_fill_amount` instead when the same fill should apply to several draw calls. Always pair the push with a deferred pop.

#### Text

```go
draw_text :: proc() {
    rect := UI.get_screen_rect().center_rect().grow(200, 300, 50, 300);

    ts := UI.default_text_settings();
    ts.size = 48;
    ts.color = {1, 1, 1, 1};
    ts.halign = .CENTER;
    ts.valign = .CENTER;

    UI.text(rect, ts, "Hello, World!");

    score := 1500;
    UI.text(rect, ts, "Score: %", {score});

    pct := 67;
    UI.text(rect, ts, "% %%", {pct}); // "67 %"

    // Returns the actual rendered rect
    actual_rect := UI.text_sync(rect, ts, "Dynamic text");
}
```

### Layout: start from rects

```go
layout_example :: proc() {
    screen := UI.get_screen_rect();
    safe := UI.get_safe_screen_rect();

    center := screen.center_rect();
    top_left := screen.top_left_rect();
    bottom_right := screen.bottom_right_rect();

    button_rect := center.grow(50, 150, 50, 150);
    icon_rect := center.grow(64);
}
```

### Use cut for layout

The cut functions **must** be used for layouting when placing multiple UI elements:

```go
draw_panel :: proc() {
    rect := UI.get_safe_screen_rect().inset(20);

    header := rect.cut_top(80);
    footer := rect.cut_bottom(60);
    body := rect;

    UI.quad(header, core_globals.white_sprite, {0.1, 0.1, 0.1, 0.8});
    UI.quad(footer, core_globals.white_sprite, {0.1, 0.1, 0.1, 0.8});
    UI.quad(body, core_globals.white_sprite, {0.05, 0.05, 0.05, 0.8});
}
```

### Auto-scaling and unscaled rects

Regular rect functions take **points** and scale them by `UI.get_current_scale_factor()` (based on a 1080-point-tall reference canvas).

Use the `_unscaled` variants only when a value is already expressed in actual screen pixels, such as a dimension measured from an existing rect:

```go
layout_with_unscaled :: proc(window_rect: Rect, items: []string) {
    ts := UI.default_text_settings();
    list_item := window_rect.top_rect().grow_bottom(20);

    for item: items {
        UI.text(list_item, ts, item);

        // height() is already in screen pixels, so do not scale it again
        list_item = list_item.offset_unscaled(0, -list_item.height());
    }
}
```

### Buttons

```go
draw_buttons :: proc() {
    rect := UI.get_safe_screen_rect()
        .bottom_center_rect()
        .grow(40, 150, 40, 150)
        .offset(0, 100);

    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    bs.sprite = get_asset(Texture_Asset, "$AO/new/modal/buttons_2/button_2.png");
    bs.press_scaling = 0.35;

    result := UI.button(rect, bs, ts, "Click Me!");
    if result.clicked {
        log_info("Button was clicked!");
    }
}
```

#### Button sprite rules

`UI.default_button_settings()` already provides a complete button style. For a custom button, set `sprite` and optionally `sprite_hovered` and `sprite_pressed`. When the optional sprites are null, the base sprite is reused.

### Modals

Use `UI.begin_modal` / `UI.end_modal` for a dimmed backdrop that closes when the player taps outside the modal or presses Escape.

```go
Player :: class : Player_Base {
    settings_open: bool;
}

draw_settings_modal :: proc(player: Player) {
    if !player.settings_open return;

    UI.begin_modal(UI.get_screen_rect(), "settings_modal", ref player.settings_open);
    defer UI.end_modal();

    window := UI.get_safe_screen_rect().center_rect().grow(240, 360, 240, 360);
    UI.quad(window, core_globals.white_sprite, {0, 0, 0, 0.9});

    ts := UI.default_text_settings();
    ts.halign = .CENTER;
    ts.valign = .CENTER;
    UI.text(window, ts, "Settings");
}
```

### UI state management

Use `defer` for every push/pop pair:

```go
draw_layered_ui :: proc() {
    UI.push_screen_draw_context();
    defer UI.pop_draw_context();

    UI.push_layer(10); // Keep game UI below native chat (layer 75).
    defer UI.pop_layer();

    UI.push_color_multiplier({1, 1, 1, 0.5});
    defer UI.pop_color_multiplier();

    // Draw UI here
}
```

### IDs for repeated elements

When drawing lists or repeated items, push unique IDs:

```go
draw_list :: proc(items: []string) {
    rect := UI.get_safe_screen_rect().inset(20);
    bs := UI.default_button_settings();
    ts := UI.default_text_settings();

    for i: 0..<items.count {
        UI.push_id("item_%", {i});
        defer UI.pop_id();

        item_rect := rect.cut_top(60);
        if UI.button(item_rect, bs, ts, items[i]).clicked {
            log_info("selected item %", {i});
        }
    }
}
```

### Common sizes

Some sizes that are known to look good across devices:

* Screen text points: Title 52, Body 36
* Screen rect points: Simple dialog 600x400, Standard button 210x74, Exit button 65x65
* World-space text size: 0.30 meters

### Best practices

* Use `defer` for every push/pop pair.
* Push IDs for repeated elements.
* Use unscaled functions with computed dimensions.
* Fit icon aspect ratios with `rect.fit_aspect(texture.get_aspect())`.
* Always check `is_local_or_server()` before drawing interactive UI.


# @ao\_serialize

Select fields for scene, Inspector, and JSON serialization.

`@ao_serialize` is the annotation you place on struct/class fields to opt them into the engine's serialization system. Adding this allows you to edit script fields in the editor for easy customization and is also required if you intend to use JSON serializations/save data for structs.

```go
Enemy :: class : Component {
    // Saved, shown in inspector, included in scene data
    @ao_serialize max_health: int;
    @ao_serialize patrol_radius: float;

    // Runtime-only — not saved, not in inspector
    current_target: v2;
    aggro_timer: float;
}
```

## What it does

When you mark a field with `@ao_serialize`, the engine will:

1. **Show it in the inspector** so you can edit it on entities in the editor.
2. **Include it in JSON serialization** (`Save.set_json` / `Save.try_get_json`).

Fields without `@ao_serialize` exist only in memory at runtime. They start from their normal initializer each time the component is created, but are not included in scene or JSON serialization.

## Supported types

`@ao_serialize` works with all common CSL types:

| Type              | Example                                                              |
| ----------------- | -------------------------------------------------------------------- |
| Integers          | `s8`, `s16`, `s32`, `s64` / `int`                                    |
| Unsigned          | `u8`, `u16`, `u32`, `u64` / `uint`                                   |
| Floats            | `f32` / `float`, `f64`                                               |
| Booleans          | `bool`                                                               |
| Strings           | `string`                                                             |
| Vectors           | `v2`, `v3`, `v4`                                                     |
| Enums             | Any user-defined enum                                                |
| Fixed arrays      | `[N]T`                                                               |
| Dynamic arrays    | `[..]T`                                                              |
| Structs / classes | Nested types (their `@ao_serialize` fields are included recursively) |
| Engine references | Entities, components, and assets where supported                     |

{% hint style="info" %}
For nested structs/classes, only the fields marked `@ao_serialize` inside the nested type are serialized. The annotation doesn't cascade — you must mark each field individually.
{% endhint %}

## Using with components

The most common use is on component fields. These become editable in the editor's inspector and are saved as part of the scene.

```go
Chest :: class : Component {
    @ao_serialize capacity: int;
    @ao_serialize loot_table: string;
    @ao_serialize is_locked: bool;

    // Runtime state — no need to serialize
    has_been_opened: bool;
}
```

You can set `capacity`, `loot_table`, and `is_locked` per-entity in the editor. When the scene loads, those values are restored automatically.

## Using with the Save system

`@ao_serialize` also controls which fields are included when you use the JSON save APIs. Only marked fields are written to JSON.

```go
Player_Progress :: class {
    @ao_serialize version: s64 = 1;
    @ao_serialize xp: s64;
    @ao_serialize level: s64 = 1;
    @ao_serialize unlocked_skins: [..]string;
}

// Save
Save.set_json(player, "progress", ref progress);

// Load
progress := new(Player_Progress);
if !Save.try_get_json(player, "progress", ref progress) {
    // New player — the class defaults are already initialized.
}
```

For full details on the Save system, see [Save System](/data-and-persistence/save) and [JSON Serialization](/data-and-persistence/json).

## Using with standalone JSON

You can serialize any annotated type to/from a JSON string, independent of the Save system:

```go
config: My_Config;
config.difficulty = 3;

json_str := JSON.serialize(ref config);
// json_str contains only @ao_serialize fields

loaded: My_Config;
JSON.try_deserialize(json_str, ref loaded);
```

## Default values and schema changes

Class fields can have constant inline defaults. Put `@ao_serialize` before the field declaration:

```go
Player_Progress :: class {
    @ao_serialize version: s64 = 2;
    @ao_serialize max_health: int = 100;
    @ao_serialize unlocked_skins: [..]string;
}
```

When a serialized field is missing, class deserialization keeps that field's default. Unknown fields are ignored. Struct fields cannot have inline defaults, so initialize a struct before deserializing when it needs non-zero values.

Fixed-array deserialization is bounded by the destination size. Extra JSON items are ignored. If the input is shorter, untouched elements keep their initialized values.

## What NOT to serialize

Not every field should be serialized. Leave `@ao_serialize` off fields that are:

* **Derived at runtime** (positions computed each frame, cached lookups)
* **Temporary state** (timers, cooldown counters, frame-local flags)
* **Large data that changes every frame** (unnecessary save overhead)

A good rule of thumb: if the value is set once (in the editor or on load) and rarely changes, serialize it. If it's recomputed every frame, don't.

Marking a component field does not by itself persist runtime changes across sessions. Use Save, Economy, or Inventory when that persistence is required.

## Common patterns

### Enums for mode selection

```go
AI_Mode :: enum {
    IDLE;
    PATROL;
    CHASE;
}

Guard :: class : Component {
    @ao_serialize mode: AI_Mode;
    @ao_serialize patrol_speed: float;
}
```

### Nested structs

```go
Spawn_Point :: struct {
    @ao_serialize position: v2;
    @ao_serialize radius: float;
}

Spawner :: class : Component {
    @ao_serialize points: [..]Spawn_Point;
    @ao_serialize spawn_interval: float;
}
```

### Asset references

Some fields reference engine assets (textures, prefabs, sounds). These are serialized as asset identifiers and resolved automatically on load. See the built-in components (like `Sprite_Renderer`) for examples.


# Save System

The save system provides a key-value data storage system for persisting player progress and preferences across sessions.

The Save system is a simple **key-value store**. You give it a string key (like `"xp"`), and it stores a value that will still be there the next time the player joins.

There are two kinds of save data:

* **Player save**: one player's personal data (XP, upgrades, settings, inventory, etc)
* **Game-wide save**: shared by everyone in the game (world records, server settings, global counters, etc)

{% hint style="warning" %}
Do not use the save system to store purchases players made with sparks. Use the [Purchasing/Product APIs](/core-engine-concepts/purchasing-product-apis) instead.
{% endhint %}

### Quick start (save + load a stat)

The most common pattern is:

* Load saved values in `ao_start`
* Save again whenever the value changes

```go
Player :: class : Player_Base {
    xp: s64;
    level: s64;

    ao_start :: method() {
        // Defaults are used for brand new players
        xp    = Save.get_int(this, "xp", 0);
        level = Save.get_int(this, "level", 1);
    }
}

add_xp :: proc(player: Player, amount: s64) {
    player.xp += amount;

    // ... your level-up logic here ...

    // Save immediately when it changes
    Save.set_int(player, "xp", player.xp);
    Save.set_int(player, "level", player.level);
}
```

### Player save (per-player data)

Player save is scoped to a single player. Every player has their own isolated key/value data.

Supported types:

* **String**: `Save.set_string` / `Save.get_string`
* **Integer (`s64`)**: `Save.set_int` / `Save.get_int`
* **Float (`f64`)**: `Save.set_f64` / `Save.get_f64`
  * For ordinary CSL `float` (`f32`) values, `Save.set_float` / `Save.get_float` are convenience aliases that use the same f64 storage and cast at the API boundary.
* **Inventory item reference**: `Save.save_item_reference` / `Save.get_item_reference`
* **JSON (advanced)**: `Save.set_json` / `Save.try_get_json`

```go
// Preferences
Save.set_string(player, "selected_skin", "knight");
music_volume := Save.get_f64(player, "music_volume", 0.8);

// Deleting a key (useful when migrating/removing old data)
Save.delete_key(player, "old_key_name");
```

{% hint style="info" %}
Always provide a sensible default to scalar getters. New players won't have keys yet; scalar `get_*` calls return that default, while `get_item_reference` returns `null`.
{% endhint %}

### Saving an equipped item

Use an item reference to remember a selection from any of the player's persistent inventories without copying item ownership or properties into Save:

```go
Save.save_item_reference(player, "equipped_weapon", weapon);

weapon := Save.get_item_reference(player, "equipped_weapon");
if weapon != null {
    equip_weapon(player, weapon);
}

Save.save_item_reference(player, "equipped_weapon", null); // Clear the selection
```

`Save.save_item_reference` accepts items in the player's auto-saved `default_inventory` or any inventory created with `Items.create_player_inventory`. Named inventories qualify even when default inventory auto-save is disabled. References survive slot rearrangement within that inventory and rejoining. Removing, dropping, destroying, or transferring an item to any other inventory invalidates its old references, even for the same player or if the item later returns. Save it again to select it in its new inventory. A stack fully consumed by a merge also becomes `null`; references to the surviving stack remain valid.

Create all named inventories in `Player.ao_start` before resolving saved references or JSON records. An inventory that has not been created cannot resolve its items yet; getters return `null` without creating it. Existing saved references remain readable.

### Saving “bigger” data (JSON)

If you have a little bundle of fields (like progress + unlocked things), it's often nicer to save it as one JSON blob.

Only fields marked with `@ao_serialize` are saved.

```go
Player_Progress :: class {
    @ao_serialize version: s64 = 1;
    @ao_serialize max_health: s64 = 100;
    @ao_serialize unlocked_skins: [..]string;
    @ao_serialize equipped_weapon: Item_Instance;
}

save_progress :: proc(player: Player, progress: Player_Progress) {
    Save.set_json(player, "progress", ref progress);
}

load_progress :: proc(player: Player) -> Player_Progress {
    progress := new(Player_Progress);
    if !Save.try_get_json(player, "progress", ref progress) {
        // Missing key (or parse failed) already has the class defaults.
    }
    return progress;
}
```

{% hint style="info" %}
`Save.try_get_json` returns `false` if the key doesn't exist or the JSON string is malformed. Allocate class data before loading because the false path does not allocate it. Missing fields keep their class defaults, and unknown fields are ignored.
{% endhint %}

`Item_Instance` fields (including derived item types) use the same inventory references as `Save.save_item_reference`. Every non-null item must belong to one of that player's persistent inventories. Transferring it to any other inventory invalidates the reference, even for the same player. A missing or invalidated item, or one that no longer matches the field's derived type, becomes `null`. Create named inventories before loading the record. The inventory remains authoritative for ownership and item data.

### Save versioning (migrating old data safely)

If you ever change your save format, keep a `version` key and migrate older saves forward.

```go
Player :: class : Player_Base {
    hp: f64;

    ao_start :: method() {
        save_version := Save.get_int(this, "version", 0);

        if save_version < 6 {
            // Example migration: hp used to be an int, now it's a float
            save_version = 6;
            old_hp := Save.get_int(this, "hp", 100);
            Save.delete_key(this, "hp");
            Save.set_f64(this, "hp", old_hp.(f64));
        }

        Save.set_int(this, "version", save_version);

        // Load current format
        hp = Save.get_f64(this, "hp", 100);
    }
}
```

### Game-wide save (shared by everyone)

Game-wide save is shared across the whole game, not per-player.

```go
// Global record holder
Save.set_game_string("world_record_holder", player.get_username());
holder := Save.get_game_string("world_record_holder", "nobody yet");

// Global counters (atomic increment, safe when many players update it)
Save.increment_game_int("total_games_played", 1);
total := Save.get_game_int("total_games_played", 0);

// Delete either kind of game-wide value
Save.delete_game_key("retired_event");
```

{% hint style="info" %}
Use `Save.increment_game_int` for counters that multiple players might update at the same time (kills, joins, rounds played, etc). Its `optimistic_update` parameter defaults to `true`, so the local cached value changes immediately.

`Save.delete_game_key` queues and deduplicates deletions with the normal game-save flush. A later `set_game_string` for the same key cancels the queued delete. A later `increment_game_int` keeps the delete, resets the stored counter to zero, and then applies the increment.
{% endhint %}

### Common patterns

#### Booleans

Save booleans as `0/1`:

```go
// Save
Save.set_int(player, "tutorial_complete", tutorial_complete ? 1 : 0);

// Load
tutorial_complete = Save.get_int(player, "tutorial_complete", 0) != 0;
```

#### Key naming

Keys are just strings, so pick names that won't collide later:

* `"xp"`, `"level"`, `"selected_skin"`
* `"tycoon.cash"`, `"tycoon.upgrades.mouth_level"`

If you have multiple games connected via game parenting (hub + minigames), see [Cross-Game Products/Data](/data-and-persistence/cross-game-products-data) for how save data can be shared.

### Additional APIs

```go
Save :: struct {
    // Persistent selections across the player's persistent inventories
    save_item_reference :: proc(player: Player, key: string, item: Item_Instance);
    get_item_reference :: proc(player: Player, key: string) -> Item_Instance;

    // Delete keys
    delete_key      :: proc(player: Player, key: string);
    delete_all_keys :: proc(player: Player);

    // Enumerate keys
    get_all_keys :: proc(player: Player) -> []string;

    // Game-wide
    delete_game_key      :: proc(key: string);
    get_all_game_strings :: proc() -> []Save_Game_String;
    get_all_game_ints    :: proc() -> []Save_Game_Int;
    get_all_game_keys    :: proc() -> []Save_Game_Key;

    // Ordered/ranked data (leaderboards)
    ordered_set     :: proc(document: string, key: string, value: f64);
    ordered_get     :: proc(document: string, key: string, default: f64,
                            userdata: Object,
                            callback: proc(entry: Ordered_Save_Entry, userdata: Object));
    ordered_get_all :: proc(document: string, offset: s64, limit: s64,
                            userdata: Object,
                            callback: proc(entries: []Ordered_Save_Entry, userdata: Object));
}
```

`ordered_get` and `ordered_get_all` are asynchronous and execute on the server. Call them from the normal shared gameplay path; do not add a `Game.is_server()` guard.

Callback arrays are valid only during the callback, so copy any entries you need to keep:

```go
Ranking_State :: class : Component {
    entries: [..]Ordered_Save_Entry;

    refresh :: method() {
        Save.ordered_get_all(
            "weekly_score",
            0,
            100,
            this,
            proc(results: []Ordered_Save_Entry, userdata: Object) {
                state := userdata.(Ranking_State);
                state.entries.clear();
                for result: results {
                    state.entries.append(result);
                }
            }
        );
    }
}
```

Each completed `ordered_get` request invokes its callback once. A missing entry or request failure returns the supplied default. Each completed `ordered_get_all` request also invokes its callback once; an empty result or request failure supplies an empty array. Request failures are logged.


# JSON Serialization

For saving more advanced data structures, you can serialize them to JSON and store them in the Save system.

CSL can serialize certain data structures into JSON for you. This is most useful when you want to save a “bundle” of related fields (like player progress, upgrades, unlocks, etc) without managing lots of separate save keys.

Under the hood, this uses the Save system's JSON APIs:

* `Save.set_json(player, key, value)`
* `Save.try_get_json(player, key, out) -> bool`

{% hint style="info" %}
JSON save/load is **per-player** (it takes a `player`). Game-wide save currently supports strings + ints only.
{% endhint %}

### What gets saved?

Only fields marked with `@ao_serialize` are included in the JSON.

```go
Player_Progress :: class {
    @ao_serialize version: s64 = 1;

    @ao_serialize xp: s64;
    @ao_serialize level: s64 = 1;

    @ao_serialize unlocked_skins: [..]string;
    @ao_serialize equipped_weapon: Item_Instance;
}
```

### Saving JSON

Save the whole structure under one key:

```go
// Note: Only @ao_serialize fields are written
Save.set_json(player, "progress", ref progress);
```

In Save JSON, `Item_Instance` fields and derived item fields persist as selections from any of that player's persistent inventories. Transferring an item to another inventory invalidates its references, even for the same player. Saving an item outside that player's persistent inventories raises an error. Missing or invalidated references and derived-type mismatches load as `null`. Create named inventories before loading the record. Standalone `JSON.serialize` does not provide this item-reference behavior.

### Loading JSON (with defaults)

`Save.try_get_json` returns `false` if the key is missing or the JSON string is malformed. Allocate class data before loading because the false path does not allocate it. Missing fields keep their class defaults, and unknown fields are ignored.

```go
load_progress :: proc(player: Player) -> Player_Progress {
    progress := new(Player_Progress);

    if !Save.try_get_json(player, "progress", ref progress) {
        // New player (or malformed JSON) already has the class defaults.
    }

    return progress;
}
```

{% hint style="warning" %}
Changing a stored field to an incompatible type is invalid input and can fail loudly. Use a new field or save key when the old value cannot be converted safely.
{% endhint %}

### Versioning & migrations

If you expect your JSON schema to change, include a `version` field in the structure and migrate after load.

Best practices for staying compatible:

* **Prefer adding new fields** with sensible class defaults.
* Keep a version field when a new value must be derived from old data.
* If you need a hard break, consider saving under a **new key** (e.g. `"progress_v2"`) and keeping a fallback loader.

Migration Example:

```go
load_progress_and_migrate :: proc(player: Player) -> Player_Progress {
    progress := new(Player_Progress);

    if !Save.try_get_json(player, "progress", ref progress) {
        // The class defaults already describe a new player.
    }

    // Migrate forward
    if progress.version < 2 {
        progress.version = 2;
        // Example: v2 introduced unlocked_skins (initialize it)
        progress.unlocked_skins = .{};
    }

    // Write back the migrated version
    Save.set_json(player, "progress", ref progress);
    return progress;
}
```

### Fixed arrays

Fixed-array input is bounded by the destination:

* Extra JSON items are ignored.
* Missing items leave the remaining destination elements unchanged.

### When to use JSON vs simple keys

* Use **simple keys** (`Save.set_int`, `Save.set_string`, etc) for a handful of values you read/write often.
* Use **JSON** when you want to store a cohesive structure (progress, loadouts, unlocks) and keep your save logic centralized.

### Standalone JSON API

You can also serialize/deserialize JSON independently of the Save system (e.g. for logging, networking, or custom storage):

```go
JSON :: struct {
    serialize       :: proc(obj: ref $T) -> string;
    try_deserialize :: proc(json: string, out_value: ref $T) -> bool;
}
```


# Economy

Economy APIs let you create custom currencies (coins, gems, XP, etc) and automatically persist them for each player across sessions.

* Each currency is stored **per-player**
* Balances are **automatically persisted** across sessions
* Balances can be viewed/edited in the creator portal (see [Editing/Viewing Player Data](/data-and-persistence/editing-viewing-player-data))

### Economy API reference

```go
Economy :: struct {
    register_currency     :: proc(currency: string, icon: Texture_Asset);
    deposit_currency      :: proc(player: Player, currency: string, amount: s64);
    get_balance           :: proc(player: Player, currency: string) -> s64;
    can_withdraw_currency :: proc(player: Player, currency: string, amount: s64) -> bool;
    withdraw_currency     :: proc(player: Player, currency: string, amount: s64);
    delete_save_data      :: proc(player: Player);
}
```

### Registering a currency (one time)

Before you use a currency name, register it with an icon.

```go
ao_before_scene_load :: proc() {
    // Pick an icon from your /res folder
    coin_icon := get_asset(Texture_Asset, "ui/coin.png");

    Economy.register_currency("Coins", coin_icon);
    Economy.register_currency("XP", coin_icon); // example (use a different icon ideally)
}
```

{% hint style="info" %}
Currency names are just strings. Pick a consistent name and stick to it (for example `"Coins"` vs `"coins"`).
{% endhint %}

### Reading a player's balance

```go
coins := Economy.get_balance(player, "Coins");
```

### Giving currency (rewards)

Use `deposit_currency` whenever a player earns currency.

```go
on_enemy_killed :: proc(player: Player) {
    Economy.deposit_currency(player, "Coins", 10);
    Economy.deposit_currency(player, "XP", 3);
}
```

### Spending currency (shops/upgrades)

Always check `can_withdraw_currency` before withdrawing. Deposit and withdrawal amounts must be non-negative.

```go
UPGRADE_COST :: 50;

try_buy_upgrade :: proc(player: Player) -> bool {
    if !Economy.can_withdraw_currency(player, "Coins", UPGRADE_COST) {
        Notifier.notify(player, "Not enough coins!");
        return false;
    }

    Economy.withdraw_currency(player, "Coins", UPGRADE_COST);

    // Grant the upgrade here...
    Notifier.notify(player, "Upgrade purchased!");
    return true;
}
```

## Built-in shops

A shop contains categories, and each category contains products. Create the shop after registering its currencies. Keep its handles in scene-wide fields or globals because they are valid only for the current scene.

```go
shop: u64;

grant_shop_product :: proc(
    player: Player,
    product: u64,
    userdata: Object
) -> bool {
    if Game_Product.get_id(product) == "health_upgrade" {
        player.max_health += 10;
        return true;
    }

    // Returning false cancels the purchase and does not charge the player.
    return false;
}

ao_before_scene_load :: proc() {
    coin_icon := get_asset(Texture_Asset, "ui/coin.png");
    Economy.register_currency("Coins", coin_icon);

    shop = Economy.create_shop("upgrade_shop");
    upgrades := Shop.add_category(shop, "Upgrades");

    Shop_Category.add_product(
        upgrades,
        "health_upgrade",
        "Health Upgrade",
        "Adds 10 maximum health.",
        "ui/health_upgrade.png",
        "Coins",
        50,
        "",
        Item_Rarity.COMMON.(s64),
        ""
    );

    Shop.set_purchase_handler(shop, null, grant_shop_product);
}
```

The purchase handler must grant the product and return `true`. Returning `false` rejects the purchase.

Draw the shop from player UI code. `Shop.draw` returns `true` while the shop remains open:

```go
Player :: class : Player_Base {
    max_health: int = 100;
    shop_open: bool;

    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        if shop_open {
            shop_open = Shop.draw(shop, UI.get_safe_screen_rect());
        }
    }
}
```

`Shop.set_purchase_modifier` can change a product's price or button per player. `Shop.set_custom_display` can replace product-card drawing. Both callbacks use the same explicit `userdata` pattern as the purchase handler.

### Resetting a player's economy data

If you need to wipe all economy balances for a player (for example, an admin reset button or a game mode reset), you can delete their economy save data:

```go
reset_economy :: proc(player: Player) {
    Economy.delete_save_data(player);
    Notifier.notify(player, "Your economy data was reset.");
}
```

{% hint style="warning" %}
`Economy.delete_save_data` is destructive. Use it sparingly, and consider adding confirmations/admin-only access.
{% endhint %}

### Economy vs Save

* Use **Economy** for "currencies" (coins, gems, XP, tickets) where you want auto-persistence + portal editing.
* Use **Save** for everything else (settings, quest state, unlock lists, complex progress structures). See [Save System](/data-and-persistence/save)


# Cross-Game Products/Data

If you have multiple games that share the same data (e.g. a hub world and minigames) you can link them together automatically using the parent game system.

### Setting up game parenting

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-c30da1039399fb5cc98ac2364f5bb4344a4038a9%2Fimage.png?alt=media" alt="Parent Game ID field in game settings"><figcaption></figcaption></figure>

When you set the parent game ID, the child game uses the parent's data store for the shared systems below.

#### Types of data shared:

* Products & Player Purchases
* Player save data
* Game wide save data
* Leaderboards
* Generated assets

If you only want to share **some** data (for example if you have separate xp systems for each game) you can prefix your save keys with the current game ID in code.

Use `Game.get_game_id()` to fetch the ID at runtime:

```go
// little helper you can write to make this easier
get_scoped_key :: proc(local_key: string) -> string {
    return format_string("%:%", {Game.get_game_id(), local_key});
}

save_player_xp :: proc(player: Player, xp: s64) {
    Save.set_int(player, get_scoped_key("xp"), xp);
}

load_player_xp :: proc(player: Player) -> s64 {
    return Save.get_int(player, get_scoped_key("xp"), 0);
}
```

Use the same prefix for every read, write, migration, and portal edit of that key.

### Migrating Data Between games

{% hint style="warning" %}
If you did not set up game parenting initially and need to do so retroactively please contact us to migrate each game's data to the parent game first. This is a manual and potentially error prone process (if you have keys with the same name in each game already) so we highly suggest you consider this up front!
{% endhint %}


# Editing/Viewing Player Data

You can view and edit save data you've written for players at any time from the creator portal!

### Player Scoped Data

All of a player's save data in one place including

* Any [Economy](/data-and-persistence/economy) currency values for that player
* Any persisted [Inventory](/core-engine-concepts/inventory) items for that player
* Any custom save data you've persisted for that player

Select **Player** and enter a username or user ID. Use **Add Field** only with a key and value type that the game already understands.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-451d6efed195223f0ea37cd6d2afc50ee20696c2%2Fimage.png?alt=media" alt="Player save data with Economy and Inventory sections"><figcaption></figcaption></figure>

### Game Scoped Data

View and edit global game keys you've set with APIs like \`Save.set\_game\_string\`. Use the edit control on a key, change its value, and save.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-56440e752f6ba59c707730c70337452a964f4330%2Fimage.png?alt=media" alt="Game-wide Save keys in the creator portal"><figcaption></figcaption></figure>

### Leaderboards

{% hint style="info" %}
Leaderboard scores cannot currently be edited in the web interface
{% endhint %}

The leaderboard view shows the top entries for the selected dataset.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-acc604531bd2b39cb923f173339505013a6cf66f%2Fimage.png?alt=media" alt="Leaderboard rankings in the creator portal"><figcaption></figcaption></figure>

### Purchases

Select **Purchases**, enter a player, then filter by product or grant status. Use **Regrant** only when the game's purchase handler can safely run again.

See [Purchase Handler and Grant Failures](/core-engine-concepts/purchasing-product-apis#the-purchase-handler-granting) before changing a purchase record.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-cb64884cbe71d6c55419e9b37092b015553b16cc%2Fimage.png?alt=media" alt="Player purchase records and Regrant actions"><figcaption></figcaption></figure>


# Overview

Earn Sparks from your game's products and rewarded ads, withdraw earnings to an All Out account, and arrange a cash payout.

Players spend Sparks on [game passes and consumable products](/monetization/in-game-products) in your game. The payout-eligible portion of those purchases becomes your game's earnings.

Your game also earns **2 Sparks per verified completed mobile rewarded ad**. The [ads guide](/monetization/in-game-ads) explains which ads count and how to see ad earnings over time.

## From game earnings to payment

1. **Earn:** Eligible Sparks spent on your products and verified game ad credits add to your **Game Spark balance**. Refunds and previous withdrawals reduce that balance.
2. **Withdraw:** On your game's **Monetization** page, select **Withdrawal**. The creator or an admin can transfer earnings to the creator or a current editor. The recipient receives **70%**, rounded down to whole Sparks; the rest is deducted as fees.
3. **Cash out:** Once the Sparks are in the recipient's All Out account, [contact the All Out team](/going-all-out/developer-support). The cash-out rate is **$1 USD per 100 received Sparks**. The team deducts those Sparks from the account when arranging payment.

For example, withdrawing 1,000 eligible Sparks sends 700 Sparks to the recipient. Those 700 Sparks have a cash-out value of $7 USD. Confirm the payment method, minimum, timing, and any local-currency conversion with the team under your creator agreement.

{% hint style="info" %}
The Withdrawal page transfers Sparks into an All Out account. A money payout is arranged separately with the team.
{% endhint %}

## Existing redemptions

As announced on September 10–11, 2026, developers already redeeming earnings before the September 10 announcement will receive the same payout for those earlier earnings. Going forward, use the new withdrawal system and contact the team to cash out. Earlier sales are not automatically added to the new withdrawal balance.

[Read the Spark withdrawal guide](/monetization/spark-withdrawals) for eligibility, fees, rounding, recipients, refunds, and common questions.


# Spark Withdrawals

How to withdraw eligible game earnings, understand your balance and fees, and cash out with the All Out team.

Withdrawals transfer your game's eligible Spark earnings to an All Out account. The recipient can then contact the team to exchange those Sparks for money.

{% hint style="info" %}
**Cash-out note:** To receive money for Sparks you have withdrawn, [contact the All Out team](/going-all-out/developer-support). The rate is **$1 USD per 100 received Sparks**. We will deduct those Sparks from your account when arranging payment.

If you were already redeeming earnings before the September 10, 2026 announcement, your payout for those earlier earnings will stay the same. Future earnings use the new withdrawal system.
{% endhint %}

## Withdraw your game earnings

1. Open your live game in the Creator Portal and select **Monetization**.
2. Check your **Game Spark balance** and select **Withdrawal**.
3. Choose a **Recipient**: the game's creator or one of its current editors.
4. Enter the **Amount to withdraw**. Use a whole number of at least **2 Sparks**, up to the amount shown as **Available to withdraw**.
5. Check **Fees** and **Recipient receives** carefully, then select **Withdraw** and confirm.

The same process is available on desktop and mobile. Only the game creator or an All Out admin can initiate a withdrawal. Editors can view earnings and receive Sparks, but being an editor does not allow them to withdraw independently.

{% hint style="warning" %}
All Spark withdrawals are final and cannot be reversed. Check both the recipient and the amount before confirming.
{% endhint %}

## Fees and rounding

The amount you enter is deducted from the game's balance in full. The recipient receives **70%**, rounded down to a whole Spark. The remaining Sparks are deducted as fees.

| Amount withdrawn from game | Recipient receives |       Fees | Value when cashed out |
| -------------------------: | -----------------: | ---------: | --------------------: |
|                   2 Sparks |            1 Spark |    1 Spark |             $0.01 USD |
|                  10 Sparks |           7 Sparks |   3 Sparks |             $0.07 USD |
|                 100 Sparks |          70 Sparks |  30 Sparks |             $0.70 USD |
|               1,000 Sparks |         700 Sparks | 300 Sparks |             $7.00 USD |

The fee is normally 30%; rounding can make it a larger share of a small withdrawal. Each withdrawal is rounded separately, so combining small amounts can leave the recipient with more Sparks.

The cash-out rate applies to **the Sparks the recipient actually receives**. For example, a 1,000-Spark game withdrawal produces 700 personal Sparks, worth $7 USD when cashed out. The 70% share has already been applied during the transfer.

The examples show conversion values, not a promise of immediate cash payment for small amounts. The 2-Spark minimum is the minimum game-to-account transfer. Confirm money payout minimums, methods, timing, and any currency conversion with the team under your creator agreement.

## Understanding your balance

Your game balance includes payout-eligible Sparks spent on your game's products since tracking began and **2 Sparks per verified completed mobile rewarded ad**, minus eligible refunds and previous withdrawals. Ad credits begin when the feature is enabled; earlier views are not backfilled. **Available to withdraw** shows how much of that balance you can transfer now, before fees.

Your game balance is separate from:

* **Your personal Spark balance:** It increases when a withdrawal is sent to your account, rather than every time a player buys a product in your game.
* **Total Sparks spent:** Some player spending is ineligible for payout, and older sales may have no eligibility record.
* **The reporting date range:** The date filter changes the product and ad reports. Your game wallet reflects its remaining tracked earnings across the full tracking period.

The **Ad earnings** card on Monetization shows completed ads, ad Sparks and a daily chart. These credits go directly to the game and are separate from any Sparks a player receives for watching an ad. They use the same withdrawal fee as other game earnings. See the [ads guide](/monetization/in-game-ads) for eligible ads.

### Which Sparks are eligible?

For product sales, eligibility depends on how the player obtained the Sparks. Players see a single Spark balance; the system keeps track of the eligible portion and spends it first. Only the eligible portion actually spent on your game's products contributes to product earnings. Verified game ad credits are separately eligible in full before withdrawal fees.

For new player credits, the current rules are:

| Spark source                                                                                                    | Portion eligible for creator earnings       |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| Purchased Spark packs, including purchased gifts                                                                | 70%, rounded to the nearest whole Spark     |
| Spark wheel rewards                                                                                             | 25%, rounded to the nearest whole Spark     |
| Direct ad rewards                                                                                               | 100%                                        |
| Paid-track battlepass Spark rewards                                                                             | 100%                                        |
| VIP daily bonuses                                                                                               | The amount above the five free daily Sparks |
| Sparks received from a game withdrawal                                                                          | 100%                                        |
| Ordinary free daily bonuses, free battlepass rewards, promos, events, referrals, grants, and other free rewards | Ineligible                                  |

These percentages classify Sparks when the player receives them. They are separate from the 70% creator share applied when withdrawing game earnings. For example, buying a new pack of 100 Sparks supplies 70 eligible Sparks. Spending the entire pack on your products contributes 70 Sparks to your game's eligible balance; withdrawing those 70 sends 49 Sparks to the recipient.

Previously recorded eligibility is preserved. These rules do not retroactively change old transactions.

### Why are my old sales missing from the withdrawal balance?

The new earnings tracking began on **September 10, 2026 at 20:15 UTC**. Earlier purchases without recorded eligibility are not automatically converted into a new withdrawable game balance. An old lifetime sales total can therefore be much larger than the amount now available to withdraw.

Developers who were already redeeming earnings before the September 10 announcement keep the same payout for those earlier earnings. [Contact the team](/going-all-out/developer-support) about those existing redemptions; they are handled separately from the new wallet.

## Refunds

If a tracked product purchase is refunded, its eligible portion is deducted from your game earnings. Refunds appear on the date they are issued, so the spending report for a short period can show a negative net amount.

A refund after a withdrawal can leave the game with a **refund deficit**. In that case, **Available to withdraw** is zero. Future eligible earnings must cover the deficit before you can withdraw again. The earlier withdrawal itself remains final.

## If a withdrawal cannot be confirmed

An interrupted connection can leave a withdrawal unconfirmed even if the Sparks were transferred. If the page offers **Retry withdrawal**, use it with the same details. The system checks the original request and recovers its result without paying it twice.

While the result is uncertain, the recipient and amount are locked. If retrying does not resolve it, contact the team before starting another withdrawal.

## Cashing out

After withdrawing to your All Out account, contact the team through [Developer Support](/going-all-out/developer-support) or the **Support** link on the Withdrawal page. Include the account receiving the Sparks and say that you want to cash out your withdrawn earnings.

The rate is **$1 USD per 100 received Sparks**. The team deducts the Sparks when arranging payment. Confirm your payment method, applicable minimum, schedule, and any local-currency conversion with the team. Selecting **Withdraw** does not automatically initiate a bank payment.

## References

* [Monetization overview](/monetization/overview): the path from game sales to a money payout.
* [In Game Products](/monetization/in-game-products): game passes, consumables, and purchase fulfillment.
* [Developer Support](/going-all-out/developer-support): contact the All Out team for payout arrangements.
* Matt's developer announcement, September 10–11, 2026: prior redemptions, the new cash-out rate, and team-assisted payments.


# In Game Products

Consumables and game passes are created in similar ways but have slightly different use cases!

### Product Types

#### Game Passes

Use game passes for persistent one-time purchases:

* Unlocking admin passes
* Permanent power ups/abilities
* Buying houses

#### Consumables

Use consumables for products you want people to be able to buy multiple times:

* Health potions
* Buffs that expire over time
* One time in-game coin purchases

### Creating Products

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-c14e477cb11e87cac616ceb940669663e39b8081%2Fimage.png?alt=media" alt="Add Product form in the Monetization tab"><figcaption><p>To create a product click the "+ Add Product" button on the Monetization tab of your game's Creator Portal</p></figcaption></figure>

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-e6392255f1c97f0c51da4ac8aeab7d7a8177ef4e%2Fimage.png?alt=media" alt="Product form with the consumable option clear"><figcaption><p>If the consumable box is not checked this will be a "Game Pass"</p></figcaption></figure>

{% hint style="warning" %}
Uploading product images is currently restricted to admins only. Please reach out on [Developer Support](/going-all-out/developer-support) to upload thumbnails
{% endhint %}

### Understanding the products list

The Monetization page shows your products and their activity for the selected date range:

* Product names, prices, and IDs. Copy a product ID to use it in your [purchase code](/core-engine-concepts/purchasing-product-apis).
* Sales and Spark spending history.
* **Payout-eligible** and **Ineligible** Spark spending. Only the eligible portion contributes to the game's withdrawal balance.
* **Grant Failures**, so you can find purchases whose in-game reward still needs to be delivered.

The **Game Spark balance** shows your remaining tracked eligible earnings after refunds and previous withdrawals. It covers the tracking period independently of the report's date filter.

Select **Withdrawal** to transfer eligible earnings to the creator or a current editor. The recipient receives 70%, rounded down to whole Sparks. To exchange received Sparks for money at $1 USD per 100 Sparks, contact the All Out team. See [Spark Withdrawals](/monetization/spark-withdrawals) for fees, permissions, refunds, and cash-out details.

#### Purchase Handlers / Grant Failures

When a user buys a product in your game, we run the `ao_purchase_handler` function you've defined in your game code.

Your purchase handler will normally grant an item or feature to a player based on which product ID they bought. Return `true` only after the grant succeeds.

If your purchase handler fails—for example, if their inventory was full and you didn't handle that scenario—return `false`. It will count as a "grant failure" and be displayed in the Products page on the Creator Portal so you can fix the issue.

Grant failures will be **automatically retried each time the player joins until they succeed.** Handlers must be safe to run again and must not partly grant a reward before returning `false`.

If your purchase handler was successful, but for some reason the user lost the item due to a game bug or otherwise, you can **re-run** the purchase handler for that user by marking the purchase as ungranted in the [Editing/Viewing Player Data](/data-and-persistence/editing-viewing-player-data) "Purchases" section. Confirm that replaying the handler will not duplicate the reward first.

To try your purchase handler without spending Sparks, buy the product in a game launched from the editor. See [Testing purchases](/core-engine-concepts/purchasing-product-apis#testing-purchases).


# In Game Ads

All Out allows developers to show players advertisements in their game

### Rewarded

Rewarded ads are optional ads players can watch to earn something in your game. For example, you could have a Prize Wheel that players spin by watching an ad to have a chance of earning extra coins or items in your game.

#### Creator earnings

Your game earns **2 Sparks for each completed mobile rewarded ad** once All Out receives the ad provider's verified completion. This includes rewarded ads requested by your game and ads watched to redeem an ad-enabled game product. The Sparks are added automatically to your **Game Spark balance**, separately from the reward you give the player.

Open your game's **Monetization** page to see **Ad earnings**, completed ads and daily ad Sparks. Use the same date filters as your product reports. Ad earnings contribute to the game balance even if you have no paid products.

These are gross earnings before the normal withdrawal fee. For example, 10 completed ads earn 20 Sparks; withdrawing those 20 sends 14 Sparks to the recipient after fees. See [Spark Withdrawals](/monetization/spark-withdrawals).

Credits begin with verified completions received after this feature is enabled. Earlier ad views are not backfilled. Browser ads, interstitials, draft games and All Out's global daily-wheel ads do not earn these game credits.

#### API Reference

```go
Ads :: struct {
    prompt_rewarded_ad :: proc(
        player: Player,
        reward_id: string,
        title: string,
        description: string,
        texture: Texture_Asset
    );

    is_ad_showing :: proc() -> bool;
}
```

Use a stable `reward_id` for each reward. Grant the reward only from `ao_ads_reward_handler`, which runs after the platform reports a completed rewarded ad:

```go
show_coin_ad :: proc(player: Player) {
    if Ads.is_ad_showing() return;

    icon := get_asset(Texture_Asset, "ui/coin.png");
    Ads.prompt_rewarded_ad(
        player,
        "coins_100",
        "Earn 100 coins",
        "Watch an ad to receive 100 coins.",
        icon
    );
}

ao_ads_reward_handler :: proc(player: Player, reward_id: string) -> bool {
    if reward_id != "coins_100" return false;

    Economy.deposit_currency(player, "Coins", 100);
    return true;
}
```

Register the currency before using this example; see [Economy](/data-and-persistence/economy). Return `true` only after the reward was granted. Return `false` for an unknown ID or when the grant could not be completed.

### Interstitial

Interstitial ads are shown between rounds or during periods of low activity in your game to generate revenue, but do not directly reward players.

For example you could show an interstitial after each round of Murder Mystery while players would be waiting for the next round to start anyways.


# Publishing Your Game

### Publishing Checklist

Treat this as a “release candidate” checklist. Aim to check every box.

#### Game readiness

* [ ] Your game has a complete and substantial core loop
* [ ] The win/lose (or end) condition is obvious
* [ ] New players can understand the goal in under 30 seconds
* [ ] You have onboarding: tutorial, signs, NPC dialog, or a first-time flow
* [ ] You have a default session length target (short/medium/long) and it feels right

#### Multiplayer + stability

* [ ] You tested with multiple players (not just solo)
* [ ] Late-join works (joining an in-progress game)
* [ ] Disconnect/reconnect doesn’t break a session
* [ ] Players can’t soft-lock themselves (stuck, no inputs, no escape)
* [ ] Spawns/respawns are safe and consistent

#### Performance + feel

* [ ] The game loads quickly enough for streaming audiences
* [ ] There are no obvious frame drops in busy moments
* [ ] Camera and controls feel consistent across the whole game
* [ ] Audio levels are reasonable (no clipping, no extreme volume jumps)

#### Art + clarity

* [ ] Assets and textures look cohesive
* [ ] UI text is readable at typical play distances
* [ ] Key interactables are visually distinct from background props

#### Metadata (what players see before they join)

* [ ] Title + subtitle describe the game in one sentence
* [ ] Loading message explains the core loop or controls
* [ ] Thumbnail/preview is accurate and not misleading
* [ ] You have a short “streamer pitch” (one-liner) ready to paste anywhere

{% hint style="info" %}
Need help with store-facing assets? See [Uploading Metadata](/creator-portal-web/uploading-metadata).
{% endhint %}

#### Community Standards

Games on All Out must be generally appropriate for all ages.

* [ ] Your game must be free from extreme realistic violence/blood, nudity, or romantic content
* [ ] Games cannot claim to offer players sparks or other paid virtual currencies
* [ ] Games cannot reference drugs or alcohol
* [ ] Text in games should be free from all profanity

#### Rights and attribution

* [ ] You have the rights to everything you ship (art, audio, brands, logos)
* [ ] You follow any attribution requirements for external assets (if any)

We reserve the right to remove games at any time, for any reason.

### Publishing flow

{% stepper %}
{% step %}

#### 1) Save Your Project

If you have a [Git repository](/going-all-out/collaborating-and-version-control), we recommend creating a commit before publishing, so you'll be able to track what's changed between releases.
{% endstep %}

{% step %}

#### 2) Click the "Upload" button in the top right of the editor

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-593802b689c1476183b9936e78c9d91db798f7e3%2Fimage.png?alt=media" alt="Editor upload dialog with the Set Active Now option"><figcaption></figcaption></figure>

**Set Active Now** makes the uploaded version active immediately. Clear it to upload a version without activating it.
{% endstep %}

{% step %}

#### 3) Monitor Your Launch

After you publish a new version, monitor active servers and check [Game Error Monitoring](/creator-portal-web/game-error-monitoring)

Use [Managing Servers/Updates](/creator-portal-web/managing-servers-updates) to rematchmake or stop servers if needed.
{% endstep %}

{% step %}

#### 4) Ship improvements fast

Check player feedback and make adjustments!

Players can leave feedback via [Feedback/Ratings](/creator-portal-web/feedback-ratings).
{% endstep %}
{% endstepper %}

### After you publish

* Hop in and hang out with your community!
* Check for player [Feedback/Ratings](/creator-portal-web/feedback-ratings) and make adjustments
* Reach out if you need any help! See [Developer Support](/going-all-out/developer-support).

If you're publishing an update to your game, decide if you want to use the rematchmaking feature to automatically send players to updated servers with [Managing Servers/Updates](/creator-portal-web/managing-servers-updates). Activating a version changes new sessions; existing sessions continue until they end, rematchmake, or are stopped.


# Collaborating & Version Control

All Out is flexible and allows you to use any version control system you'd like, but we recommend uploading your project to GitHub!

There are a number of reasons to use a version control tool like Git as you develop your game:

* Easily backup and revert to earlier changes
* Work together with other developers on your team
* Easily open your project on other devices

If you're unfamiliar with Git/GitHub, there is a great video tutorial series here: <https://www.youtube.com/watch?v=8Dd7KRpKeaE>

To get started, create a GitHub account and a repository and then authorize the All Out app to use that repository.

You can authorize by heading to the Collaborate page on your game and clicking the manage GitHub connection option.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2FzVfuRMF00sqr3vb13ECH%2Fimage.png?alt=media&amp;token=5a2fa136-aa9b-4f12-b701-11bef9d00c4b" alt="Manage GitHub connection control on the Collaborate page"><figcaption></figcaption></figure>

Then you're ready to select the repository you made earlier back on your game's collaborate page:

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2F42e5SIuLZp61D3gdGpqL%2Fimage.png?alt=media&amp;token=3b73cd31-634a-4f62-82e8-669998fc80d4" alt="Repository selector on the Collaborate page"><figcaption></figcaption></figure>

When you link a Git repo, the integration can clone it into an editor's games folder and use branches or pull requests to review changes. Repository access is controlled by the GitHub installation and repository permissions, so make sure each collaborator has the access they need.

**You can also install GitHub's desktop client to manually manage your Git changes:**

Their desktop client is pretty easy to use!

1. Download GitHub Desktop: <https://desktop.github.com/download/>
2. Add the existing repository, or create a repository in the game project folder. ![Adding an existing project folder in GitHub Desktop](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-9f8b8ac64e54b118ee32e4675782032720cdd0c5%2Fimage.png?alt=media)
3. Create an initial commit and publish your repository to GitHub!


# Handling Engine/Platform Updates

Most updates require no changes from you as a developer. However, updates that affect the network or CSL language, called "Protocol Updates", will require changes.

### Protocol Updates

#### Release Frequency

Since CSL is new we're currently **releasing protocol updates roughly each month** to rapidly integrate your feedback and improve performance.

#### Release Notes

We will publish "work in progress" release notes on the [Changelog](https://docs.allout.game/changelog/) as we prepare the next protocol update for release.

#### Preparing Your Game

As we get closer to releasing a protocol update, we will communicate with devs in the #game-dev channel of our Discord

### Testing Protocol Updates

All Out allows you to switch your game to "Nightly" mode which will allow you to preview protocol updates for local development and staging copies of your game before the update goes live.

To do so, change the "Platform Version" selector on the Create page of the site to "Nightly" and relaunch the editor.

{% hint style="warning" %}
Do not publish from nightly to production versions of your game, players will not be able to join.
{% endhint %}


# Developer Support

All Out is new, and we're super excited to hear what you think and offer support if we can help!

### Discord

You can [join our community Discord](https://discord.com/invite/RZcyXamfue) and participate in the community developers channel. We're there too to help!

If you've published a game you can also ping a moderator/admin to receive access to the #game-dev channel! We're available regularly to help troubleshoot and improve your game.

Additionally, you'll have access to the #share-your-game channel at launch to help promote what you've built and get feedback from players!

### Email

If you prefer email, don't hesitate to reach out to <help@allout.game> and we'll do our best to respond within 24 hours.


# Improving Performance

### Loading Performance

Game load times are primarily determined by the size of the assets in your project.

* Mipmaps (1/8 resolution versions of your textures) are included in the initial game payload
* Textures used by Sprite\_Renders with "Wait for Load" checked will block loading waiting for high resolution versions (should only be used for spawn areas)
* Other high resolution textures are streamed and loaded after join

{% hint style="warning" %}
We currently include mipmaps of every asset in your res directory even if you don't include it in the scene which can affect load performance if you have large unused assets.
{% endhint %}

### Client Performance

You can press **F3** at any time to open a client profiler. Check the "Capturing" checkbox to start recording and uncheck it to pause and review.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-dd50599a97abf44ea6ecee7fef7d6efba62c6931%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

On gaming PCs we recommend targeting \~2ms total frame times. This will give you headroom for low end mobile devices which will typically be 4-6x slower.

### Network Performance

Basically: reduce the number of entities in your scene doing stuff. We'll add more guides here soon :D


# Uploading Metadata

Investing time in how your game appears on the All Out platform can be the difference between good metrics and great metrics!

### Titles, Subtitles, and Loading Messages

Configure these on the matchmaking page and the base manage game page.

### Thumbnails and Previews

You can upload appropriate thumbnails, carousel images, and preview videos from the "Manage Game" section of the Create page on web or mobile.


# Managing Servers/Updates

You can view all the servers/instances of your game currently running from the "Servers" tab of the Creator Portal.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-50385245b32463ebcf487071caf3366237a4fc13%2Fimage.png?alt=media" alt="Running game servers listed in the Creator Portal"><figcaption></figcaption></figure>

### Releasing Updates/Rematchmaking

When you upload a new version of your game with **Set Active Now** enabled, All Out will automatically route new players to the latest version. Existing servers keep running their current version until they end or you take an action on them.

You can view the currently active version of your game and the time it was published from the main creator portal page:

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-64f282cd680b157521460fe92cae46b920c02470%2Fimage.png?alt=media" alt="Active game version and rollback controls"><figcaption><p>You can one-click rollback to prior versions of your game</p></figcaption></figure>

A previous version can be made active again with one click. This changes the version used by new sessions, but does not move players who are already on a server.

If you're releasing an important update and you'd like to shift all players to the new version immediately, press **Rematchmake Old Servers**.

Players on old servers will re-enter matchmaking for the active version. Clients may reconnect or reload, and session-only state may not carry into the new server, so test the rematchmaking flow before using it during a large release.

### Stopping Servers

If for any reason you need to shut off servers, press **Stop All Servers** or select **Stop Server** on a particular instance card. Stopping a server disconnects its players and can discard state that has not been saved to persistent storage.


# Feedback/Ratings

Players can leave feedback you can read to help improve your game.

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-9be204463cfcb1a8650114f5caed225687e332aa%2Fimage.png?alt=media" alt="Feedback section in the Creator Portal"><figcaption></figcaption></figure>

### Ratings

After players try your game, they'll have the option to leave a thumbs up or thumbs down rating. The percentage of ratings that are upvotes calculates your game's rating.

![Thumbs-up rating percentage displayed for a game](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-e0324857295417d0d508f941d13f58a107dd07ab%2Fimage.png?alt=media)

### Feedback

When players leave negative reviews they'll be prompted to add feedback explaining their rating. That feedback is visible to you in the Feedback section of the Creator portal.

Sometimes feedback can be quite harsh, so don't worry too much about negative comments, just use them as inspiration for things to improve.

#### Responding to Feedback

We recommend clicking the player's avatar to DM them if you need more information or to let them know you've made improvements based on their suggestion, maybe they'll give your game another try!


# Game Error Monitoring

Players will find clever ways to break your game! Be prepared to monitor for errors and make fixes!

All Out automatically collects error telemetry from clients & servers running your game and gives you access to it via the Errors tab on the create page of the site/app. Errors within your scripts are almost always issues you can easily fix with AI agents or by analyzing the line number referenced, however, some errors that show in this page may be engine side issues, in which case please reach out to us for help!

<figure><img src="https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-3fec7281cf463fa69c0161255665d68a22e67b74%2Ferror-reporting-dashboard.png?alt=media" alt="Errors tab showing game exceptions, engine errors, and crashes"><figcaption></figcaption></figure>


# Roblox

If you've built games on Roblox, there are some key differences you'll need to know to get started on All Out!

If you’re coming from Roblox Studio + Lua, this page will help you translate your mental model to All Out + CSL (All Out’s scripting language).

### The big differences (at a glance)

* **Entities + components (not Instances + Services)**: worlds are built out of entities in the [Hierarchy](/using-the-editor/hierarchy), and behavior comes from adding components (built-in or scripted).
* **Multiplayer is “built in”**: CSL is designed so most gameplay code can be written like singleplayer while the engine syncs relevant state.
* **Editor workflow is closer to “scene editing”**: you place entities, tweak their components in the [Inspector](/using-the-editor/inspector), and reuse groups via [Prefabs](/using-the-editor/prefabs).
* **CSL is statically typed**: it will feel closer to Go/Odin than Lua. (See [Getting Started with CSL](/scripting/syntax).)
* **Gameplay transforms are 2D**: position and scale use `v2`, rotation is one angle in degrees, and sprite layers control draw order.

***

### Roblox → All Out cheat sheet

| Roblox concept                         | All Out / CSL concept             | Notes                                                                                                                                                                             |
| -------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Roblox Studio**                      | **All Out Editor**                | You launch it from the Creator Portal (see [Launching The Editor](/getting-started/first-game)).                                                                                  |
| **Place / Experience**                 | **All Out Game project**          | Your local project folder includes a `scripts` directory for `.csl` files.                                                                                                        |
| **Explorer / Workspace**               | **Hierarchy**                     | The hierarchy shows all entities in the current world/scene.                                                                                                                      |
| **Instance / Model / Part**            | **Entity**                        | “Folder-like” organization is done by parenting entities (see “Folders” in [Hierarchy](/using-the-editor/hierarchy)).                                                             |
| **Properties window**                  | **Inspector**                     | Where you edit entity/component fields and add components.                                                                                                                        |
| **Attributes**                         | **Serialized component fields**   | Expose fields to the editor with `@ao_serialize` (example below).                                                                                                                 |
| **CFrame / Vector3**                   | **2D entity transform**           | Position and scale are `v2`; rotation is one angle in degrees.                                                                                                                    |
| **Clone()**                            | **`Scene.instantiate`**           | Load a `Prefab_Asset`, then instantiate it into the scene.                                                                                                                        |
| **Model reuse**                        | **Prefabs**                       | Linked instances preserve root properties, but component fields are not independent overrides (see [Prefabs](/using-the-editor/prefabs)).                                         |
| **ProximityPrompt / ClickDetector**    | **Interactable component**        | Add `Interactable`-derived component and implement `can_use`/`on_interact` (see [Interactables](/core-engine-concepts/interactables)).                                            |
| **Touched / Region3 / GetPartsInPart** | **Triggers / range queries**      | Use trigger callbacks for enter/exit behavior and `Scene.get_all_components_in_range` for broad sensing (see [Navmesh & Collision](/core-engine-concepts/navmesh-and-collision)). |
| **Tools / abilities / mobile buttons** | **Abilities**                     | Built-in mobile/PC friendly button UI + cooldown patterns (see [Abilities](/core-engine-concepts/abilities)).                                                                     |
| **DataStoreService**                   | **Save + JSON + economy systems** | See [Save System](/data-and-persistence/save) and [JSON Serialization](/data-and-persistence/json).                                                                               |
| **ScreenGui / GuiObjects**             | **All Out UI system**             | See [UI Fundamentals](/ui/fundamentals).                                                                                                                                          |

***

### Your first hour (recommended path)

1. Follow the “apple collection” walkthrough in [Building a Basic Game](/getting-started/testing-and-publishing).
2. Skim CSL basics in [Getting Started with CSL](/scripting/syntax) (especially `ao_start`, `ao_update`, and creating components).
3. Learn how to attach your new components to world objects via the [Inspector](/using-the-editor/inspector).

***

### Scripting mental model (Lua → CSL)

In Roblox you often write a `Script`/`LocalScript` that finds objects (`Workspace.Foo.Bar`) and connects events. In All Out, you typically:

* Write **component classes** in CSL (behavior).
* Attach them to entities in the editor (or spawn entities and add components at runtime).
* Store tunable values on the component with **`@ao_serialize`** (so you can tweak per-entity in the inspector).

Here’s a small example that feels like “a Script parented under an object”, but expressed as a CSL component:

```go
import "core:ao"

// Attach this component to an entity in the editor.
Coin_Pickup :: class : Interactable {
    @ao_serialize coins: s64;
    used: bool;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Collect");
        radius = 1.25;
        required_hold_time = 0.0;
    }

    can_use :: method(player: Player) -> bool {
        return !used;
    }

    on_interact :: method(player: Player) {
        if used return;
        used = true;

        Economy.deposit_currency(player, "Coins", coins);
        entity.destroy();
    }
}
```

Register the `Coins` currency during scene setup before depositing it. Global lifecycle procs such as `ao_start` and `ao_update` run in each scene simulation; they are not server-only. See [Game/Frame Lifecycle](/scripting/game-frame-lifecycle).

***

### Interactions you’ll recognize (and how they translate)

#### ProximityPrompt → Interactable

If you used `ProximityPrompt.Triggered` in Roblox, the closest equivalent is an `Interactable`-derived component. Start here:

* [Interactables](/core-engine-concepts/interactables)

#### “Touched” pickups / trigger zones

Roblox’s `BasePart.Touched` is a very common pattern for pickups, damage zones, and checkpoints. In All Out, add an enabled collider, set `is_trigger`, and assign `on_trigger_start`, `on_trigger_stay`, or `on_trigger_end`. The other collider is passed to the callback. A `Movement_Agent` is not required.

Use `Scene.get_all_components_in_range` when you need broad sensing or a distance check. Solid collision does not provide a general Roblox-style `Touched` callback.

See [Navmesh & Collision](/core-engine-concepts/navmesh-and-collision).

***

### Player actions (Tools / keybinds / mobile controls)

If you’re used to Roblox “tool” scripts or hand-rolled input + cooldown systems, use **Abilities** instead. They’re designed to work well on both mobile and PC with consistent UI.

* [Abilities](/core-engine-concepts/abilities)

***

### Saving data (DataStoreService → Save system)

For persistence (currencies, unlocks, progression, cosmetics), start with:

* [Save System](/data-and-persistence/save)
* [JSON Serialization](/data-and-persistence/json)
* [Economy](/data-and-persistence/economy)
* [Cross-Game Products/Data](/data-and-persistence/cross-game-products-data)

***

### UI (ScreenGui → All Out UI)

Roblox UI often starts with `ScreenGui` and `Frame/TextButton` hierarchies. In All Out, start here:

* [UI Fundamentals](/ui/fundamentals)
* [UIDoc Quick Start](/ui/uidoc-quick-start)

Draw player UI from that player's `ao_late_update` inside `is_local_or_server()`.

***

### Common porting gotchas for Roblox devs

* **No “workspace services” tree**: don’t look for `ReplicatedStorage`/`ServerScriptService` equivalents—use the editor’s hierarchy + your project’s `scripts` folder.
* **Don’t guard gameplay with `Game.is_server()`**: normal gameplay runs on the predicting client and authoritative server.
* **Prefab component fields aren’t independent overrides**: unlink the instance or author another prefab when copies need different component values.
* **Think “components” instead of “tags”**: where you’d use CollectionService tags, you’ll often add a marker/behavior component instead.
* **Remember the scene is 2D**: use renderer layers instead of a Z transform.

***

### Where to go next

* Build your first game: [Building a Basic Game](/getting-started/testing-and-publishing)
* Learn the editor basics: [Introduction/Scene View](/using-the-editor/introduction-scene-view)
* Learn scripting basics: [Getting Started with CSL](/scripting/syntax)
* Learn core gameplay building blocks: [Interactables](/core-engine-concepts/interactables), [Inventory](/core-engine-concepts/inventory), [Abilities](/core-engine-concepts/abilities)


# Unity

If you've built games with Unity, there are some key differences you'll need to know to get started on All Out!

***

If you're coming from Unity, you'll feel at home with **entities + components**, a **hierarchy**, an **inspector**, and reusable **prefabs**. The biggest mindset shift is that All Out games are **multiplayer-first**: normal gameplay runs on the predicting client and authoritative server, while the engine syncs supported state for you.

## TL;DR: What’s different vs Unity

* **Multiplayer is the default**: you generally **do not** write RPCs, SyncVars, or Netcode spawning logic.
* **Write one gameplay path**: don't guard normal gameplay with `Game.is_server()` or you'll disable prediction.
* **Be intentional about player UI**:
  * Draw a player's gameplay UI from that player's `ao_late_update` inside `is_local_or_server()`.
  * Use `is_local()` only for player-specific visual overrides.
* **Avoid global singleton state**: multiple players connect to the same session. Prefer storing state on the player or on world components.
* **All Out gameplay is 2D**: position and scale use `v2`, while rotation is one angle in degrees.
* **Mobile-first**: avoid keyboard-only assumptions unless your game explicitly targets PC.

## Concept mapping (Unity → All Out)

| Unity                     | All Out                                         |
| ------------------------- | ----------------------------------------------- |
| Scene                     | Scene (world)                                   |
| GameObject                | Entity                                          |
| Transform                 | 2D entity transform (position/rotation/scale)   |
| Component / MonoBehaviour | Component (CSL class deriving from `Component`) |
| Prefab                    | Prefab asset (created in-editor)                |
| Hierarchy window          | [Hierarchy](/using-the-editor/hierarchy)        |
| Inspector window          | [Inspector](/using-the-editor/inspector)        |
| `Instantiate(prefab)`     | `Scene.instantiate(prefab_asset)`               |
| `Start()` / `Update()`    | `ao_start` / `ao_update(dt)` lifecycle          |

## Project layout: where “scripts” and “assets” live

* **Scripts**: Your game code lives in `.csl` files. New projects start with a `main.csl` that imports the engine and defines lifecycle entry points. See [Getting Started with CSL](/scripting/syntax).
* **Assets**: Game assets live under your project’s `/res` directory and are referenced by path **without** the `/res` prefix (example: `"ui/button.png"`). See [Assets and Resources](/core-engine-concepts/assets-and-resources).

### Imports (important difference)

In Unity, each C# script is compiled and can use its own `using` directives. In All Out, keep imports centralized:

* Import `"core:ao"` in `main.csl`
* If you add a folder (like `ui/`), import the folder **once** in `main.csl`
* Avoid adding imports in other files

Example:

```go
// main.csl
import "core:ao"
import "ui" // optional: brings all files under /scripts/ui into scope
```

## Lifecycle: MonoBehaviour → CSL

In Unity you typically attach a `MonoBehaviour` to a GameObject and implement:

* `Start()` / `Awake()`
* `Update()` / `LateUpdate()`
* `OnDestroy()`

In CSL, you’ll commonly use:

* Global procs in `main.csl` (for game-level setup)
* Component lifecycle methods on your components

Example component:

```go
// orbiter.csl
Orbiter :: class : Component {
    center: v2;
    radius: float;
    speed: float;
    angle: float;

    ao_start :: method() {
        center = entity.local_position;
        radius = 2.0;
        speed = 1.0;
        angle = 0.0;
    }

    ao_update :: method(dt: float) {
        angle += speed * dt;
        offset := v2{cos(angle) * radius, sin(angle) * radius};
        entity.set_local_position(center + offset);
    }
}
```

Global lifecycle procs run in each participating scene simulation; they are not server-only. Late-joining clients may receive a component whose start lifecycle already ran, so rebuild presentation from synchronized state in `ao_on_state_sync` when needed. See [Game/Frame Lifecycle](/scripting/game-frame-lifecycle).

## Prefabs: Unity Prefabs → All Out Prefabs

All Out prefabs are created in the editor and can be reused or spawned at runtime.

* **Create**: see [Prefabs](/using-the-editor/prefabs)
* **Spawn at runtime**:

```go
spawn_enemy :: proc() {
    prefab := get_asset(Prefab_Asset, "Enemies/BasicEnemy.prefab");
    e := Scene.instantiate(prefab, {10, 5});
}
```

{% hint style="info" %}
Linked instances preserve root properties, but component-field changes inside an instance are not independent overrides. See [Prefabs](/using-the-editor/prefabs).
{% endhint %}

## “Serialized fields” (Inspector-exposed variables)

Unity uses `[SerializeField]` and public fields to expose values in the Inspector. In CSL, use `@ao_serialize` to expose a field to the editor.

```go
Damage_On_Touch :: class : Component {
    @ao_serialize damage: int = 10;
}
```

Then add your component to an entity in the [Inspector](/using-the-editor/inspector) and tweak values per entity.

## Spawning and querying: Instantiate/Find → Scene APIs

Unity patterns:

* `new GameObject()` / `Instantiate()`
* `FindObjectOfType<T>()`, `GetComponentsInChildren<T>()`

All Out patterns:

```go
// Create and destroy entities
e := Scene.create_entity();
e.set_local_position({0, 0});
e.destroy();

// Iterate entities (when you truly need "everything")
for e2: entity_iterator() {
}

// Iterate components of a specific type
for player: component_iterator(Player) {
}
```

## Collision & triggers

An enabled collider with `is_trigger` set can call `on_trigger_start`, `on_trigger_stay`, and `on_trigger_end`. Each callback receives the trigger collider and the other collider. A `Movement_Agent` is not required.

CSL does not expose a general solid-contact callback equivalent to `OnCollisionEnter`. Use a trigger for enter/exit behavior. For broad sensing, query nearby components:

```go
nearby: [..]Pickup;
Scene.get_all_components_in_range(entity.local_position, 2.0, ref nearby);

for p: nearby {
    // check distance / apply effect / etc.
}
```

See [Navmesh & Collision](/core-engine-concepts/navmesh-and-collision).

## Multiplayer mindset: inputs, UI, and “where code runs”

In Unity you can often assume “my client owns my character”. In All Out, write gameplay once for the shared predicted path:

* **Gameplay input/UI**: draw it from the player in `ao_late_update` inside `is_local_or_server()`.
* **Player-specific visual overrides**: use `is_local()`.

See [Networking Fundamentals](/scripting/networking-fundamentals).

## Common Unity-to-All-Out gotchas

* **Singleton managers**: prefer per-player or per-entity state instead of global `GameManager` style singletons.
* **Import habits**: import engine and folders from `main.csl` (don’t scatter imports across many files).
* **Server guards**: don't wrap normal gameplay in `Game.is_server()`.
* **3D transforms**: decide how the design maps into a 2D scene and use renderer layers for draw order.
* **Assuming singleplayer**: always think “what happens with 10 players connected?”
* **Hardcoded desktop input**: avoid requiring keyboard/mouse unless intentional.

## Where to go next

* [Getting Started with CSL](/scripting/syntax)
* [Hierarchy](/using-the-editor/hierarchy)
* [Inspector](/using-the-editor/inspector)
* [Prefabs](/using-the-editor/prefabs)


# Unreal

If you've built games with Unreal, there are some key differences you'll need to know to get started on All Out!

***

This guide is for Unreal Engine developers (Blueprints / C++) transitioning to **All Out** and **CSL** (All Out’s custom scripting language).

## The big shift (mental model)

In Unreal you often think in terms of:

* **Actors in a World** (spawned, replicated, owned)
* **Components** attached to Actors
* **Blueprint graphs / C++** driving gameplay
* **RPC + Replication** you author explicitly

In All Out you’ll usually think in terms of:

* **Entities in a Scene** (with Components)
* **Components** (engine-provided + your own CSL components)
* **Abilities** for player actions (mobile-first UI + cooldown + aiming)
* **Shared predicted gameplay** where state sync is automatic (no custom RPC plumbing for most gameplay)
* **2D transforms** using `v2` position and scale plus one rotation angle

## Quick mapping: Unreal → All Out / CSL

| Unreal                                    | All Out / CSL                                               | Notes                                                                             |
| ----------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `UWorld` / Level                          | `Scene`                                                     | Entities exist in the scene; you can also create/destroy them at runtime.         |
| `AActor`                                  | **Entity**                                                  | Entities have 2D transforms and components.                                       |
| Actor location                            | `Entity.local_position` / `world_position`                  | Decide how Unreal X/Y/Z maps into the game's 2D X/Y plane.                        |
| Actor rotation                            | `Entity.local_rotation`                                     | All Out uses one rotation angle in degrees.                                       |
| Actor scale                               | `Entity.local_scale`                                        | All Out uses `v2`; there is no gameplay Z scale.                                  |
| `UActorComponent`                         | `Component`                                                 | You author gameplay by writing CSL components and attaching them to entities.     |
| `BeginPlay`                               | `ao_start`                                                  | Component lifecycle entry point.                                                  |
| `Tick(float DeltaTime)`                   | `ao_update(dt)` / `ao_late_update(dt)`                      | Use late update for UI/input patterns used by the engine (e.g., ability buttons). |
| Blueprint graphs                          | CSL code                                                    | Text-based, compiled as part of your project.                                     |
| Pawn/Character                            | `Player_Base` subclasses                                    | Your player logic usually lives on a `Player` component/class.                    |
| Input mappings                            | Abilities + keybinds                                        | Mobile-first: prefer ability buttons over raw input.                              |
| Replication (`Replicated` vars)           | Automatic state sync                                        | Write one predicted gameplay path instead of separate client/server versions.     |
| RPCs (`Server`, `Client`, `NetMulticast`) | Usually not needed                                          | Use engine facilities (e.g., notifications) instead of custom RPC sprawl.         |
| Actor spawning                            | `Scene.create_entity()` / `Scene.instantiate(Prefab_Asset)` | Prefabs are assets and can be instantiated.                                       |
| `UAsset` references                       | `get_asset(...)`                                            | Assets live under `/res` and are referenced by path.                              |

## Your first CSL file (imports)

CSL uses a single “root” import pattern: import in `main.csl`, and don’t sprinkle imports across every file.

```go
// main.csl
import "core:ao"
import "ui"   // if you create a /ui folder and want it in scope
```

## Entities & components (vs Actors & Components)

### Creating an entity at runtime

```go
entity := Scene.create_entity();
entity.set_local_position({10, 20});
entity.set_local_scale({2.0, 2.0});
entity.set_local_rotation(0);
```

### Adding and accessing components

```go
my := entity.add_component(My_Component);
sprite := entity.get_component(Sprite_Renderer);

// Destroy entity (and its components)
entity.destroy();
```

### Writing a custom component (life cycle)

```go
Spinner :: class : Component {
    speed: float;

    ao_start :: method() {
        speed = 1.0;
    }

    ao_update :: method(dt: float) {
        entity.set_local_rotation(entity.local_rotation + speed * dt);
    }
}
```

Global lifecycle procs run in each participating scene simulation; they are not server-only. Late-joining clients may receive a component whose start lifecycle already ran, so rebuild presentation from synchronized state in `ao_on_state_sync` when needed. See [Game/Frame Lifecycle](/scripting/game-frame-lifecycle).

### Iterating entities/components

```go
for e: entity_iterator() {
    // ...
}

for p: component_iterator(Player) {
    // ...
}
```

## Player actions: use Abilities (instead of raw input)

Unreal projects often start with input bindings (Enhanced Input) and then build UI/UX on top. In All Out, **Abilities** are the default way to implement player actions with:

* A consistent **mobile-friendly** button UI
* Cooldowns
* Optional aiming (drag-to-aim on mobile, mouse aim on PC)

Draw ability buttons from `Player.ao_late_update` inside `is_local_or_server()`:

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            draw_ability_button(this, Shoot_Ability, 0);
            draw_ability_button(this, Dodge_Roll, 1);
        }
    }
}
```

See: [Abilities](/core-engine-concepts/abilities) for the full API and patterns.

## Networking: “replication” is not your job (most of the time)

### What’s different from Unreal replication

* Normal gameplay runs on the predicting client and authoritative server.
* You generally **do not write RPCs** for standard gameplay flows.
* You must still design with **multiple players** in mind: avoid global state; store per-player state on the player instance.
* Do not guard normal gameplay with `Game.is_server()` or you'll disable prediction.

### Player UI vs local visual overrides

Use these patterns:

* `is_local_or_server()` for a player's **inputs + gameplay UI**
* `is_local()` only for **player-specific visual overrides**

```go
Player :: class : Player_Base {
    ao_late_update :: method(dt: float) {
        if this.is_local_or_server() {
            // inputs + gameplay UI
        }
        if this.is_local() {
            // visual overrides only this player should see
        }
    }
}
```

See [Networking Fundamentals](/scripting/networking-fundamentals).

## Assets, prefabs, and paths

### The `/res` folder

Assets are in `/res`. When referencing assets, **omit `/res`** from the path.

```go
button := get_asset(Texture_Asset, "ui/button.png");
click := get_asset(SFX_Asset, "sfx/click.wav");
font := get_asset(Font_Asset, "$AO/fonts/Barlow-Black.ttf"); // engine assets
```

### Prefabs

Prefabs are assets and can be instantiated through `Scene`:

```go
prefab := get_asset(Prefab_Asset, "Enemies/Slime.prefab");
enemy_entity := Scene.instantiate(prefab);
```

## Collision & overlap events (common Unreal pitfall)

An enabled collider with `is_trigger` set can call `on_trigger_start`, `on_trigger_stay`, and `on_trigger_end`. Each callback receives the trigger collider and other collider. A `Movement_Agent` is not required.

CSL does not expose a general solid-hit callback equivalent to `OnHit`. Use a trigger for gameplay overlap behavior. For broad sensing, query nearby components:

```go
nearby: [..]Pickup;
Scene.get_all_components_in_range(player_pos, 2.0, ref nearby);
for p: nearby {
    // ...
}
```

See: [Navmesh and collision](/core-engine-concepts/navmesh-and-collision).

## UI differences (UMG vs CSL UI)

All Out games are **mobile-first**, so avoid building keyboard-only UX. Use the engine’s UI utilities and ability buttons.

For custom UI, start with [UI Fundamentals](/ui/fundamentals) or [UIDoc Quick Start](/ui/uidoc-quick-start).

## Match flow, inventory, interactables

If you’re looking for equivalents to common Unreal gameplay systems:

* **Interactables**: [Interactables](/core-engine-concepts/interactables)
* **Inventory**: [Inventory](/core-engine-concepts/inventory)
* **Matchmaking / hub games**: [Matchmaking / hub games](/core-engine-concepts/matchmaking-hub-games)
* **Movement agents / NPCs**: [Movement agents / NPCs](/core-engine-concepts/movement-agents-npcs)
* **Spine animations** (if you used Paper2D / flipbooks): [Spine](/core-engine-concepts/spine)

## Unreal-to-CSL “gotchas”

* **Don’t build a custom RPC/replication layer**: start with the shared predicted gameplay path and let the engine sync state.
* **Don't guard gameplay with `Game.is_server()`**: normal gameplay must also run on the predicting client.
* **Avoid global singletons for gameplay state**: multiple players connect; store state on the player or the relevant component instance.
* **Imports are centralized**: import folders once from `main.csl`, not per-file.
* **Prefer Abilities for actions**: it solves mobile UX + cooldowns + aiming consistently.
* **Map 3D designs into 2D**: use renderer layers instead of a Z transform.

## Next steps

* Read [Abilities](/core-engine-concepts/abilities)
* Read [Navmesh and collision](/core-engine-concepts/navmesh-and-collision)
* Skim [Interactables](/core-engine-concepts/interactables) and [Inventory](/core-engine-concepts/inventory)


# Windows Defender Exclusion

The editor creates and scans many project files. On some systems, Microsoft Defender real-time scanning can make those operations noticeably slower. Measure the problem before adding an exclusion; most projects do not need one.

{% hint style="warning" %}
An exclusion bypasses real-time malware scanning for that path. Exclude only the narrowest trusted project or generated-cache folder that shows the slowdown. Do not exclude your entire games directory, Downloads folder, repository collection, or any location containing untrusted projects.
{% endhint %}

{% stepper %}
{% step %}

#### 1) Open Windows Security

Open **Windows Security** from the Start menu (or search for it).

![Windows Security in the Start menu](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-cf6a83e7aa8fe8627f6b566699eada7b386020f9%2Fimage.png?alt=media)

*Windows Security app.*
{% endstep %}

{% step %}

#### 2) Go to Virus & threat protection

From the left sidebar, select **Virus & threat protection**.

![Virus and threat protection entry in Windows Security](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-f5d51aeda3f1d95c009757e29d7bf136f60e1bc3%2Fimage.png?alt=media)

*Virus & threat protection entry.*
{% endstep %}

{% step %}

#### 3) Open Virus & threat protection settings

Scroll to **Virus & threat protection settings**, then click **Manage settings**.

![Manage settings under Virus and threat protection settings](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-1981769ec0af530cac98436ff608508cf2fbb2f4%2Fimage.png?alt=media)

*Manage settings button.*
{% endstep %}

{% step %}

#### 4) Open exclusions

Scroll to **Exclusions**, then click **Add or remove exclusions**.

![Add or remove exclusions control](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-d6be9b2e0b6f7a7611a4acf1d2f9a869655c5f46%2Fimage.png?alt=media)

*Add or remove exclusions.*
{% endstep %}

{% step %}

#### 5) Add a new exclusion

Click **Add an exclusion**.

![Add an exclusion menu](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-21e10ecf1e918d51ab2189781f7adbdbde3121ff%2Fimage.png?alt=media)

*Add an exclusion dropdown.*
{% endstep %}

{% step %}

#### 6) Choose “Folder”

Select **Folder**.

![Folder option in the Add an exclusion menu](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-39899941d0934ca65aa193f811be5f0f5b72d1c2%2Fimage.png?alt=media)

*Choose a folder exclusion only after measuring a scanning-related slowdown.*
{% endstep %}

{% step %}

#### 7) Pick the narrowest affected folder

Select one trusted project's folder, or only the generated subfolder identified by your performance investigation. Do not select the parent directory that contains every project.

![Folder picker for a narrow project exclusion](https://3803321901-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FzA8RGUKJ88fD0oXVALlz%2Fuploads%2Fgit-blob-8e4fce155effe8c6b11a948105caed01dfc5513d%2Fimage.png?alt=media)

*Confirm the folder selection.*
{% endstep %}
{% endstepper %}

## Removing the exclusion

Return to **Windows Security → Virus & threat protection → Manage settings → Add or remove exclusions**. Select the exclusion and choose **Remove**. Remove it after troubleshooting, when the project is no longer active, or before placing third-party files in that path.

For managed devices and current Windows-specific details, see Microsoft's [Defender Antivirus exclusion guidance](https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-exclusions-configure).


# C# Documentation

New All Out projects use CSL. C# project documentation remains available at [all-out-games.github.io/allout](https://all-out-games.github.io/allout/). Do not use those examples as CSL API reference.


# Changelog

New updates and improvements

{% updates format="full" %}
{% update date="2026-09-22" %}

## Protocol 48 - September 22nd, 2026

Scheduled for September 22nd. Protocol 48 adds managed tutorials, named persistent inventories, saved item references, funnel analytics, and new CSL language features. It also reduces runtime memory and simulation overhead and expands the editor's world-building and animation tools.

These notes cover the changes since protocol 47, including fixes and editor/client improvements delivered during the 47.x update cycle.

<details>

<summary>Compatibility and Migration</summary>

* CSL `Player_Base.is_admin()` has been replaced by `is_game_owner()`, `is_game_editor()`, and `is_owner_or_editor()`. CSL chat commands without explicit permission annotations now default to the game owner and their team.
* The built-in player-list/leaderboard overlay has been removed. Its C# `PlayerList`, `Leaderboard`, and leaderboard-open APIs remain as obsolete no-ops. Build any game-specific player list in your own UI; persistent leaderboard storage is unaffected.
* CSL `Entity.inverse_model_matrix` is now a method: replace field reads with `entity.inverse_model_matrix()`.
* CSL `Entity.world_position`, `world_rotation`, `world_scale`, and `model_matrix`, plus `AO_Player.username_rect` and `full_nameplate_rect`, are now getter properties. Existing field-style reads keep working while the derived values are calculated on demand instead of dirtying synchronized state. Continue using `set_world_position()` for assignment.
* `Sprite_Renderer.layer` and `Spine_Animator.layer` now use `s64`; their C# `Layer` properties use `long`. CSL `Inventory_Draw_Options.hotbar_item_count`, `columns`, `rows`, and `force_select_hotbar_index` now use `int` instead of `s32`.
* Sprite renderer color is consistently named `color`: reflected/editor JSON uses `Sprite_Renderer.color`, and C# uses `Sprite_Renderer.Color`. Existing serialized `tint` fields still load, and C# `Tint` remains an obsolete alias.
* CSL `Spine_Instance.color_multiplier` and the promoted field on `Spine_Animator` are now `color`. C# `SpineInstance.ColorMultiplier` is now `Color`; old serialized field names and the obsolete C# alias remain supported.
* The `Entity.set_parent` argument is now `keep_world_transform`, or `keepWorldTransform` in C# and editor operations. Update named arguments; this preserves position, rotation, and scale. Editor operations still accept the previous argument spellings.
* `Type_Info.printable_name` has been removed. Use `type_info.id` for a printable type name.
* Taking a raw address with `&` requires `#unsafe`. Use `ref` for ordinary by-reference arguments.
* Procedure values can no longer be cast directly or treated as pointer-sized integers. Callback parameters that are stored or returned must be marked `#no_captures`; pass non-capturing procedures and explicit userdata to persistent callbacks such as UIDoc, shop, ordered-save, and Spine listeners.
* `UIK.card_begin` now takes typed procedure callbacks and `Object` userdata instead of `u64` values.
* Tutorial text-only `Tutorial_Step.message` / C# `TutorialStep.Message`, legacy `ReadText` steps, and Continue/Dismiss controls have been removed. Attach instructions to gameplay steps instead. C# tutorial and step IDs are now read-only after definition.
* Projects using earlier protocol 48 nightlies must rename `Save.save_item` / `get_item` to `save_item_reference` / `get_item_reference` (C#: `SaveItemReference` / `GetItemReference`) and create named inventories only during the owning player's startup callback. Existing saved references remain readable.
* Editor MCP `client_action` and `client_input` now reply in plain text: `ok`, the value read, or `Error: <reason>`. `includeDiagnostics: true` appends a compact input-delivery report. `server_confirmation: confirmed` is now `input_acked`, since acknowledgement does not guarantee that a handler ran.
* `Test.press_key`, `Test.release_key`, and `Test.tap_key` now take the `Input` enum instead of Sokol keycodes. Duplicate `Test` procedures were removed or consolidated, and inventory helpers use `""` for the local player's inventory.
* Added `Test.select_hotbar_slot(slot, inventory_id = "")`. Save, variable, component, entity, balance, and inventory assertions retry briefly; every test must execute at least one `Test.assert_*` call.
* `Test.use_ability` and ability-button clicks reject aimed touch abilities; use `Test.drag_ability`. Click helpers reject out-of-viewport and disabled controls, and focused text fields keep key input from gameplay.
* Editor MCP help and action names were consolidated under `client_action` / `client_input`; `inventory_count` is now `read_item_count`, `interact_with` is now `interact`, and remote asset search uses `asset_remote_search` with `relatedTo`.
* Unknown UIDoc binding attributes now fail compilation. Other unrecognized `data-*` attributes warn; bind `data-style-color` on a `currentColor` background layer for state-dependent backgrounds.

</details>

<details>

<summary>Tutorials and Guided Onboarding</summary>

* Added managed tutorial sequences in CSL and C# through `Tutorial.define` / `Tutorial.Define`, with per-player progress, persistent completion, revisioned definitions, and queued tutorials.
* Steps can guide players to a location or entity, wait for a UI click, check a gameplay condition, count reported events, or count successful ability uses. Dynamic target resolvers support targets created or replaced during play.
* Tutorials support start, restart, pause, resume, cancel, and status queries. Repeating Start preserves existing progress and completed runs; Restart explicitly begins a new run.
* Added world arrows and animated route trails, UI highlights, and ability-button glow and hand cues. Aimed abilities show a drag gesture on touch devices, and cues stop obscuring controls while the player interacts.
* Instructions and progress appear below the top HUD. CSL can use `bind_ui`, `get_view`, and the styleable UIDoc `<tutorial-panel>` element with optional Start/Restart controls; C# exposes matching view data and immediate-mode cue registration.
* Ability steps count successful gameplay actions. CSL abilities report success through `Tutorial.report_ability_used`; C# default ability activation reports it automatically. Optional completion predicates can require the intended gameplay result.
* Tutorial clicks and progression are checked against the active step and run, preventing stale acknowledgements from completing a restarted or different step.

</details>

<details>

<summary>Inventories, Item References, and Saving</summary>

* Added named player inventories through `Items.create_player_inventory(player, key, capacity)` and C# `Inventory.CreatePlayerInventory`. Create them during that player's `ao_start` / `Awake` on both client and server, then retain the returned inventory.
* Named inventories restore synchronously, save automatically, synchronize with joining players, and are cleaned up when their owner leaves. They persist independently of the default inventory's auto-save setting. Repeated startup calls with the same key return the existing inventory.
* Saved capacity is preserved when larger than the requested initial capacity. Existing default-inventory saves require no migration, and removing a named-inventory registration does not erase its saved data.
* Added `Save.save_item_reference` / `get_item_reference` and C# equivalents for items in any of the player's persistent inventories. References survive slot rearrangement but become invalid when an item is removed, dropped, destroyed, or transferred to another inventory, including another inventory owned by the same player.
* CSL `Save.set_json` / `try_get_json` supports `Item_Instance` and derived item fields using those same references. Missing, invalidated, or incompatible item references become `null`. Create named inventories before resolving references into them; default-inventory references require auto-save to be enabled.
* Added `Items.mark_item_dirty(item)` to persist direct changes to serialized CSL item fields, and `Item_Instance.entry_id_in_inventory` for inspecting an item's inventory-scoped identity.
* CSL integer saves now preserve the full `s64` range. Added `Save.set_float` / `get_float` convenience aliases.
* Player-data saving sends changed keys and retries failed saves. Editor Stop drains pending persistence work before shutdown, and player authentication/save state is protected from netsync races.
* Inventory loading better validates item types, duplicate or invalid slots, and capacities. Fixed custom item identity across script hotloads, inventory restoration using the wrong VM context, and invalid class references returned by `Items.draw_hotbar(...).selected_item`.

</details>

<details>

<summary>Analytics and Player Services</summary>

* Added CSL `Analytics.funnel_step(player, name, steps, step)` for gameplay milestones that appear in Studio. Steps are 1-based, with 2–20 ordered labels; keep the name and labels consistent across calls. Changing them starts a separate series while preserving old data.
* Funnel events are queued by live servers, with limits of 256 events per player session and 20 per second. Client and editor-play calls return `false`; a `true` result means queued, not confirmed delivery. Report each milestone once.
* Added `Server.transfer_player_to_server` and C# `Server.TransferPlayerToServer` to move a player to an existing instance of the same game, respecting access and capacity checks.
* Added `Purchasing.prompt_vip_purchase` / C# `PromptVIPPurchase` to open the local player's VIP purchase flow.
* Added `Player_Base.get_friend_referral_count()` and C# `Player.FriendReferralCount`. The count reflects recorded friend-code referrals at server join, independently of capped Spark bonuses.
* Added server-side `Push_Notifications.send_now` / `schedule` and C# `PushNotifications.SendNow` / `Schedule` for predefined game notifications. Recipients may be offline; scheduled times use UTC Unix seconds within the next 30 days, with one pending notification per game/player.
* Improved discovery, installation, and first-game attribution, client-version reporting, and tracking across server transfers.

</details>

<details>

<summary>CSL Language and Compiler</summary>

* Dynamic arrays support compound literals, including runtime expressions and class defaults: `values: [..]int = {1, 2, 3}`. Mutable class defaults receive independent backing storage.
* Added string concatenation with `+` and `+=`, and leading-dot float literals such as `.52` and `-.52`.
* Added multi-value and parallel assignment, including procedure results, swaps such as `a, b = b, a`, and bit-field destinations. Use `_` to discard a result. Right-hand expressions are evaluated left to right before destinations are resolved or written.
* Procedure literals can capture enclosing parameters and local variables when invoked immediately or passed directly as synchronous callbacks. Capturing literals cannot be assigned to variables, returned, or stored; unannotated callback parameters can only be invoked or forwarded directly.
* Added closure-friendly `Entity.add_component` and `Scene.instantiate` initialization callbacks without explicit userdata. They run synchronously before startup; `add_component` passes the concrete component type.
* Derived classes expose a read-only `base` view for parent method calls such as `base.method()`. Method lookup remains based on the receiver's static type.
* Class casts reject unrelated types at compile time and validate base-to-derived casts against the concrete object at runtime.
* Added `ceil`, `floor`, and `round` for `f32` and `f64`, with additional double-precision math overloads. `round` rounds halfway cases away from zero. Common scalar/vector math and RNG helpers now compile to dedicated VM instructions.
* Fixed vector/scalar compound assignments, constant vector arithmetic, and the two-point `in_range(a, b, distance)` calculation.
* Fixed cross-scope and polymorphic overload resolution, procedure-variable shadowing, inherited lifecycle hooks with overloads, procedure values in compound class defaults, and null checks on callback parameters.
* Improved diagnostics for malformed control flow, invalid initializer fields, unsupported exponent literals, missing semicolons, unresolved names, invalid primitive-member access, misplaced `ref` / `defer`, missing inventory-move arguments, and invalid closure lifetimes.
* Long wrapped `else if` chains no longer cause recursive compiler stack growth. Single unbraced `#unsafe` declarations retain their surrounding scope.

</details>

<details>

<summary>Gameplay, Multiplayer, and Audio</summary>

* Keyboard and IME text now travels with per-frame input and survives resimulation, so replicated UIDoc and immediate-mode text fields retain typed values on the server. Input is limited to 64 UTF-8 bytes per frame; longer pastes are truncated at a character boundary with a warning.
* UIDoc buttons activate on the server even when document text changes every frame. Controls under a later `consume=true` document now ignore the press during client prediction too.
* Added `has_*_reason(reason)` and `has_any_*_reason()` queries for freeze, invisibility, name visibility/offset, ghosting, movement-input disabling, joystick disabling, and emote blocking. Reasons remain counted: each add needs a matching remove. `is_emote_blocked()` is soft-deprecated.
* Added `Player.override_movement_input_for_next_step(input)`, including a zero-vector override. It applies for one movement step and can deliberately bypass disabled-input reasons. Direct use of `input_override` is soft-deprecated.
* Added `Spine_Bone_Transform`, `try_get_bone_local_transform` on CSL Spine instances/animators, and `Texture_Asset.get_world_size()` for placing attachment artwork.
* Added `SFX_Desc.unique_id` / C# `SFX.PlaySoundDesc.UniqueId` for stable sound-event identity across prediction and replay, plus C# `AOMath.MixU64`. Distinct rapid events need distinct IDs; the same event keeps its ID across resimulation.
* Fixed replayed or prematurely stopped one-shot sounds, delayed authoritative seek timing, orphaned sound handles, and sound-ID table leaks. Footstep identity is more stable through reconciliation.
* CSL chat commands ignore surrounding whitespace and fall back to case-insensitive command matching. Player arguments accept complete names with digits, underscores, or quoted spaces.
* Fixed state-machine float-variable lookup, optional Spine animation handling, and initial global-leaderboard refresh when its ID is assigned after startup.
* Improved cleanup when C# constructors or pre-Awake initializers throw, prevented script destruction of animator-owned Spine instances, and fixed shop callback lifetimes and product pointers after synchronization.
* Fixed `Texture_Asset.get_pixels_per_unit()` returning 0 on the server and on clients before the texture loads; it now returns the same value `get_world_size()` uses
* A failed `assert()` now reports its script line instead of an engine crash, and the error screen shows its message
* Spark purchases can be tested in Start Game and `run_tests` without spending Sparks. The prompt exposes a stable **Buy** control, test automation can reach modal controls, and editor tests do not report grants, grant failures, or abandoned purchases.

</details>

<details>

<summary>Performance and Memory</summary>

* CSL compilation releases its syntax tree after code generation. Runtime type information uses shared strings and non-networked storage, reducing retained memory and the state copied during reconciliation.
* Fieldless classes and asset handles avoid unused payload allocations; entities, sprite-renderer caches, and VM object metadata use less memory.
* CSL component callbacks reuse interpreter setup, common engine calls use faster handlers, and object-resolution caching distributes entries more effectively. Fixed stale or peer-local cache entries, type-info GC-root handling, and runtime type-info array migration during hotload.
* Android release builds enable the intended optimization level and ThinLTO, while stack tracing avoids repeated thread-local lookups. Profiling identifies CSL callback names and batches more clearly.
* Reduced work in Spine animation/rendering, entity iteration, navmesh nearest-point queries, draw sorting, mesh uploads, text measurement, and UIDoc bindings/rendering.
* Smaller cooked fonts, shared immutable Spine buffers, compact source data, and stronger terrain compression reduce asset size. Native downloads support Brotli where available; web overlaps required rig loading with join setup while keeping required avatars available before spawn.

</details>

<details>

<summary>Editor, Assets, and Iteration</summary>

* Faster project opening and Start Game through parallel asset preparation/publishing, overlapping compilation and scene loading, cached scene packages and asset hashes, and C# assembly reuse by content hash.
* Large scenes save incrementally, and undo/redo preserves the save cache. Project asset, scene, compile, and backup caches are consolidated under `.ao`.
* Project opening checks asset publication; Start Game waits for pending uploads. Failed publishing can be retried from the play controls, and transient upload failures are retried automatically.
* Added loading-phase feedback, the project name in the window title, **File → Force Recompile Scripts**, and **File → Clear Project Caches**.
* Replaced assets are reprocessed even when file timestamps are unchanged or older. Prefab changes refresh linked instances correctly, and interrupted asset loads can recover on a later join.
* Spine preview state and Box Collider cursor tracking no longer keep scenes dirty or interfere with undo. Improved GPU cleanup during project reload and asynchronous screenshot capture.
* macOS editors can precompile Windows shaders natively. Publishing removes unused Spine source data where it is not needed and preserves source data required for runtime merging.

</details>

<details>

<summary>Agent and MCP Tools</summary>

* Added terrain inspection, configuration, painting, and collider tracing; tilemap inspection, layer configuration, painting, and stamping; and texture preparation for tiling.
* Added collider generation from sprite alpha, dynamic canvases, or a Spine pose, plus reusable scene-creation batches.
* Added `spine_extend` for new skins and animations and `player_rig_extend` for authoring, updating, and removing custom player animations. Both validate native playback and return visual previews.
* Expanded Spine inspection with bone/slot filters, layout and attachment information, and more accurate player/rig size references.
* Scene discovery supports component filters, including multiple component types. Live inspection can read selected fields, nested component paths, local-player state, abilities, and inventories.
* Scene edits reject ambiguous names and unapplied properties, report world/local transforms more clearly, and preserve texture tiling. Tilemap inspection validates layer indices before reading geometry. Screenshots include camera/world-size context and optional collider visualization.
* Compile tools report running-server hotload acknowledgement and client readiness. Logs and UIDoc diagnostics are more compact, include source context, and preserve useful results across supported agent clients.
* Live test actions verify the intended interaction or movement result, reject disabled controls, preserve joystick/touch input, and select hotbar slots through acknowledged input. Tutorial state and device-aware ability/scroll actions are inspectable.
* Expanded agent model choices with GPT-6 Astra, Muse Spark 1.3, GLM 5.3 Flash, DeepSeek Flash 4.1, and Union Alpha; refreshed Windows/macOS agent bundles.
* Improved cloud-build previews, feedback and follow-up handling, separated fast inference from workflow mode, and fixed startup/file watching in Unicode Windows paths. Budget reminders no longer interrupt active work. Editor-launched control listeners remain local to the machine.

</details>

<details>

<summary>UI, Web, and Native Clients</summary>

* Fixed UIDoc interaction flicker during resimulation and input passing through consuming UI. Missing-glyph and compile warnings are clearer and grouped; text queued from CSL `ao_draw` now renders correctly.
* UIDoc runtime inspection reports authored sRGB `color` and `background` hex values instead of stored linear values, and editor color fields write matching sRGB hex.
* Improved mobile hold/drag hints, preserved aimed-ability edge taps, enlarged the shop close target, and corrected tablet layout. Notification and friend-request popups no longer block unrelated gameplay input.
* Fixed mobile glyph-atlas upload races, pending-glyph/nameplate sizing, queued text-material lifetimes, and frame loss when discarded draw queues overflow.
* Web clients can run without native memory64 support and use WebSocket transport when WebTransport is unavailable, including embedded/Discord activity environments. Added mobile-web touch/keyboard handling and CrazyGames fixes.
* Improved repeated web game sessions, loading retries, and C# browser bridges, including SyncVar lifetime handling and tutorial ability controls. Fixed a web terrain-upload cleanup deadlock and reduced generated C# RPC receive-frame pressure.
* Android startup, background/resume, shutdown, and WebView-renderer recovery are more reliable. Fixed unwanted keyboard opening, tablet safe areas, billing-catalog/price restoration, and unnecessary native rendering behind the lobby.
* iOS notifications support sender/game imagery and foreground forwarding. Notification permission reporting, Apple sign-in errors, and privacy/ATT sequencing are more reliable. Updated the iOS scene lifecycle and reduced package size while preserving required C# entry points.
* Windows client/editor bundles are signed, with migration to the per-user launcher and its native loading screen. Updated Apple build support and kept nightly editor uploads from activating production releases.
* Disconnects during loading and immediate error dismissal recover more reliably, including cancellation of pending server transfers. Asset/storage and matchmaking errors provide clearer diagnostics.
* Corrected the swapped Baddie cosmetic definitions and their published asset coverage.

</details>
{% endupdate %}

{% update date="2026-08-24" %}

## Protocol 47 - August 24th, 2026

Protocol 47 adds state-preserving CSL code and asset hotloading, deterministic particle systems, rich-text UI, custom typed C# items, and expanded live-event APIs. It also improves editor workflows, compiler diagnostics, rendering performance, and runtime reliability.

<details>

<summary>Compatibility and Migration</summary>

\* C# component \`Update\`, \`LateUpdate\`, \`PredictUpdate\`, and \`PredictLateUpdate\` callbacks are now grouped by concrete component type instead of running in global component-creation order \* Do not rely on callback order between different component types \* \`Inventory\_Draw\_Options.default()\` is now hard-deprecated \* Use \`Inventory\_Draw\_Options.inventory\_default()\` for a full inventory or \`Inventory\_Draw\_Options.hotbar\_default()\` for the previous six-slot hotbar behavior \* Multi-return procedure signatures should now parenthesize their return types \* Use \`proc() -> (A, B)\`; the legacy \`proc() -> A, B\` form still compiles but reports a warning \* Particle-system emission and sizing now use intent-level configuration \* Replace \`spawn\_type\`, \`particle\_count\`, and \`emission\_rate\` with \`Particle\_Emission.burst(count)\` or \`Particle\_Emission.continuous(rate)\`; continuous slot capacity is derived automatically \* Replace \`half\_size\_over\_lifetime\` with full-extents \`size\_over\_lifetime\`, doubling existing values to preserve their visual dimensions \* A component's active \`desc\` is read-only; customize \`get\_desc()\` and pass the result to \`play()\` \* CSL GC-root ownership is now explicit \* \`new(T)\` is rejected when \`T\` is a \`#gc\_root\` type; use \`#alloc\_root(T)\` with a matching \`#free\_root\` \* \`#alloc\_root\`, \`#free\_root\`, \`#rootify\_object\`, and \`#unrootify\_object\` must be used from a \`#unsafe\` context \* Hotload migration requires unique global and procedure link IDs \* Ambiguous IDs now fail compilation and report both declaration or instantiation sites \* \`Entity.inverse\_model\_matrix\` is now calculated on demand \* Replace field reads such as \`entity.inverse\_model\_matrix\` with the method call \`entity.inverse\_model\_matrix()\` \* Added the dry-run-first \`tools/upgrade\_protocol47.py\` migration helper \* It can migrate unambiguous inventory draw defaults and, with a running editor MCP endpoint, directly assigned anonymous procedures that have link-ID collisions \* Pass \`--write\` to apply its proposed changes; Git-backed projects with dirty scripts are rejected unless explicitly allowed

</details>

<details>

<summary>CSL Hotloading and Live Development</summary>

* CSL edits can now hotload into open editor scenes, open prefabs, and running Start Game processes without restarting the session
  * Compatible globals, statics, class and component fields, strings, arrays, procedure values, player references, and component inheritance are migrated to the new program
  * Incompatible fields are reinitialized, removed classes become null, and removed component types require confirmation before their instances are destroyed
  * A compile failure leaves every scene on the previous program instead of applying a partial hotload
* Added the optional server-side `ao_on_hotload()` callback
  * Newly added ability types are also attached to players who already exist when the hotload completes
* The menu beside **Add Client** provides **Hotload code (Alt+F9)**, **Hotload assets (Alt+F10)**, and persistent automatic-hotload toggles
* Changed, newly added, and removed asset IDs can hotload into a Start Game server and its clients while preserving live asset references and Spine bindings
  * Removed IDs disappear from new lookups and CSL handle tables while existing raw references retain their last-good objects; re-adding the same ID revives it in place
  * Spine asset reloads also refresh live-event presenter ghosts
* Hotload compile errors are forwarded to connected clients, and editor checkpoints from an older code generation are rejected instead of restoring incompatible VM memory

</details>

<details>

<summary>Particle Systems and Cosmetic Drawing</summary>

* Added deterministic 2D particle effects through `Particle_System_Component` and `Particle_System_Desc`
  * Supports burst and continuous emission, local and world simulation space, prewarming, rectangular spawn regions, color and size over lifetime, gravity, friction, rotation, and render-layer controls
  * `play()` validates and starts an immutable descriptor, `restart()` replays it, and `stop()` disables it
  * Continuous emitters derive the smallest safe reusable slot pool from emission rate and maximum lifetime
  * WORLD-space drawing cannot advance past its latest birth-position update, including during runtime startup and re-enable gaps
  * Added presets for shotgun blasts, fireworks, explosions, sparks, smoke, dust, fountains, embers, snow, rain, confetti, and magic auras
* Particle evaluation and drawing now run through native quad batching for an approximately 10x speedup
* Added cosmetic-only CSL draw callbacks that run on renderable submissions and skip resimulation
  * Global `ao_draw(dt)` and `ao_editor_draw(dt)` callbacks
  * Component `ao_draw(dt)` and effect `effect_draw(dt)` callbacks
  * Gameplay and interactive UI must remain in `ao_update` or `ao_late_update`

</details>

<details>

<summary>UI and Rendering APIs</summary>

* Added opt-in immediate-mode rich text in CSL and C# through `Text_Settings.rich_text` and `UI.TextSettings.RichText`
  * Supports nested color and absolute or relative size tags plus inline texture images with offset, scale, and rotation controls
  * Rich text preserves shaping across style runs, wraps correctly, and keeps line height based on the configured base text size
* Added `.RADIAL` to CSL `Fill_Direction` and C# `IM.FillDirection`
  * Radial fills start at 12 o'clock; signed `-1..0` values fill counterclockwise and `0..1` values fill clockwise
  * Added C# `IM.PushFillAmount`, `IM.PopFillAmount`, and `IM.PUSH_FILL_AMOUNT`
* Added `Quad_Params.rotation_degrees` for rotated CSL immediate-mode quads
* Added `UI.get_next_serial()` and `UI.set_next_serial()` for preserving deterministic immediate-mode UI identity across custom drawing flows

</details>

<details>

<summary>New CSL APIs and Compiler Behavior</summary>

* CSL inventories now support `for item, slot: inventory.slots()`
  * Every slot is visited in order, and empty slots yield `null`
* Added `Save.delete_game_key(key)` for deleting either a string or integer game-wide save value through the normal batched save flow
* Added hexadecimal integer literals, `Math.exp`, `Math.log`, and `rng_mix`
* Multi-return signatures support the parenthesized `proc() -> (A, B)` form, and declaration arity mismatches now report a targeted error
* CSL type information now exposes tuple element types and enum backing types and distinguishes `ref T` references from `*T` pointers
* `try_get_constant_procedure` now resolves compatible overloads, inherited methods, and the nearest same-named method declaration in the inheritance chain
* Arithmetic and comparisons between different primitive numeric types now choose a common type only when both conversions are lossless
  * This includes exactly representable integer-to-float widening and preserves the flexibility of untyped negative constants; lossy conversions still require an explicit cast
* Polymorphic procedure values can now be inferred from an expected callback type, including nested generic callback signatures
* Contiguous integer switches compile to jump tables for faster dispatch
* Misplaced file-scope loops and invalid method extensions now report targeted compiler errors instead of entering broken parser paths
* Fixed false link-ID collisions involving polymorphic enum and bit-field values and equivalent generic instantiations

</details>

<details>

<summary>C# API Changes</summary>

* Added `Save.DeleteGameKey(id)` for deleting either a string or integer game-wide save value
* Added strongly typed item definitions and instances
  * Set `ItemDescription.DefinitionType` to an `Item_Definition` subclass and `ItemDescription.InstanceType` to an `Item_Instance` subclass before calling `Item_Definition.Create()`
  * Definition fields are initialized independently by client and server code and are not serialized
  * Instance fields marked `[Serialized]` sync from server to clients and are saved and restored with the item; unmarked fields remain runtime-only
  * After directly changing a serialized instance field on the server, call `Inventory.MarkItemDirty(item)`; use `ServerForceSyncInventory` when clients must receive it immediately

</details>

<details>

<summary>Live Events and Player Experience</summary>

* Expanded the live-event API in CSL
  * Added `Live_Events.is_active`, `Live_Events.is_local_player_presenting`, `Live_Events.presenter_count`, and `Live_Events.get_presenter`
  * `Live_Event_Presenter` exposes the presenter's slot, display name, speaking state, and client-local ghost entity
* Expanded the live-event API in C#
  * Added `LiveEvents.IsActive`, `IsLocalPlayerPresenting`, `PresenterCount`, `GetPresenter`, and `TryGetPresenterBySlot`
  * `LiveEventPresenter` exposes the same presenter and ghost state
* Presenter ghosts are excluded from normal scene and component iteration and are cleaned up when an event ends
* Live-event streaming now has more resilient buffering, playout recovery, presenter transforms, skin and Spine animation playback, voice handling, and failure reporting
  * Multi-presenter streams now share an ordered mixer timeline, preserve genuine simultaneous speech, substitute bounded silence for missing chunks, and publish only contiguous completed parts
* Player rig and cosmetic assets were cleaned up, including more reliable coloring and rendering for live-event ghosts
* Mobile players who repeatedly tap an aimed ability instead of dragging now see an animated drag gesture hint

</details>

<details>

<summary>Editor and Developer Workflow</summary>

* Inspector component workflows now support **Copy Component**, **Paste Values**, and **Paste As New**
  * Components can be dragged between entities to move them while preserving their values and references
  * Paste operations work across multi-selected entities when the component types are compatible
* Texture settings can be edited and applied to multiple selected project assets at once
* Open prefab editors now detect external filesystem changes and offer to reload the affected prefab instead of silently overwriting those changes
* New projects now start with a preprocessed default player rig and matching scene configuration
  * Production editor builds copy the baked template immediately, while asset-less development builds generate the same fallback locally
* Cloud-build bundles now rebuild their CSL script section from authoritative project source alongside their scene data
* `assets_validate` now runs preprocessing and returns structured diagnostics, including for preprocessing exceptions and malformed Spine atlas fields
* Scene-persisting automation tools now warn when an active Start Game session must restart before their changes become visible
* Added `memory_snapshot` and `memory_report` automation tools for comparing live allocations and tracking leak candidates in running Start Game clients or servers
* Live client automation now shares one action table with the scripted `Test.*` API
  * `client_input` replaces `client_click`, `client_click_and_hold`, `client_press_key`, and `client_type_text`; it sends `click_at`, `click_button`, `use_ability`, `scroll_widget`, `tap_key`, `press_key`/`release_key`, `type_text`, and world-space `set_mouse`/`click_mouse`
  * `client_action` replaces `test_action` for waits, movement, interaction, admin commands, state reads, assertions, and save/currency/inventory mutation; `describe_client_action` replaces `describe_test_action` and reports which tool owns an action
  * Every click blocks until the client and server observed it and returns a `delivery` report covering gesture delivery, widget activation, UIDoc handler execution, and server acknowledgement
  * `in_game_screenshot` now waits for pending asset loads to settle before capturing, like `Test.screenshot`
  * Added `Test.click_at`, `Test.tap_key`, `Test.press_key`, `Test.release_key`, and `Test.type_text` to the scripted Test API
* Multiphase builds now expose additive phase-specific MCP tools and skills: world authoring first, scripting and live iteration second, then validation in the final review phase
* MCP tool definitions now avoid duplicated client-action and live-testing guidance, and minified JSON responses no longer retain separator spaces
* Bundled Claude Code sessions now suppress the unused LSP and report-findings tool schemas
* macOS editor storage now uses the user's Application Support directory, project discovery tolerates unreadable folders, and bundled Claude, Codex, and OpenCode runtimes were refreshed on macOS and Windows

</details>

<details>

<summary>Runtime Performance and Reliability</summary>

* C# component dispatch and offscreen leaderboard rendering do less per-frame work
* Rich-text shaping, particle drawing, contiguous switches, and polymorphic compilation are faster and more reliable
* Suspending or resuming the app releases keyboard, mouse, and touch state so mobile multitasking cannot leave controls stuck
* Custom C# item payloads survive netsync memory relocation, and force-syncing an inventory also marks its serialized item changes for persistence
* Replacing JSON-backed CSL values no longer invalidates aliased strings or managed arrays
* Large CSL programs keep immutable bytecode out of the fixed non-networked game-state region, reducing Start Game and hotload memory pressure
* Start Game uses a larger non-networked arena and bounds internal hashtable probes so corrupt tables fail loudly instead of hanging

</details>
{% endupdate %}

{% update date="2026-08-03" %}

## Protocol 46 - August 3rd, 2026

Protocol 46 adds runtime tilemap authoring, engine-managed live events, device emulation, substantially richer UIDoc styling, and new CSL and C# APIs. It also improves keybinds, inventory security, cloud builds, automation, asset loading, voice, and developer-facing diagnostics.

<details>

<summary>Compatibility and Migration</summary>

* Player Spine tracks
  * The engine-owned player skin animation moved from track 10000 to track 10
  * Tracks 0, 1, 2, and 10 are reserved; custom player animation layers should use tracks 3 through 9
* C# inventory handles
  * `Item_Definition.Id` and `Item_Instance.Definition` are now read-only and cached
  * Remove code that assigns either property
* C# player-list rendering
  * `Player.GetSpineInstanceForPlayerList()` was removed
  * The built-in player list now renders profile avatars
* UIDoc binding expressions
  * Treat `class` and `data-key` values as static; `{{...}}` interpolation is supported only in text and image `src`
  * `data-for-key`, `data-scroll-zoom`, and `data-style-*` take direct binding expressions without mustaches
  * Use `data-style-color`, `data-style-opacity`, image effects, or explicit list contents for dynamic presentation
  * `data-if` is limited to top-level bool bindings, with optional `!` inversion

</details>

<details>

<summary>Runtime Tilemap API</summary>

* Added full runtime tilemap access in both CSL and C#
  * Query and resize tilemap dimensions
  * Add, remove, configure, and clear ground or wall layers
  * Configure automatic or manual layer sorting, tile size, tint, masks, outlines, wall tops, collision, inset, and offset
  * Add, replace, weight, scale, offset, and remove a layer's textures
  * Query, set, and clear individual tiles
  * Convert between tile, local, and world coordinates
* CSL additions include `Tile_Layer_Kind`, `Tile_Layer_Mode`, and the new `Tilemap_Component` methods
  * CSL tilemap mutations are predicted scene state and participate in rollback and reconciliation
* C# additions include `TilemapLayerKind`, `TilemapLayerMode`, and matching `Tilemap_Component` properties and methods
  * C# tilemap mutations affect the current simulation only; multiplayer games must invoke equivalent changes on every peer through their existing RPC flow
* Layer indices shift when a layer is removed, invalid indices fail loudly, wall height must be at least 2, and each layer supports at most 256 textures
* Tilemap layer tinting now affects rendered tiles
* Tilemap textures now have independent X/Y repeat scale and tile-space offset controls, including wall-top textures
* Tilemaps now follow entity X/Y scale in rendering, editor painting, coordinate conversion, collision, and navmesh input
* Transform-only tilemap collision updates reuse cached local geometry instead of rescanning and regrouping tiles
* Vertically expanded wall rows preserve their independent outlines and Y sorting when they overlap, including across chunk boundaries

</details>

<details>

<summary>UIDoc UI and Styling</summary>

* Added project fonts through CSS `@font-face`
  * TTF and OTF `Font` assets can provide multiple weights and styles for one family
  * UIDoc selects the nearest face and can synthesize a missing bold or slanted face
  * Fonts load asynchronously and automatically invalidate text layout when ready
* Expanded text styling with `font-family`, real `font-weight`, `font-style`, `letter-spacing`, improved line-height behavior, and up to eight text shadows
* Added `data-text-reserve` for stable intrinsic layout when frequently changing text has a known widest sample
* Expanded backgrounds and effects
  * Linear, radial, and conic gradients with up to eight stops and Oklab or sRGB interpolation
  * Up to eight box shadows, including inset shadows
  * `filter: blur(...)`, alpha-aware `drop-shadow(...)`, and `backdrop-filter: blur(...)`
  * `image-tint`, `image-grayscale`, directional and radial image fills, bound fill amounts, and improved `object-fit`
  * Added paint-only `data-style-color` and `data-style-image-fill-amount` bindings that avoid structural layout invalidation
* Expanded layout support
  * `calc(...)` addition and subtraction for supported lengths
  * `position: fixed`, richer inset/size units, relative translation percentages, flex alignment and wrapping improvements, and stricter property-specific value validation
* Tailwind-style lowering now covers more layout, text, color, shadow, transform, and image-effect utilities
  * Responsive and interaction variants, bracket values, and `@apply` are supported when they lower to the UIDoc subset
* Added more reliable keyed-list identities and documented exact runtime test selectors for repeated controls
* Scroll layout, zooming, nested state restoration, font layout, clipping, and transformed hit testing are more reliable
* UIDoc draws faster, and paint-only binding changes avoid unnecessary relayout
* Unsupported declarations, values, selectors, malformed HTML, and unsupported media queries now produce clearer asset diagnostics

</details>

<details>

<summary>New CSL APIs and Behavior</summary>

* Haptics
  * Added `Haptics.play_impact` with `.LIGHT`, `.MEDIUM`, and `.HEAVY` impact types
* Ads
  * Added `Ads.request_interstitial() -> bool`
  * Rewarded and interstitial ads can now be hosted by the web client as well as native clients
* `World_Progress_Bar.draw` now safely does nothing on the server instead of entering client UI code
* Ability buttons respect `draw_but_dont_use_keybind`, honor remapped bindings, and no longer render the word `Unbound`
* CSL chat-command arguments typed as `Player` now receive the player named in the argument instead of the command sender

</details>

<details>

<summary>C# API Changes</summary>

* Added `Camera.IsVisible` overloads for a world point, world rect, `Sprite_Renderer`, and `Spine_Animator`
  * Clients test the local camera; servers return true when any player camera sees the target
  * The optional `apron` expands the tested bounds
* Added `UI.TextSettings.LetterSpacing`
* Inventory and items
  * Added `Inventory.GetItemQuantityById`, `Inventory.HasItemById`, and `Inventory.GetFirstItemById`
  * Added allocation-conscious `Item_Instance.GetMetadatas(string[] keys, ref string[] values)`
  * Added `Inventory.RequireOwnership`, `OwnerUserIds`, `AddOwnerUserId`, `RemoveOwnerUserId`, and `ClearOwnerUserIds`
  * Built-in player inventories automatically require their player's user ID
  * Custom inventories remain shared by default; adding an owner automatically enables ownership enforcement
  * Unauthorized client merge, swap, and use commands are rejected by the server
* Save callbacks
  * `Save.GetOfflinePlayer` now invokes its callback exactly once for every completed request
  * Unknown players and failed requests supply empty data, and failures are logged
  * Ordered save-data reads follow the same callback-on-failure contract with an empty result

</details>

<details>

<summary>CSL Compiler, JSON, and Project Validation</summary>

* Constant fixed-size arrays now report the correct `.count`
* CSL compilation is faster and uses less temporary memory, and inclusive and reverse loops execute with lower VM overhead
* The compiler now reports `No CSL script files found` for an empty scripts tree instead of accepting an unusable project
* JSON handling is safer and more standards-compliant
  * Strings and object keys are escaped correctly when serializing
  * Escaped object keys are decoded when parsing
  * Mixed-type arrays no longer enter the numeric-array serializer and crash
  * Reading a JSON array into a fixed-size array stops at the shorter length instead of reading or writing out of bounds
* Added compiler semantic-analysis output for editor and language-server integrations on Windows and macOS
  * The output includes compiler diagnostics, declarations, types, scopes, and identifier occurrences

</details>

<details>

<summary>Live Events, Voice, and Player UI</summary>

* Added engine-managed live events with streamed presenter voice, transforms, skins, and Spine animation state
* Audience clients receive synchronized, client-local presenter ghosts without adding those ghosts to replicated gameplay state
* Live-event playback includes buffering, recovery, presenter status, and failure reporting for unstable connections
* Voice capture and playback quality and voice isolation were improved
* The in-game player list now uses current profile avatars and an unknown-avatar fallback
* Chat filtering recognizes word boundaries for short blocked terms, reducing false positives while expanding coverage

</details>

<details>

<summary>Editor and Developer Workflow</summary>

* Added phone and tablet emulation to the editor play toolbar
  * Profiles cover small and large phones, a foldable, and tablets
  * Emulation uses native landscape resolution, safe areas, cutouts, rounded-screen clipping, phone/tablet device classification, and remapped mouse input
  * `Fit` fills the client window; `Physical` estimates real-world display size from monitor density
* UIDoc authoring improvements
  * The visual editor now uses the production runtime more consistently for selection, layout, style editing, generated wrappers, saving, and diagnostics
  * Added `uidoc_create_asset` and live `uidoc_runtime_inspect` automation tools
  * Renamed the generated interface inspection tool from `uidoc_contract` to `uidoc_interface`
* Local asset search now has relevance ranking, aliases, type intent, project/engine/all scopes, pagination, file-change-aware hashes, and world-size metadata
* Project agent setup now writes `opencode.json` in addition to Claude, Cursor, Codex, and generic MCP discovery files
* Cloud builds now package scene source and perform authoritative scene packing from that source
  * Scene-export failures and timeouts surface the relevant server log instead of hanging or silently producing stale output
  * Cloud-build draft projects can still be packaged when the local scripts currently do not compile, so server-side diagnostics remain available
* Editor filesystem failures now produce clearer errors on Windows and macOS
* Editor numeric fields preserve float precision, Windows clipboard paste handles UTF-8 safely, and linked prefab descendants are hidden from the editable hierarchy
* New-project creation rejects the entire games directory and folders that already contain another All Out project
* Exceptionally long entity filenames no longer crash project loading

</details>

<details>

<summary>Automation, Testing, and Diagnostics</summary>

* Game startup now launches a consistent compiled snapshot
  * Source changes during startup, compile failures, launch failures, automation bind failures, process exits, and readiness timeouts return distinct structured errors
* `test_action` accepts common argument aliases, normalizes component paths, and returns clearer invalid-argument, timeout, unavailable-client, ambiguous-target, and not-found errors
  * Live count bounds and inventory moves now use the equivalent CSL `Test.*` parameter names, and `describe_test_action` returns each action's schema, aliases, and example
* Non-idempotent test actions such as interactions and inventory operations no longer fail intermittently when transient input is dropped
* Test clients and dummy peers are cleaned up when a run ends, including partial or failed launches
* Client and server crashes now retain structured process attribution, exit status, CSL and native top frames, artifact paths, and log paths
* Fatal runtime errors shown in the editor and to players include more useful error categories and context
* Automation screenshots, UI inspection, input, test result discovery, and macOS child-process handling are more reliable

</details>

<details>

<summary>Runtime, Platform, and Performance</summary>

* Keybind remapping is now kept per player, validated, saved atomically, restored by action name, and fully reconciled with the server
  * Duplicate assignments are removed consistently and corrupt keybind files are ignored safely
  * Engine-owned UI and fullscreen controls now honor the local player's remapped bindings
* Desktop clients can use the static on-screen joystick with a mouse
* Asset loading and game transitions
  * Bundled preview textures no longer stall unrelated asset loading during startup
  * Android joins do less upfront audio work and avoid asset-loading stalls
  * Failed web downloads and decodes, draw contexts, and game-leave resources release more memory reliably
* Web clients have more accurate memory and frame-time reporting, including clean handling of standby and resume gaps
* Web hosts can set an engine-owned UI scale multiplier without changing game-authored UI scale
* Idle scenes skip unnecessary lighting and tonemapping passes
* Android window, input queue, WebView, fatal-signal reporting, and game-transition lifecycle handling are more robust
* Linux asset processing accepts projects up to 2 GiB and server-side Redis reconnect handling is more resilient
* UIDoc rendering, CSL analysis, runtime tilemap updates, and network prediction recovery are faster and more stable

</details>
{% endupdate %}

{% update date="2026-07-15" %}

## Protocol 45 - July 15th, 2026

Protocol 45 adds UIDoc and scene-owned paint canvases, expands CSL gameplay and rendering APIs, improves live development tooling, and delivers substantial multiplayer, pathfinding, collision, scripting, and runtime improvements.

<details>

<summary>Compatibility and Migration</summary>

* Client version
  * The client version is now 1.45.1
  * Android version code is now 492
* Protocol and game data
  * Protocol 45 clients and servers cannot communicate with protocol 44 builds
  * Deploy matching client and server builds together
  * The game-state memory version increased from 58 to 75
  * Rebuild and republish games and packed scene data after upgrading
* Replicated C# components
  * Replicated component IDs now support values above 255
  * The limit remains 255 serialized components per entity
  * Entity payloads over 65,535 bytes now fail explicitly instead of being truncated
* CSL template strings
  * Backtick strings now support `{expression}` interpolation
  * Literal braces in existing raw backtick strings must be written as `{{` and `}}`
  * A lone closing brace is now a compile error
  * Interpolated templates require `import "core:basic"` and are not compile-time constants
  * Backtick strings without interpolation remain raw constants
* Numeric literals
  * Literals larger than 64 bits now fail compilation
  * Values outside an explicit integer suffix or enum range now fail instead of silently wrapping or truncating
  * Fractional literals can no longer use an integer suffix
* Source builds
  * Native Windows, Linux, and iOS builds now require Rust/Cargo for UIDoc layout support
  * iOS builds also require the `aarch64-apple-ios` Rust target

</details>

<details>

<summary>UIDoc Screen-Space UI</summary>

* Added `.uidoc` directory assets for authoring responsive screen-space UI
  * Each asset contains an `index.html` and can include an `index.css`
  * Supported content includes text, images, buttons, text inputs, conditions, and repeated lists
  * CSL can bind text, bools, floats, image paths, lists, inputs, and events
* Layout and styling
  * Supports responsive flex and block layouts, safe areas, interaction states, and client-specific viewport sizing
  * Supports vertical and horizontal scrolling, nested scroll-state restoration, and bound `data-scroll-zoom`
  * Tailwind-style utilities, responsive and pseudo-state variants, arbitrary values, and `@apply` can be lowered during asset baking when enabled by the stylesheet
  * Layout accounts for each client's viewport, locale, safe area, and replicated UI scale
* New CSL surface
  * Added `UIDoc_Asset`, `UIDoc_Event`, `UI.uidoc`, binding and list helpers, event polling, and text-input value access
* UIDoc uses a deliberately constrained HTML/CSS subset
  * It is deterministic engine UI, not an embedded browser
  * JavaScript, browser APIs, forms, external URLs, CSS grid, variables, transitions, and keyframes are not supported
  * Unsupported output fails asset validation instead of silently degrading
* Existing immediate-mode UI and UIK projects remain supported
  * Guidance for new screen-space UI now favors UIDoc

</details>

<details>

<summary>Visual UIDoc Editor</summary>

* Selecting a UIDoc replaces the entity-centric scene, hierarchy, and inspector with UIDoc authoring tools
* The scene view uses the production UIDoc parser, layout, and renderer for an exact runtime-backed preview
* Elements can be selected, moved, resized, inserted, duplicated, deleted, reordered, and reparented
* Design and Styles inspectors edit common HTML and CSS properties, including colors
* The Interface inspector infers fields, inputs, lists, and actions directly from UIDoc markup
* Saving generates a typed CSL wrapper under `scripts/generated/uidoc`
* UIDoc edits have document-aware save, undo/redo, external-change detection, and compiler diagnostics

</details>

<details>

<summary>New CSL Gameplay and Rendering APIs</summary>

* Scene-owned dynamic canvases
  * `Dynamic_Canvas` can create, destroy, fill, stamp, set, and sample pixel canvases
  * `Sprite_Renderer` can display a canvas
  * Spine attachments can override their texture with a canvas and optionally use skeleton-space UVs
  * Animated Spine hit positions can be converted back to stable canvas coordinates
  * Canvas state participates in sync, prediction, rollback, and reconciliation
  * Current limits are 512×512 pixels, 16 canvases per scene, and 4 MiB of canvas pixels per scene
* Scene color sampling
  * `Player.request_scene_color_sample` and `Player.try_get_scene_color_sample` support eyedropper-style mechanics
  * Sample results are client-authored visual input and must not be trusted as authoritative game state
* Dynamic canvas methods are currently available from CSL
  * C# receives the generated `Dynamic_Canvas` handle but not the full canvas API
* Interactables
  * Added subtitle getters, setters, and editable subtitle color
* Emotes and effects
  * Added equipped-emote checks, counted block reasons, trigger and cancellation APIs
  * Added `Effect_Base.get_duration_remaining`
* Inventory
  * Added `Inventory.has_item_id`
  * Added `Inventory_Draw_Options.on_before_draw` and `on_after_draw`
* Rendering and interpolation
  * Added `get_fixed_delta_time`, `Camera.size_last_frame`, `world_to_screen_interpolation_offset`, and `UI.quad_interpolated`
  * Added `UI.player_avatar` for drawing a static player avatar
  * Added interpolated aiming helpers and an Entity overload for tutorial arrows
* Spine and player materials
  * Added helpers for using a player's material and destroying an entity after its current animation
  * Added canvas painting options and skeleton-space attachment overrides
  * Added `color_replace_get_color` for reading avatar palette colors
* Sound documentation now covers `SFX.fade_out_and_stop` and controlling returned sound IDs from server-side CSL

</details>

<details>

<summary>Player and Runtime Changes</summary>

* CSL games now support the standard emote wheel
  * Equipped thumbnails, synchronized effects and sounds, looping, gameplay block reasons, and movement cancellation are supported
  * Fixed several emote state and headless-server crashes
* Interaction prompts can display a localized, independently colored subtitle
* Instant interactables with no hold duration now trigger correctly from a mobile tap
* Aiming indicators, target reticles, tutorial arrows, and dropped-item UI now interpolate more smoothly
* Multiplayer
  * Ongoing state and drop-in traffic is substantially smaller
  * Recovery from missing updates and long hitches is more reliable
  * Prediction now continues catching up instead of accumulating a permanent resimulation backlog
  * Web clients use the correct simulation budget on high-refresh-rate displays
  * Rollback no longer repeats UI side effects such as click sounds
* Scroll views
  * New views remain at the start until deliberately scrolled
  * Horizontal-only views accept horizontal wheel input
  * Transient zero-sized layouts no longer corrupt scroll offsets
  * Recreated UIDoc elements retain their scroll position
* UI scale now participates correctly in prediction and replay, reducing layout and hit-target mismatches after changing scale
* Android hardware volume buttons continue controlling system volume while in-game
* Global leaderboard names containing formatting characters such as `%` now display literally
  * Offscreen leaderboards are also culled
* Dropped items can be culled visually without freezing their gameplay movement
* Reparenting an entity beneath a disabled entity immediately applies the correct disabled state
* Rewarded-ad and CSL purchase grants more reliably wait for client replication before processing
* Restored the built-in CSL green-button asset reference
* All Collider-derived components on an entity now participate in CSL physics
  * Projects using multiple colliders may observe newly correct contacts

</details>

<details>

<summary>CSL Compiler and Project Workflow</summary>

* Compiler diagnostics
  * Capturing procedure literals remain unsupported and now produce an explicit error
  * `return` outside a procedure now reports an error instead of crashing
  * Very long boolean chains and large switches no longer overflow the compiler's native stack
* Script-heavy workloads benefit from optimizer, bytecode-inlining, and value-formatting improvements
* Core library vendoring
  * Opening a CSL project copies the engine core library into `scripts/.ao_core`
  * `import "core:*"` resolves against that project copy
  * The copy is included when publishing and is not rewritten when unchanged
  * Treat `.ao_core` as generated content and do not edit it manually

</details>

<details>

<summary>C# API Changes</summary>

* `Vector2`, `Vector3`, and `Vector4` now explicitly implement component-wise `Equals` and `GetHashCode`
  * Their behavior in dictionaries and sets is now consistent
* Added `Camera.WorldToScreenInterpolationOffset`
* Built-in aiming indicators and target reticles now use frame interpolation
* Aiming helpers gained optional interpolation-offset parameters
  * Normal calls remain source-compatible
  * Reflection, delegates, or method groups relying on the previous exact arity may need updating

</details>

<details>

<summary>Editor, Automation, and Testing</summary>

* Editor `compile` can hot-reload CSL into a running editor-launched game and reconnect clients to the updated state
  * Scene edits still require a stop and restart
* Restored and hardened live game tools for starting and stopping games, screenshots, UI-tree inspection, clicking and holding, keyboard input, and text input
* Added `test_action` for running individual Test.Runner actions in a live game
  * Supported actions include waiting, movement, navmesh pathfinding, interaction, abilities, assertions, inventory and save operations, and variable or component inspection
* Added `assets_validate`, `uidoc_interface`, and `uidoc_diagnostics`
* Direct `client_wait` and `client_health` integrations should migrate to `test_action(wait)` and the newer automation health classification
* `Test.pathfind_to` now follows the navmesh
* Added `Test.assert_balance_range` for timing-tolerant economy assertions
* Injected test movement is no longer discarded by mobile joystick recomputation
* Screenshot requests time out cleanly instead of occasionally hanging
* Test execution fails fast when a game launch is already in progress
* Automation and compile failures now provide clearer diagnostics and stack traces

</details>

<details>

<summary>Performance, Hosting, and Stability</summary>

* Navmesh pathfinding and rebuilds are substantially faster, especially for repeated paths and unreachable targets
* Collision-heavy CSL games now avoid testing every collider against every other collider
* Web builds use a smaller initial memory reservation and clean up failed asset decode and download paths more reliably
* Linux-hosted servers account for container memory limits while loading assets, reducing startup and prewarming out-of-memory failures
* Asset prewarming and UIDoc replication have lower peak memory use

</details>
{% endupdate %}

{% update date="2026-06-13" %}

## Protocol 44 - June 13th, 2026

This protocol update focuses on CSL compiler performance and memory use, better callback resolution, smoother sync/interpolation behavior, and several runtime fixes for strings, assets, and world-space UI.

<details>

<summary>Sync and Interpolation</summary>

* Network sync now uses the protocol 44 32hz variable sim/sync cadence
  * Game simulation now advances at a lower fixed cadence than rendering
  * Rendering can still look smooth by drawing with an interpolation offset from the previous rendered position to the current simulated position
* Entity-attached rendering
  * World-space rendering calls inside supported component callbacks are automatically attached to that component's entity and interpolated
  * In those callbacks, `UI.push_world_draw_context()` is enough for labels, prompts, health bars, and similar entity-following visuals to move smoothly between sim ticks
  * Outside component callbacks, use `UI.begin_world_space_ui(entity)` and `defer UI.end_world_space_ui()` when drawing world-space UI or other immediate-mode visuals that should follow an entity
  * `UI.begin_world_space_ui(entity)` pushes the world draw context and uses the entity's built-in interpolation data
  * Use `entity.mark_teleported()` when an entity moves discontinuously and you explicitly do not want the next render to interpolate across that jump
  * Large teleports are also detected automatically: if an entity moves more than 3 world units since the previous frame, interpolation is skipped for that frame
* Manual interpolation offsets
  * Use `UI.push_interpolation_offset(offset)` before any custom rendering that needs interpolation
  * Always pair it with `UI.pop_interpolation_offset()`, usually through `defer UI.pop_interpolation_offset()`
  * The offset should represent the movement from the last rendered position to the current position
  * Component entity anchoring uses the same interpolation-offset stack, so nested manual offsets follow normal push/pop order
* Custom non-entity positions
  * Use `Position_Interpolation_Helper` in your own class or struct when you own a position that is not covered by an entity's built-in interpolation
  * Store one helper per moving thing, call `.update(current_position)`, and pass the returned offset to `UI.push_interpolation_offset`
* Animated fill amounts
  * Use `Float_Interpolation_Helper` with `UI.quad(..., params={fill=UI.quad_fill(helper.update(progress), .RIGHT)})` for smoothly animated fills such as progress bars
  * Use `UI.quad_fill(progress, .RIGHT)` when the fill amount does not need interpolation
  * `.RIGHT` fills left to right as the amount moves from `0` to `1`; `.NONE` leaves the quad unfilled and is the zero-initialized value
  * `Quad_Params` also accepts `nine_slice` data for the same draw call

```csl
Particle :: class {
    position: v2;
    velocity: v2;
    interp: Position_Interpolation_Helper;

    update :: method(dt: float) {
        position += velocity * dt;

        UI.push_interpolation_offset(interp.update(position));
        defer UI.pop_interpolation_offset();

        // Draw the particle here.
    }
}
```

* Interpolation anchor context
  * Supported component lifecycle callbacks now automatically push that component entity's interpolation offset while the callback runs
  * This applies to `ao_start`, `ao_update`, `ao_late_update`, `ao_end`, `ao_on_interactable_used`, and `ao_on_holding_interactable`
  * `ao_can_use_ability`, `ao_can_use_interactable`, and `ao_on_state_sync` are intentionally not anchored; keep them small, fast, and pure
  * This applies to sim-owned world-space draw contexts only; present/combine rendering does not read component interpolation anchor state
  * C# component update loops also scope this with push/pop per component, so exceptions cannot leak a stale interpolation anchor

</details>

<details>

<summary>CSL Language and Compiler Changes</summary>

* New procedure-resolution directives
  * `#optional_procedure_of_call(call)` was added
  * It works like `#procedure_of_call(call)`, but unresolved optional calls return an empty procedure value instead of producing a hard error
  * `#procedure_types_are_leniently_compatible(src_type, dst_type)` was added as a compile-time bool check for callback/interface compatibility
* Interface and callback matching
  * Interface procedure requirements now allow compatible derived-class parameter types where the call is safe
  * Overloaded interface method implementations now resolve to the concrete implementation on the actual receiver type
  * Procedure type-vs-overload mistakes now produce clearer compile errors
* Polymorphic assignability constraints
  * Polymorphic type parameters can now constrain assignability with syntax like `$T/Base`
  * This supports APIs that accept a base type while preserving the caller's concrete type
* Range loop bounds
  * Integer widening is now allowed for range bounds when the widening conversion is safe
  * Invalid non-widening range bounds now report a compile error instead of crashing
* New GC helper
  * `#unrootify_object(value)` was added
  * It removes a root from an existing object without freeing the object immediately
* Diagnostics and performance
  * Misplaced `give` statements now produce a targeted error
  * Source locations were compacted internally, preserving file/line reporting while reducing compiler memory
  * The compiler received major memory and throughput optimizations for large scripts

</details>

<details>

<summary>Core API and Runtime Changes</summary>

* `Interactable.set_listener`
  * Now accepts a concrete component listener type instead of collapsing the listener to `Component`
  * Callback lookup now uses CSL call resolution through `#optional_procedure_of_call`
  * Inherited and overload-nested `can_use`, `on_interact`, and `on_holding` callbacks now resolve to the right concrete listener procedure
  * `Interactable` now also declares the `can_use` interface callback
* World-space UI
  * `UI.begin_world_space_ui(entity)` now pushes world draw context in addition to interpolation offset
  * Passing `null` is supported and uses a zero interpolation offset
  * `UI.end_world_space_ui()` now pops both interpolation and draw context
* Global leaderboard package
  * `core:global_leaderboard` adds a reusable `Global_Leaderboard : Component` for world-space ranked scoreboards backed by ordered saves
  * Import it with `import "core:global_leaderboard"` and add `Global_Leaderboard` to a scene entity
  * Set `leaderboard_id` on the component, optionally set `optional_title`
  * On the server, call `Global_Leaderboard.increment_score(player, leaderboard_id, amount)` to add to a player's score
* String getters
  * `Player.get_username()`, `Player.get_user_id()`, `Player.get_name_override()`, and `Entity.get_name()` now cache rooted CSL strings
  * Repeated calls avoid unnecessary string allocation while still updating when the native value changes
* Asset handles
  * Spine skeleton, attachment texture, and cosmetic APIs now resolve CSL asset handles through the scene asset lookup table
  * This fixes cases where CSL-visible assets are represented by script stubs instead of direct native VM objects
* Formatting
  * `tprint`/formatted string output now recognizes `Format_Int` and `Format_Float` by link id
  * Padded integer and float formatting now works in cases where the format helper type is not pointer-identical to the cached runtime type

</details>
{% endupdate %}

{% update date="2026-05-21" %}

## Protocol 43 - May 27th, 2026

This protocol update tightens CSL validation, removes several legacy APIs, changes UIK interaction handling, and updates a few core library behaviors.

<details>

<summary>Breaking CSL Changes</summary>

* Struct and class parameters
  * Explicit `$` on struct or class parameters is now illegal
  * `struct($T: typeid)` → `struct(T: typeid)`
  * This affects patterns like `List`, `Hashtable`, and `Component_Iterator`
  * Polymorphic procedures like `proc($T: typeid, ...)` still work
* Stray `$identifier` usage
  * Standalone `$identifier` is now rejected instead of being treated like `identifier`
* Reserved keywords
  * `bit_field` is now a reserved keyword
  * Any identifier named `bit_field` will now fail
* String and char escapes
  * Unknown escape sequences now produce a compile error
  * They no longer slip through or panic at runtime
* Invalid code now fails earlier
  * Calling non-procedure or non-polymorphic types now errors
  * Comparing incompatible types now errors

</details>

<details>

<summary>Core API Breaks</summary>

* `core:ao/generated`
  * `src/csl/core/ao/generated.csl` was removed
  * Its contents were folded into `core:ao`
  * Direct imports of `core:ao/generated` now break
* `game_support.csl`
  * `src/csl/game_support.csl` was deleted
  * Code relying on its empty `Player :: class : Player_Base` must now define `Player` directly
* Generated `self` fields
  * Generated `self` fields were removed from wrappers like `Entity`, `Component`, `State_Machine*`, `Item_Definition_Base`, `Item_Instance_Base`, and `Inventory_Base`
* Item definition getters
  * `Item_Definition_Base.get_name()` and `Item_Definition_Base.get_id()` were removed from the base API
  * `Item_Definition` still keeps soft-deprecated getters
  * Code typed as `Item_Definition_Base` must use `.name` and `.id`
* Spine instance hierarchy
  * `Spine_Instance_Base` was removed
  * `Spine_Instance` now inherits `Spine_Instance_Fields` directly
  * Explicit references or casts to `Spine_Instance_Base` now break
* Skeleton APIs
  * `Spine_Animator.set_skeleton()` and `Spine_Instance.set_skeleton()` now return `u64`
  * `get_skeleton()` now takes an optional `id: u64 = 0`
* Collider hierarchy
  * `Circle_Collider`, `Box_Collider`, `Polygon_Collider`, and `Edge_Collider` now inherit from `Collider`
  * They no longer inherit directly from `Component`
  * Normal component usage should still work
  * Reflection and direct parent checks can break

</details>

<details>

<summary>UIK Breaks</summary>

* Interact handles were removed
  * `UIK.Interact_Handle` was removed
  * Interactive APIs now return `Interact_Result` directly
  * This affects `panel_begin_interactive`, `toggle`, `card_end`, both `tab` overloads, `button`, `icon_button`, `icon_button_label`, and `title_area`
* Resolve flow changed
  * `UIK.resolve(handle)` was removed
  * `UIK.clicked`, `UIK.hovering`, `UIK.pressed`, and related helpers now take `Interact_Result`
  * You can also read `.clicked`, `.hovering`, and related fields directly
* Deferred handle patterns
  * Old patterns that store handles and resolve them after `UIK.end()` should be rewritten to the new simpler pattern
* Yellow enum names
  * `..._YELLOW1` was renamed to `..._YELLOW`
  * `..._YELLOW2` was renamed to `..._ORANGE`

</details>

<details>

<summary>Library and behavior changes</summary>

* Math helpers
  * Generic `lerp`, `clamp`, `min`, and `max` were replaced with concrete overloads
  * `lerp` now supports `v2`, `v3`, `v4`, and `float`
  * `clamp`, `min`, and `max` now support `int` and `float`
  * Calls on other types can now break
* UIK layout helpers
  * `rect_sidebar_left`, `rect_sidebar_right`, `rect_topbar`, `rect_hotbar`, and `rect_dialog` changed layout behavior
  * Their signatures stay the same
  * Existing layouts may shift

</details>

<details>

<summary>Developer workflow</summary>

* `csl.exe <folder>` now defaults to compile and run
* Use `csl.exe <folder> -no_run` for compile-only behavior

</details>
{% endupdate %}

{% update date="2026-04-07" %}

## Protocol 42 - April 16th, 2026

This protocol update brings major CSL breaking changes, stricter runtime enforcement, an overhauled SFX system, and several new engine APIs.

<details>

<summary>Breaking Changes</summary>

* Type system
  * `b8`, `b16`, `b32`, and `b64` were removed. Only `bool` remains.
  * Scripts using those type names will fail to compile.
* Entity hierarchy fields were replaced with methods
  * `entity.parent` → `entity.get_parent()`
  * `entity.first_child` / `entity.last_child` → `entity.get_first_child()` / `entity.get_last_child()`
  * `entity.next_sibling` / `entity.prev_sibling` → `entity.get_next_sibling()` / `entity.get_prev_sibling()`
  * `entity.first_component` / `entity.last_component` → `entity.get_first_component()` / `entity.get_last_component()`
* Component navigation fields were replaced with methods
  * `component.prev_component_on_entity` → `component.get_prev_component_on_entity()`
  * `component.next_component_on_entity` → `component.get_next_component_on_entity()`
* Component state fields were replaced with accessors
  * `component.enabled` → `component.get_enabled()` / `component.set_enabled()`
* Component iterator updates
  * `next(ref Component_Iterator)` was removed. Use `.next()` instead.
* Effects system was restructured
  * `entity.active_effect` → `entity.get_active_effect()` and now safely returns `null` when no effects exist
  * `entity.first_effect` → `entity.get_first_effect()`
  * `entity.last_effect` → `entity.get_last_effect()`
* List construction changed
  * `List.append` is now hard-deprecated for list construction. Use `[..]T` instead.
* Math functions
  * `atan2` changed parameter order from `atan2(x, y)` to `atan2(y, x)` to match standard C behavior
  * Generic `min(a, b)` and `max(a, b)` procedures were removed from `basic.csl`
  * `normalize(v)` no longer includes a zero-length safety check and may now produce `NaN` or `inf`
  * `sin`, `cos`, `atan2`, `pow`, and `sqrt` changed from foreign calls to intrinsic VM opcodes
  * Serialized bytecode is incompatible with previous builds
  * New `f64` overloads were added for `sin`, `cos`, `atan2`, `pow`, and `sqrt`
* Removed APIs
  * The `Gif` asset class was removed entirely, including the `Gif` type, `UI.Gif()` widget, `UIGif` component, and related internal calls in C# and CSL
  * `Sprite_Renderer.WaitForLoad` was removed and now only exists as an obsolete no-op in C#
* Runtime enforcement
  * UI functions now panic outside player update context on the client
  * This affects `ui_button`, `ui_begin_button`, `ui_end_button`, `ui_push_scroll_view`, `ui_pop_scroll_view`, `ui_compute_scroll_bar_rect`, `ui_drag_drop_source`, `ui_drag_drop_target`, `ui_blocker`, `ui_begin_modal`, and `ui_end_modal`
  * These must be called from `core_player_update` or `core_player_late_update`
* SFX system overhaul
  * SFX playback is now authoritative on the server
  * The server tracks active sounds persistently, including ID, duration, elapsed time, and fade-out state
  * Clients now mirror the server's active sound list
  * `sfx_stop` now works correctly across the network

</details>

<details>

<summary>CSL Language Changes</summary>

* Method call syntax now uses `.` instead of `->`
  * `b->set_x(20);` → `b.set_x(20);`
* Fixed-size array literals now support values directly
  * `[4]int{1, 2, 3, 4}` is now valid
* Casting now supports prefix syntax in addition to suffix casting
* Half-closed ranges now support `^`
* Hashtables are now supported
* Class members now support default values, including reassignment in derived classes
* Implicit integer widening is now allowed for safe widening conversions
  * Example: `s32` → `s64`
* `switch` cases now support multiple values and inclusive ranges

```csl
switch level {
    case 1, 2, 3: tier = .BEGINNER;
    case 4..10:   tier = .INTERMEDIATE;
    case 11..20, 25, 30..50: tier = .ADVANCED;
    default:      tier = .UNKNOWN;
}
```

{% hint style="warning" %}
Migration tooling note: do **not** blindly replace every `->` with `.`. Return type definitions still use `->`, so a global search-and-replace will corrupt valid code.
{% endhint %}

</details>

<details>

<summary>Deprecations</summary>

* `case:` with no expression in a `switch` is deprecated. Use `default:` instead.
* Duplicate `default` cases now produce a compile error.
* `get_real_time()` is now soft-deprecated. Use `rng_root_seed()` for RNG seeding.
* `rng_seed_time()` is now soft-deprecated and returns `rng_root_seed()` internally.

</details>

<details>

<summary>New APIs</summary>

* `Datetime`
  * Includes `year`, `month`, `day`, `hour`, `minute`, and `second` as `s64`
* `get_nanoseconds_since_epoch() -> s64`
  * Returns UTC nanoseconds
* `get_utc_datetime() -> Datetime`
* `rng_root_seed() -> u64`
  * Returns the deterministic game-instance RNG seed
* `SFX.fade_out_and_stop(id, fade_time)`
* `Player_Base.get_name_override() -> string`
* `Player_Base.set_name_override(name)`
* `core_globals.server_on_chat_message_received`
  * Callback signature: `proc(player: Player, message: string)`
* `Texture_Asset.get_uvs() -> (v2, v2)`
  * Returns low and high UV coordinates
* `Time.nanoseconds_since_epoch() -> u64`
* `SFX_Desc.local_only: bool`
  * Hard-deprecated. Use `SFX_Desc.specific_to_player`.
* `SFX_Desc.specific_to_player: Player`
  * Tracks the sound on the server but only triggers playback on the target player's client
* `Sprite_Renderer.mask_in_shadow: bool`
* `Spine_Animator.mask_in_shadow: bool`
* Spine animation APIs
  * `Spine_Animator.set_on_event()`
  * `Spine_Animator.set_on_animation_start()`
  * `Spine_Animator.set_on_animation_end()`
  * `Spine_Event_Data`
* Spine animator access
  * All Spine methods are now available directly on `Spine_Animator`
  * `.instance` is no longer required
* `Entity.set_name()`
* `Entity.compare_name(name: string) -> bool`
* Scene inventory and save APIs
  * Added inventory capacity APIs
  * Added auto-save APIs
* Voice and VOIP APIs
  * `voice_set_range(float)`
  * `voice_get_range()`
  * `Scene::voip_range`
* UIK
  * New API for building UIs
  * Docs are coming soon

</details>
{% endupdate %}

{% update date="2026-03-06" %}

## Protocol 41 - March 3rd, 2026

This protocol update improved CSL netcode performance dramatically and introduced new **Terrain and Tilemap editor tools and a built in agent to help with scripting and world building!**

<details>

<summary>New Features</summary>

* Terrain tool
  * Paint grasslands, oceans, shores, caves and more with broad high performance brush strokes
* Tilemap editor tool
  * Bring a tile set (or use one of ours) and create tiled maps using the new tilemap component
* Agent pane
  * Hooks into Claude Code or Codex-Cli and can build maps, write scripts, and design UI
* New MCP features
  * Tools for world building
  * Tools for reading game logs
  * Tools for adding full SFX from our catalog to your games!

</details>

<details>

<summary>API Changes</summary>

* Improved leaderboard culling APIs

</details>

<details>

<summary>Bug Fixes</summary>

* Major crash fixes & performance improvements!

</details>
{% endupdate %}

{% update date="2026-01-30" %}

## Protocol 40 - Feb 9th, 2026

This protocol update introduces CSL and makes major improvements to the editor workflow for new projects. Protocol 40 is expected to launch around Feb 3rd

<details>

<summary>New Features</summary>

* Adds support for our new game programming language. You'll see new experiences built with this soon!
* Voice chat quality and moderation accuracy improved
* Added chat spam prevention
* Core app features are now translated into 34 languages!
* Improved text rendering on low-end devices
* Android now supports deeplinks from web/push notifications into the app.

</details>

<details>

<summary>Editor Changes</summary>

* The editor now has you select a "game projects" directory at startup and will make any game there available to you\
  When creating a new game from Campfire, we now automatically fill in the ao.project with your game ID.
* Added saving progress bar to the top of the editor when saving
* Added in-editor asset search
* Assets in your project can now be rearranged in folders directly in the editor
* Improved project upload (zip) speed
* Launching the game for testing will use the latest version of the scene even if you haven't saved yet
* You can now change the scale of Sprite\_Renderers directly in editor
* You can now create and save custom layouts for editor panel positions
* The MCP server is now installed automatically
* CSL is now default for new projects. If you need C# you must remove csl: true from ao.project

</details>

<details>

<summary>API Changes</summary>

* Added C# Save.GetAllKeys()

</details>

<details>

<summary>Bug Fixes</summary>

* No major bug fixes this release

</details>
{% endupdate %}
{% endupdates %}


