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

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.

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.

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:

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.

Last updated