C# State Machine Definition Reference

Specifications for states, operations, continuing processing, rewards, and random numbers written in C#

You define the data a game holds and how it handles player operations in a C# class. This page is the reference for that format and its APIs.

You use LocalStateMachineKit to generate definitions. It generates the GSL to register with GS2-StateMachine from the C# definition. When you use speculative execution, it also generates C# classes for calling operations from Unity.

You choose where the generated GSL runs with hostProfile in the generation settings. This page assumes the default, LocalReplayV1.

hostProfileExecution modelDifferences from this page
LocalReplayV1 (default)StateMachine speculative execution. The client runs operations, and the server replays the operation history to verify itNone
RealtimeRoomV1Runs inside a Realtime roomClasses, records, Start, and Step are written the same way. Uses the Room API instead of GameRewards and GameRandom

For RealtimeRoomV1 inputs, the Room API, sync fields and control, and execution rules inside a room, see the Room Definition Reference.

Definition Components

A definition consists of the following elements.

ElementMeaningExamples
StateGame data carried over to the next operationItem counts, HP, the selected route
OperationA method describing an input accepted from outside and how it is handledUse an item, choose a route
ResultData kept when the game finishesFinal score, whether it was cleared
StepA value returning the state after processing and what to do nextWait for an operation, continue processing, finish

“State” here means the data that is stored. “Which operations are accepted now” is specified separately with Step.

Keep the three types of Step<state type, result type, type of reasons for rejecting input> consistent throughout the definition. The examples below abbreviate this long type name as Step with using Step = ....

Complete Definition Example

This is the definition of Counter, which holds a number Count. Add adds the specified number, and Finish finishes. Add and Finish are operation names defined in this example.

using Gs2.LocalStateMachine.Authoring;
using Step = Gs2.LocalStateMachine.Authoring.Step<
    GameRules.CounterState, GameRules.CounterResult, GameRules.CounterError>;

namespace GameRules;

public record CounterState(int Count);
public record CounterResult(int Count);
public enum CounterError { InvalidAmount = 1001 }

public static class Counter
{
    public static Step Start() =>
        Step.WaitFor(nameof(Add), new CounterState(0)).Or(nameof(Finish));

    public static Step Add(CounterState state, int amount)
    {
        if (amount < 1 || amount > 10)
            return Step.Ignore(CounterError.InvalidAmount);

        var next = state with { Count = state.Count + amount };
        return Step.Stay(next);
    }

    public static Step Finish(CounterState state) =>
        Step.End(new CounterResult(state.Count), state);
}

Start runs once at the beginning. In this example, it sets Count to 0 and accepts Add and Finish. After Add is processed, the same two operations are still accepted; after Finish is processed, the machine finishes.

Declaration Rules

Rules Class

public static class Counter
{
    // Declare Start, operations, automatic steps, and helper functions
}

The class name becomes the state machine name. Put one rules class in one source folder. The state and result records and enums may be split into separate files within the same folder.

Only static methods can be declared in the class. Fields, properties, constructors, and type parameters cannot be used.

Methods

Step below refers to Step<TState, TResult, TCode> with the same state, result, and reason types.

KindDeclaration formWhen it is called
Startpublic static Step Start()When the game starts. Required
Operationpublic static Step Add(TState state, int amount)When an Add input is received from outside
Automatic stepprivate static Step Resolve(TState state)When specified by Step.Next
Helper functionprivate static int Calculate(int value)When called as an ordinary C# function from a method in the definition

The difference between operations and automatic steps is whether they wait for external input. Automatic steps run without waiting for input. To proceed to a method that returns Step, use WaitFor or Next; don’t call it directly like Add(...).

Methods must be synchronous static methods. The following declarations cannot be used.

  • Overloads of methods with the same name
  • async or generic methods
  • ref / out / in parameters, params, optional parameters

For names of types, methods, parameters, and so on, use ASCII letters, digits, and _, starting with a letter or _. Reserved names in the generated code and name collisions are checked at generation time.

Who Passes Arguments

Start takes no arguments. In other methods that return Step, the first parameter is the state type.

public static Step Add(CounterState state, int amount, GameRewards rewards)

With this declaration, the caller passes only amount. The runtime passes the current state and rewards for rewards. In the generated Unity class, you call game.Add(3).

ParameterWho passes the valueWhere it can be used
State as the first parameterRuntimeRequired in operations and automatic steps
GameRewardsRuntimeOperations and automatic steps. At most one each
GameRandom<TStream>RuntimeOperations and automatic steps. At most one each
Any other dataThe caller of the operationPublic operations only

You cannot pass new external input to an automatic step. To carry values from an operation to an automatic step, include them in the state. The input name revision is reserved and cannot be used.

To use random numbers or rewards during initialization, create the initial state in Start and proceed with Next to an automatic step that has the required parameters.

Data Types

State, Result, and Operation Input

TypeHandling
intSigned 32-bit integer
longSigned 64-bit integer. Handled without narrowing to int32 or floating point
float / doubleHandled at their respective floating-point precision. NaN and Infinity cannot be stored or sent
boolBoolean
stringA non-null string
enumAn enum declared in the source with an underlying type of int
recordA data type with properties of the types above. Use the declaration form below

Make states and results records that declare properties as parenthesized parameters, as follows. This declaration form is called a positional record. For operation inputs, you can specify the basic types in the table, enums, and records.

public record CounterState(int Count);
public record CounterResult(int Count);
public record MoveInput(int X, int Y);

Create the updated state with new or with and pass it to Step.

var next = state with { Count = state.Count + amount };
return Step.Stay(next);

Record properties cannot contain other records, arrays, collections, or null. Inheritance, type parameters, additional members, property default values, and record struct are also not supported.

Kinds of Enums

UseDeclaration exampleNumeric rules
Ordinary enums for state, input, etc.enum Choice { None, Small, Large }Don’t specify numeric values explicitly
TCode: Ignore reasonsenum Error { InvalidAmount = 1001 }Explicitly assign a positive int32 value to each member. No duplicates
TStream: random number usesenum RandomSource { Drop = 1 }Explicitly assign an int32 value of 0 or more to each member. No duplicates

Manage error codes and random stream IDs as values whose meaning does not change after operation begins. A single rules class uses only one enum type for random numbers.

SDK Data in Reward Requests

SDK objects used to build rewards are handled separately from the states and operation inputs above. You can use the nullable types, arrays, and nested Models that the SDK declares. You cannot store SDK objects in TState or operation inputs. For details, see GameRewards.

Supported C# Syntax

The C# syntax you can use in a definition is as follows.

CategorySupported
Declarations and variablesLocal variables of supported types, assignment, constants
Creating datanew and with for positional records; creating SDK requests, Models, and arrays
Branchingif, conditional expressions, switch expressions with constants, when, and a final _
Loopsfor, while, break, continue
FunctionsPrivate helper functions in the same class, named arguments, return
Logical operationsbool negation and comparison, short-circuit evaluation of && / `
Numeric operationschecked int32 / int64 addition, subtraction, multiplication, division, and remainder; float / double arithmetic; supported numeric conversions; compound assignment to local variables
Numeric functionsint32 Math.Min / Math.Max
SDK nullable valuesHasValue, Value, GetValueOrDefault, ??

General collections, foreach, nested records, async, and calls to arbitrary external APIs are not supported. Integer arithmetic is generated with overflow checking. Loops and function calls have execution limits.

Processing Scope of a Single Operation

When an operation is received from outside, its method runs. If it returns Next, the specified automatic step also runs right after. This whole sequence is treated as a single input.

  • When it reaches WaitFor or Stay, it waits for the next input with the updated state.
  • When it reaches End, it keeps the final state and result and finishes.
  • If it reaches Ignore along the way, this input’s state changes, random number advances, and reward requests are rolled back, and it waits in the same state as before the input. The input is not added to the operation history either.

Processing completion here is the result of local execution. Server verification happens separately when you send the operation history with Session Confirm.

Step API

Step is a return value that specifies what happens next. Below, TState is the state type, TResult is the result type, and TCode is the enum of reasons for rejecting input.

APIWhat happens nextState kept
WaitForWaits for the specified operationsThe state passed as the argument
StayWaits for the same combination of operations as before the inputThe state passed as the argument
NextImmediately runs the specified automatic stepPasses the argument’s state to the subsequent step
EndFinishesThe argument’s state. If omitted, the current state
IgnoreRolls back this input and waits againThe state before the input

Specify the targets of WaitFor, Or, and Next with nameof(...) pointing to a method in the same rules class. String literals, variables, and Start cannot be specified.

WaitFor / Or

Specifies the operations accepted next.

static Step WaitFor(string target, TState state);
Step Or(string target);

Makes state the next state and waits for input of the public operation specified by target. To accept multiple operations, chain them as WaitFor(...).Or(...).Or(...).

return Step.WaitFor(nameof(Add), nextState).Or(nameof(Finish));

Or is chained to a WaitFor call. The same operation cannot be specified more than once.

Stay

Updates the state and keeps accepting the same operations.

static Step Stay(TState state);

Makes state the next state and waits with the same combination of operations as just before this input was accepted. It does not mean calling the running function again.

return Step.Stay(nextState);

For example, if Add and Finish were being accepted, returning Stay from Add keeps accepting both. It cannot be used in Start or its automatic steps before the first wait for input is reached.

Next

Runs the next step without waiting for input.

static Step Next(string target, TState state);

Passes state and immediately runs the private automatic step specified by target.

return Step.Next(nameof(Resolve), nextState);

An example declaration of the target is private static Step Resolve(CounterState state). You can also specify random number and reward parameters if needed. Public operations, Start, and methods that require new player input cannot be targets.

Whether processing is adopted or rolled back follows Processing Scope of a Single Operation. Cycles of Next are rejected at generation time. To wait for input repeatedly, use WaitFor or Stay.

End

Finishes the game and keeps a result.

static Step End(TResult result);
static Step End(TResult result, TState state);

Keeps the result and finishes the state machine. After finishing, no new operations are accepted.

ArgumentsBehavior
result onlyKeeps the state at that point and sets the result
result, stateMakes the specified state the final state and sets the result
return Step.End(new CounterResult(nextState.Count), nextState);

Merely creating a local variable nextState does not update the kept state. To keep the updated state, pass it as the second argument. When finishing immediately in Start, also specify the initial state as the second argument.

Finishing and requesting rewards are independent. You don’t need to return End to request rewards.

Ignore

Rolls back an input that the game rules cannot accept, with a reason.

static Step Ignore(TCode code);

Does not adopt this input and returns a reason. Specify a member of the error enum directly as code. It cannot be specified from a variable.

return Step.Ignore(CounterError.InvalidAmount);

The scope of the rollback follows Processing Scope of a Single Operation. It cannot be used in Start or in automatic steps before the first wait for input.

GameRewards: Requesting Rewards

Specify rewards determined in the game as GS2 SDK grant requests. Add GameRewards to the parameters of an operation or automatic step. You can use it in the middle of the game as well.

Grant(request)

void Grant<TRequest>(TRequest request) where TRequest : class;

You can pass to request only the grant requests that the SDK registers in AcquireAction. Requests for read APIs or consume APIs are rejected at generation time.

You can find grant operations in each service’s transaction actions under Acquire Action. For example, check what to grant in Inventory’s acquire actions and use the corresponding SDK Request type. Whether the SDK you use supports that type is checked at generation time.

In the following example, each time Add increases Count, the same number of parts is requested. Write the usings at the top of the definition file and Add inside the rules class.

using Gs2.Gs2Inventory.Request;
using Gs2.Gs2Inventory.Model;

// Operation inside the Counter class
public static Step Add(CounterState state, int amount, GameRewards rewards)
{
    if (amount < 1 || amount > 10)
        return Step.Ignore(CounterError.InvalidAmount);

    var next = state with { Count = state.Count + amount };
    rewards.Grant(new AcquireSimpleItemsByUserIdRequest()
        .WithNamespaceName("rewards")
        .WithInventoryName("bag")
        .WithAcquireCounts(new[] {
            new AcquireCount().WithItemName("parts").WithCount(amount)
        }));
    return Step.Stay(next);
}

rewards, bag, and parts are the names of the resources to grant to. When using a real service, specify registered names. The caller still calls game.Add(3), and the runtime passes GameRewards.

Values You Can Specify

Use the SDK’s Request and Model types as they are.

SDK valueHandling
int? / long?Keeps the respective integer range
float? / double?Keeps the respective precision. NaN and Infinity cannot be sent
bool, strings, nullSpecify according to the SDK’s declarations
Arrays and nested ModelsBuild them with the SDK types

You can use new, object initializers, With, property reads and writes, and array creation and indexing. You can also build them with variables and private helper functions.

Grant saves the content at the time it is called. Changing the original object afterwards does not change the already requested reward. The recipient, TimeOffsetToken, and DuplicationAvoider are set by the runtime, so don’t specify them in the definition. Whether 0, negative numbers, or null are allowed follows the specification of the grant API.

For reference settings when using SDK types, see SDK DLL References. bindings.json is not required.

Requesting vs. Paying Out

Grant requests a reward. It does not increase local inventory on the spot. Requests from inputs that end in Ignore are rolled back.

The actual payout happens after the operation history is sent to the server and verified. Accepting input locally, server verification, and applying rewards are separate stages. For how to confirm, see Session Confirm.

Grant(id, amount): Specifying with an Enum

void Grant<T>(T id, int amount) where T : Enum;

For Inventory SimpleItems only, you can also specify a reward enum and a count. For example, define enum Reward { Parts } and call rewards.Grant(Reward.Parts, amount). Configure where to grant separately in bindings.json.

In this form, amount is int32. If it is 0, no request is issued; a negative value is a runtime error. The numeric ranges and constraints differ from the form that passes an SDK request.

GameRandom: Random Numbers

Generates random numbers whose results the server can reproduce. Add GameRandom<TStream> to the parameters of an operation or automatic step. TStream is an enum representing the use of the random numbers.

RandomStream Stream(TStream stream);
int RangeInclusive(int min, int max);

Choose the use with Stream, and get an int32 in a range that includes both ends with RangeInclusive. For example, RangeInclusive(2, 4) returns 2, 3, or 4. min > max is a runtime error.

In the following example, each Add increases Count by 2 to 4. Write the enum outside the rules class and Add inside the class.

public enum RandomSource { Count = 1 }

// Operation inside the Counter class
public static Step Add(CounterState state, GameRandom<RandomSource> random)
{
    var amount = random.Stream(RandomSource.Count).RangeInclusive(2, 4);
    var next = state with { Count = state.Count + amount };
    return Step.Stay(next);
}

In this case, the caller calls game.Add(). Random number arguments are not passed. Use a separate enum member for each use, and keep IDs whose meaning does not change after operation begins. For the numeric rules, see Kinds of Enums.

System.Random and UnityEngine.Random cannot be used. Generation and replay use the runtime’s seed and usage state. In the case of Ignore, the common rollback rules apply.

Validation and Failure Behavior

SituationBehavior
Unsupported syntax or types, inconsistent transition targetsDiagnostics are shown at generation time. Source diagnostics include the file name, line, and column
Violation of game-specific acceptance conditionsCheck inside the operation and return a reason with Step.Ignore
Mismatches in input types, numeric ranges, enum candidates, required record fields, or revisionAlso checked in the generated runtime code. The input is not adopted, as an internal Retry, which is distinguished from game-specific Ignore reasons
Calling an operation from the Session that is not accepted while waitingException
Runtime errors such as division by zeroTreated as errors. Unlike Ignore, returning to the pre-input state and continuing is not guaranteed
Generation failureThe previously successful output is kept

Ignore and runtime errors are recovered differently. Don’t represent ordinary input rejection with exceptions; use Ignore.