# GameFabric Integration

This document covers the `GameFabricGameServerProviderPlugin`, which integrates Pragma Engine with [GameFabric](https://docs.gamefabric.com/) for multiplayer game server allocation. It explains configuration, region selection, the allocation flow, GameFabric console setup, and how to run everything locally using ngrok.

---

## Table of Contents

1. [Overview](#overview)
2. [Architecture](#architecture)
3. [Configuration Reference](#configuration-reference)
4. [Region Selection and the Game Instance Plugin](#region-selection-and-the-game-instance-plugin)
5. [Allocation Flow](#allocation-flow)
6. [Ping Servers and Latency](#ping-servers-and-latency)
7. [GameFabric Console Setup](#gamefabric-console-setup)
8. [Example Unreal Game Server](#example-unreal-game-server)
9. [Running Locally with ngrok](#running-locally-with-ngrok)

---

## Overview

The `GameFabricGameServerProviderPlugin` implements Pragma's `GameServerProviderPlugin` interface. When a game instance needs a 
dedicated server, this plugin sends an allocation request to GameFabric's REST API. GameFabric then finds an available game server in the 
requested region. The allocated game server connects back to Pragma to complete the handshake.

### Key Files

| File | Purpose |
|------|---------|
| `GameFabricGameServerProviderPlugin.kt` | Plugin entry point, handles allocation requests with primary/fallback region logic |
| `GameFabricApi.kt` | HTTP client for GameFabric's allocation and ping REST endpoints |
| `GameFabricRegionPingCache.kt` | Caches ping server addresses so clients can measure latency to each region. Optional usage in your PartyPlugin |
| `gameFabricGameInstance.proto` | Protobuf definitions for ping data and allocation error payloads |
| `gameInstanceExt.proto` | Defines `ExtAllocateGameServer` with `desired_region`, `game_server_version`, and `server_args` |

---

## Configuration Reference

The plugin is configured under `GameInstanceService.gameServerProviderPlugin` in your YAML config:

```yaml
game:
  pluginConfigs:
    GameInstanceService.gameServerProviderPlugin:
      class: "pragma.gameinstance.gamefabric.GameFabricGameServerProviderPlugin"
      config:
        gameFabric:
          gameFabricUrl: "https://STUDIO-IDENTIFIER.gamefabric.dev"
          allocatePath: "allocator/prod/us/allocate"
          pingPath: "ping"
          allocatorToken: "<your-bearer-token>"
          gameServerZonesByProviderRegion:
            us-west: "US_WEST"
            eu-central-1: "EU_CENTRAL"
        pragmaGamePartnerBackendAddress: "http://localhost:10100"
        fallbackRegion: "us-west"
        fallbackEnvironment: "prag"
        fallbackArmadaSet: "sample-game-armada-set"
```

### Config Fields

| Field | Description                                                                                                                                                                  |
|-------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `gameFabric.gameFabricUrl` | Base URL for your GameFabric studio (e.g. `https://egret.gamefabric.dev`) |
| `gameFabric.allocatePath` | REST path appended to the base URL for allocation requests |
| `gameFabric.pingPath` | REST path for fetching ping server addresses |
| `gameFabric.allocatorToken` | Bearer token for authenticating with the GameFabric allocator API |
| `gameFabric.gameServerZonesByProviderRegion` | Optional. Maps GameFabric region names (e.g. `us-west`) to Pragma `GameServerZone` values (e.g. `US_WEST`). Only regions present in this map are exposed to clients for ping |
| `pragmaGamePartnerBackendAddress` | The address the allocated game server uses to connect back to Pragma's Game Partner Gateway |
| `fallbackRegion` | If the primary region allocation fails, retry in this region. Leave blank to disable fallback |
| `fallbackEnvironment` | Used when `ExtAllocateGameServer.game_server_version` is empty. Determines which GameFabric environment to target |
| `fallbackArmadaSet` | Used when the armada set cannot be parsed from `game_server_version` |

### Game Server Version Format

The `game_server_version` field in `ExtAllocateGameServer` uses the format:

```
<environment>::<armadaSet>
```

For example: `prag::sample-game-armada-set`

- The part before `::` is sent as the `env` attribute to GameFabric.
- The part after `::` is sent as the `armadaset` attribute.
- If the field is empty, `fallbackEnvironment` and `fallbackArmadaSet` are used instead.

Note that the two attributes `env` and `armadaset` will need to be configured on your GameFabric AramdaSet as labels in the [General 
Settings](https://STUDIO_IDENTIFIER.gamefabric.dev/ui/ENV_NAME/armadas/set/ARMADASET_NAME/settings/general) area:

| Label key                       | Label value            |
|---------------------------------|------------------------|
| allocator.nitrado.net/armadaset | sample-game-armada-set |
| allocator.nitrado.net/env       | prag                   |

---

## Region Selection and the Game Instance Plugin

The `desired_region` used for allocation comes from the `ExtAllocateGameServer` protobuf message, which is populated in your **custom `GameInstancePlugin`** implementation.

### How it works

1. The `GameInstancePlugin` interface has methods like `handleBackendCreateRequest` and `handlePlayerCreateRequest` that are called when a game instance is being created.
2. Inside these methods, you call `gameInstanceSnapshot.allocateGameServer(ext)` passing an `ExtAllocateGameServer` with your desired allocation parameters.
3. The `GameFabricGameServerProviderPlugin` receives this ext in `startAllocationForGameInstance` and reads `ext.desiredRegion` as the primary region.

### Default behavior (no custom plugin)

The default `GameInstancePlugin.handleBackendCreateRequest` implementation calls:

```kotlin
gameInstanceSnapshot.allocateGameServer(ExtAllocateGameServer.getDefaultInstance())
```

This means `desired_region` will be an **empty string**. In this case, the `GameFabricGameServerProviderPlugin` will directly use `fallbackRegion` as the primary allocation region.

**If you do not provide a region through a custom `GameInstancePlugin`, you must configure `fallbackRegion` in the plugin config, otherwise allocation will fail.**

### Setting the region in a custom GameInstancePlugin

To properly set the region, override the relevant handler in your `GameInstancePlugin` and populate the ext:

```kotlin
override suspend fun handleBackendCreateRequest(
    gameInstanceSnapshot: GameInstance.GameInstance,
    requestExt: ExtBackendCreateRequest,
    playersToAdd: List<PlayerToAdd>,
) {
    playersToAdd.forEach { player ->
        gameInstanceSnapshot.addPlayer(player.playerId, player.partyId, player.teamNumber)
    }

    val ext = ExtAllocateGameServer.newBuilder()
        .setDesiredRegion("us-west")                          // from player ping results or matchmaking data
        .setGameServerVersion("prag::sample-game-armada-set") // environment::armadaSet
        .build()

    gameInstanceSnapshot.allocateGameServer(ext)
}
```

---

## Allocation Flow

When `startAllocationForGameInstance` is called:

1. **Parse identifiers** from `ExtAllocateGameServer`:
    - `environment` = part before `::` in `game_server_version` (or configured `fallbackEnvironment`)
    - `armadaSet` = part after `::` (or configured `fallbackArmadaSet`)
    - `primaryRegion` = `desired_region` (lowercased or configured `fallbackRegion`)

2. **Build the allocation request**:
    - `attributes`: `{ "env": "<environment>", "armadaset": "<armadaSet>" }` (used by GameFabric to find the correct fleet)
    - `payload`: `{ "GameInstanceId", "PartnerGameToken", "PartnerSocialToken", "pragmaBackendAddress", "ServerArgs" }` (forwarded to 
      the game server as Agones annotations)

3. **Send allocation to GameFabric** (primary region):
    - POST to `<gameFabricUrl>/<allocatePath>` with bearer token auth
    - If successful, the game server is allocated and will connect back to Pragma

4. **Fallback on failure**:
    - If the primary region fails and `fallbackRegion` is configured, retry with the fallback region
    - If both fail, throw a `GameServerAllocationApplicationError` containing the HTTP response details

5. **Game server connects back**:
    - The allocated game server receives `pragmaBackendAddress` and partner tokens in the payload
    - It uses the Pragma SDK to call `LinkV1` to make itself available for players to connect to it.

---

## [OPTIONAL] Ping Servers and Latency

`GameFabricRegionPingCache` could be used (eg: in a PartyPlugin) to periodically fetche ping server addresses from GameFabric so that 
game clients can measure latency to each region and choose the best one.

- The ping endpoint returns servers grouped by region and protocol (ICMP, UDP, WSS).
- Only regions present in `gameServerZonesByProviderRegion` are exposed to clients.
- Client ping results can be used in matchmaking or your `GameInstancePlugin` to set the `desired_region`.

---

## GameFabric Console Setup

### Environment Variables

This is an example of environment variables configured directly on the region, therefore being inherited by all armada sets. These are 
set in the GameFabric console and are used by the allocator sidecar running alongside your game server:

| Type           | Variable Name              | Secret              | Value / Key                                                          |
|----------------|----------------------------|---------------------|----------------------------------------------------------------------|
| Pod Field      | `ALLOC_PRIORITY`           | --                  | `metadata.regionTypePriority`                                        |
| Key-Value Pair | `ALLOC_REGION`             | --                  | `us-west`                                                            |
| Key-Value Pair | `ALLOC_URL`                | --                  | `https://STUDIO_IDENTIFIER.gamefabric.dev/allocator/prod/us/servers` |
| Key-Value Pair | `ALLOC_PAYLOAD_ANNOTATION` | --                  | `payload-`                                                           |
| Key-Value Pair | `ALLOC_PAYLOAD_FILE`       | --                  | `/app/shared/payload.json`                                           |
| Secret         | `ALLOC_TOKEN`              | `allocator-prod-us` | `registry_token`                                                     |

**What each variable does:**

- **`ALLOC_PRIORITY`** -- The pod field used to determine allocation priority. `metadata.regionTypePriority` allows GameFabric to prefer servers in the requested region.
- **`ALLOC_REGION`** -- The region this fleet is deployed to (e.g. `us-west`). This must match one of the keys in `gameServerZonesByProviderRegion` in the Pragma config.
- **`ALLOC_URL`** -- The URL the allocator sidecar uses to register with the GameFabric allocator service so it can receive allocation requests.
- **`ALLOC_PAYLOAD_ANNOTATION`** -- Prefix for Agones annotations that contain payload data. The allocation payload (GameInstanceId, tokens, pragmaBackendAddress, ServerArgs) is written as annotations prefixed with this value.
- **`ALLOC_PAYLOAD_FILE`** -- Path where the allocator sidecar writes the payload JSON file. The game server reads this file on startup 
  and watches for changes to this file in order to get Pragma connection details.
- **`ALLOC_TOKEN`** -- Secret reference for authenticating the allocator sidecar with the GameFabric allocator service. `allocator-prod-us` is the secret name, `registry_token` is the key within that secret.

See the [Automatically registering game servers](https://docs.gamefabric. com/multiplayer-servers/multiplayer-services/server-allocation/automatically-registering-game-servers) section for more details on these 
environment variables.

### Console Configuration Steps

1. **Create an Environment** in the GameFabric console (e.g. `prag`).
2. **Create an Armada Set** within that environment (e.g. `sample-game-armada-set`).
3. **Create a Fleet** within the armada set for each region you want to support.
4. **Configure the environment variables** listed above on each fleet. The `ALLOC_REGION` value must differ per fleet (e.g. `us-west`, `eu-central-1`).
5. **Create the allocator secret** (`allocator-prod-us`) containing the `registry_token` that matches the `allocatorToken` in your Pragma config.
6. **Deploy your game server container image** to the fleet. The container must watch `/app/shared/payload.json` for changes in order to 
   get details Pragma assigns the specific game instance the game server will be associated with.

### [OPTIONAL] Mapping Regions

The `gameServerZonesByProviderRegion` config maps GameFabric region names to Pragma `GameServerZone` values. Each region in GameFabric that you want to allocate to must have an entry here:

```yaml
gameServerZonesByProviderRegion:
  us-west: "US_WEST"
  eu-central-1: "EU_CENTRAL"
```

The keys (e.g. `us-west`) must match the `ALLOC_REGION` values set on GameFabric fleets, and the values (e.g. `US_WEST`) are the Pragma-side identifiers used in matchmaking and game instances.

---

## Example Unreal Game Server

The `unreal/SampleGame` directory contains a working Unreal Engine 5 project that demonstrates a dedicated game server integrating with both Pragma and GameFabric. It includes a server-side game mode, a client-side player controller, a Dockerfile for containerizing the server, and a script to build and push the image to GameFabric's container registry.

### Project Structure

| Path | Purpose |
|------|---------|
| `Source/SampleGame/SampleGameGameModeBase.cpp/.h` | Dedicated server game mode - connects to Pragma, handles allocation, links the game instance, manages player connections |
| `Source/SampleGame/MyPlayerController.cpp/.h` | Client-side player controller - login, party, matchmaking, game instance UI |
| `Source/SampleGameServer.Target.cs` | Server build target (`TargetType.Server`) |
| `Source/SampleGame/SampleGame.Build.cs` | Module dependencies including `PragmaSDK`, `Agones`, `OnlineSubsystemSteam` |
| `Dockerfile` | Containerizes the cooked Linux server build with the GameFabric game server wrapper |
| `push-image.sh` | Helper script to build the Docker image and push it to the GameFabric container registry |

### How the Game Mode Works

`SampleGameGameModeBase` supports two operating modes, selected automatically at startup based on whether command-line parameters are present:

**Local development mode** - When `-PragmaGameInstanceId=` is passed on the command line (along with partner tokens and backend address), the server connects to Pragma immediately. This is useful for iterating locally without GameFabric infrastructure (LocalProcessGameServerProviderPlugin would need to be configured on your locally running pragma-engine).

**GameFabric mode** - When no `PragmaGameInstanceId` is on the command line, the server assumes it is running inside a GameFabric-managed container. It polls every 2 seconds for the allocation payload file at `/app/shared/payload.json`, which the allocation sidecar writes when the server receives an allocation. The payload contains:

| Field | Description |
|-------|-------------|
| `GameInstanceId` | The Pragma game instance this server is allocated to |
| `PartnerGameToken` | Auth token for the Game Partner Backend |
| `PartnerSocialToken` | Auth token for the Social Partner Backend |
| `pragmaBackendAddress` | URL of the Pragma backend the server should connect to |

Once the payload is read, the server connects to Pragma, calls `Link()` with the game instance ID, and then calls `ConnectMorePlayers()` to make the server available to clients. When the game ends (triggered by a client sending `"IWON"`), it calls `EndGame()` and shuts down via Agones.

### Dockerfile

The Dockerfile packages the cooked Unreal dedicated server for deployment on GameFabric:

```dockerfile
FROM ubuntu:22.04

# Runtime dependencies for the Unreal dedicated server
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates libssl3 libcurl4 libfontconfig1 \
        libfreetype6 libglu1-mesa libsm6 libxrandr2 \
        libxinerama1 libxcursor1 \
    && apt-get clean -y && rm -rf /var/lib/apt/lists/*

# GameFabric game server wrapper (gswrapper)
ARG GSW_VERSION=v2.6.0
ADD https://github.com/GameFabric/gswrapper/releases/download/${GSW_VERSION}/gsw_linux_x86_64 \
    /app/gsw
RUN chmod +x /app/gsw

# Copy the cooked LinuxServer build
COPY --chown=gameserver:gameserver Build/LinuxServer/ /app/gameserver/

# gswrapper is the entrypoint -- it manages Agones lifecycle
# and tails game logs for observability
ENTRYPOINT ["/app/gsw", \
    "--shutdown.ready=1h", \
    "--shutdown.allocated=24h", \
    "--tail-log.paths=/app/gameserver/SampleGame/Saved/Logs/SampleGame.log", \
    "--"]

# Template variables {{ .GameServerIP }} and {{ .GameServerPort }}
# are injected by gswrapper at runtime
CMD ["/app/gameserver/SampleGameServer.sh", \
    "-server", "-log", \
    "-Hostname={{ .GameServerIP }}", \
    "-ExternalPort={{ .GameServerPort }}"]
```

Key aspects:

- **gswrapper** acts as the entrypoint. The wrapper provides a number of convenience features to facilitate the integration with GameFabric, including injecting runtime variables like `{{ .GameServerIP }}` and `{{ .GameServerPort }}` into the CMD arguments. See the [gswrapper documentation](https://docs.gamefabric.com/multiplayer-servers/multiplayer-services/game-server-wrapper) for details.
- **`--shutdown.ready=1h`** -- if the server sits in the `Ready` state for more than 1 hour without receiving an allocation, gswrapper shuts it down.
- **`--shutdown.allocated=24h`** -- maximum lifetime of an allocated server session.
- The `Build/LinuxServer/` directory must contain the output of cooking the `SampleGameServer` target.

### Building and Pushing the Image

`push-image.sh` builds the Docker image and pushes it to GameFabric's container registry. Run it from the `unreal/SampleGame` directory (where the Dockerfile lives):

```bash
./push-image.sh -p <registry-password> -t <tag>
```

| Flag | Description |
|------|-------------|
| `-p` | Service-account password for the GameFabric container registry |
| `-t` | Tag for the image (e.g. `v1.0.0`, `build-1234`) |

The script uses three environment variables with sensible defaults that you should override for your project:

| Variable | Default | Description |
|----------|---------|-------------|
| `GF_REGISTRY_URL` | `registry.gamefabric.io/your-org` | Your GameFabric container registry URL |
| `GF_BRANCH` | `development` | The registry branch/namespace for the image |
| `GF_USERNAME` | `your-service-account` | Service-account username for registry authentication |

Example with overrides (or replace the overrides in the script itself):

```bash
GF_REGISTRY_URL="registry.gamefabric.io/my-studio" \
GF_BRANCH="production" \
GF_USERNAME="deploy-bot" \
./push-image.sh -p "$REGISTRY_PASSWORD" -t v1.0.0
```

The script will:
1. Log in to the GameFabric container registry
2. Build the image for `linux/amd64`
3. Tag it as `<GF_REGISTRY_URL>/<GF_BRANCH>/samplegame-server:<tag>`
4. Push it to the registry

After pushing, configure your GameFabric ArmadaSet to use this image for fleet deployments.

---

## [OPTIONAL] Running Locally with ngrok

Follow the article [here](https://help.pragma.gg/articles/5683456270-exposing-a-local-pragma-engine-to-the-outside-world-with-ngrok) to learn how to expose your locally running pragma-engine to the outside world using ngrok.
