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


# C# ステートマシン定義リファレンス

C# で記述する状態、操作、処理の継続、報酬、乱数の仕様



C# のクラスで、ゲームが保持するデータと、プレイヤーの操作に対する処理を定義します。このページは、その書式と API のリファレンスです。

定義の生成には **LocalStateMachineKit** を使用します。C# の定義から、GS2-StateMachine へ登録する GSL を生成します。投機的実行を使う場合は、Unity から操作を呼ぶための C# クラスも生成します。

- 実際に作成して動かす手順：[チュートリアル]()
- 生成した C# クラスをゲームから使う方法：[StateMachine の投機的実行 Session API]()
- 出力先や SDK 参照の指定：[生成設定・登録]()

生成する GSL の実行先は、生成設定の [`hostProfile`](#project) で選びます。このページは、既定値の `LocalReplayV1` を前提に説明します。

| `hostProfile` | 実行方式 | このページとの違い |
| --- | --- | --- |
| `LocalReplayV1`（既定値） | StateMachine の投機的実行。クライアントで操作を実行し、サーバーが操作履歴を再生して検証する | なし |
| `RealtimeRoomV1` | Realtime のルーム内で実行する | クラス・record・Start・Step の書き方は同じ。[GameRewards](#rewards) と [GameRandom](#random) の代わりに Room API を使う |

`RealtimeRoomV1` の入力、Room API、同期項目と操作権、ルーム内での実行規則は [ルーム定義リファレンス]()を参照してください。

## 定義の構成要素 {#definition}

一つの定義は、次の要素で構成します。

| 要素 | 意味 | 例 |
| --- | --- | --- |
| 状態 | 次の操作へ引き継ぐゲームデータ | 所持数、HP、選択済みのルート |
| 操作 | 外部から受け付ける入力と、その処理を記述したメソッド | アイテムを使う、ルートを選ぶ |
| 結果 | ゲーム終了時に残すデータ | 最終スコア、クリアしたかどうか |
| Step | 処理後の状態と、次に何をするかを返す値 | 操作を待つ、処理を続ける、終了する |

ここでいう「状態」は保存するデータです。「現在どの操作を受け付けるか」は、別に `Step` で指定します。

`Step<状態の型, 結果の型, 入力を拒否する理由の型>` の三つの型を定義全体で揃えます。以下の例は `using Step = ...` でこの長い型名を `Step` と略記しています。

### 定義の全体例

数値 `Count` を保持する `Counter` の定義です。`Add` で指定した数を加え、`Finish` で終了します。`Add` と `Finish` は、この例で定義した操作名です。

```csharp
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` は開始時に一度実行します。この例では Count を 0 にし、Add と Finish を受け付けます。Add の処理後も同じ二つの操作を受け付け、Finish の処理後は終了します。

## 宣言規則 {#declarations}

### ルールクラス

```csharp
public static class Counter
{
    // Start、操作、自動処理、補助関数を宣言する
}
```

クラス名がステートマシン名になります。一つのソースフォルダーにルールクラスを一つ置きます。状態・結果の record と enum は、同じフォルダー内でファイルを分けても構いません。

クラス内に宣言できるのは static メソッドです。フィールド、プロパティ、コンストラクター、型パラメーターは使用できません。

### メソッド

以下の `Step` は、同じ状態・結果・理由の型を指定した `Step<TState, TResult, TCode>` を指します。

| 種類 | 宣言の形 | 呼び出されるタイミング |
| --- | --- | --- |
| 開始処理 | `public static Step Start()` | ゲームの開始時。必須 |
| 操作 | `public static Step Add(TState state, int amount)` | 外部から Add の入力を受け取ったとき |
| 自動処理 | `private static Step Resolve(TState state)` | `Step.Next` で指定されたとき |
| 補助関数 | `private static int Calculate(int value)` | 定義内のメソッドから通常の C# 関数として呼んだとき |

操作と自動処理の違いは、外部からの入力を待つかどうかです。自動処理は入力を待たずに実行します。`Step` を返すメソッドへ進む場合は `WaitFor` または `Next` を使い、`Add(...)` のように直接呼び出しません。

メソッドは同期的な static メソッドにします。次の宣言は使用できません。

- 同名メソッドのオーバーロード
- async、ジェネリックメソッド
- ref / out / in 引数、params、省略可能な引数

型・メソッド・引数などの名前には ASCII の英字または `_` で始まる英数字と `_` を使用します。生成コードの予約名や名前の衝突は生成時に検査します。

### 引数を渡す側

`Start` は引数を取りません。それ以外の `Step` を返すメソッドでは、第 1 引数を状態型にします。

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

この宣言では、呼び出し元が渡すのは `amount` だけです。現在の `state` と報酬用の `rewards` は実行環境が渡します。生成される Unity 用クラスでは `game.Add(3)` と呼びます。

| 引数 | 値を渡す側 | 使用できる場所 |
| --- | --- | --- |
| 第 1 引数の状態 | 実行環境 | 操作・自動処理で必須 |
| `GameRewards` | 実行環境 | 操作・自動処理。それぞれ最大一つ |
| `GameRandom<TStream>` | 実行環境 | 操作・自動処理。それぞれ最大一つ |
| 上記以外のデータ | 操作の呼び出し元 | public な操作のみ |

自動処理へ新しい外部入力を渡すことはできません。操作から自動処理へ値を引き継ぐ場合は状態に含めます。入力名 `revision` は予約されているため使えません。

初期化で乱数や報酬を使う場合は、Start で初期状態を作り、`Next` から必要な引数を持つ自動処理へ進みます。

## データ型 {#types}

### 状態・結果・操作入力

| 型 | 扱い |
| --- | --- |
| `int` | 符号付き 32 ビット整数 |
| `long` | 符号付き 64 ビット整数。int32 や浮動小数点へ狭めずに扱う |
| `float` / `double` | それぞれの浮動小数点精度で扱う。NaN・Infinity は保存・送信できない |
| `bool` | 真偽値 |
| `string` | null でない文字列 |
| enum | ソース内で宣言した、基底型が `int` の列挙型 |
| record | 上記の型をプロパティに持つデータ型。下記の宣言形式を使用する |

状態と結果は、次のように丸括弧の引数でプロパティを宣言する record にします。この宣言形式を positional record と呼びます。操作入力には、表の基本型・enum・record を指定できます。

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

更新後の状態は `new` または `with` で作り、Step に渡します。

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

record のプロパティに、別の record、配列、コレクション、null を含めることはできません。また、継承、型パラメーター、追加メンバー、プロパティの既定値、`record struct` は対象外です。

### enum の種類 {#enum-kinds}

| 用途 | 宣言例 | 数値の規則 |
| --- | --- | --- |
| 状態・入力などの通常の enum | `enum Choice { None, Small, Large }` | 数値を明示指定しない |
| `TCode`：Ignore の理由 | `enum Error { InvalidAmount = 1001 }` | 正の int32 値を各メンバーに明示。重複不可 |
| `TStream`：乱数の用途 | `enum RandomSource { Drop = 1 }` | 0 以上の int32 値を各メンバーに明示。重複不可 |

エラーコードと乱数ストリーム ID は、運用開始後も意味を変えない値として管理します。一つのルールクラスで使用する乱数用 enum は一種類です。

### 報酬リクエスト内の SDK データ

報酬の組み立てに使用する SDK オブジェクトは、上記の状態・操作入力とは別の扱いです。SDK が宣言する nullable、配列、入れ子 Model を使用できます。SDK オブジェクトを `TState` や操作入力へ保存することはできません。詳細は [GameRewards](#rewards) を参照してください。

## 対応する C# 構文 {#language}

定義の中で使用できる C# の構文は次のとおりです。

| 分類 | 対応範囲 |
| --- | --- |
| 宣言・変数 | 対応型のローカル変数、代入、定数 |
| データの作成 | positional record の `new` と `with`、SDK リクエスト・Model・配列の作成 |
| 分岐 | if、条件式、定数・when・最後の `_` を使う switch 式 |
| 繰り返し | for、while、break、continue |
| 関数 | 同じクラス内の private 補助関数、名前付き引数、return |
| 論理演算 | bool の否定・比較、`&&` / `||` の短絡評価 |
| 数値演算 | checked int32 / int64 の加減乗除・剰余、float / double の演算、対応する数値変換、ローカル変数への複合代入 |
| 数値関数 | int32 の `Math.Min` / `Math.Max` |
| SDK の nullable 値 | `HasValue`、`Value`、`GetValueOrDefault`、`??` |

一般的なコレクション、foreach、入れ子 record、async、任意の外部 API 呼び出しは対象外です。整数演算はオーバーフローを検査する設定で生成します。ループと関数呼び出しには実行量の上限があります。

## 一回の操作の処理範囲 {#execution}

外部から一つの操作を受け取ると、そのメソッドを実行します。`Next` が返った場合は、指定した自動処理も続けて実行します。この一連の処理を、一回の入力として扱います。

- `WaitFor` または `Stay` に到達すると、更新後の状態で次の入力を待ちます。
- `End` に到達すると、最終状態と結果を保持して終了します。
- 途中で `Ignore` に到達すると、今回の状態変更・乱数の進行・報酬要求を取り消し、入力前と同じ状態で待ちます。操作履歴にも加えません。

ここでの処理完了はローカル実行の結果です。サーバーの検証は、別途 [Session の Confirm](#confirmation) で操作履歴を送って受けます。

## Step API {#step}

Step は次の動作を指定する戻り値です。以下の `TState` は状態型、`TResult` は結果型、`TCode` は入力を拒否する理由の enum を指します。

| API | 次の動作 | 保持する状態 |
| --- | --- | --- |
| `WaitFor` | 指定した操作を待つ | 引数の状態 |
| `Stay` | 入力前と同じ操作の組み合わせを待つ | 引数の状態 |
| `Next` | 指定した自動処理を直ちに実行する | 引数の状態を後続処理へ渡す |
| `End` | 終了する | 引数の状態。省略した場合は現在の状態 |
| `Ignore` | 今回の入力を取り消し、再び待つ | 入力前の状態 |

`WaitFor`、`Or`、`Next` の処理先は、同じルールクラスのメソッドを指す `nameof(...)` で指定します。文字列リテラルや変数、Start は指定できません。

### WaitFor / Or

次に受け付ける操作を指定します。

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

`state` を次の状態とし、`target` で指定した public な操作の入力を待ちます。複数の操作を受け付ける場合は、`WaitFor(...).Or(...).Or(...)` と連結します。

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

`Or` は WaitFor の呼び出しに連結します。同じ操作を重複指定できません。

### Stay

状態を更新し、同じ操作を引き続き受け付けます。

```csharp
static Step Stay(TState state);
```

`state` を次の状態とし、**今回の入力を受け付ける直前と同じ操作の組み合わせ**で待機します。実行中の関数をもう一度呼ぶ意味ではありません。

```csharp
return Step.Stay(nextState);
```

たとえば Add と Finish を受け付けていた場合、Add から Stay を返すと両方を引き続き受け付けます。最初の入力待ちに到達する前の `Start` とその自動処理では使用できません。

### Next

入力を待たずに、次の処理を実行します。

```csharp
static Step Next(string target, TState state);
```

`state` を渡して、`target` で指定した private な自動処理を直ちに実行します。

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

呼び出し先の宣言例は `private static Step Resolve(CounterState state)` です。必要なら乱数・報酬の引数も指定できます。public な操作、`Start`、新しいプレイヤー入力を必要とするメソッドは対象にできません。

処理の採用・取消は[一回の操作の処理範囲](#execution)に従います。`Next` の循環は生成時に拒否します。入力を繰り返し待つ場合は `WaitFor` または `Stay` を使用します。

### End

結果を残してゲームを終了します。

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

結果を保持してステートマシンを終了します。終了後は新しい操作を受け付けません。

| 引数 | 動作 |
| --- | --- |
| `result` のみ | その時点の状態を保持し、結果を設定する |
| `result, state` | 指定した状態を最終状態とし、結果を設定する |

```csharp
return Step.End(new CounterResult(nextState.Count), nextState);
```

ローカル変数 `nextState` を作っただけでは、保持される状態は更新されません。更新後の状態を残す場合は第 2 引数へ渡します。`Start` で即時終了する場合も、初期状態を第 2 引数に指定します。

終了と報酬の要求は独立しています。報酬を要求するために End を返す必要はありません。

### Ignore

ゲームのルール上受け付けられない入力を、理由付きで取り消します。

```csharp
static Step Ignore(TCode code);
```

今回の入力を採用せず、理由を返します。`code` にはエラー enum のメンバーを直接指定します。変数からの指定はできません。

```csharp
return Step.Ignore(CounterError.InvalidAmount);
```

取り消す範囲は[一回の操作の処理範囲](#execution)に従います。`Start` と、最初の入力待ちより前の自動処理では使用できません。

## GameRewards：報酬の要求 {#rewards}

ゲーム内で決まった報酬を、GS2 SDK の付与リクエストとして指定します。操作または自動処理の引数に `GameRewards` を追加します。ゲームの途中でも使用できます。

### Grant(request)

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

`request` に渡せるのは、SDK が `AcquireAction` に登録している付与リクエストです。参照 API や消費 API のリクエストは生成時に拒否します。

付与する操作は、各サービスのトランザクションアクションの **Acquire Action** から探せます。たとえば [Inventory の入手アクション](#acquire-action)で付与内容を確認し、対応する SDK の Request 型を使用します。使用中の SDK がその型に対応しているかは生成時に検査します。

次の例では Add で Count を増やすたびに、同じ数の parts を要求します。using は定義ファイルの先頭、Add はルールクラス内に記述します。

```csharp
using Gs2.Gs2Inventory.Request;
using Gs2.Gs2Inventory.Model;

// Counter クラス内の操作
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`、`parts` は付与先のリソース名です。実サービスで使用する場合は、登録済みの名前を指定します。呼び出し元は引き続き `game.Add(3)` と呼び、GameRewards は実行環境が渡します。

### 指定できる値

SDK のリクエスト型と Model 型をそのまま使用します。

| SDK の値 | 扱い |
| --- | --- |
| `int?` / `long?` | それぞれの整数範囲を保持する |
| `float?` / `double?` | それぞれの精度を保持する。NaN・Infinity は送信不可 |
| bool・文字列・null | SDK の宣言に従って指定する |
| 配列・入れ子 Model | SDK の型で組み立てる |

`new`、オブジェクト初期化子、`With`、プロパティの読み書き、配列の生成・添字アクセスを使用できます。変数や private 補助関数を使って組み立てることもできます。

Grant は呼び出した時点の内容を保存します。その後に元のオブジェクトを変更しても、要求済みの報酬は変わりません。受取人、`TimeOffsetToken`、`DuplicationAvoider` は実行環境が設定するため、定義では指定しません。0・負数・null の可否などは、付与先 API の仕様に従います。

SDK 型を使う場合の参照設定は[SDK DLL の参照](#sdk-references)を参照してください。`bindings.json` は不要です。

### 要求と払い出しの違い

Grant は報酬を要求する処理です。ローカルの在庫をその場で増やす処理ではありません。Ignore になった入力の要求は取り消します。

実際の払い出しは、操作履歴をサーバーへ送り、検証を受けた後に行われます。**ローカルでの入力受付、サーバー検証、報酬反映は別の段階です。** 確認方法は [Session の Confirm](#confirmation)を参照してください。

### Grant(id, amount)：enum による指定

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

Inventory の SimpleItem に限り、報酬 enum と個数でも指定できます。たとえば `enum Reward { Parts }` を定義し、`rewards.Grant(Reward.Parts, amount)` と呼びます。付与先は別途 [bindings.json](#bindings) に設定します。

この形式の amount は int32 です。0 なら要求を発行せず、負数は実行エラーです。SDK リクエストを渡す形式の数値範囲・制約とは異なります。

## GameRandom：乱数 {#random}

サーバーが同じ結果を再現できる乱数を生成します。操作または自動処理の引数に `GameRandom<TStream>` を追加します。`TStream` は乱数の用途を表す enum です。

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

`Stream` で用途を選び、`RangeInclusive` で両端を含む範囲の int32 を得ます。たとえば `RangeInclusive(2, 4)` の戻り値は 2・3・4 です。`min > max` は実行エラーです。

次の例では、Add 一回につき Count を 2〜4 増やします。enum はルールクラスの外、Add はクラス内に記述します。

```csharp
public enum RandomSource { Count = 1 }

// Counter クラス内の操作
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);
}
```

この場合、呼び出し元は `game.Add()` と呼びます。乱数の引数は渡しません。用途ごとに enum メンバーを分け、ID は運用開始後も意味を変えない値にします。数値の規則は [enum の種類](#enum-kinds)を参照してください。

`System.Random` や `UnityEngine.Random` は使用できません。生成・再生には実行環境のシードと使用状況を使います。Ignore の場合は[共通の取消規則](#execution)に従います。

## 検証と失敗時の動作 {#validation}

| 場面 | 動作 |
| --- | --- |
| 未対応の構文・型、遷移先の不整合 | 生成時に診断を表示する。ソースの診断にはファイル名・行・列が付く |
| ゲーム固有の受付条件に違反 | 操作内で検査し、`Step.Ignore` で理由を返す |
| 入力型・数値範囲・enum 候補・record 必須フィールド・リビジョンの不一致 | 生成した実行コードでも検査する。内部の Retry として入力を採用せず、ゲーム固有の Ignore 理由と区別する |
| 待機中に受け付けていない操作を Session から呼ぶ | 例外 |
| ゼロ除算などの実行エラー | エラーとして扱う。Ignore と異なり、入力前へ戻ってそのまま続行することは保証しない |
| 生成の失敗 | 前回成功した生成物を残す |

Ignore と実行エラーは復帰方法が異なります。通常の入力拒否は例外で表現せず Ignore を使用してください。




