> For the complete documentation index, see [llms.txt](/llms.txt)


# Writing Rules in GDScript

How to write the rules for stateMachine rooms in GDScript, and generate and register them from Godot



You can write the rules for `stateMachine` rooms in GDScript as well as in C#. If you build your game with Godot only, you can write rules without setting up a C# environment.

GDScript rules do not run in Godot. GS2's realtime rules compiler (`gs2-realtime-compiler`) converts them into the same GSL as C# rules, and into classes used in Godot. So what you can do and the execution rules are the same as for C# rules. For the meaning of the Room API, the execution rules, and the limits, see the [Room Definition Reference](). This page explains how to write rules in GDScript and the steps to generate and register them.

Rule scripts use the SDK's stubs (`Gs2RoomRules`, `Gs2Record`, `Gs2Step`, `Gs2Room`, `Gs2RoomSync`). So editor completion and static type checking work.

## Example {#example}

```gdscript
class_name TickRoom
extends Gs2RoomRules

const STATE := TickState
const RESULT := TickResult
const ERROR := TickError


class TickState extends Gs2Record:
	var steps: int


class TickResult extends Gs2Record:
	var finished: bool


enum TickError { INVALID_OPERATION = 5 }

## @room_schema(5)
enum Character { X = 0, Y = 1, LAST_TICK = 2 }

## @room_input
enum TickInput {
	## @input_range(-1, 1)
	MOVE_X = 0,
}


static func start() -> Gs2Step:
	Gs2Room.define_tick_rate(30)
	Gs2Room.define_simulated_prefab(Character)
	return Gs2Step.wait_for(on_tick, TickState.new({steps = 0}))


static func on_tick(state: TickState, tick: int) -> Gs2Step:
	for i in range(Gs2Room.object_count(Character)):
		_move(Gs2Room.object_at(Character, i), tick)
	return Gs2Step.stay(state.with({steps = state.steps + 1}))


## @predict(Character)
static func _move(id: int, step: int) -> void:
	Gs2Room.set_field(id, Character.X, Gs2RoomMath.clamp(Gs2Room.get_field(id, Character.X) + Gs2Room.input(id, TickInput.MOVE_X) * 10, -1000, 1000))
	Gs2Room.set_field(id, Character.LAST_TICK, step)
```

## Rule Structure {#structure}

| Element | How to write it | C# equivalent |
| --- | --- | --- |
| Rules | `class_name <Name>` and `extends Gs2RoomRules`. One rule per script | The rules class |
| State, result, error | `const STATE := <record>`, `const RESULT := <record>`, `const ERROR := <enum>` | The type arguments of `Step` |
| Records | Write only typed fields in `class <Name> extends Gs2Record:`. Create one with all fields specified using `<Name>.new({field = value, ...})`, and copy with `record.with({field = value})` | record |
| Start | `static func start() -> Gs2Step` | `Start` |
| Handlers | `on_interact(state, object_id: int, method: int)`, `on_timer(state, object_id: int, event_id: int)`, `on_tick(state, tick: int)`, `on_spawned(state, object_id: int)` | `Interact`, `Timer`, `Tick`, `Spawned` |
| Helpers | `static func _name(...)`. Returns a value or `-> void` | private methods |
| Constants | `const NAME := 3` | `const int` |

Make every function `static`, and give types to parameters and return values. Handler parameter names are the names the room passes, so you cannot change them (you can prefix an unused parameter with `_`, as in `_object_id`).

Supported types are `int` (computed as int32 in the room; going out of range fails that whole processing), `bool`, `String`, records, and enums. `float` can be used only for constants written to float fields (`Gs2Room.set_float`) and for tag arguments.

You cannot name the error enum `Error` (it collides with GDScript's built-in `Error`). Give it a name specific to the rules, such as `TickError`.

## Step {#step}

| GDScript | C# | Meaning |
| --- | --- | --- |
| `Gs2Step.wait_for(on_tick, state)` | `Step.WaitFor(nameof(Tick), state)` | Waits for the handler's event in a new task |
| `.or_wait_for(on_timer)` | `.Or(nameof(Timer))` | Also waits for another handler at the same time |
| `Gs2Step.next(_claim, state)` | `Step.Next` | Runs a function next within the same event |
| `Gs2Step.stay(state)` | `Step.Stay(state)` | Updates the state while staying in the current task |
| `Gs2Step.end(result[, state])` | `Step.End` | Ends the rules |
| `Gs2Step.ignore(TickError.MEMBER)` | `Step.Ignore` | Rejects the event (all changes made are rolled back) |

Specify handlers by the function itself (`on_tick`), not by a name string.

## Tags {#tags}

Instead of C# attributes, write tags in `##` comments, one per line. Unknown tags and tags in the wrong place are errors.

| Tag | Where to write it | C# equivalent |
| --- | --- | --- |
| `## @room_schema(7, standard=POSITION, position_precision=0.5, rotation_precision=...)` | enum | `[RoomSchema]`. `standard=` is `POSITION` / `ROTATION` / `TRANSFORM` |
| `## @field(int\|bool\|float, precision=0.5, default=1.5)` | Schema member | `[RoomField]` |
| `## @room_input` | enum | `[RoomInput]` |
| `## @input_range(-1, 1)` | Input member | `[InputRange]` |
| `## @room_action` | enum | `[RoomAction]` |
| `## @room_timer` | enum | `[RoomTimer]` |
| `## @predict(Character)` | Function | `[Predict]`. `static func _name(id: int[, tick: int]) -> void` |

## Gs2Room {#room-api}

You can use the same API as C#'s `Room`, with snake_case names. GDScript has no generics or overloads, so write them as follows.

| How to write it | Example | C# |
| --- | --- | --- |
| Pass the schema as the first argument | `Gs2Room.define_simulated_prefab(Character)`, `Gs2Room.spawn(Crate, x, y)` | `Room.DefineSimulatedPrefab<Character>()`, `Room.Spawn<Crate>(x, y)` |
| Use `_raw` when passing a numeric Prefab ID | `Gs2Room.define_server_prefab_raw(30, 1991)`, `Gs2Room.spawn_raw(30, 0, 0, 0)` | `Room.DefineServerPrefab(30, ...)` |
| Reading and writing fields | `get_field` / `set_field`; for bool, `get_bool` / `set_bool`; for float constants, `set_float` | `Room.Get` / `Room.Set` |
| Standard fields | `get_standard` / `set_standard(id, Gs2RoomSync.StandardField.ROTATION_Y, v)` | `StandardField` |
| 2D | `spawn(S, x, y)`, `raycast_2d`, `overlap_2d` | The forms without z |

The other functions (`input` / `input_clamped`, `object_count` / `object_at`, `is_prefab` / `prefab_of` / `owner_of` / `exists` / `find_owned`, `despawn`, `after` / `cancel_timer`, `sender_connection_id` / `sender_user_id`, `define_hitbox` / `define_relevance`, and so on) mean the same as in C#. For integer math, you can use `Gs2RoomMath` (`clamp`, `abs`, `sign`, `sqrt`, `length2`, `length3`, `mul_div`, and the constants `INT32_MIN` / `INT32_MAX`) and `mini` / `maxi`.

## Name Mapping {#naming}

GDScript names are mapped to GSL names by fixed rules. Names that do not fit the rules are errors.

| GDScript | Name in GSL |
| --- | --- |
| `start`, `on_tick`, `_move` | `Start`, `Tick`, `Move` |
| Parameters and local variables `object_id` (`_object_id`) | `objectId` |
| Record field `box_id` | `BoxId` |
| Enum member `LAST_TICK` | `LastTick` |
| Classes and enums | Unchanged (PascalCase) |

Error messages are shown with GDScript syntax and names.

## Supported Syntax {#syntax}

- `var x: int = ...` / `var x := ...`, `const`
- `if` / `elif` / `else`, `while`, `for i in range(...)` (the end value is evaluated only once)
- `match` (constant patterns and `_`)
- `return`, `pass`, `break`, `continue`
- Arithmetic (`+ - * / %`), comparisons, `and` / `or` / `not`, `a if c else b`
- `@warning_ignore("...")`

You cannot write arrays, dictionaries (except as arguments to a record's `new` / `with`), subscripts, `self`, `await`, lambdas, signals, `in` / `is` / `as`, bitwise operations, `**`, or float arithmetic. If you do, you get an error with its position.

Integer division triggers GDScript's `integer_division` warning. Disable this warning for the rules directory, or write `@warning_ignore("integer_division")` before the statement.

## Generation and Registration {#deployment}

### Set Up the Compiler

Get the `gs2-realtime-compiler` for your OS from the same release as the SDK ([how to get it]()), and set its location in the Godot editor setting `gs2/realtime/compiler_path`.

### Generate

Run **Project > Tools > GS2 > Build Realtime Rules**. With a single rule, the following project settings are used. If the default locations are fine, you do not need to set them.

| Setting | Meaning | Default |
| --- | --- | --- |
| `gs2/realtime/rules_directory` | Where the rule scripts are placed | `res://rules` |
| `gs2/realtime/generated_directory` | Output location for the classes the game uses | `res://generated/realtime` |
| `gs2/realtime/package_directory` | Output location for the `package.zip` to register | `res://../build/realtime` |

Each location can hold only one rule script. With several rules, put each rule in its own location, and list `{"rules": ..., "generated": ..., "package": ...}` sets in the project setting `gs2/realtime/rule_sets`. When `rule_sets` is not empty, the three settings above are not used.

You can also generate from the command line.

```sh
gs2-realtime-compiler --build path/to/rules --output path/to/build
```

The following classes are output to `generated_directory`. Files that are no longer generated are removed, and the `.uid` of unchanged files is kept, so references from scenes do not break.

| File | Use |
| --- | --- |
| `<SchemaName>.realtimeschema` | The sync schema to specify in `Gs2NetworkEntity.schema` |
| `<SchemaName>_schema.gd` (`class_name <SchemaName>Schema`) | Field slots and typed accessors |
| A script for each input and action enum | Constants used as in `world.set_input(TickInput.MOVE_X, 1)` and `door.interact(DoorAction.OPEN)` |
| A script for the error enum | Constants for refusal result codes |
| `<RuleName>_prediction.gd` | The prediction code. Enable it with `<RuleName>Prediction.register(world)` |

The generated classes have a `class_name`, and `class_name` is shared across the whole project. If a name collides with another rule's output or another script in the project, that rule is not output and you get an error (names that differ only in case also count as collisions). For example, if two rules both have `enum PlayerState`, both generate `PlayerStateSchema`, so rename one of them.

Rule scripts are not used to run the game. Placing them in the project makes the editor's type checking work, but exclude them with an export filter.

### Register

Registration follows the same steps as for C# rules. Specify `main.gsl` in `package.zip` as the `payload` of GS2-StateMachine's [UpdateStateMachineMaster](#updatestatemachinemaster), and specify the rules' `class_name` as `mainStateMachineName`. For the Realtime namespace settings, see [Generation and Registration in the Room Definition Reference](#deployment).

GSL generated from GDScript is identical to GSL generated from C# rules with the same content, except for the comments that show positions in the source file.



