# Authentication
Source: https://docs.qwacks.com/authentication
The SDK signs your game in and manages player sessions for you.
Flock authentication has **two layers**, and the SDK handles both:
1. **Game** — the SDK authenticates your game with the credentials you configure once (API Key,
Game ID, Game Version). Every request it makes is then identified as your game.
2. **Player** — players sign in through the SDK, which then manages their session automatically for
the rest of play.
## Configure your game
Set your credentials in your engine's settings. The editor also **verifies the connection** and
**resolves your Game Version**, both required before the SDK can start.
Credentials live in **Flock → Settings** — see [Unity setup](/sdk/unity#setup).
With **Auto-Initialize On Load** the SDK authenticates as your game at startup; turn it off to
initialize it yourself. Reach everything through `FlockClient.Instance`.
Credentials live in **Project Settings → Plugins → Flock SDK** — see
[Unreal setup](/sdk/unreal#setup).
With **Auto-Initialize On Load** the SDK authenticates as your game at startup; turn it off to
initialize it yourself. Reach everything through `UFlockSubsystem::Get(this)`.
## Sign in a player
Players sign in with one of several methods. Each resolves to the same player account, and the SDK
keeps them signed in afterward.
Every method is a `LoginWith…Async` call on `FlockClient.Instance.Authentication`. They **throw**
`FlockException` on failure, so wrap them in `try/catch` to give bad credentials or a dead network
a clean retry path.
```csharp theme={null}
// Email / password
await FlockClient.Instance.Authentication.LoginWithEmailAsync(email, password);
// Device ID — frictionless guest sign-in keyed to the device
await FlockClient.Instance.Authentication.LoginWithDeviceAsync(deviceId);
// Platform identity — also LoginWithAppleAsync, LoginWithSteamAsync,
// LoginWithFacebookAsync, LoginWithDiscordAsync
await FlockClient.Instance.Authentication.LoginWithGoogleAsync(idToken);
```
For the platform methods the SDK exchanges a credential your platform SDK already produced; it
does not obtain one for you. Google wants an **ID token**, Apple an **identity token**, Steam a
**session ticket**.
**Registering is a separate call.** Logging in never creates an account, so a new player needs
`RegisterWith…Async` first. A successful register also signs the player in, so no login call
follows on the happy path.
Registering an identity that *already* has an account is not an error — the call returns `null`
and leaves the player **signed out**, so follow that case with a login:
```csharp theme={null}
FlockAuthProvider auth = FlockClient.Instance.Authentication;
// null means "this identity already has an account" — created nothing, signed in nobody.
if (await auth.RegisterWithDeviceAsync(deviceId) == null)
await auth.LoginWithDeviceAsync(deviceId);
```
Afterward, read `FlockClient.Instance.CurrentPlayerId` or handle `FlockEvents.OnAuthenticated`.
Picking a display name? Check it's free first — advisory, since another player can still claim it
in between:
```csharp theme={null}
bool available = await FlockClient.Instance.Authentication.IsNameAvailableAsync("PlayerName");
```
Every method is an async node under *Flock | Auth*, or a call on the auth provider in C++. Nothing
throws — Unreal builds with exceptions off — so check `Result.bSuccess`, or wire the node's
**On Failure** pin.
```cpp theme={null}
UFlockSubsystem* Sdk = UFlockSubsystem::Get(this);
// Email / password
Sdk->GetAuthProvider()->LoginWithEmail(Email, Password, OnLogin);
// Device ID — frictionless guest sign-in keyed to the device
Sdk->GetAuthProvider()->LoginWithDevice(DeviceId, OnLogin);
// Platform identity — also LoginWithApple, LoginWithSteam,
// LoginWithFacebook, LoginWithDiscord
Sdk->GetAuthProvider()->LoginWithGoogle(IdToken, OnLogin);
```
For the platform methods the SDK exchanges a credential your platform SDK already produced; it
does not obtain one for you. Google wants an **ID token**, Apple an **identity token**, Steam a
**session ticket**. Facebook and Discord are login-only — an account must already exist.
**Registering is a separate call.** Logging in never creates an account, so a new player needs
`RegisterWith…` first. Registering does **not** sign the player in, so follow it with a login.
Registering an identity that *already* has an account is reported as a *success* with
`bAlreadyRegistered` set, so you can route the player to a login rather than show them a failure.
Afterward, read `Flock Get Player Id` / `GetPlayerId()`, or handle the `OnAuthenticated` event.
Picking a display name? Check it's free first — advisory, since another player can still claim it
in between:
```cpp theme={null}
Sdk->GetAuthProvider()->IsNameAvailable(TEXT("PlayerName"), OnChecked);
```
## Password reset
For email/password accounts, a two-step emailed-code flow. The first call always succeeds — it
never reveals whether an email is registered.
Resetting works signed out: the email and the emailed code name the account, so a player who cannot
sign in can still reset their password from your sign-in screen.
```csharp theme={null}
// 1. Player enters their email; Flock emails them a reset code
await FlockClient.Instance.Authentication.ForgotPasswordAsync(email);
// 2. Player enters the code and their new password
await FlockClient.Instance.Authentication.ResetPasswordAsync(email, code, newPassword);
```
```cpp theme={null}
Sdk->GetAuthProvider()->ForgotPassword(Email, OnSent);
Sdk->GetAuthProvider()->ResetPassword(Email, Code, NewPassword, OnReset);
```
## Email verification
Optional, code-based, and always under your control — the SDK never sends verification emails on
its own. Typically used while the player is signed in (their session is attached automatically),
but neither call blocks on it.
```csharp theme={null}
await FlockClient.Instance.Authentication.SendEmailVerificationAsync();
// Player enters the emailed code
await FlockClient.Instance.Authentication.VerifyEmailAsync(code);
```
```cpp theme={null}
Sdk->GetAuthProvider()->SendEmailVerification(OnSent);
Sdk->GetAuthProvider()->VerifyEmail(Code, OnVerified);
```
## Signing out
`Logout` clears the session on the device — the standard client sign-out. For shared devices or a
compromised session, revoking additionally kills the player's session **server-side**, so a stolen
session token stops working. Shutting the SDK down is not a sign-out: the sign-in saved on the device
stays, and the next start restores the same player.
```csharp theme={null}
// Routine sign-out
FlockClient.Instance.Authentication.Logout();
// Hardened sign-out: revoke on the server, then clear locally
await FlockClient.Instance.Authentication.RevokeTokenAsync();
FlockClient.Instance.Authentication.Logout();
```
```cpp theme={null}
// Routine sign-out
Sdk->GetAuthProvider()->Logout();
// Hardened sign-out: revoke on the server, then clear locally
Sdk->GetAuthProvider()->RevokeToken([Sdk](TFlockResult)
{
Sdk->GetAuthProvider()->Logout();
});
```
## Sessions are managed for you
Player sessions are short-lived under the hood, but **the SDK refreshes them automatically** — you
don't have to think about tokens or expiry. If a player needs to sign back in, the SDK raises
`OnAuthExpired` on its event hub.
# Changelog
Source: https://docs.qwacks.com/changelog
Notable changes to Flock and its SDKs.
* **Parties, matchmaking and sessions.** Friends form a party with an invite code, find a match
alone or as a party, or join a friend's game by its join code. The SDK keeps each seat, follows
the party, the search and the session in the background, and raises events when they change.
See [Multiplayer](/guides/multiplayer).
* **`session.StartNetcodeAsync(networkManager)` connects a session's players** over Netcode for
GameObjects 1.x or 2.x. Players on the host's network connect directly; everyone else goes
through Flock's relay, so nobody forwards a port. The host lets in only the session's players.
See [Connecting players](/guides/connecting-players).
* **Relayed players move to a direct path** to the host when the two can reach each other, and
the relay stays ready in case that path goes quiet (`WhenNeeded` only).
* **Flock's relay on Cloudflare's network** (1.84.0): each player tells the host its relay
address, so relays that give every player's address an IP of its own work, and the SDK keeps
trying until it finds a way through to a relay server that does not answer every connection.
Phones on mobile data now find their own address too, so they reach other players directly
more often.
* **When the host leaves, another player takes over.** Each game starts its netcode again on
`HostChanged`, and on `RelayLost` when its relay stops working.
* **Samples:** Play With Friends and Quick Match, one component each, in the package's
Samples/Multiplayer folder.
* Multiplayer error codes, each with a fix hint, under `FlockErrorCode`.
* **Unreal Engine 5.4 is supported**, so the range is now 5.4 to 5.8, and a 5.4 project installs the plugin
unchanged. 5.4 is built and linked in a real project; the full test suite has not been run on it yet, as it
is on 5.5 to 5.8.
* **Known engine issue on 5.4 and 5.5:** a Development package ends with an access violation about 60 seconds
after it quits. It is the engine's, fixed by Epic in 5.6. See the [Unreal SDK guide](/sdk/unreal#requirements).
* **Purchases grant their contents server-side, and the SDK tells you what was granted.**
`Purchase` completes with `FFlockPurchaseResult` — the rewards handed out, the wallet afterwards,
and the inventory entry if the item created one. See [Shops & Inventory](/guides/shops-and-inventory).
* **New:** `Consume(InventoryId)` spends an owned item and returns what it granted, with the same
money safety as a purchase: an ambiguous failure surfaces instead of being re-sent, so a reward is
never granted twice. In Blueprint, **Flock Consume Inventory Item**.
* `Rewards` on a catalog item advertises what it will give before it is bought, and `Type`
distinguishes a currency pack from a consumable or a standard item.
* **Breaking:** `Purchase` completes with `FFlockPurchaseResult` instead of `FFlockPlayerInventory`.
The inventory entry moved to `.Inventory`, and it is **empty for a currency pack**, which hands its
contents over outright and creates nothing to own — use *Has Inventory Row* before reading it. A
generated `Purchase` macro's output pin is renamed; regenerate after upgrading.
* **New:** *Flock Cancel All Scheduled Notifications* and *Flock Get Pending Schedules* Blueprint
nodes. Both were C++-only, so a "clear my reminders" button could not be built in a graph — and the
obvious substitute, a ForEach over *Flock Cancel Scheduled Notification*, fires every cancel in one
frame where they race over the stored list. The node walks them sequentially.
* New pure nodes for reward kinds — *Is Currency Reward*, *Flock Currency Reward Type* — because a
reward's type is a string the server owns. Comparing it to a hand-typed value risks a typo reading
as "not a currency reward" and silently skipping the grant.
* **`GetScheduled()` asks the server which reminders a player has pending**, so it sees reminders
set before a reinstall or on another device. Filter with
`FlockScheduledNotificationStatuses::Pending` / `::Delivered` / `::Canceled`, or the
*Flock Schedule Status* nodes in Blueprint. See [Game Notifications](/guides/notifications).
* **`CancelAllScheduled()` now cancels everything the player has pending**, wherever it was
scheduled. It falls back to the local record only when the server cannot be reached.
* `GetPendingSchedules()` is unchanged and still synchronous and network-free; it is now the offline
fallback rather than the primary list.
* **`GetScheduledAsync()` asks the server which reminders a player has pending.** Until now the
only list was the SDK's own record of what *this install* had scheduled, so a reinstall or a
second device lost the handle and the player could not cancel their own reminders. See
[Game Notifications](/guides/notifications).
* Filter by state with `ScheduledNotificationStatuses.Pending`, `.Delivered` or `.Canceled`.
Pending is the default.
* **`CancelAllScheduledAsync()` now cancels everything the player has pending**, wherever it was
scheduled — not just what the current install tracked. It falls back to the local record when
the server cannot be reached, so an offline caller still cancels what it knows about.
* `GetPendingSchedules()` is unchanged and still synchronous and network-free; it is now the
offline fallback rather than the primary list.
* **Purchases grant their contents server-side, and the SDK tells you what was granted.** A
purchase returns `PurchaseResult` — the rewards handed out, the wallet afterwards, and the
inventory entry if the item created one — so a game can show "+500 Gold" without a second call.
See [Shops & Inventory](/guides/shops-and-inventory).
* **New:** `ConsumeAsync(inventoryId)` spends an owned item and returns what it granted. Consuming
moves currency, so it carries a purchase's money safety: an ambiguous failure surfaces instead of
being re-sent, and a reward is never granted twice.
* `ShopItem.Rewards` advertises what an item will give before it is bought, and `ShopItem.Type`
distinguishes a currency pack from a consumable or a standard item.
* **Breaking:** `PurchaseAsync` returns `PurchaseResult` instead of `PlayerInventory`. The
inventory entry moved to `result.Inventory`. Call sites that used the return value directly need
that one change; the compiler will point at each of them.
* `Granted` and `Rewards` are never null — an item that grants nothing yields an empty list.
* **Errors say what to do, not just what broke.** A failed call used to read `Validation failed
(HTTP 400)`, with the server's own explanation buried in the raw response body. `FlockException.Message`
now carries the call that failed, the server's reason, the coded identifier, and a `Fix:` line —
a device login against an unregistered device names `RegisterWithDeviceAsync` outright. See
[Error handling](/sdk/unity#error-handling).
* The fix is specific to the credential: the same `player.invalid_login_credentials` reads as
"register this device first" on device sign-in and "wrong password" on email sign-in.
* **New on the exception:** `ServerMessage`, `Hint` and `Operation`, so you can compose your own
text instead of parsing the message. `FlockErrorHints.For(errorCode)` returns the same next-step
wording for any code. Keep branching on `ErrorCode` — codes are stable, wording is not.
* **Malformed-request errors are readable.** Responses that name an offending field now surface as
`body.player_data: Input should be a valid dictionary` instead of a bare "Validation failed".
* **A missing generated accessor now explains itself.** Calling a codegen accessor before running
**Sync Schemas** is a *compile* error, so no SDK error could ever fire for it. The SDK now watches
compilation and prints the missing step, and the **Code Generation** tab shows whether a sync has
ever run. See [Work with your game data](/sdk/unity#work-with-your-game-data).
* **Game notifications.** A per-player inbox with unread counts and read/mark-all-read, reminders
your game schedules and cancels server-side, the readable template catalog, and push device-token
registration. Every call has a Blueprint node. See [Game Notifications](/guides/notifications).
* Scheduling is addressed by the template's **name** — what you see on the dashboard — with the ID
resolved for you.
* **Push tokens are registered, not fetched.** Get the token from your push plugin (Firebase Cloud
Messaging, OneSignal) and hand it to `RegisterDeviceToken`. Unreal ships no way to obtain an
Android token itself, so a plugin is required there rather than optional.
* Registering on a platform push does not support fails with a validation error rather than
guessing one — check `Flock Get Current Device Platform` before offering the option.
* Inbox reads are offline-cached; read receipts are never queued, and scheduling is never retried
after an ambiguous failure, so a network blip cannot double-book a reminder.
* **Leaderboards.** Read the top of a board, the signed-in player's own rank, and the players
around them, filtered by window and country. Boards are addressed by name. Scores need no submit
call — they come from the [player data](/guides/player-data) field the board is configured
against. See [Leaderboards](/guides/leaderboards).
* Standings, board configuration and placements are offline-cached, so a leaderboard screen shows
the last-known board when the network is down.
* **Leaderboards.** Read the top of a board, the signed-in player's own rank, and the players
around them, filtered by window and country. Scores need no submit call — they come from the
[player data](/guides/player-data) field the board is configured against. See
[Leaderboards](/guides/leaderboards).
* **Game notifications.** A per-player inbox with unread counts, read/mark-all-read, push device
registration, and locally scheduled reminders your game can cancel. The template catalog is
readable from the game, and `FlockEvents.OnNotificationReceived` fires as new items arrive. See
[Game Notifications](/guides/notifications).
* **Account linking.** One player can hold several credentials — device, email, Google, Apple,
Facebook, Steam, Discord — and link or unlink them at runtime. Every call returns the updated
credential list. See [Account Linking](/guides/account-linking).
* **Breaking:** `Notification.ScheduleAsync` now takes the template's **name**, not its ID. The
parameter is still a string in the same position, so existing calls keep compiling and fail at
runtime instead. Migration: pass the name you gave the template on the dashboard.
* **Auth surface completed** — password reset, email verification, server-side token revocation,
and a display-name availability preflight.
* **Every error now carries a code.** Catch `FlockException` and branch on the typed
`ErrorCode`, e.g. `FlockErrorCode.ShopInsufficientFunds`, instead of matching message text.
* **Fixed:** `Commands.UpdatePlayerDataAsync` failing with HTTP 422, and a stray space in the
configured API URL breaking every request on WebGL.
* **The Unreal SDK is available** — player auth, save data, economy, remote config, downloadable
assets, and analytics as a native plugin. Every feature works from Blueprint as a self-contained
async node, with no Target pin to wire. See the [Unreal SDK guide](/sdk/unreal).
* **Typed code generation from your own schemas.** One click turns templates, configs, and shops
into typed Blueprint structs, enums, and one-node reads, writes, and purchases — no C++ and no
compile step. Projects that prefer C++ can switch the target and get a generated module instead.
* **Offline-safe by design.** Config, game, shop-catalog and template reads are cached to disk and
served without a connection; player-data writes queue and replay. Currency grants deliberately do
not queue, so they cannot double-credit.
* **Engine support is a verified range: UE 5.5 to 5.8** — every version in the range is compiled
and run against the full test suite before release. Newer engines warn rather than block.
* Leaderboards, notifications, and account linking are Unity-only for now.
* **Unexpected-termination detection.** A run that dies without a clean quit is reported on the
next launch as an automatic `app_termination` event, classified `background_kill` or
`abnormal` — see [Analytics & Events](/guides/analytics-and-events#unexpected-termination-detection).
* **Breaking:** `LogExceptionAsync` / `LogErrorAsync` / `LogEventAsync` are now synchronous
`LogException` / `LogError` / `LogEvent`. They only ever enqueued locally, so the old `await`
resolved on enqueue, not delivery. Migration: drop the `await` and the `Async` suffix.
* **New `Analytics.FlushAsync()`** — a real await that drains everything queued to the server,
for the rare moment delivery must be confirmed. Automatic flushing still runs as before.
* Unreal SDK: matching update in progress.
* **Leaderboards** — rank players on any numeric player-data field, with weekly and seasonal
windows, country scopes, and player exclusions.
* **Game Notifications** — templates, campaigns (all players or a segment; now, scheduled, or
recurring), and a per-player inbox across in-app, email, and push.
* **Developer Alerts** — log-threshold alert rules with severities, cooldowns, and a trigger
history in the Alert Center.
* Public developer documentation portal, with Unity and Unreal SDK guides.
# Players
Source: https://docs.qwacks.com/concepts/players
Accounts, profiles, and custom data for the people who play your game.
A **player** is an account within a single game. Players sign in with one or more methods, carry a
profile, and accumulate custom data, inventory, and currency.
## Identity
A player can authenticate through several providers — email/password, device id, Google, Apple,
and Steam — all resolving to the same player account. See
[Players & Auth](/guides/players-and-auth) for the sign-in flows.
## Profiles & templates
A **player template** defines the shape of a player's profile for a game version — the fields every
player has and their defaults. This keeps player records consistent and lets your game evolve the
schema per version.
## Custom data
Beyond the template, **player data** lets you store arbitrary documents per player — level, coins,
settings, progression — as JSON. See [Player Data](/guides/player-data).
```
Player
├─ identity (email / device / Google / Apple / Steam)
├─ profile (shape defined by a player template)
├─ player data (custom JSON documents)
├─ inventory (owned shop items)
└─ wallet (currency balances)
```
# Studios & Games
Source: https://docs.qwacks.com/concepts/studios-and-games
The two top-level objects every Flock integration is built around.
## Studios
A **studio** is your team and tenant. It owns your games, members, billing, and plan. You create a
studio once in the [dashboard](https://flock-saas.qwacks.com); everything else lives under it. The studio
is resolved automatically from your game's credentials — you never deal with it in the SDK.
## Games
A **game** is the container for a single title's backend: its players, economy, configuration,
versions, and analytics. Each game has:
* **Credentials** — the API Key, Game ID, and Game Version you configure the SDK with.
* **A signing key** — Flock secures each game's player sessions with a per-game key, so a session
for one game is never valid for another. The SDK uses this transparently.
* **Versions** — see [Versions](/concepts/versions).
Studios and games are created in the dashboard. Grab the game's credentials from its settings.
## How they fit together
```
Studio ──owns──▶ Game ──issues──▶ credentials ──▶ configured in the SDK
│
└──secures──▶ player sessions (managed by the SDK)
```
Because the studio and game come from your credentials, integration stays simple: configure the SDK
once, sign a player in, and call into features.
# Versions
Source: https://docs.qwacks.com/concepts/versions
Publish and target game versions without breaking live players.
A **game version** is a published snapshot of your game's server-side configuration — its shops,
currencies, feature flags, player templates, and more. Versions let you evolve the backend while
existing players keep running against the version they shipped on.
## Environments
Versions move through environments as they mature:
| Environment | Use |
| - | - |
| `dev` | Active development and internal testing. |
| `staging` | Pre-release validation. |
| `prod` | Live, player-facing. |
## Targeting a version
Each client build runs against a specific game version, which you set on the SDK client. Everything
the SDK reads — shops, items, configs, templates — resolves against that version. So you can stand
up a new economy in `dev`, validate it in `staging`, and promote it to `prod` without touching live
clients.
Pin each client build to the version it was tested against, and only roll players forward once the
new version is validated.
# Getting Started
Source: https://docs.qwacks.com/data-duck/getting-started
Data Duck — market intelligence for game studios.
**Data Duck is currently under development!** Full documentation and SDK support are on the way.
Reach out to the Qwacks team for early access.
Data Duck is the Qwacks platform for **market intelligence** — track competitors, analyze store
data across platforms, and generate market reports for your games.
Check back here as we roll out guides and SDK support. In the meantime, explore the
[Flock](/introduction) documentation to see how the Qwacks SDKs work.
# Account Linking
Source: https://docs.qwacks.com/guides/account-linking
Let one player sign in with several credentials — device, email, or a social account.
A Flock player is **one account that can hold several credentials**. The same player might start
on a device ID, add an email and password later, and attach Google after that — signing in with
any one of them lands on the same account, with the same progress.
This is what makes device-first onboarding safe. A player can start instantly with no sign-up
form, then attach a real credential once they're invested, and keep everything if they lose the
device.
credentials belong to a **player**; a player belongs to a game.
## The credential types
`device_id`, `email`, `google`, `apple`, `facebook`, `steam`, and `discord`. A player can hold at
most one of each — linking a second Google account to the same player is rejected rather than
silently replacing the first.
Linking lives on the authentication provider, beside the login calls, and every method needs
a **signed-in player**.
```csharp theme={null}
// What is this player currently able to sign in with?
List accounts =
await FlockClient.Instance.Authentication.GetLinkedAccountsAsync();
// Attach an email/password credential to the signed-in player.
accounts = await FlockClient.Instance.Authentication.LinkEmailAsync("player@example.com", "hunter2");
// Attach a social credential using the same token your login flow already obtains.
accounts = await FlockClient.Instance.Authentication.LinkGoogleAsync(idToken);
// Detach one.
accounts = await FlockClient.Instance.Authentication.UnlinkAsync(FlockCredentialProvider.Google);
```
There is one method per provider — `LinkEmailAsync`, `LinkDeviceAsync`, `LinkGoogleAsync`,
`LinkAppleAsync`, `LinkFacebookAsync`, `LinkSteamAsync`, `LinkDiscordAsync` — each taking the
same token your login path already gets from that platform. `UnlinkAsync` takes a
`FlockCredentialProvider` instead.
**Every one of these returns the full, updated credential list**, so link and unlink double as
a refresh — you rarely need to call `GetLinkedAccountsAsync` again straight afterwards.
Each `PlayerLinkedAccount` carries `Provider`, `ProviderUserId`, `Email`, and `EmailVerified`.
**A player cannot unlink their last credential.** Removing it would leave an account nobody
can sign in to, so the server refuses with
`FlockErrorCode.PlayerCannotUnlinkLastCredential`. Disable the unlink button when the list
has one entry rather than letting the player discover this as an error.
**Errors worth handling by name:**
| Code | Means |
| - | - |
| `PlayerAccountAlreadyLinked` | That credential is already attached — to this player, or to a different one. |
| `PlayerAccountNotLinked` | Unlinking something the player doesn't have. |
| `PlayerCannotUnlinkLastCredential` | The last remaining sign-in method. |
| `PlayerInvalidLinkRequest` | The token was rejected by the provider. |
**There is no account-merge flow.** If a credential already belongs to a *different* player,
linking fails with `PlayerAccountAlreadyLinked` — Flock will not fold two players' progress
together, because there is no safe general answer to which save wins. Decide in your game
what to offer: keep the current account, or sign out and into the other one.
Credential state is never cached and never queued offline — it is security state, and a stale
answer is worse than no answer. Every call goes to the server.
## Password resets on a linked email
Once an email is linked, the player has a password they can reset. `ResetPasswordAsync` (Unreal:
`ResetPassword`) works signed out, with the email and the code `ForgotPasswordAsync` sent, so it
does not matter how the player signed in this time.
Account linking is available in both the [Unity SDK](/sdk/unity) and the
[Unreal SDK](/sdk/unreal).
See [Players & Auth](/authentication) for the sign-in calls themselves.
# Achievements
Source: https://docs.qwacks.com/guides/achievements
Define achievements as config that tracks player progress automatically.
In Flock, an **achievement** is a kind of **config/template** — defined alongside your
[game config](/guides/remote-config) and [player templates](/concepts/players) — that
**auto-generates a player data entry** to track each player's progress. You don't call a separate
achievements API: you configure the achievement, and Flock provisions the matching
[player data](/guides/player-data) for you to read and update.
achievements are configured per **game version** (as game config + player templates).
Define achievements on the **Achievement Config** page — this is the source of truth. Each
achievement becomes part of your game config and player templates, and Flock creates a player
data entry to hold progress.
`UnlockAchievementAsync` takes the achievement name, or a generated enum after a schema sync so
it cannot be mistyped. The SDK resolves the `achievement`-tagged row for you.
```csharp theme={null}
PlayerData row = await FlockClient.Instance.Commands.UnlockAchievementAsync("first_win");
// After a sync — typed, so a name the server doesn't have fails at compile time:
row = await FlockClient.Instance.Commands.UnlockAchievementAsync(FlockAchievementId.FirstWin);
```
The typed overload is **additive** — the raw-string one stays public — but prefer it.
If you keep achievement details (display name, description, reward) in a game config tagged
`achievement`, codegen wires them to the same enum, so you award and look up details with one
value:
```csharp theme={null}
// Returns the matching entry — a type generated from your own config.
var firstWin = await FlockClient.Instance.Config.GetAchievementDetailsAsync(FlockAchievementId.FirstWin);
```
Unlocks queue offline and replay when the connection returns.
`Flock Unlock Achievement` takes the achievement name, or a generated dropdown after a schema sync
so it cannot be mistyped. The SDK resolves the `achievement`-tagged row for you.
```cpp theme={null}
Sdk->GetCommandProvider()->UnlockAchievement(TEXT("first_win"), OnUnlocked);
// After a sync, with the id baked in:
FFlockGenerated::UnlockAchievement(this, EFlockAchievementId::FirstWin, OnUnlocked);
```
Unlocks queue offline and replay when the connection returns.
Achievements are configured, not coded. Define them on the **Achievement Config** page; the SDK
then surfaces them through game config and player data.
# Analytics & Events
Source: https://docs.qwacks.com/guides/analytics-and-events
Understand your players, and find out what went wrong.
Your game reports to Flock over **two separate surfaces**. They answer different questions, are read
by different people, and appear on different dashboards. Picking the wrong one puts your data
somewhere real but useless — so it is worth two minutes up front.
events and analytics are aggregated per **game**.
| | Analytics | Log events |
| - | - | - |
| Answers | What did players do? | What went wrong? |
| Read by | Design, product, LiveOps | Engineering |
| Covers | Sessions, screen views, transactions | Debug entries, logic errors, exceptions |
| Where to read it | **Dashboards → Game Metrics** | **Diagnostics → Events** and **Diagnostics → Errors** |
A crash is not a funnel step. Sending one through the other surface does not fail and does not warn —
the entry is accepted, stored, and shown on a dashboard nobody was looking at.
## Analytics
Engagement, retention and monetization, built from the sessions, screen views and transactions your
game reports. Read on **Dashboards → Game Metrics**.
Sessions start and end automatically around sign-in. Screen views tell you where players spend
their time, and purchases report their own transactions with no call from you.
```csharp theme={null}
FlockClient.Instance.Analytics.RecordScreenView("shop");
// Optional: await delivery of everything queued (before a critical moment).
// The result counts what was sent and what the server refused for good.
FlockFlushResult flushed = await FlockClient.Instance.Analytics.FlushAsync();
```
Everything here is off when analytics are disabled in your configuration, and respects the
player's consent decision where your game asks for one.
The *Flock | Analytics* nodes need no Target pin and are safe no-ops before the SDK initializes.
Sessions start and end automatically around sign-in, and purchases report their own transactions.
Gameplay events — a level completed, an item viewed — go through **Flock Track Event**
(`TrackAnalyticsEvent`), never through a log entry, which lives in the *Flock | Diagnostics* drawer.
Build an event's properties with the **Flock Event Property** nodes, which keep a number a number so
the dashboards can chart it.
```cpp theme={null}
Sdk->RecordAnalyticsScreenView(TEXT("shop"));
Sdk->TrackAnalyticsEvent(TEXT("level_complete"),
FFlockCommandData().Set(TEXT("level"), 3).Set(TEXT("flawless"), true), TEXT("progression"));
```
Events are written to disk and delivered while a player is signed in; one recorded before anyone
signs in is credited to whoever signs in next. Everything here is off when **Analytics Enabled** is
unticked.
## Sessions
A session is the unit everything else is attributed to, and the SDK manages its whole life for you.
| | |
| - | - |
| **Starts** | on sign-in, not at startup — a session belongs to a player, so there is nobody to attribute one to before then |
| **Ends** | on sign-out, on quit, and on timeout |
| **Backgrounding** | does **not** end a session — it pauses it, and time spent backgrounded is not counted as playtime |
| **Timeout** | a player who comes back after being away longer than the session-timeout setting (30 seconds by default) gets a **new** session; the old one is closed |
That last row is the one worth knowing. Without it, a player who backgrounds your game overnight and
returns in the morning reports a single fourteen-hour session, and every average you build on top of
that is wrong.
Sessions are per sign-in, so a shared device reports one session per player rather than one per
launch. Signing out closes the session before the next player starts theirs.
Withdrawing analytics consent **discards** the session in progress rather than sending it. Objecting
to further processing is not a request to transmit one last record.
## Log events
Developer diagnostics, in exactly three categories. Which one you pick decides where the entry
appears.
| Category | Appears on | Use for |
| - | - | - |
| Debug | **Diagnostics → Events** | Trace and context — things that are not faults |
| Error | **Diagnostics → Errors** | Something wrong that did not raise |
| Exception | **Diagnostics → Errors** | A caught failure, with its stack trace |
These are for diagnostics, not gameplay. A level-complete recorded as a debug entry lands in
**Diagnostics → Events**, where nobody building a retention chart will find it — and it will not
appear in Game Metrics at all.
Log calls are **synchronous and never block gameplay**: they enqueue to an on-disk buffer and the
SDK batches and delivers them for you.
```csharp theme={null}
FlockClient.Instance.Analytics.LogEvent("matchmaking started"); // debug
FlockClient.Instance.Analytics.LogError("quest ended with no reward"); // logic error
FlockClient.Instance.Analytics.LogException(ex); // exception
```
```cpp theme={null}
Sdk->LogDiagnosticEvent(TEXT("matchmaking started"),
FFlockMetadata().Add(TEXT("queue"), TEXT("ranked")));
Sdk->LogDiagnosticError(TEXT("quest ended with no reward"), Details);
```
Logging returns immediately: entries are written to disk and delivered later, so a call is cheap
and nothing is lost to a crash or a dead network. A run that dies without a clean quit is reported
on the next launch.
These nodes live under *Flock | Diagnostics*, apart from the analytics ones, and their Extra Data is a
map of strings built with the **Flock Metadata** nodes. They were called *Flock Log Event*, *Error* and
*Exception* (`LogAnalyticsEvent` and friends) before SDK 1.21.0; the old names still work and the editor
points at the new ones.
## Unexpected-termination detection
If a play session ends without a clean quit — a crash, a hang the player force-killed, or the OS
evicting the game — the SDK detects it on the **next launch** and reports one `app_termination`
event automatically. No integration needed.
| Property | Meaning |
| - | - |
| `classification` | `background_kill` (killed while backgrounded — normal on mobile) or `abnormal` (died in the foreground without quitting) |
| `previous_session_id` | The session that died |
| `last_alive_at` | Approximate time of death |
| `unhandled_exception_count` | Exceptions seen that run — context, not proof of a crash |
Watch the `abnormal` rate per game version — a jump after a release is your earliest stability
signal, no crash reporter required. Closing the game normally (in-game quit, Alt-F4, swiping the
app away) never counts as `abnormal`.
Name things consistently. Whichever surface you are on, a small vocabulary of well-chosen names
makes a dashboard readable and a sprawling one makes it useless.
# Assets
Source: https://docs.qwacks.com/guides/assets
Host and deliver your game's downloadable content.
**Assets** are files your game downloads at runtime — textures, audio, bundles, config blobs, and
more. Flock hosts them and serves them to your game through the SDK, so you can update content
without shipping a new build.
assets belong to your **game**; **asset configurations** describe and group them.
Two pages work together:
* **Assets** — upload and manage the files themselves.
* **Asset Configuration** — define each asset's metadata and grouping, so your game can resolve
the right file at runtime.
Fetch assets through the SDK and cache them in your game, resolving each one by the keys defined
in your **asset configuration**. Because assets are served from Flock rather than baked into the
build, you can swap or add content live.
Resolve each asset by the name you gave it in your **asset configuration**, then download it
through the asset provider. Downloads stream straight to disk and are cached there, so the
second request for the same version is a local read rather than a transfer.
```cpp theme={null}
UFlockSubsystem* Sdk = UFlockSubsystem::Get(this);
FFlockAssetProvider* Assets = Sdk->GetAssetProvider();
// The whole index — for a manifest, or to drive a preload screen.
Assets->GetAll([](TFlockResult> Index) {});
// Download by name, straight into a texture.
Assets->DownloadTexture(TEXT("title_banner"), FFlockAssetProgress(),
[](TFlockResult Result)
{
if (Result.IsSuccess())
{
// Result.Value is ready to use.
}
});
```
Four download flavours cover the common cases: `DownloadTexture`, `DownloadText`,
`DownloadBytes`, and `DownloadFile`, which hands back a path on disk. Each takes either an
`FFlockAsset` or an id-or-name string, plus an `FFlockAssetProgress` delegate that reports
bytes received against bytes total while the transfer runs.
**Audio.** There is no texture-style helper for sound. Use `DownloadFile` and load the path
through your own audio pipeline — the engine has no runtime importer for the compressed
formats a CDN usually holds.
**Preloading.** `Preload` and `PreloadWhere` fetch a batch behind a loading screen and report
how many landed. A preload is best-effort by definition: one unreachable asset does not fail
the batch.
In Blueprint the same calls are *Flock Get Assets*, *Flock Get Asset*, *Flock Download Asset
Texture* / *Text* / *Bytes* / *File*, and *Flock Preload Assets*, under **Flock | Assets**.
The download nodes add an **On Progress** pin that fires repeatedly as bytes arrive. Cache
questions need no network, so they are plain functions: *Flock Is Asset Cached*, *Flock Get
Uncached Assets*, *Flock Get Cached Asset Path*, *Flock Clear Asset Cache*.
**Sign-in.** Asset reads and downloads work signed out.
Keep large or frequently-changing content out of your build and deliver it as Flock assets — you
can update it without a client release.
# Bans
Source: https://docs.qwacks.com/guides/bans
Record detailed, feature-scoped ban information for your players.
Flock bans are **informational**. A ban derives from your **feature configuration** and is applied
on a **per-feature** basis: Flock gives you the tools to record rich ban details and expose a
player's ban status, but **what to do about a ban is up to your game** — we don't block anything for
you.
bans derive from **feature config** and are applied per **feature**.
Bans are driven by your **feature configuration** (Configurations → Features). Create and manage
detailed ban records on the **Manage Bans** page, scoped to the relevant feature.
`GetBanAsync` returns the ban record for a player. Flock surfaces the status; **the enforcement
is yours** — restrict a feature, show a message, or anything else.
```csharp theme={null}
PlayerBan ban = await FlockClient.Instance.Player.GetBanAsync(FlockClient.Instance.CurrentPlayerId);
if (ban?.Data != null && ban.Data.TryGetValue("multiplayer", out FeatureBan feature))
Debug.Log($"Banned from multiplayer: {feature.Reason} (until {feature.EffectiveDatetime})");
```
The player ID is required — there is no signed-in default, so pass `CurrentPlayerId`.
**Per-feature bans are keyed by feature name** in the record's `Data` map, each entry carrying
`Reason`, `BanDuration`, and `EffectiveDatetime`. An unbanned player is a **success with nothing
in `Data`**, not a failure — check the map rather than treating the call as a pass/fail.
Bans are never cached: they are security state that can change server-side at any moment.
`Flock Get Player Ban` returns the record for the signed-in player, or another if you supply an ID.
```cpp theme={null}
Sdk->GetPlayerProvider()->GetBan(FString(),
[](TFlockResult Ban)
{
if (Ban.bSuccess && Ban.Value.IsBanned()) { /* ... */ }
});
```
**An unbanned player is a success, not a failure** — you get an empty record, so check
`IsBanned()` rather than the result flag. Per-feature bans are keyed by feature name in the
record's `Data` map.
Bans are never cached: they are security state that can change server-side at any moment.
Bans are purely informational — Flock provides the data, your game decides the action. Acting on a
player's ban status is implemented by you, within the game.
# Connecting Players
Source: https://docs.qwacks.com/guides/connecting-players
Connect a session's players over Netcode for GameObjects, through Flock's relay when needed.
Once players share a [session](/guides/sessions), the SDK connects them over your game's
networking. The host's game is the server. Players on the host's network connect to it directly,
and everyone else goes through **Flock's relay**, so nobody forwards a port. The host lets in only
the session's players, and when the host changes, every game starts its networking again for its
new part.
the relay is switched on or off per **game**, under **Multiplayer settings**.
Flock's relay is not live in production yet. Until it is, players connect on the host's network, or through a port
forwarded to the host, and a player on another network gets `RelayFailed` with `relay_not_offered`.
## Starting the netcode
One call does the whole hand-shake, for the host and for each player. On the host it starts
Netcode for GameObjects as host and says where to reach it. On a player it waits for that (up to
60 seconds), connects, and proves the player's seat.
```csharp theme={null}
FlockNetcodeStartResult started = await session.StartNetcodeAsync(NetworkManager.Singleton);
switch (started.Outcome)
{
case FlockNetcodeStartOutcome.Restarted:
return; // a newer start on this NetworkManager took over
case FlockNetcodeStartOutcome.StartedAsHost:
ShowMessage("Hosting. Code: " + session.JoinCode);
break;
case FlockNetcodeStartOutcome.Connected:
ShowMessage(started.ThroughRelay ? "Connected through Flock's relay." : "Connected.");
break;
case FlockNetcodeStartOutcome.Refused:
ShowMessage("The host refused you: " + started.RefusedReason);
break;
case FlockNetcodeStartOutcome.RelayFailed:
ShowMessage("The relay could not be used: " + started.RelayFailureReason);
break;
default:
ShowMessage("Could not connect: " + started.Outcome);
break;
}
```
A start that neither hosts nor connects keeps the player's seat; call `session.LeaveAsync()`
if the player cannot play.
Connecting players is not available in the Unreal SDK yet.
| `FlockNetcodeStartOutcome` | Meaning |
| - | - |
| `StartedAsHost` | This device hosts, and players can reach it. |
| `Connected` | This device joined the host. `ThroughRelay` says whether through the relay. |
| `Refused` | The host refused this player; `RefusedReason` says why. |
| `RelayFailed` | The relay was needed and could not be used; `RelayFailureReason` says why. |
| `HostNeverPublished` | The host did not say where to reach it within 60 seconds (`FlockNetcodeOptions.WaitForHost`). |
| `CouldNotConnect` | The host did not answer. |
| `CouldNotStart` | Netcode for GameObjects refused to start, such as when the port is in use. |
| `SessionEnded` | The session ended while waiting. |
| `NotDirect` | The host published a connection of its own kind, such as Steam; see [Other networking](#other-networking). |
| `Restarted` | The game started the netcode again before this start ended, and the newer start took over. |
## Through the relay, or directly
`FlockNetcodeOptions.Relay` chooses who goes through the relay:
| `FlockRelayUse` | Who goes through the relay | Pick it for |
| - | - | - |
| `WhenNeeded` (default) | Players on the host's network try its local address for a few seconds, then the relay if it does not answer. Everyone else goes through the relay, then moves to a direct path when they and the host can reach each other. | Almost every game. |
| `Always` | Every player, on the host's network too, and they stay on the relay. Players never learn the host's IP address, nor the host theirs. | Streamers and competitive play, or networks that keep devices apart. |
| `Never` | Nobody. Players reach the host on its network, or on a port forwarded to it. | LAN games, or a game that does not want relay traffic. |
```csharp theme={null}
await session.StartNetcodeAsync(NetworkManager.Singleton, new FlockNetcodeOptions { Relay = FlockRelayUse.Always });
```
### Direct paths
With `WhenNeeded`, once the host has let a relayed player in, the two try to reach each other directly: each asks the relay,
or a STUN server Flock lists, where it sees it, they swap those addresses through the relay, and each sends a few small packets to the other. When they get
through both ways, the game's packets go directly, usually a shorter path than the relay's, and the relay has nothing left to
carry. The relay stays ready: a direct path that goes quiet for 4 seconds hands the packets back to it. The game does nothing in
either case. Most home routers allow this; when both sides are behind routers that give each destination a port of its own (as
some mobile carriers do), the packets stay on the relay. From SDK 1.83.0.
The trade-off: with `WhenNeeded`, the host and a relayed player learn each other's IP address once the player is let in. Use
`Always` to keep them hidden.
When the relay cannot be used, `RelayFailureReason` is one of:
| Reason | When |
| - | - |
| `relay_not_offered` | **Hand out relay credentials** is off for the game, or Flock lists no relay. |
| `relay_paused` | The studio's relay is paused until its bill is paid. |
| `relay_unreachable` | The network blocks UDP to the relay, as some school, office and hotel networks do. |
| `relay_not_on_this_platform` | A web player: a browser cannot send UDP. |
| `relay_not_offered_by_host` | The host takes nobody through the relay: its `Relay` is `Never`, or its relay could not be opened. |
| `relay_too_many_logins` | More than 12 relay logins in a minute, or 10 for one player in one session: do not start the netcode in a loop. |
The server can add reasons, so keep a default.
## Who is joining
The host started by `StartNetcodeAsync` lets in only players whose seat Flock confirms, one
connection each. Name players by their connection, never by what a player's game sends:
```csharp theme={null}
string playerId = session.PlayerForConnection(clientId); // on the host
```
Add your own check after Flock's, such as a ban list or a team limit:
```csharp theme={null}
FlockNetcodeOptions options = new FlockNetcodeOptions
{
ApproveJoiningPlayer = (player, response) =>
{
if (bannedPlayers.Contains(player.PlayerId))
{
response.Approved = false;
response.Reason = "banned";
}
},
};
```
A refused player's `RefusedReason` is `no_join_token`, `join_token_invalid`, `already_connected`,
`flock_unreachable`, `session_ended`, `not_host`, `refused_by_the_game` or `game_check_failed`.
The host's check of each joining player is billed as one API call.
Both ends of a game must run Flock SDK 1.77.0 or later, and the same major version of Netcode for
GameObjects: a host refuses players on an older Flock SDK, or on another major version of Netcode.
## When the host changes
When the host leaves or its game stops, Flock makes the player seated longest the host and raises
`HostChanged` on every game. Netcode for GameObjects cannot move a running game to a new host, so
each game shuts its netcode down and starts it again: the new host hosts, and the others join it.
Do the same on `RelayLost`, raised when this device's relay stops working.
```csharp theme={null}
session.HostChanged += StartAgain;
session.RelayLost += reason => StartAgain();
async void StartAgain()
{
if (session.HasEnded)
return;
NetworkManager.Singleton.Shutdown();
await session.StartNetcodeAsync(NetworkManager.Singleton); // waits for the shutdown to finish
}
```
Players are back about 20 seconds after a host leaves, and about a minute after a host's game
crashes (the **Host timeout**). Everything the old host held is gone: its spawned objects, network
variables and scene state. Keep what a new host needs on every player, such as a snapshot of the
game the host sends every few seconds, and rebuild from it. While waiting for a crashed host to be
replaced, players lose the netcode connection first; keep the session and wait for `HostChanged`.
## Other networking
Games on Steam, Epic Online Services or their own networking still use Flock's parties,
matchmaking, sessions and join check. They publish and read the connection themselves:
```csharp theme={null}
// The host says how to reach it: a kind of its own and the values it needs, under 4 KB.
await session.PublishConnectionAsync("steam", new Dictionary { ["lobby"] = lobbyId.ToString() });
// A player waits for it; null when the time passes or the session ends.
FlockMultiplayerSessionConnection connection = await session.WaitForConnectionAsync(TimeSpan.FromSeconds(60));
// The player proves its seat with a token, good for 60 seconds, sent over your own networking.
FlockMultiplayerSessionJoinToken token = await session.RequestJoinTokenAsync();
// The host checks it and learns who the player is; a bad token throws MultiplayerInvalidJoinToken.
string playerId = await session.VerifyJoinTokenAsync(tokenFromPlayer);
```
`PublishDirectConnectionAsync(port)` publishes this device's own address, and a player's
`connection.FindDirectAddressAsync()` picks the one to use: the host's local address on a shared
network, its public address otherwise. Outside a local network, players reach that address only
through a port forwarded to the host.
# Currencies & Wallets
Source: https://docs.qwacks.com/guides/currencies-and-wallets
Define soft and hard currencies and track player balances.
**Currencies** are the units your economy runs on — coins, gems, energy, or anything else. Each
game defines its own currencies; every player has a **wallet** holding their balances.
currencies are defined per **game**; each player has a wallet of balances.
Define your currencies in the dashboard — a currency is just a named unit; you decide whether
it's earned, purchased, or both. You can also inspect player wallets here.
`AddGameFundsAsync` credits the signed-in player's wallet. The SDK resolves the
`currency`-tagged player template for you; a schema sync bakes its id so the call skips that
lookup. Balances read back as ordinary player data — the wallet *is* the row for that template.
```csharp theme={null}
// Grant. Resolves the "currency"-tagged template at runtime.
PlayerData wallet = await FlockClient.Instance.Commands.AddGameFundsAsync("gold", 500);
// After Code Generation → Sync Schemas, with a typed currency:
wallet = await FlockClient.Instance.Commands.AddGameFundsAsync(FlockFundId._100, 500);
// Read the signed-in player's balances.
PlayerData balances = await FlockClient.Instance.Player.GetMyDataByTagAsync("currency");
```
**Grants are money-safe.** A grant fails rather than queueing when the server is unreachable, and
is never re-sent after an ambiguous failure — a timeout may mean the credit already landed, so the
SDK will not risk crediting twice. Only provably-unprocessed failures (408/429) retry, so wrap the
call in `try`/`catch` and let the player retry deliberately.
`FlockFundId` members are currency **ids**, not names (hence `_100` — a leading underscore is
added when an id starts with a digit). Currency names live only on the dashboard's admin
endpoint, which the SDK's API key cannot reach.
`Flock Add Game Funds` credits the signed-in player's wallet. The SDK resolves the
`currency`-tagged template, and a schema sync bakes its id so the call skips that lookup.
```cpp theme={null}
Sdk->GetCommandProvider()->AddGameFunds(TEXT("coins"), 250, OnCredited);
// After a sync, with a typed currency:
FFlockGenerated::AddFunds(this, EFlockCurrencyId::Gold, 100, OnCredited);
```
**Grants are money-safe.** A grant fails rather than queueing when the server is unreachable, and
is never re-sent after an ambiguous failure — a timeout may mean the credit already landed, so the
SDK will not risk crediting twice. Read balances back as ordinary player data.
[Shop purchases](/guides/shops-and-inventory) debit the relevant wallet automatically as part of
the transaction — you don't adjust balances by hand for a normal buy.
# Developer Alerts
Source: https://docs.qwacks.com/guides/developer-alerts
Get notified when errors spike in your game — before players tell you.
**Developer alerts** watch your game's log stream and tell your team when something goes wrong at
scale — a crash loop after a release, a payment error spiking, a backend call suddenly failing.
You define the rule once; Flock does the watching.
alert rules are defined per **game**.
Everything lives in the **Alert Center**. **Error Logs** is the raw stream your game reports;
**Alert Rules** is where you turn a pattern in that stream into an alert. A rule says: when this
**log type** (optionally narrowed to a specific **error code**) occurs at least **N times**
within a **window of minutes**, fire an alert at a chosen **severity** — info, warning, or
critical.
Each rule has a **cooldown** (an hour by default) so a sustained incident produces one alert,
not a page of duplicates, and you can **test** a rule against recent logs before enabling it.
When a rule fires, the alert lands in your team's notification bell, and the **Events** page
keeps the history — what fired, when, and the counts behind it.
There's nothing to integrate on the game side: rules evaluate the logs your game already reports
to Flock, so if your errors show up under **Error Logs**, they're alertable.
Start with one critical rule on your most player-visible failure — a crash or failed purchase —
with a generous threshold, and tighten it as you learn your game's normal error rate.
# Game Patches
Source: https://docs.qwacks.com/guides/game-patches
Ship incremental config changes to a game version as patches.
**Patches** let you make incremental changes to a [game version](/concepts/versions)'s
configuration after it's been created — without standing up a whole new version.
patches apply to a single **game version**.
A game version's configuration **cannot be edited once that version is in production**. Patch a
`dev` or `staging` version, validate it, then promote — production versions are locked to keep live
players stable.
Create and manage a version's patches on the **Game Patches** page (Configurations → Game
Patches), while the version is in `dev` or `staging`.
Your game receives the patched configuration automatically — the generated config accessor
returns the patched values for the running game version, so a normal read needs nothing extra
and never has to know whether a patch applied.
```csharp theme={null}
// Already patched, if a patch applies to this game version.
var gameplay = await FlockClient.Instance.Config.GetGameplayAsync();
```
Inspecting the patches **themselves** — listing them, or fetching one by id — is not part of
the Unity public API. Those reads are `internal`, used by code generation. Use the dashboard
to see which patches exist and what they change.
`Flock Resolve Config Data` already returns the patched values for the running game version, so a
graph usually needs nothing else — it never has to know whether a patch applied.
```cpp theme={null}
Sdk->GetConfigProvider()->ResolveConfigData(ConfigId, OnResolved);
```
To inspect the patches themselves — all patches, by id, or by config — use the config provider in
C++; those reads are not yet exposed as Blueprint nodes.
# Leaderboards
Source: https://docs.qwacks.com/guides/leaderboards
Rank players on any numeric stat your game already tracks.
A **leaderboard** ranks players on a numeric stat — high score, fastest time, season points. In
Flock, leaderboards are **config-driven**: you point one at a field your game already writes
through [player data](/guides/player-data), and Flock keeps the standings from there. There's no
separate score-submission call.
leaderboards are defined per **game**; each entry belongs to a player.
Create and manage leaderboards under **Player Management → Leaderboards**. A leaderboard is
defined by:
* **Source** — the [player template](/concepts/players) and numeric field to rank on.
* **Direction** — whether **higher** or **lower** values rank better (points vs. lap times).
* **Aggregation** — keep each player's **best** value, their **latest**, or the running
**sum**.
* **Value type** — integer, float, or duration.
* **Window** — **all-time**, **weekly**, or **seasonal**. Seasonal boards follow the
**seasons** you define alongside them, each with a start and end date.
* **Scope** — one **global** board, or per-**country** standings.
From the same page you can maintain a live board: **exclude** a player (and lift the
exclusion later), **reset** a board, or **backfill** it from the player data your game has
already written.
A leaderboard's structural definition — its source and how it ranks — is locked once the
board is live. To rank something else, create a new leaderboard.
Scores flow in automatically: when your game writes the configured field through
[player data](/guides/player-data), the relevant leaderboards pick the change up — best,
latest, or sum, per your aggregation. If you already track the stat, there's nothing new to
send.
Boards are addressed by **name** — the same name you gave the board on the dashboard.
```csharp theme={null}
// Top of the board.
Standings top = await FlockClient.Instance.Leaderboard.GetStandingsAsync("weekly_score", limit: 20);
foreach (StandingEntry entry in top.Items)
Debug.Log($"#{entry.Rank} {entry.PlayerName} — {entry.Score}");
// The signed-in player's own rank, and the players on either side of them.
PlayerRank mine = await FlockClient.Instance.Leaderboard.GetMyRankAsync("weekly_score");
Standings neighbours = await FlockClient.Instance.Leaderboard.GetAroundMeAsync("weekly_score", neighbours: 5);
```
`GetByNameAsync` returns the board's configuration — value type, direction, aggregation,
window type and scope — when you need to format scores or label the UI correctly.
**Windows.** Every read defaults to the board's live window. Pass
`FlockLeaderboardWindow.Season("season-id")` for a finished season, or
`FlockLeaderboardWindow.Period("2026-W31")` for one specific weekly period. Per-country
boards take a `country` argument on the same calls.
**Sign-in.** `GetStandingsAsync` and `GetByNameAsync` work signed out — standings are public.
`GetMyRankAsync` and `GetAroundMeAsync` need a signed-in player and throw without one.
`PlayerRank.Rank` and `.Score` are nullable: they come back `null` when the player has no
entry on that board yet, which is not an error.
**Refused scores.** A board can turn a score away even though the write succeeded. The write's
answer lists those boards in `LeaderboardRefusals`, each with its `LeaderboardName`, a `Reason`
(`LeaderboardRefusalReasons.ScoreBounds`: outside what the board accepts; `GainLimit`: the
player's recent gains fill its allowance), `Retryable` and `RetryAfterSeconds`. Only
`UpdatePlayerDataAsync` and `UpdatePlayerDataFieldAsync` report them, so an empty list elsewhere
means "not reported here", not "accepted".
Scores flow in automatically: when your game writes the configured field through
[player data](/guides/player-data), the relevant leaderboards pick the change up — best,
latest, or sum, per your aggregation. If you already track the stat, there's nothing new to
send.
Boards are addressed by **name** — the same name you gave the board on the dashboard.
```cpp theme={null}
UFlockSubsystem* Sdk = UFlockSubsystem::Get(this);
FFlockLeaderboardProvider* Boards = Sdk->GetLeaderboardProvider();
// Top of the board.
Boards->GetStandings(TEXT("weekly_score"),
[](TFlockResult Top)
{
for (const FFlockStandingEntry& Entry : Top.Value.Items)
{
UE_LOG(LogTemp, Log, TEXT("#%d %s"), Entry.Rank, *Entry.PlayerName);
}
});
// The signed-in player's own rank, and the players on either side of them.
Boards->GetMyRank(TEXT("weekly_score"), [](TFlockResult Mine) {});
Boards->GetAroundMe(TEXT("weekly_score"), [](TFlockResult Neighbours) {});
```
`GetByName` returns the board's configuration — value type, direction, aggregation, window
type and scope — when you need to format scores or label the UI correctly. Its `FormatScore`
renders a duration board as a clock rather than raw seconds.
In Blueprint the same calls are *Flock Get Leaderboard*, *Flock Get Leaderboard Standings*,
*Flock Get My Rank* and *Flock Get Standings Around Me*, under **Flock | Leaderboard**.
**Windows.** Every read defaults to the board's live window. Pass
`FFlockLeaderboardWindow::Season("season-id")` for a finished season, or
`FFlockLeaderboardWindow::Period("2026-W31")` for one specific weekly period. Per-country
boards take a country argument on the same calls.
**Sign-in.** `GetStandings` and `GetByName` work signed out — standings are public.
`GetMyRank` and `GetAroundMe` need a signed-in player and fail without one.
`FFlockPlayerRank` carries a `Ranked` flag: it is `false` when the player has no entry on that
board yet, which is a valid result rather than an error. Check it before reading `Rank` or
`Score`.
**Refused scores.** A board can turn a score away even though the write succeeded. The write's
answer lists those boards in `LeaderboardRefusals`, each with its `LeaderboardName`, a `Reason`
(`score_bounds`: outside what the board accepts; `gain_limit`: the player's recent gains fill
its allowance; compare with the *Flock Refusal Reason* nodes or `FlockLeaderboardRefusalReasons`),
`Retryable` and `RetryAfterSeconds` (-1 when the server gave none). Only Update Player Data and
Update Player Data Field report them, so an empty list elsewhere means "not reported here", not
"accepted".
Pair a seasonal leaderboard with a [segment](/guides/segments) of your top players to reward
them when the season closes.
# Matchmaking
Source: https://docs.qwacks.com/guides/matchmaking
Fill a game from a queue, alone or as a party.
**Matchmaking** puts players who want the same kind of game together. Each mode of your game is a
**queue** with a smallest and largest number of players. A player, or a whole
[party](/guides/parties), searches in a queue; once the queue holds enough players, Flock makes a
match and seats the players in a [session](/guides/sessions). A party is matched as one group and
never split. Players are matched in the order they search.
matchmaking queues are defined per **game**.
Make queues under **Multiplayer → Matchmaking** with **Create Queue**:
* **Queue Name**: the name your game searches by, letter for letter, case included.
* **Min Players** and **Max Players**: the smallest and largest match.
* **Same game version only**: matches players of one build with each other only. Leave it on
unless different builds can play together.
* **Allowed Segments**: only players in these [segments](/guides/segments) may search here.
Leave it empty for everyone.
* **Queue metadata**: notes for your own reference. Matching does not read them.
**Multiplayer → Matchmaking Activity** shows who is waiting in each queue now, and the matches
made.
Search alone. A search that ends without a match is an outcome, not an exception.
```csharp theme={null}
CancellationTokenSource searching = new CancellationTokenSource();
cancelButton.onClick.AddListener(searching.Cancel);
try
{
FlockMatchmakingResult found = await FlockClient.Instance.Multiplayer.FindMatchAsync("coop-4", null, searching.Token);
if (found.IsMatched && found.Session != null)
await PlayAsync(found.Session);
else if (found.Outcome == FlockMatchmakingOutcome.Expired)
ShowMessage("Nobody found in 5 minutes. Search again?");
else
ShowMessage("The search stopped.");
}
catch (OperationCanceledException)
{
ShowMessage("Search cancelled."); // the search is cancelled on the server too
}
catch (FlockException ex) when (ex.ErrorCode == FlockErrorCode.MatchmakingQueueNotFound)
{
ShowMessage("No queue is named coop-4.");
}
```
Search as a party. The leader's game starts the search, and every member's game hears it
through the party, so handle the result in one place: `SearchEnded` is raised on every member's
game, the leader's included.
```csharp theme={null}
party.SearchStarted += () => ShowSearching();
party.SearchEnded += result =>
{
if (result.IsMatched && result.Session != null)
_ = PlayAsync(result.Session);
else
ShowLobby();
};
if (party.IsLeader)
await party.FindMatchAsync("coop-4");
```
The SDK checks a search every 3 seconds. `FlockMatchmakingOptions.Attributes` passes your own
data with a search; matching does not use it yet.
Matchmaking is not available in the Unreal SDK yet.
## How a search ends
| `FlockMatchmakingOutcome` | When | What to do |
| - | - | - |
| `Matched` | Players were matched. `Session` is where they play; `PlayerIds` lists them, the longest waiting first. | Connect the players. If `Session` is null, `GetMySessionAsync` finds it; with **Create a session when a match forms** off, there is none and your game makes one. |
| `Expired` | Nobody was found within 5 minutes. | Offer to search again. |
| `PartyChanged` | A player joined or left the party during the search. | The leader searches again. |
| `Cancelled` | Another game stopped it: a party member's, or the player's own on another device. | Show the lobby. |
A search your game cancels through its token ends the call with an `OperationCanceledException`
instead. The server can add outcomes, so treat any other as ended without a match.
## Party members and game versions
On a **Same game version only** queue, the server takes each member's version from their game's
last read of the party. Have every member's game read the party (`GetMyPartyAsync`) once it is
running, or the search is refused with `MatchmakingVersionMismatch`.
## What matching does not use yet
Matching is first come, first served. Skill, region and latency rules, teams and backfill are not
built yet; what a search sends in its attributes is stored for them. Until then, make a queue per
mode and region (`coop-4-eu`, `coop-4-na`).
## Errors
| `ErrorCode` | When |
| - | - |
| `MatchmakingQueueNotFound` | No queue has that exact name. |
| `MatchmakingPlayerNotEligible` | The player is not in the queue's allowed segments. |
| `MatchmakingPartyTooLarge` | The party is bigger than the queue's largest match. |
| `MatchmakingNotPartyLeader` | A party member tried to start the search. |
| `MatchmakingVersionMismatch` | Party members are on different builds. |
# Multiplayer
Source: https://docs.qwacks.com/guides/multiplayer
Parties, matchmaking and sessions, and players connected through Flock's relay.
Flock's **multiplayer** brings players together and connects them. Friends form a
[party](/guides/parties), the party or a lone player [finds a match](/guides/matchmaking) or joins
a friend's game by code, and every game is a [session](/guides/sessions): one player hosts, the
others hold seats. The SDK then [connects the players](/guides/connecting-players) over your
game's networking. Players on the host's network connect to it directly; everyone else goes
through Flock's relay, so nobody forwards a port, and moves to a direct path to the host when the two can
reach each other. When the host leaves, another player takes over.
matchmaking queues and multiplayer settings are defined per **game**; parties and seats belong to a player.
Friends who stay together from one game to the next.
Fill a game from a queue, alone or as a party.
One game: a host, its players and a join code.
Netcode for GameObjects, the relay, and host changes.
## Setting up
Open **Multiplayer → Sessions** and click **Multiplayer settings**. The defaults suit most games:
| Setting | Default | What it does |
| - | - | - |
| **Max players per session** | 8 | Seats in a session a player hosts or a match makes, from 2 to 16. |
| **Participant timeout (seconds)** | 90 | A player silent for longer than this loses their seat. |
| **Host timeout (seconds)** | 60 | A host silent for longer than this is replaced by the player seated longest. |
| **Session max age (seconds)** | 14400 | Every session ends after this long (4 hours), however busy it is. |
| **Create a session when a match forms** | On | Matched players get a session to play in. |
| **Hand out relay credentials** | On | Players on different networks connect through Flock's relay. When off, only players on the host's network, or on a port forwarded to it, can connect. |
Then make a queue for each mode under **Multiplayer → Matchmaking**; see
[Matchmaking](/guides/matchmaking).
1. Install **Netcode for GameObjects** 1.x or 2.x from the Package Manager. Flock's netcode
support compiles only when it is installed. Every player of a game must use the same major
version. Multiplayer is verified on Unity 2021.3 with Netcode 1.14 and on Unity 6000.3 with
Netcode 2.13.
2. In **Flock → Settings**, set **Party Refresh Seconds**: how often the SDK reads the player's
party in the background (10 by default; 0 turns it off).
3. Use one `NetworkManager` with Unity Transport, left at its default message size.
4. Sign the player in before any multiplayer call, and make every call from the main thread.
Every multiplayer event is raised on the main thread.
5. Scripts that use multiplayer start with `using Flock;` and `using Flock.Providers;` (add
`using Flock.Exceptions;` for `FlockErrorCode` and `using Unity.Netcode;` for `NetworkManager`).
A script in an assembly definition of its own references `Flock.Runtime`, and
`Flock.Multiplayer.Netcode` for `StartNetcodeAsync`.
Every call is on `FlockClient.Instance.Multiplayer`, or on an object it returns.
The package's **Samples/Multiplayer** folder has two one-screen samples:
**FlockPlayWithFriendsSample** (host, read out the code, a friend joins it) and
**FlockQuickMatchSample** (players find each other in a queue). Add either to an empty
GameObject and press Play in two copies of the game.
Multiplayer is not available in the Unreal SDK yet.
## Which setup fits your game
| Your game | Players meet by | Relay |
| - | - | - |
| Local co-op: devices on one network (a LAN party, one Wi-Fi) | A session's join code | Only when needed (the default): players on the host's network connect directly. Or never, for no relay at all. |
| Online co-op with friends on other networks | A session's join code | Only when needed |
| Online matchmaking with strangers | A queue | Only when needed |
| Friends matched together, against or with others | A party, then a queue | Only when needed |
| Streamers and competitive play, where nobody may see another's IP address | Any of the above | Always, on every game |
| Steam, Epic Online Services or your own networking | Any of the above | Not used |
Several players on one device (split screen, shared controllers) need none of this: that is one game
with one Flock player, and Flock's multiplayer connects devices.
* **Local co-op:** nothing beyond the multiplayer settings.
* **Online co-op with friends:** leave **Hand out relay credentials** on.
* **Online matchmaking:** a queue for each mode under **Multiplayer → Matchmaking**, with its
smallest and largest match.
* **Friends matched together:** a queue whose largest match holds the whole party.
* **Streamers and competitive play:** nothing more; the choice is made in the game.
**Local co-op, and online co-op with friends.** One player hosts and shares the join code (read out
in the room, or through your game's own lobby, friends list or chat); the others join with it.
Players on the host's network connect directly; with the default `WhenNeeded`, a friend on another
network comes through Flock's relay, then moves to a direct path when the two can reach each other.
Phones and PCs play together the same way. The **FlockPlayWithFriendsSample** does exactly this.
```csharp theme={null}
// The host's game.
FlockMultiplayerSession session = await FlockClient.Instance.Multiplayer.HostSessionAsync(4);
joinCodeLabel.text = session.JoinCode;
await session.StartNetcodeAsync(NetworkManager.Singleton);
// Each other player's game, with the code the host shared.
FlockMultiplayerSession joined = await FlockClient.Instance.Multiplayer.JoinSessionAsync(typedCode);
FlockNetcodeStartResult started = await joined.StartNetcodeAsync(NetworkManager.Singleton);
// started.ThroughRelay is false for a player on the host's network.
```
For a LAN-only game, pass `new FlockNetcodeOptions { Relay = FlockRelayUse.Never }` on every game; a
player elsewhere then gets `CouldNotConnect` unless a port is forwarded to the host.
**Online matchmaking.** The **FlockQuickMatchSample** does this.
```csharp theme={null}
FlockMatchmakingResult found = await FlockClient.Instance.Multiplayer.FindMatchAsync("casual-1v1");
if (found.IsMatched && found.Session != null)
await found.Session.StartNetcodeAsync(NetworkManager.Singleton); // the player who waited longest hosts
else
ShowMessage("No match: " + found.Outcome);
```
**Friends matched together.** The friends form a [party](/guides/parties) and the leader searches for
all of them; every member's game hears the result.
```csharp theme={null}
party.SearchEnded += async result =>
{
if (result.IsMatched && result.Session != null)
await result.Session.StartNetcodeAsync(NetworkManager.Singleton);
};
if (party.IsLeader)
await party.FindMatchAsync("squads");
```
**Streamers and competitive play.** Every packet goes through Flock's relay and no game learns
another's IP address; each packet takes the relay's extra hop.
```csharp theme={null}
await session.StartNetcodeAsync(NetworkManager.Singleton, new FlockNetcodeOptions { Relay = FlockRelayUse.Always });
```
**Steam, Epic Online Services or your own networking:** see
[Other networking](/guides/connecting-players#other-networking).
Multiplayer is not available in the Unreal SDK yet.
## What the SDK does for you
* **Keeps each seat.** It sends the session's heartbeat every 20 seconds while the game runs.
* **Keeps your objects current.** It reads the party, a search and the session in the background,
and raises events when something changed: a player joined, the leader or host changed, a search
ended, the session ended.
* **Finds the session again** after the game restarts (`GetMySessionAsync`), and the party
(`GetMyPartyAsync`).
* **Gives the seat up when the game quits**, so the others see it at once.
## What counts as an API call
The background reads (the party, a search, the session, waiting for the host to say where it is)
and heartbeats are **not** billed as API calls. They share a limit of **120 a minute per player**;
the SDK's defaults use under 60. Actions are billed as usual: making or joining a party or session,
starting a search, leaving, the host's check of each joining player, relay logins, and a relayed
player telling the host its relay address (once for each address).
## Which games it fits
The host is one of the players, sessions hold up to 16, and matching is first come, first served.
| Fits well | Fits with care | Not a fit today |
| - | - | - |
| Co-op games for 2 to 8 players: dungeon crawlers, survival, horde shooters | Casual 1v1 and 2v2 versus, not ranked | MMOs and always-on worlds |
| Party games, brawlers, racing with friends | Fast shooters: the host has an advantage, and a player who stays on the relay plays through its extra hop | Battle royale and other games of 50 to 100 players |
| Board, card and turn-based games | 9 to 16 players: the host sends to everyone, which counts on a phone | Ranked play that needs dedicated servers and skill matching |
| Phone and PC playing together | Worlds kept by a player: save the state off the host | Browser games (a browser cannot send UDP) |
## Limits today
* **Flock's relay is not live in production yet.** Until it is, players connect on the host's network or through a
port forwarded to the host.
* **No dedicated servers.** A player hosts every session.
* **No skill or region matching yet.** Flock stores what a search sends but matches in the order
players search. Use a queue per mode and region.
* **A host cannot remove a player, and a session's data is set once** when it is created.
* **Netcode for GameObjects 1.x and 2.x only.** Other networking uses the session's published
connection and join token directly; see [Connecting players](/guides/connecting-players).
* **Web players** can use parties, matchmaking and sessions, but not the relay: a browser cannot
send UDP.
# Game Notifications
Source: https://docs.qwacks.com/guides/notifications
Send messages to your players — in-app inbox, email, or push.
**Game notifications** let you talk to your players: announce an event, celebrate a milestone,
win back a lapsed cohort. You author a message once as a **template**, send it with a
**campaign**, and players receive it on the **channels** you chose — the in-game inbox, email,
or push.
templates and campaigns are defined per **game**; the inbox is per **player**.
Notifications live under **Marketing**:
* **Templates** — the reusable message: a title and body (with a locale), the channels it can
go out on, and **variables** you fill in at send time — write the season-start announcement
once with the season name as a variable, and reuse it every season.
* **Campaigns** — an actual send: pick a template, fill its variables, choose the
**audience** — every player, or a [segment](/guides/segments) — and send **now**, at a
**scheduled** time, or on a **recurring** cadence. A scheduled campaign can be canceled
any time before it goes out.
* **Delivery Status** — how a send went: its lifecycle from scheduled through sending to
sent, with per-channel delivery counts and any failures.
**Push credentials.** Push is the one channel that needs setup before anything is delivered.
Under **Settings → Push Notifications**, configure the providers you ship on — credentials are
per game, stored encrypted, and **write-only**: they can be replaced but never read back.
* **Firebase Cloud Messaging (FCM)** — delivers to **Android and web**. Paste the full contents
of the Firebase service-account JSON file.
* **Apple Push Notification service (APNs)** — delivers to **iOS**. Provide the `.p8` signing
key, its **Key ID**, your **Team ID**, and the app's **Bundle ID**. Tick **Use APNs sandbox**
for development builds; a sandbox token will not accept production credentials, or the
reverse.
Until a provider shows **Configured**, push tokens register successfully and nothing is ever
delivered on that platform. That is the most common cause of "push is broken" — the client side
reports success because registration genuinely succeeded.
**The inbox.** Fetch the signed-in player's notifications, show an unread badge, and mark
items read one at a time or all at once.
```csharp theme={null}
PaginatedResponse inbox = await FlockClient.Instance.Notification.GetInboxAsync();
NotificationSummary summary = await FlockClient.Instance.Notification.GetSummaryAsync();
Debug.Log($"{summary.UnreadCount} unread");
await FlockClient.Instance.Notification.MarkReadAsync(inbox.Items[0]);
await FlockClient.Instance.Notification.MarkAllReadAsync();
```
`GetSummaryAsync` returns the unread count plus the newest few items, so a badge and a
preview cost one call. Prefer it to refetching the whole inbox on a timer — inbox reads count
toward your plan's API usage like any other SDK call.
**Push.** How you get a device token depends on the platform, and the SDK is explicit about
which half it can do for you:
* **iOS** — `RegisterThisDeviceAsync()` asks the OS for notification permission and registers
the resulting APNs token in one call. It requires `com.unity.mobile.notifications` in the
project.
* **Android** — fetch the FCM token from Firebase yourself, then pass it to
`RegisterDeviceTokenAsync(token)`. The Firebase messaging client isn't installable over UPM,
so the SDK cannot ship it as a dependency and deliberately doesn't try.
* **Everywhere else** (desktop, console, Editor) — both calls throw, because the push backend
has no value for those platforms.
Re-register whenever the OS issues a new token. `UnregisterDeviceTokenAsync` removes one.
**Reminders your game schedules itself.** Schedule a template to arrive later — "energy full
in 4 hours" — and cancel it if the player comes back first.
```csharp theme={null}
ScheduledNotification pending = await FlockClient.Instance.Notification.ScheduleAsync(
"energy_refilled", TimeSpan.FromHours(4));
await FlockClient.Instance.Notification.CancelScheduledAsync(pending);
```
The first argument is the template's **name**, not its ID. Pass `variables` to fill the
template's placeholders, `channels` to restrict delivery, and `locale` to pick a
localisation. An unknown name throws `FlockErrorCode.NotificationTemplateNotFound` before
anything is scheduled.
**What the player has pending.** `GetScheduledAsync()` asks the server, so it sees reminders set
before a reinstall or on the player's other device — not just the ones this install created.
```csharp theme={null}
PaginatedResponse pending =
await FlockClient.Instance.Notification.GetScheduledAsync();
foreach (ScheduledNotification s in pending.Items)
Debug.Log($"{s.Id} delivers at {s.DeliverAt}");
// Cancels every pending reminder the player has, wherever it was set.
int cancelled = await FlockClient.Instance.Notification.CancelAllScheduledAsync();
```
Pending is the default; pass `ScheduledNotificationStatuses.Delivered` or `.Canceled` to see the
others. `GetPendingSchedules()` remains as a synchronous, no-network fallback covering what
*this install* scheduled — useful offline, but prefer `GetScheduledAsync` when you can reach the
server.
Scheduling is **not retried** after an ambiguous failure, so a network blip cannot leave a player
with two of the same reminder. Cancelling is idempotent.
**Reading the catalog.** `GetTemplatesAsync()` lists the templates your game can schedule and
`GetTemplateByNameAsync(name, locale)` fetches one. Both are game-scoped rather than
per-player, so unlike the rest of this provider they work signed out.
**Reacting to arrivals.** `FlockEvents.OnNotificationReceived` fires once per notification the
SDK hasn't surfaced before, oldest first. It rides the fetches your game already makes and
adds no traffic — there is no realtime channel and the SDK deliberately does not poll.
**The inbox.** Fetch the signed-in player's notifications, show an unread badge, and mark items
read one at a time or all at once.
```cpp theme={null}
FFlockNotificationProvider* Notifications = UFlockSubsystem::Get(this)->GetNotificationProvider();
Notifications->GetNotifications([](TFlockResult Page)
{
// Page.Value.Total is the whole inbox; Page.Value.Items is this page.
});
Notifications->GetSummary([](TFlockResult Summary)
{
// Summary.Value.UnreadCount, plus the newest few in Summary.Value.Items.
});
Notifications->MarkRead(NotificationId, nullptr);
Notifications->MarkAllRead(nullptr);
```
`GetSummary` returns the unread count plus the newest few items, so a badge and a preview cost
one call. Prefer it to refetching the whole inbox on a timer — inbox reads count toward your
plan's API usage like any other SDK call.
Inbox reads are backed by the offline cache, so a notifications screen shows the last-known
messages when the network is down rather than an error. Mark-read calls are **never** queued
offline: a read receipt replayed an hour later would mark messages the player never saw.
**Push.** The SDK registers a device token; it does not fetch one. Get the token from your push
plugin — Firebase Cloud Messaging's `On Token Received`, or OneSignal — and pass the string in:
```cpp theme={null}
Notifications->RegisterDeviceToken(Token, [](TFlockResult Result) {});
```
The platform is taken from the running build. On desktop, console and in the Editor the call
fails with a validation error rather than guessing a platform — a token filed under the wrong
one is accepted and then never delivers. Call `Flock Get Current Device Platform` first to hide
an "enable notifications" toggle where push cannot work. `UnregisterDeviceToken` removes one;
re-register whenever the OS issues a new token.
Where the token comes from differs by platform:
* **iOS — nothing extra needed.** Unreal surfaces the APNs token itself and Flock speaks APNs
directly, so there is no Firebase in this path. Bind
`FCoreDelegates::ApplicationRegisteredForRemoteNotificationsDelegate`, hex-encode the bytes,
and pass the string in.
* **Android — a Firebase-based plugin, for now.** Unreal's own Android push plugin is built on
Google Cloud Messaging, which Google decommissioned in 2019.
**Built-in token acquisition is planned** — a one-call iOS wrapper and a first-party Android
binding — so push will not require a third-party plugin. `RegisterDeviceToken` does not change
when they land; only where the string comes from.
**Reminders your game schedules itself.** Schedule a template to arrive later — "energy full in
4 hours" — and cancel it if the player comes back first.
```cpp theme={null}
Notifications->ScheduleByTemplateName(TEXT("energy_refilled"),
FDateTime::UtcNow() + FTimespan::FromHours(4),
[Notifications](TFlockResult Pending)
{
Notifications->CancelScheduled(Pending.Value.Id, nullptr);
});
```
The first argument is the template's **name**, not its ID — the ID is resolved internally and
memoized. Pass `Variables` to fill the template's placeholders and `Channels` to restrict
delivery; leave either empty and the template's own defaults apply. An unknown name fails with
a validation error before anything is scheduled.
Scheduling is **not retried** after an ambiguous failure, so a network blip cannot leave a
player with two of the same reminder. Cancelling is idempotent.
**What the player has pending.** `GetScheduled()` asks the server, so it sees reminders set before
a reinstall or on the player's other device — not just the ones this install created.
```cpp theme={null}
Notifications->GetScheduled([](TFlockResult Pending)
{
for (const FFlockScheduledNotification& Entry : Pending.Value.Items)
{
// Entry.Id is what Cancel Scheduled Notification takes.
}
});
// Cancels every pending reminder the player has, wherever it was set.
Notifications->CancelAllScheduled([](TFlockResult Result) { /* Result.Value cancelled */ });
```
Pending is the default; pass `FlockScheduledNotificationStatuses::Delivered` or `::Canceled` to
see the others. In Blueprint this is **Flock Get Scheduled Notifications**, whose `Status` pin
defaults to pending — feed it from the *Flock Schedule Status* nodes rather than typing a value,
since the wire spelling is the single-l `canceled`.
The listing is **never cached**: a reminder is delivered server-side with nothing to tell the
client, so a stored page would report one that has already fired as still pending. It needs a
network and a signed-in player.
`GetPendingSchedules()` remains as a synchronous, no-network fallback covering what *this install*
scheduled — useful offline, but prefer `GetScheduled` when you can reach the server.
`CancelAllScheduled` falls back to it only when the server read fails; the two are never merged.
In Blueprint these are **Flock Get Scheduled Notifications**, **Flock Cancel All Scheduled
Notifications** and **Flock Get Pending Schedules**. The `Status` pin defaults to pending — feed it
from the *Flock Schedule Status* nodes rather than typing a value, since the wire spelling is the
single-l `canceled`. Get Pending Schedules is a plain callable node, not an async one: it makes no
network call.
Use **Flock Cancel All Scheduled Notifications** rather than looping *Flock Cancel Scheduled
Notification* over a listing. A Blueprint ForEach fires every iteration in the same frame, and each
cancel rewrites the stored pending list as it completes — so parallel cancels race, and the last
one to finish restores entries the others had already removed. The dedicated node walks them
sequentially for exactly that reason.
**Reading the catalog.** `GetTemplates()` lists the templates your game can schedule and
`GetTemplateByName(Name, Locale)` fetches one. Both are game-scoped rather than per-player, so
unlike the rest of this provider they work signed out.
**Reacting to arrivals.** Two events on the hub save you diffing pages yourself:
```cpp theme={null}
UFlockEvents* Events = UFlockSubsystem::Get(this)->GetEvents();
Events->OnNotificationReceived.AddDynamic(this, &AMyActor::HandleNotification);
Events->OnUnreadCountChanged.AddDynamic(this, &AMyActor::HandleUnreadCount);
```
`OnNotificationReceived` fires once per notification the SDK hasn't surfaced before, oldest
first. `OnUnreadCountChanged` fires whenever the server reports a count — an unread-count or
summary fetch, or a mark-all-read. Both ride the fetches your game already makes and add no
traffic: there is no realtime channel and the SDK deliberately does not poll, so "received"
means *first seen by a read*. The first fetch after a fresh install is silent, so an existing
inbox doesn't arrive as a burst of events on launch.
**Blueprint.** Every call above has a node — `Flock Get Notifications`, `Flock Get Notification
Summary`, `Flock Mark Notification Read`, `Flock Schedule Notification`, `Flock Register Device
Token`, and the rest — plus pure helpers `Is Read`, `Is Pending`, `Is Delivered` and
`Is Canceled`. The two events are assignable straight off `Flock Get Events`.
This page is about messages **to players**. Alerts to your own team — error spikes, build
results — are [Developer Alerts](/guides/developer-alerts).
# Parties
Source: https://docs.qwacks.com/guides/parties
Friends who stay together from one game to the next.
A **party** is a group of friends who play together: one leader and up to 63 others. A party lasts
until its players leave it, across games and launches, so friends can play match after match
without inviting each other again. Friends join with the party's **invite code**. A player is in
one party at a time.
parties belong to the players in them, within one **game**.
There is nothing to set up. A party searching for a match shows on
**Multiplayer → Matchmaking Activity** as one entry, with its size.
Read the party after signing in: the player may still be in one from last time.
```csharp theme={null}
FlockMultiplayerProvider multiplayer = FlockClient.Instance.Multiplayer;
// The party from last time, or a new one: 2 to 64 players, 4 when not given.
FlockParty party = await multiplayer.GetMyPartyAsync()
?? await multiplayer.CreatePartyAsync(4, new Dictionary { ["mode"] = "coop" });
inviteCodeLabel.text = party.InviteCode;
party.MembersChanged += () => ShowMembers(party.Members); // in the order they joined
party.LeaderChanged += () => leaderControls.SetActive(party.IsLeader);
party.Ended += reason => ShowMainMenu(reason);
```
A friend's game joins with the code; letter case does not matter.
```csharp theme={null}
FlockParty joined = await multiplayer.JoinPartyAsync(typedCode);
if (joined.TryGetSetting("mode", out string mode))
ShowMode(mode);
```
The leader manages the party; anyone can leave it.
```csharp theme={null}
await party.UpdateAsync(settings: new Dictionary { ["mode"] = "hard" }); // replaces all settings
await party.KickAsync(playerId);
await party.MakeLeaderAsync(playerId);
await party.DisbandAsync();
await party.LeaveAsync(); // when the leader leaves, the first of the others leads
```
`party.FindMatchAsync` searches as the party; see [Matchmaking](/guides/matchmaking).
Parties are not available in the Unreal SDK yet.
## Keeping the party current
The SDK reads the party every **Party Refresh Seconds** (10 by default) and after each change your
game makes, and raises `MembersChanged` and `LeaderChanged` when something moved. Another
player's change reaches your game by the next read. These reads are not billed; see
[Multiplayer](/guides/multiplayer#what-counts-as-an-api-call).
## When the party ends
`Ended` is raised once, with a `FlockPartyEndReason`:
| Reason | When |
| - | - |
| `Left` | The player left with `LeaveAsync`. |
| `Disbanded` | The player disbanded it. |
| `Removed` | The server no longer lists the player: removed by the leader, the party disbanded by someone else, or the player left from another device. |
| `SignedOut` | The player signed out, or another player signed in. The player is still in the party on the server. |
Call `LeaveAsync` before signing out when the player means to leave.
## Errors
| `ErrorCode` | When |
| - | - |
| `PartyInvalidInviteCode` | No party has that code. |
| `PartyFull` | The party has no room. |
| `PartyAlreadyInParty` | The player is in another party; leave it first. |
| `PartyNotPartyLeader` | A call only the leader can make. |
| `PartyCannotKickLeader` | The leader tried to remove themselves. |
# Player Data
Source: https://docs.qwacks.com/guides/player-data
Store custom per-player documents — progression, settings, and state.
**Player data** lets you persist arbitrary JSON for a player: level, coins, unlocked content,
preferences — whatever your game needs. Documents are scoped to the signed-in player.
player data is per player; its **template** is defined per **game version**.
A typical setup has two sides: **define the shape in the dashboard**, then **read and write it from
the SDK**.
A **player data template** defines the structure and defaults for a kind of document, keeping
records consistent across players and versions. Define templates in the dashboard; players
inherit them automatically.
Run **Code Generation → Sync Schemas** in the Flock window to turn each template into a typed C#
class, then read and write the signed-in player's document directly:
```csharp theme={null}
// Read (paginates and caches the player's documents on first call)
var progress = await FlockClient.Instance.Player.GetPlayerProgressAsync();
// Mutate the typed object and save it back
progress.Level = 2;
progress.Coins += 100;
await progress.UpdateAsync();
```
The method and field names come from your template (`PlayerProgress` → `GetPlayerProgressAsync`).
Read a player's rows with the *Flock | Player* nodes, or the player provider in C++. You never pass
a player ID — calls act for the signed-in player.
```cpp theme={null}
Sdk->GetPlayerProvider()->GetMyDataByTag(TEXT("currency"),
[](TFlockResult Result)
{
int32 Coins = 0;
Result.Value.Data.TryGetInt(TEXT("coins"), Coins);
});
```
After a schema sync this collapses to **one node** returning a typed struct with real pins, and
writing back is a second node — see [Work with your game data](/sdk/unreal#work-with-your-game-data).
Writes queue offline and replay automatically when the connection returns.
Keep documents small and purpose-scoped (e.g. a `progression` doc and a `settings` doc) rather
than one giant blob — it makes partial updates and debugging far easier.
# Players & Auth
Source: https://docs.qwacks.com/guides/players-and-auth
Sign players in with email, device, Google, Apple, or Steam — all through the SDK.
Flock supports several player sign-in methods, all through the SDK. See
[Authentication](/authentication) for how the SDK manages game and player sessions.
players are accounts within a single **game**.
Browse and manage your game's players from the Flock dashboard — search accounts, inspect
profiles, and review linked providers.
Each method is a `LoginWith…Async` call on `FlockClient.Instance.Authentication`. They **throw**
on failure.
`LoginWithEmailAsync` — classic account sign-in.
`LoginWithDeviceAsync` — frictionless guest sign-in keyed to a device.
`LoginWithGoogleAsync` / `LoginWithAppleAsync` — platform identity.
`LoginWithSteamAsync`, `LoginWithFacebookAsync`, `LoginWithDiscordAsync`.
```csharp theme={null}
await FlockClient.Instance.Authentication.LoginWithEmailAsync(email, password);
Debug.Log($"Signed in as {FlockClient.Instance.CurrentPlayerId}");
```
**A new player must register before they can log in.** `LoginWith…Async` never creates an
account — it only signs in one that already exists. Calling it for someone who has never
registered fails with `player.invalid_login_credentials`.
This catches people most often on **device sign-in**, where it feels like a guest flow that
should just work:
```csharp theme={null}
// First launch on this device — creates the account AND signs in.
await FlockClient.Instance.Authentication.RegisterWithDeviceAsync(deviceId);
// Every launch after that.
await FlockClient.Instance.Authentication.LoginWithDeviceAsync(deviceId);
```
If you'd rather not track which one to call, register first and fall back. Registering an
identity that already has an account does **not** throw — it returns `null` and leaves the
player signed out, so you must follow it with a login:
```csharp theme={null}
FlockAuthProvider auth = FlockClient.Instance.Authentication;
// null means "this device already has an account" — not an error, but not signed in either.
if (await auth.RegisterWithDeviceAsync(deviceId) == null)
await auth.LoginWithDeviceAsync(deviceId);
```
A successful `RegisterWith…Async` also signs the player in, so there is no second call on the
happy path. If you do get the order wrong, the exception says so and names the method to call
instead; see [Error handling](/sdk/unity#error-handling).
Each method is an async node under *Flock | Auth* with `On Success` and `On Failure` pins, or a
call on the auth provider in C++. The SDK exchanges the credential your platform SDK produced for
a Flock session.
```cpp theme={null}
UFlockSubsystem* Sdk = UFlockSubsystem::Get(this);
Sdk->GetAuthProvider()->LoginWithEmail(Email, Password,
[](TFlockResult Result)
{
if (Result.bSuccess) { /* Result.Value.PlayerId */ }
});
```
**Login and register are separate calls** — registering does not sign the player in, so follow it
with a login. Facebook and Discord are login-only; an account must already exist.
Registering an identity that already has an account is reported as a *success* with
`bAlreadyRegistered` set, so you can send the player to a login rather than show them a failure.
See the [Unreal SDK guide](/sdk/unreal#authenticate-a-player) for the full provider table.
## Sessions
The SDK keeps the player signed in and refreshes their session automatically — you never handle
tokens or expiry yourself. If a player needs to re-authenticate, the SDK raises
`FlockEvents.OnAuthExpired`.
# Remote Config
Source: https://docs.qwacks.com/guides/remote-config
Change game behavior and run experiments without shipping a build.
**Remote config** lets you control values and feature flags from the server, so you can tune your
game, gate features, and run experiments without a client update.
remote config is defined per **game version** and can vary by [segment](/guides/segments).
Manage config values and feature flags for a game version in the dashboard, and vary them by
segment.
Run **Code Generation → Sync Schemas** to turn each config into a typed C# class. The SDK
resolves the right values for the signed-in player — including any segment-based overrides:
```csharp theme={null}
var gameplay = await FlockClient.Instance.Config.GetGameplayAsync();
float speed = gameplay.BaseMoveSpeed;
```
The accessor name comes from your config (`Gameplay` → `GetGameplayAsync`). Fetch at startup and
read values instead of hardcoding them.
Values resolve **patch-first**: where a [patch](/guides/game-patches) applies to the running game
version you get the patched value, otherwise the config's own — your code never learns which.
Config reads are cached and served from disk when the server is unreachable, so a returning player
still gets your tuning values offline. The cache needs one successful online read to fill.
Code generation is the intended path here, not a convenience. The raw config getters on the
provider are `internal` — the generated typed accessors are the public API.
`Flock Resolve Config Data` returns the effective values for the running game version — the patch
where one applies, the config's own data otherwise. Your graph never learns which happened.
```cpp theme={null}
Sdk->GetConfigProvider()->ResolveConfigData(ConfigId,
[](TFlockResult Data)
{
int32 Damage = 0;
Data.Value.TryGetInt(TEXT("weapons.sword.damage"), Damage);
});
```
After a schema sync it is **one node** handing back a typed struct:
Config reads are cached and served from disk when the server is unreachable, so a returning player
still gets your tuning values offline.
## Patterns
* **Feature flags** — ship code dark, then enable it for a segment or everyone.
* **Tuning** — adjust drop rates, prices, or difficulty live.
* **Experiments** — serve variants to different segments and compare with
[analytics](/guides/analytics-and-events).
Always provide safe client-side defaults so your game stays playable if a config fetch fails.
# Segments
Source: https://docs.qwacks.com/guides/segments
Group players by rules to target config, content, and matchmaking.
A **segment** is a rule-defined group of players — "players above level 10", "spent in the last 7
days", "region = EU". Segments are the targeting primitive the rest of Flock builds on.
segments are defined per **game** and evaluated against the player profile + data.
Build rule-based segments in the dashboard by specifying criteria over player fields and data.
A player's segment membership is evaluated from their profile and player data, and the SDK
resolves it at runtime — so you can branch behavior or gate content based on the segments a
player belongs to.
## What segments power
Serve different config or feature flags to different segments.
Restrict a queue to eligible segments.
Keep segment rules stable and well-named — they become dependencies of config and matchmaking, so
renaming or deleting one can change live behavior.
# Sessions
Source: https://docs.qwacks.com/guides/sessions
One game: a host, its players and a join code.
A **session** is one game. One player **hosts** it and the others hold **seats**, up to 16 in all.
A player hosts a session and reads out its **join code**, or a [match](/guides/matchmaking) makes
one. A player holds one seat at a time: hosting or joining another session gives up the one held.
When the host leaves, the player seated longest becomes the host.
sessions belong to one **game**; a seat belongs to a player.
**Multiplayer → Sessions** lists the **Live Sessions** and the **Session History**. Each
session's details show its host and **Host changes**, its **Connection**, **Join code**,
**Participants**, and the **Relay credentials drawn** by its players. **Multiplayer settings**
on the same page set the seat count, timeouts and maximum age; see
[Multiplayer](/guides/multiplayer#setting-up).
Host a session, and show its code.
```csharp theme={null}
FlockMultiplayerProvider multiplayer = FlockClient.Instance.Multiplayer;
// 2 to 16 seats (the game's setting when not given), and the game's own data, set once.
FlockMultiplayerSession session = await multiplayer.HostSessionAsync(
4, new Dictionary { ["map"] = "crypt" });
joinCodeLabel.text = session.JoinCode;
```
A friend's game joins with the code; letter case does not matter.
```csharp theme={null}
FlockMultiplayerSession joined = await multiplayer.JoinSessionAsync(typedCode);
joined.TryGetData("map", out string map);
```
Follow the session, and leave it.
```csharp theme={null}
session.PlayersChanged += () => ShowPlayers(session.Players); // seated longest first
session.HostChanged += OnHostChanged; // start the netcode again: see Connecting players
session.Ended += reason => BackToLobby(reason);
await session.LeaveAsync(); // anyone
await session.EndAsync(); // the host only: ends it for everyone
await session.MakeHostAsync(otherPlayer); // the host only: hands hosting over
```
After the game restarts, find the seat the player still holds:
```csharp theme={null}
FlockMultiplayerSession held = await multiplayer.GetMySessionAsync(); // null when seated nowhere
```
To connect the players, see [Connecting players](/guides/connecting-players).
Sessions are not available in the Unreal SDK yet.
## Game versions
A session takes only players on the host's **Game Version** unless the host passes
`sameVersionOnly: false`. A player on another build is refused with `MultiplayerVersionMismatch`;
ask them to update.
## When a player goes quiet
The SDK sends a heartbeat every 20 seconds while the game runs, so a playing player never times
out. What happens when the game stops running:
| What happened | What Flock does | What players see |
| - | - | - |
| A player quits the game | Gives the seat up at once | `PlayersChanged` |
| A player's game crashes or loses its network | Gives the seat up after the **Participant timeout** (90 s) | `PlayersChanged` |
| A phone goes to the home screen | The game stops, so its heartbeats stop; after the **Participant timeout** the seat is given up | Back in the game, the session ends as `dropped` |
| The host leaves | Makes the player seated longest the host at once | `HostChanged` |
| The host's game crashes | Replaces the host after the **Host timeout** (60 s) | `HostChanged`, about a minute later |
| The game restarts within the timeout | Keeps the seat | `GetMySessionAsync` finds it |
## When a session ends
`Ended` is raised once with the reason; the server can add reasons, so keep a default:
| `FlockMultiplayerSessionEndReason` | When |
| - | - |
| `EndedByHost` | The host ended it. |
| `HostLeft` | The host left and nobody was seated to take over. |
| `Empty` | Everyone left. |
| `Expired` | It reached the **Session max age** (4 hours by default). |
| `Left` | The player left it. |
| `Dropped` | The player's seat timed out. |
| `MovedToAnotherSession` | The player hosted or joined another session. |
| `SignedOut` | The player signed out, or another player signed in. |
`Ended` is not raised when the game quits.
## Errors
| `ErrorCode` | When |
| - | - |
| `MultiplayerInvalidJoinCode` | No session has that code. |
| `MultiplayerSessionFull` | Every seat is taken. |
| `MultiplayerSessionNotFound` | No session the player holds or held has that id. |
| `MultiplayerVersionMismatch` | The player's build differs from the host's. |
| `MultiplayerNotHost` | A call only the host can make. |
# Shops & Inventory
Source: https://docs.qwacks.com/guides/shops-and-inventory
Sell items and track what each player owns.
Flock's economy has three pieces:
* **Shops** — collections of purchasable items.
* **Shop items** — individual offers with a price and currency.
* **Inventory** — a record of what each player has purchased.
shops and items are defined per **game version**; inventory is per player.
A purchase debits the player's wallet and records an inventory entry:
```
Shop item ──purchase──▶ wallet debit ──▶ inventory entry
```
Every purchase creates its own entry, so buying the same item twice gives you two. Each entry
carries a server-set `status` and `used_at`, both read-only — there is no call in either SDK that
marks an entry used.
**A purchase is money-safe.** It is never retried after an ambiguous failure and never queued
offline, so it cannot double-charge. Inventory is always fetched fresh; the catalog is cached and
served offline, so treat a displayed price as advisory until the purchase succeeds.
## The id a purchase takes
A purchase names a **shop item** by its id: the `id` of the item inside its shop. It is not the
item's name, and it is not the shop's id. Read it from the shop rather than typing it in: the SDK
lists the shops of the Game Version your build runs, each with its items. Unreal's generated shop
items carry the id already.
A shop belongs to one Game Version, so a shop made under another version is not listed and cannot be
found by name. If the list comes back empty, check the dashboard shows the shop under the Game Version
your build runs.
**A wallet is made when the player registers.** The server builds each player's wallet from the
Game Version's currency template at registration, with every currency starting at zero. Create the
currency template before your test player registers; a player who registered before it existed, or
under another Game Version, has no wallet to pay from. See
[Currencies & Wallets](/guides/currencies-and-wallets).
When a call fails, the SDK's text says what is wrong and how to fix it. A purchase given the item's
name instead of its id reads:
```
Shop item with ID 'GoldItem' not found [shop_item.shop_item_not_found, HTTP 404]
Fix: ... take the shop item's Id ..., not the item's name or the shop's id.
```
A `request.not_found` 404 whose reason is a bare `Not Found` means the request reached no route at all (any other
reason names a record the route did not find): the API URL must be the Flock
server's address alone, with no `/v1` or other path after it.
**Purchases grant their contents server-side.** Give a shop item a type of **Currency pack** or
**Consumable** and attach rewards to it, and the server credits the player as part of the same
transaction that debits the price. Neither half can happen without the other, and the client never
decides what was granted — so a modified client cannot award itself anything, and a dropped
connection cannot leave a player charged with nothing to show for it.
A **Standard** item is the older behaviour and still the default: it debits the price and records
an inventory entry your game interprets however it likes.
Configure shops and their items for a game version in the dashboard — set prices, currencies,
and availability.
Use the SDK's shop methods to list the shops and items available for your game version, then make
a purchase for the signed-in player. `PurchaseAsync` defaults to the signed-in player and returns
everything the purchase produced — what was granted, the wallet afterwards, and the inventory
entry if the item created one.
```csharp theme={null}
// The shop item's id from its shop: not the item's name, and not the shop's id.
Shop shop = await FlockClient.Instance.Shop.GetByNameAsync("Main Shop");
ShopItem item = shop.ShopItems.Find(i => i.Name == "Gold Pack");
PurchaseResult purchase = await FlockClient.Instance.Shop.PurchaseAsync(item.Id);
foreach (ShopItemReward reward in purchase.Granted)
Debug.Log($"+{reward.Amount} {reward.Code}"); // e.g. +500 Gold
PlayerData wallet = purchase.Wallet; // balance after the grant
PlayerInventory entry = purchase.Inventory; // null for a currency pack
PaginatedResponse owned =
await FlockClient.Instance.Shop.GetPlayerInventoryAsync();
```
A currency pack pays straight into the wallet and creates no inventory entry. A **Consumable**
does the opposite: it lands in the inventory and grants when the player spends it.
```csharp theme={null}
ConsumeResult consumed = await FlockClient.Instance.Shop.ConsumeAsync(entry.Id);
foreach (ShopItemReward reward in consumed.Granted)
Debug.Log($"+{reward.Amount} {reward.Code}");
```
`Granted` is never null — an item that grants nothing returns an empty list. To show what an item
will give before the player buys it, read `ShopItem.Rewards` from the catalog.
`PurchaseAsync` spends only game currencies from the player's wallet, so a purchase is paid at once.
An item priced in a currency your currency config lists as real money (for the Game Version picked in
Flock > Settings) is refused with `FlockErrorCode.ShopRealCurrencyNeedsCheckout`. A purchase or consume that reports a wallet also
writes it into the cached player data, so the next read shows the new balance.
A declined purchase throws — catch `FlockException` and check `ErrorCode` for
`FlockErrorCode.ShopInsufficientFunds` to tell "can't afford it" apart from a network failure.
Its `Message` says what went wrong and, on a `Fix:` line, how to fix it (`Hint` holds the fix
alone), so log it while you build the shop.
Consuming carries the same guarantee as buying: an ambiguous failure surfaces rather than being
retried, so a reward is never granted twice.
Browse with the *Flock | Shop* nodes, or the shop provider in C++. An item's free-form `data` and
`stats` read by dotted path, so no JSON parsing node is needed.
`Purchase` completes with everything the purchase produced — what was granted, the wallet
afterwards, and the inventory entry if the item created one.
`Purchase` spends only game currencies from the player's wallet, so a purchase is paid at once.
An item priced in a currency your currency config lists as real money (for the Game Version the
game sends) is refused with `EFlockErrorCode::ShopRealCurrencyNeedsCheckout`.
In C++, read the shop item's id from its shop: `Purchase` takes `FFlockShopItem::Id`, not the
item's name and not the shop's id.
```cpp theme={null}
Sdk->GetShopProvider()->GetByName(TEXT("Main Shop"),
[](TFlockResult Shop)
{
for (const FFlockShopItem& Item : Shop.Value.ShopItems)
{
// Item.Id is what Purchase takes; Item.Name is for showing the player.
}
});
```
```cpp theme={null}
Sdk->GetShopProvider()->Purchase(ItemId,
[](TFlockResult Bought)
{
if (!Bought.bSuccess)
{
if (Bought.Error.ErrorCode == EFlockErrorCode::ShopInsufficientFunds)
{
// The server declined — show the player, don't retry.
}
// What went wrong, then a "Fix:" line saying how to fix it.
UE_LOG(LogTemp, Warning, TEXT("%s"), *Bought.Error.ToDisplayText());
return;
}
for (const FFlockShopItemReward& Reward : Bought.Value.Granted)
{
// e.g. +500 GOLD. Ask the reward, never compare Type to a typed-out "currency":
// the server owns that set and can add kinds.
if (Reward.IsCurrency()) { /* Reward.Code, Reward.Amount */ }
}
// Both may legitimately be absent, so ask rather than testing the Id yourself.
if (Bought.Value.HasWallet()) { /* Bought.Value.Wallet — balances after the grant */ }
if (Bought.Value.HasInventoryRow()) { /* Bought.Value.Inventory — the row the player owns */ }
});
```
A currency pack pays straight into the wallet and creates no inventory entry — `HasInventoryRow()`
is false, which is the legitimate no-row case rather than an error. A **Consumable** does the
opposite: it lands in the inventory and grants when the player spends it.
```cpp theme={null}
Sdk->GetShopProvider()->Consume(InventoryId,
[](TFlockResult Consumed)
{
for (const FFlockShopItemReward& Reward : Consumed.Value.Granted) { /* +Amount Code */ }
});
```
In Blueprint these are **Flock Purchase** and **Flock Consume Inventory Item**. Break the purchase
result for `Inventory`, `Granted` and `Wallet`, and use the pure nodes rather than testing fields
yourself: *Has Inventory Row* before reading the row, and *Is Currency Reward* to branch on a reward
kind. Comparing a reward's `Type` to a hand-typed string is the one mistake worth guarding
against — a typo reads as "not a currency reward" and silently skips the grant. A purchase or
consume that reports a wallet also writes it into the cached player data.
`Granted` is never null — an item that grants nothing returns an empty list. To show what an item
will give before the player buys it, read `Rewards` from the catalog item.
A declined purchase fails the result rather than throwing — check `Error.ErrorCode` for
`EFlockErrorCode::ShopInsufficientFunds` to tell "can't afford it" apart from a network failure.
Consuming carries the same guarantee as buying: an ambiguous failure surfaces rather than being
retried, so a reward is never granted twice.
See [Currencies & Wallets](/guides/currencies-and-wallets) for the balances that back purchases.
# Flock
Source: https://docs.qwacks.com/introduction
The game backend that handles players, economies, and live operations — so you can focus on the game.
Flock is the game backend by **Qwacks**. It gives your game player accounts, save data,
shops and inventories, currencies and wallets, leaderboards, segments, remote configuration,
and analytics — all through native SDKs that handle the backend for you.
Create a game, add the SDK, and sign in your first player in minutes.
How the SDK signs your game in and manages player sessions for you.
Drop Flock into a Unity project with the official C# SDK.
Drop Flock into an Unreal project — every feature works from Blueprint or C++.
Players, economy, live ops, and more.
## How it fits together
A **studio** owns one or more **games**. Each game has **versions** you publish over time, and
**players** who sign in and accumulate data, inventory, and currency. Your game integrates the
Flock **SDK**, configured once with your game's credentials; players then sign in through the SDK
to get their own profile, save data, and inventory.
Email, device, Google, Apple, and Steam sign-in.
Shops, items, inventory, currencies, and wallets.
Remote config, segments, leaderboards, and analytics.
Configure games and grab credentials in the [Flock dashboard](https://flock-saas.qwacks.com). The
SDK handles everything else — see the [Quickstart](/quickstart).
# Getting Started
Source: https://docs.qwacks.com/protokite/getting-started
Protokite — QA and playtesting for your games.
**Protokite is currently under development!** The Unity package and the Unreal plugin below ship today; the
rest of the documentation is on the way. Reach out to the Qwacks team for early access.
Protokite is the Qwacks platform for **QA and playtesting** — manage game builds, run QA sprints,
and stream live playtests to your team.
## Collect from your game
An optional package turns a build of your game into a playtest build: one session per launch, plus the
video, performance windows, exceptions and feedback forms the playtest asks for.
Install it from the Flock SDK's Playtesting tab, point the build at your playtest, and press Play.
Video, performance windows, exceptions, feedback forms, and the C# calls for each.
Install it, point the build at your playtest, and watch the sessions arrive.
Video, performance windows, exceptions, feedback forms, and the Blueprint nodes for each.
Check back here as we roll out the rest of the guides. In the meantime, explore the
[Flock](/introduction) documentation to see how the Qwacks SDKs work.
# What a playtest collects
Source: https://docs.qwacks.com/protokite/unity-features
Video, performance windows, exceptions and feedback forms from a Unity playtest build, and the C# calls for each.
Each of these runs only while the playtest turns it on in Protokite **and the player has allowed it**. None
of them needs code: the C# calls on this page are for a game that wants its own menus or buttons around
them, and they are safe in every build — with no playtest running they answer false, null or `TurnedOff`.
Only three work without one: a test video, the player's consent answer, and the Steam ID, which is kept for
the session to come. Setting the build up is on the [Unity package](/protokite/unity-package) page.
## What the player allowed
The build asks its player what this playtest may collect before it collects anything, and their answer
decides what the rest of this page does:
| Their answer | Video recording | Heavy analytics |
| - | - | - |
| Record my screen and collect play data | Runs | Runs |
| Record my screen only | Runs | Off |
| Collect play data only | Off | Runs |
| Collect nothing | Off | Off, and no session is started at all |
`ProtokitePlaytest.IsFeatureEnabled` already answers for both at once: a feature the playtest turns on and
the player left out reads as off, everywhere. Exception capturing follows play data there, but
[exceptions](#exceptions) themselves are the Flock SDK's, reported with its own setting whatever the player
answered.
```csharp theme={null}
if (ProtokitePlaytest.IsFeatureEnabled(ProtokitePlaytestFeatures.VideoRecording))
ShowTheRecordingIndicator();
```
**The feedback form is not covered by that answer.** It is sent only when a player fills it in and presses
Send, which is their own doing either way — and a form an earlier launch could not send is still sent
later, whatever they answered since.
### On a phone: which networks recordings upload on
On an Android build that records video, an answer that lets the screen be recorded is followed, on the same panel, by a second
question: **Upload on Wi-Fi only**, or **Upload on Wi-Fi or mobile data**. The second answer says about how much mobile data
a minute of play uses at the recording's bitrate (about 11 MB at the default 1500 kbps).
The answer decides only when recordings upload, never whether the screen is recorded. A Wi-Fi only player's recordings wait on
the phone and upload the next time it is on Wi-Fi, in the same launch or a later one, and an upload under way stops when the
phone leaves Wi-Fi and is sent again, from the start, on Wi-Fi. Any network that is not the phone's mobile data (Wi-Fi,
Ethernet, tethering) counts as Wi-Fi.
The answer is kept on the phone beside the consent answer and used by every later launch. The Protokite session starts once it
is given, so each session start carries `playtest_upload_network`: `wifi_only`, `wifi_and_mobile_data`, or `not_asked` (a
desktop, a build that asks nobody, or an answer that records nothing). The screen is recorded meanwhile; a game closed while the
question is still open has no session for those seconds, and the next launch deletes them. Nothing is asked on Windows, and
nothing waits there.
```csharp theme={null}
ProtokitePlaytest.PlayersUploadNetworkAnswer // WiFiOnly, WiFiAndMobileData, or NotAnswered
ProtokitePlaytest.SetPlaytestUploadNetwork(ProtokitePlaytestUploadNetworkChoice.WiFiOnly); // from your own menu
```
## Video recording
The game's screen is recorded from the moment the playtest loads and the player has allowed it, before
anyone signs in: one recording per launch, encoded as H.264 by the graphics card's own video encoder (the phone's, on Android) and
written as it records into an MP4 file a browser plays with nothing installed. A recording cut off by a crash
keeps every frame before the cut. Time the game spends in the background is left out.
The settings are in **Protokite → Playtest → Settings**: the first four under **Video recording (64-bit Windows)**, which a
Windows build and the editor read, and the last three under **Recordings (every platform)**. Android players read their own
section, **Video recording (Android)**; see [Recording on a phone](#recording-on-a-phone).
| Setting | Default | What it does |
| - | - | - |
| Video Width, Video Height | 1280, 720 | The largest the video is. The screen's shape is kept, a smaller window is not enlarged, and each side is rounded down to a multiple of 16 |
| Video Frames Per Second | 15 | Frames recorded each second of play |
| Video Bitrate Kbps | 1500 | The most the video uses; a still picture uses far less |
| Allow Software Encoder | off | On: a PC whose graphics card has no H.264 encoder records with Windows' own encoder on the processor instead, which costs the game frame rate (measured: about 30% of a processor core on a slow laptop, 11% on a desktop) |
| Max Recording Minutes | 60 | A recording stops for good after this many minutes of play |
| Max Recording Size Mb | 1536 | A recording stops for good before its file passes this size |
| Recordings Disk Budget Mb | 4096 | The most every recording kept on the machine may take together |
The frame is copied, scaled and converted on the graphics card, read back without waiting for it, and
encoded on the graphics card too; handing frames to the encoder and writing the file each run on a thread of
their own, and a frame that cannot keep up is dropped rather than stalling the game. When a recording ends,
the Console says what it holds, which encoder made it, and how many frames were dropped, and why.
### When a recording is uploaded
A recording is uploaded to its session once its file is finished **and** the session has started,
whichever comes second. A file is finished when:
* it reaches **Max Recording Minutes** or **Max Recording Size Mb**;
* the player presses **Upload your recording** on the feedback form, or your game calls
`ProtokitePlaytest.StopRecordingAndSendIt()`;
* your game stops it for good with `ProtokitePlaytest.StopVideoRecording()`.
On a phone whose player chose Wi-Fi only, it also waits until the phone is on Wi-Fi (see
[On a phone: which networks recordings upload on](#on-a-phone-which-networks-recordings-upload-on)).
It is sent straight from disk, never whole in memory, and counts as uploaded only when the storage accepts the
file itself. A failed upload is tried once more. An uploaded recording is deleted from the machine; one that
was not is kept with its session, and the Console says why.
**Quitting does not upload.** A whole recording cannot be sent while the game closes, so a recording still
going at quit is finished and kept, and the next launch sends it. A later launch sends every recording an
earlier one kept — a failed upload, a quit, a crash — oldest first, **even with Playtesting Enabled off**, so a
release build never strands what a playtest build recorded. A recording no session ever started for is
deleted, since it can never be sent.
Offer the player a button of your own only while `ProtokitePlaytest.CanSendTheRecording` is true. It is false
when no session has started for the recording, and a button pressed then would stop the recording and send
nothing:
```csharp theme={null}
if (ProtokitePlaytest.CanSendTheRecording && GUILayout.Button("Send my recording"))
ProtokitePlaytest.StopRecordingAndSendIt();
```
### Room on the player's disk
**Recordings Disk Budget Mb** (on Android, **Android Recordings Disk Budget Mb**) is the most every recording kept on the
machine may take together. Before a recording starts, recordings earlier launches left are deleted until it fits: test videos
first, then recordings waiting to upload, the oldest first. One being uploaded, or still held by a copy of the game that is
running, is never deleted. A recording makes room only for what its length limit records at its bitrate, with a quarter to
spare (about 810 MB at the defaults), so a short recording never deletes one it would fit beside. With less than 1 MB left,
that launch records no video, and a warning names the setting to raise.
**While a phone's uploads wait on the player's answer** (Wi-Fi only and the phone off Wi-Fi, or the question still to be
answered), a recording waiting to upload is never deleted for a new one: the new one records into the room left, and with less
than 1 MB records nothing that launch and the Console says why. At the default 1024 MB, about 94 minutes of play can wait for
Wi-Fi in full. Recordings also never take a phone's free space below the line where Android warns that storage is running out
(500 MB, or 5% of the storage when that is less): a fuller phone gives a recording less room.
A recording cut off by a crash, a power cut or a killed game is finished by the next launch with every whole
frame it holds, then uploaded to its session like any other; one whose session never started is deleted.
### Recording on a phone
Android players read their own section of **Protokite → Playtest → Settings**, **Video recording (Android)**:
| Setting | Default | What it does |
| - | - | - |
| Record Video On Android | on | Off: Android players record no video and never ask the phone for its encoders; everything else in the playtest still runs. Read as the game starts, so turning it on mid-launch takes effect from the next launch |
| Android Video Long Side | 1280 | The video's longer side, whichever way the phone is held: on a 20:9 phone, 1280x592 sideways and 592x1280 upright. A smaller screen is not enlarged, and each side is rounded down to a multiple of 16 |
| Android Video Frames Per Second | 15 | Frames recorded each second of play; 30 doubles the phone's encoding work and the frames copied from the screen |
| Android Video Bitrate Kbps | 1500 | Held as a constant rate where the phone's encoder takes one; on one that does not, a busy picture can run over |
| Android Allow Software Encoder | off | On: a phone with no hardware H.264 encoder records with Android's software encoder on the processor instead, which costs frame rate and battery |
| Slow Down The Recording When The Phone Is Hot | on | While Android says it has started slowing the phone for heat, the recording takes half its frame rate; once Android says the slowing is enough for the player to notice, the recording stops for the launch. Phones before Android 10 report no heat, and record as set |
| Stop The Recording Below Battery Percent | 15 | The recording stops for the launch below this battery level while the phone is not charging. 0: never |
| Android Recordings Disk Budget Mb | 1024 | The most every recording kept on the phone may take together, in place of **Recordings Disk Budget Mb** |
A recording stopped for heat or the battery is kept and uploaded like any other. **Max Recording Minutes** and **Max Recording
Size Mb** apply on every platform. A phone whose encoder does not take the video's size or rate records at the next size down it
does, then at 15 and 10 frames a second, and the Console says which.
When the game goes to the background, everything the recording holds is written to its file, so a game Android ends while it is
away keeps every frame recorded before it left. The phone's encoder is given back meanwhile, and the first frame after the return
starts again on a keyframe in the same file.
## Heavy analytics
The playtest sends these through the Flock SDK's analytics, under the `playtest` category, and they read on
**Dashboards → Game Metrics**:
* `performance_window`, for every ten seconds of play: the median, 95th and 99th percentile frame time,
`hitches` (frames that took **Hitch Frame Time Ms**, 60 by default, or longer), memory used and at its
peak, and `map`, the active scene. A frame time is the real time from one frame to the next, so slow
motion or a paused game is measured as the frames the player saw.
* `level_loaded`, for every scene the game loads in place of the one before, with the scene before it and
`load_seconds`, how long the load held the game up.
* **Your own events**, on the same timeline and in the same category:
```csharp theme={null}
ProtokitePlaytest.RecordPlaytestEvent("boss_fight_started",
new Dictionary { { "boss", "hydra" } });
```
`RecordPlaytestEvent` answers true when the event was queued, can be called from any thread, and sends only
while heavy analytics runs, which starts once the playtest has loaded; an event recorded earlier in the launch
answers false and is not kept. The two names the playtest sends itself are refused.
Heavy analytics needs the Flock SDK's **Analytics Enabled**: with it off, nothing is measured and the Console
says so once. The events are Flock events like any other, so they wait for a signed-in player and follow the
Flock SDK's own analytics consent. See [Analytics & events](/guides/analytics-and-events) for how the category
and the dashboards fit together.
Played offline, six windows a minute fill the Flock SDK's offline queue (**Analytics Max Cached Events**, 1000
by default) in under three hours. Raise it for playtests your testers play offline.
## Exceptions
Exceptions stay the Flock SDK's. It captures the game's exceptions from every thread, and faulted tasks nobody
awaited, with **Analytics Capture Exceptions** in **Flock → Settings → Advanced Settings** (on by default), and
reports them on **Diagnostics → Errors**. A playtest's exception switch never turns that on or off. With the Flock SDK's
switch off, the live self-test's exception step is skipped and names it; the SDK's
`Analytics.GetExceptionCaptureCoverage()` says what the capture hears.
The same exception thrown again and again reaches the dashboard as one report, then one repeat report counting
the others once the repeat window (**Analytics Exception Repeat Window**, 60 seconds) closes. `Debug.LogError`
lines are not exceptions and are not reported.
## The feedback form
When the playtest publishes a feedback form in Protokite, the player opens it over the game with **F9** and
fills it in. It is built from what you published — every question, label, help text and option — so editing
the form in Protokite needs no new build.
| Setting | Default | What it does |
| - | - | - |
| Feedback Form Key | F9 | Opens the form and closes it again. Press **Detect Key** and then the key you want, or pick it from the list. **None** leaves opening it to your game |
| Pause The Game While The Form Is Open | off | Sets the time scale to 0 while the form is open, and puts it back when it closes |
* **Send checks the answers the way Protokite does**, and shows every problem against its question before
anything is sent: a required question left empty, a rating out of range, an option the question does not
offer.
* **A sent form is kept on the device first and sent from there**, so neither a closed game nor a lost network
loses it: one that could not go now is sent when the network comes back, two minutes later, or by a later
launch. Sending again in the same session replaces the earlier answers.
* Closing it without sending keeps what was typed for the rest of the launch.
* **Upload your recording** appears while this launch records the screen and its session has started: it stops
the recording and sends it now. Opening the form never stops the recording on its own.
**Your game still reads its own keys while the form is open.** A game that moves on WASD, or opens a menu on
Escape, should ignore its input while `ProtokitePlaytest.IsFeedbackFormOpen` is true, or turn on **Pause The
Game While The Form Is Open**.
Open it from your own pause menu, and leave that entry out when the playtest published no form:
```csharp theme={null}
if (ProtokitePlaytest.CanOpenFeedbackForm && GUILayout.Button("Give feedback"))
ProtokitePlaytest.OpenFeedbackForm();
```
### A form of your own
`ProtokitePlaytest.FeedbackForm` hands your UI the questions: each field's `Id`, `Type`, `Label`, `HelpText`,
`Options` and whether it is `Required`. Answer them, check them and send them through the same checking and
keeping as the built-in form:
```csharp theme={null}
ProtokitePlaytestFormAnswers answers = new ProtokitePlaytestFormAnswers();
answers.SetRating("rating", 4);
answers.SetChosenOption("category", "Bug");
answers.SetText("title", "Fell through the floor");
answers.SetChecked("contact", false);
foreach (ProtokitePlaytestFormProblem problem in answers.FindProblems(ProtokitePlaytest.FeedbackForm))
Debug.Log(problem.FieldId + ": " + problem.Message);
ProtokitePlaytest.SendFeedbackForm(answers); // false, with a warning, when there are problems
```
Compare a field's `Type` with the `ProtokitePlaytestFormFieldTypes` constants rather than typing it, and treat a
type you do not know as text, which is how the server reads it.
Two rules are easy to miss. **Set every checkbox**, ticked or not: the server counts an unticked box as an
answer, and a needed checkbox that was never set is missing. And **a chosen option must match letter for
letter** — the server compares options exactly, so `bug` is not `Bug`.
## Calls your game can make
All on `ProtokitePlaytest`, in the `Protokite.Playtest` namespace. Call them from the main thread;
`RecordPlaytestEvent` also works from any other.
| Call | Answers or does |
| - | - |
| `Status`, `Describe(status)`, `Config` | Whether the playtest can run, why not, and what it turns on |
| `IsFeatureEnabled(name)`, with `ProtokitePlaytestFeatures.VideoRecording` / `HeavyAnalytics` / `ExceptionCapturing` | Whether a feature runs, the player's answer included |
| `PlaytestConsent`, `PlayersConsentAnswer`, `Describe(answer)` | What the player let this playtest collect |
| `SetPlaytestConsent(choice)`, `AskForPlaytestConsent()`, `IsConsentQuestionOpen` | Answering from your own screens, and asking again |
| `PlayersUploadNetworkAnswer`, `SetPlaytestUploadNetwork(choice)` | On a phone, which networks recordings upload on |
| `PlaytestSessionId` | The launch's Protokite session, once started |
| `SetSteamId(id, name)` | Names the player by their Steam ID instead of the device ID, before the session starts |
| `RecordPlaytestEvent(name, properties)` | Records one of your own playtest events |
| `IsRecordingVideo`, `StopVideoRecording()` | The launch's recording |
| `CanSendTheRecording`, `StopRecordingAndSendIt()` | Sending the recording now |
| `CanOpenFeedbackForm`, `OpenFeedbackForm()`, `CloseFeedbackForm()`, `IsFeedbackFormOpen` | The built-in form |
| `FeedbackForm`, `SendFeedbackForm(answers)` | A form of your own |
| `RecordTestVideo(seconds, out whyNot)`, `TestVideoState`, `FinishedTestVideoPath`, `TestVideoProblem` | A test video, never uploaded |
The package's sample, `Samples/PlaytestSample/ProtokitePlaytestSample.cs`, puts every one of these on one
screen: add it to a GameObject and press **Play**.
## Trying it in the editor
The setup window, **Protokite → Playtest → Setup Checks And Test Video**, has everything for testing in its
lower half. In **Play Mode** it looks like this:
* **Forget This Machine's Answer** forgets this machine's answers, so the next Play asks the consent question again.
* **Open Feedback Form** opens the form the way its key does.
* **Record Test Video** records the Game view for the seconds you set, with the video settings and **no
playtest needed**: playtesting off and nobody signed in is fine. A test video is saved under
`ProtokitePlaytest/Recordings/TestVideos/` in the game's persistent data folder, never uploaded, and the window
shows where it went. It is refused while the playtest's own recording runs, and gives way to it. Like all
video, it records on 64-bit Windows and Android; in the editor it uses the Windows settings.
* **Run Live Self-Test** checks the whole playtest against your real Protokite (below).
A game can record a test video itself, from a debug menu's "record a clip" button for instance:
```csharp theme={null}
if (!ProtokitePlaytest.RecordTestVideo(30.0, out string whyNot))
Debug.Log("No test video: " + whyNot);
```
## Checking a playtest build
The live self-test checks everything a playtest build does against your real Protokite in one go, and logs a
line per step and a count at the end. Run it in Play Mode with **Run Live Self-Test**, or from code in the
editor or a development build (a release build refuses it):
```csharp theme={null}
ProtokitePlaytestSelfTestReport report = await ProtokitePlaytestSelfTest.RunAsync();
Debug.Log($"{report.Passed} passed, {report.Failed} failed, {report.Skipped} skipped");
```
Each check sits beside a request Protokite must refuse: a wrong API key, a missing one, a version no playtest is
linked to, a session start that names no player, a form missing a needed answer or choosing an option that is
not on the list, and a form, an upload link and an end for a session that does not exist. A check that only
ever succeeds cannot tell a working build from a broken one, so a refusal passes only with its own HTTP status
and, for a form, the question it names.
* **Sign a player in first, and answer the consent question.** It signs nobody in; steps that need a session or
an answer are skipped, saying why.
* **Run it in a Play or launch of its own.** It stops this launch's recording to upload it, sends a filled-in
form that replaces one the player sent this launch, and raises two exceptions on purpose — so with **Error
Pause** on in the Console, Play pauses there until you resume it.
* **It leaves a trace on your dashboards:** one filled-in form on the session, one `playtest_self_test` event,
one exception raised twice so its repeat is counted, and the launch's recording. Each carries the run's id
(`report.RunId`), so you can find them in Flock and Protokite.
* **A step is skipped, saying why,** when the playtest does not turn its feature on, when the build has no video
encoder (every platform but 64-bit Windows and Android), and for a closed playtest unless you name one: pass a closed
playtest's Game Version ID to `RunAsync`, or type it into **Closed Playtest Version ID** in the window.
## What stays on the player's machine
Under `ProtokitePlaytest/` in the game's persistent data folder (`Application.persistentDataPath`):
| Path | Holds | Until |
| - | - | - |
| `Recordings/Playtest/` | Playtest recordings, each in a folder with the session it belongs to | Uploaded, or deleted for disk room, oldest last |
| `Recordings/TestVideos/` | Test videos | Deleted for disk room, oldest first |
| `FeedbackForms/` | Answers that could not be sent yet, with no API key in them | Sent, or refused by the server |
| `device_id.txt` | This install's device ID, when no Steam ID is set | Kept, so one install stays one player |
| `playtest_consent.json` | What the player let this playtest collect | Kept, so they are asked once and not every launch |
| `playtest_upload_network.json` | On a phone, which networks recordings may upload on | Kept, like the consent answer |
## Platforms
Everything on this page runs wherever the Flock SDK runs, except video.
**Video is recorded on 64-bit Windows**, in the editor and in players, Mono and IL2CPP alike, through
Windows' own H.264 encoder on the graphics card. The package ships no native file, so Windows 11's Smart App
Control has nothing of the package's to refuse. A PC whose graphics card has no H.264 encoder records no video
and says why once in the Console, unless **Allow Software Encoder** is on; so does a Windows N edition without its
Media Feature Pack. The setup window names the encoder Windows offers first on your own PC, and a test video shows
whether it records. Recording runs on Direct3D 11
and 12, Vulkan and OpenGL, in the Built-in Render Pipeline and URP, in Linear and Gamma colour, and needs a
graphics card that runs compute shaders.
**On Android** the screen is recorded with the phone's own hardware H.264 encoder, so the package ships no native file there
either. It is measured on one flagship phone (a Galaxy S23 Ultra), so treat its figures as a best case. Against the same game
built without the package, drawing 400 moving lit cubes at the phone's full resolution at 60 frames a second, a recording at the
defaults cost **no frame rate** in IL2CPP (64-bit) or Mono (32-bit) players, and collecting play data alone cost nothing
measurable. What recording does cost sits on the one frame in four that is captured: about 1 to 3.5 ms more of the graphics
card's time and up to 2 ms more of the render thread on those frames, and about 6% of a processor core in the phone's media
service; a Mono build's main thread also spends about 3 ms more on them. The package adds about 7 MB to an IL2CPP build and
1.4 MB to a Mono one. Android players read their own settings (see [Recording on a phone](#recording-on-a-phone)). A phone whose
graphics cannot run compute shaders, or that has no hardware H.264 encoder, records no video and says why once. Unity builds
Mono for 32-bit ARM only; a 64-bit build is IL2CPP. x86 Android devices are not measured.
**"No video on this platform"** means exactly that and nothing more: a build for any other platform — macOS,
Linux, iOS, WebGL, or 32-bit or ARM64 Windows — has no video encoder, says once in the
Console that it records no video, and runs everything else: the session, the consent question, heavy analytics,
exceptions and the feedback form. The setup window tells you in advance: "Players built for … record no video". A
game that draws nothing (a server build, or one started with `-nographics` or `-batchmode`) records nothing
either, and cannot show the consent question: answer it from code with `SetPlaytestConsent`, or turn asking
off.
**On WebGL** the playtest's files — the consent answer, the device ID and feedback forms waiting to be sent — are
copied to the browser's storage after every change, so they are there on the player's next visit wherever the
browser keeps the site's data. A private window forgets them when it closes.
# Unity package
Source: https://docs.qwacks.com/protokite/unity-package
Turn your Unity game into a playtest build for Protokite, step by step.
Protokite Playtest is an optional Unity package that turns a build of your game into a **playtest build**.
While a playtest runs, every launch of the game becomes one session on the playtest's **Sessions** page,
and the playtest decides what that session collects. It needs no code: install it, point it at your
playtest, sign a player in, and press **Play**.
The Flock SDK's **Playtesting** tab installs it in one click. `ProtokitePlaytest-.unitypackage` is
also attached to every Flock Unity SDK release, at the same version.
## What a playtest collects
| Playtest feature | What the build sends | Where it shows |
| - | - | - |
| Always | One session per launch, named after the player's Steam ID or this install's device ID | Protokite **Sessions** |
| Video recording | A recording of the game's screen, uploaded when it ends | The session's recording player |
| Heavy analytics | A performance window every ten seconds of play, each scene load, and your own playtest events | Flock **Dashboards → Game Metrics**, under the `playtest` category |
| Exception capturing | Nothing extra — the Flock SDK reports exceptions with its own setting, whatever the playtest turns on | Flock **Diagnostics → Errors** |
| A published feedback form | The player's answers | The session card, and the playtest's **Feedback form** page |
Every one of them is switched on the playtest's page in Protokite, so changing what a playtest
collects needs no new build — and the player is asked first, so a launch collects only what they
allowed. See [Press Play](#step-5-press-play). Each feature is described on
[What a playtest collects](/protokite/unity-features).
## Before you start
* **The Flock SDK is set up and working** in the project: **Api Url**, **Api Key**, **Game Name** and
**Game Version** in **Flock → Settings**, with a **Resolved Version ID** filled in. See the
[Unity SDK](/sdk/unity) page.
* **A playtest exists in Protokite.** Creating one makes a Flock game version for it, named `pt-`
followed by the test's ID.
* **Video is recorded on 64-bit Windows and Android.** Every other platform runs the rest of the playtest and
says once in the log that it records no video. See [Platforms](/protokite/unity-features#platforms).
## Step 1: Install the package
In the Unity menu bar, choose **Flock → Settings** and select the **Playtesting** tab. It names the
version it will install, which always matches your Flock SDK.
It downloads the package from the Flock SDK's release and imports it into `Assets/ProtokitePlaytest`.
No Git is needed. Unity compiles it, and the tab then shows the installed version with two buttons you
use in the next steps: **Open Playtest Settings** and **Check Playtest Setup**.
Two other ways in, both at the **same version as your Flock SDK**: double-click
`ProtokitePlaytest-.unitypackage` from the release page, or in **Window → Package Manager → + → Add
package from git URL** enter
`https://github.com/QwackStack/FlockUnitySDK.git?path=/ProtokitePlaytest~#v` (this route needs Git
installed). Leave the package out of projects that are not running playtests.
## Step 2: Point the build at the playtest
Protokite finds the playtest from the playtest's own Flock version. The build sends it to Protokite only: your
**Game Version** stays your game's, with everything you set up under it in Flock.
Choose **Protokite → Playtest → Settings**, or press **Open Playtest Settings** on the Playtesting tab.
The first time, this creates `Assets/Resources/ProtokitePlaytestSettings.asset` with playtesting off and
selects it in the Inspector. Tick **Playtesting Enabled**.
**Protokite Api Url** is already set to production, `https://api-protokite.qwacks.com`; change it only to
point at a local Protokite such as `http://localhost:8020`. The URL must start with `http://` or `https://`
and contain no spaces; one that does not is refused rather than tidied up.
Open the test in Protokite and copy its ID: the Flock version ID its **SDK** block shows, or the test's own
ID from the page's address (after `/tests/`). Paste either into **Playtest ID** in the playtest settings.
The editor finds the playtest's version through Flock, shows it under **Resolved Version ID**, and says
below it what Flock found.
**Your Game Version stays your game's.** Only the playtest's own requests to Protokite carry the playtest's
version, so your player templates, configs and shop keep working in a playtest build.
**Leave Playtest ID empty and a build joins your game's newest playtest**, whichever is newest in Protokite when
the build starts, even one you have closed. A playtest created later is joined from the next launch, never in
the middle of one. Fill in Playtest ID to choose. A **Game Version** that is itself a playtest's name, `pt-`
followed by the test's ID, still joins that playtest, as before 1.68.0.
A Playtest ID that has not been resolved leaves playtesting off, and a build with **Playtesting Enabled** on is
refused until it is. Open the playtest settings while Flock can be reached; **Resolve Again** asks again.
## Step 3: Check your setup
Choose **Protokite → Playtest → Setup Checks And Test Video**, or press **Check Playtest Setup** on the
Playtesting tab. The window checks what a build of this project needs for a playtest. A check that passes
has a green tick and says so:
| The window says | Once |
| - | - |
| Playtesting is turned on | The playtest settings exist and **Playtesting Enabled** is on |
| Protokite API URL is usable | The URL is an `http` or `https` address with a host and no spaces |
| Playtest ID chooses this build's playtest | **Playtest ID** is resolved to a playtest's version |
| The game's newest playtest | **Playtest ID** is empty, and a build joins the game's newest playtest |
| Game Version is a playtest's version | With **Playtest ID** empty: Flock's **Game Version** is a playtest's name, `pt-`, resolved to the ID a build sends |
| Players built for this platform record video | The build target is 64-bit Windows (x64), or Android with **Record Video On Android** on |
A check that fails has a red mark, says what to change, and has a button that opens the place to change it:
**Open Playtest Settings**, **Open Flock Settings**, **Open Build Settings**, **Resolve Playtest ID**, or **Set Game
Version To pt-…** when Playtest ID is empty and you pasted the playtest's ID into Game Version. The Playtest ID, or with
it empty the Game Version, is checked with Flock when the window opens and whenever the settings change; **Check Again**
asks again.
"Players built for … record no video" does not stop the playtest: a build for another platform runs
everything else and records no video.
## Step 4: Sign a player in
**A playtest never signs a player in.** Its session starts once a Flock session reaches the server, and a
Flock session starts when your game signs a player in, with **Analytics Enabled** and **Analytics Auto
Start Session** on in **Flock → Settings → Advanced Settings** (or when you call `StartSessionAsync`), and
consent given when **Analytics Require Explicit Consent** is on.
A device sign-in is enough and needs no account:
```csharp theme={null}
await FlockClient.Instance.Authentication.LoginWithDeviceAsync(SystemInfo.deviceUniqueIdentifier);
```
The first time a device signs in, register it with `RegisterWithDeviceAsync` instead; the
[Unity SDK](/sdk/unity#authenticate-a-player) page covers sign-in in full. The package's sample has a
**Sign In With This Device** button that does both, for a build with no sign-in screen of its own.
## Step 5: Press Play
**The build asks its player what the playtest may collect, and collects nothing until they answer.** The
question is drawn over the game as soon as this build's playtest has loaded, and offers four answers:
record the screen and collect play data, the screen only, play data only, or **nothing at all**, which
leaves the build behaving as one with **Playtesting Enabled** off.
It is answered with the mouse, or with a tap on a phone. While it is on screen the cursor is shown and free, and it goes back to how
your game had it once the player answers. Nothing is selected when it appears, so your game's own Submit key
presses no answer, and a click in the question's first half second is ignored.
**It is the playtest's own question.** The Flock SDK's **Analytics Require Explicit Consent** is a different
one, asked by your game in your game's words about your game's analytics. Neither answer moves the other, and
the question says so in front of the player.
The answer is kept on the player's machine and used by every later launch, and each session start carries
`playtest_consent` and `playtest_consent_asked`, so a session with no recording reads as a player who asked
for none rather than a build that went wrong. To be asked again in the editor, press **Forget This
Machine's Answer** in the setup window.
**On a phone, a second question follows** an answer that lets the screen be recorded: whether recordings may upload on mobile
data or wait for Wi-Fi. See [On a phone: which networks recordings upload on](/protokite/unity-features#on-a-phone-which-networks-recordings-upload-on).
Your game can take the question over from its own menus:
```csharp theme={null}
ProtokitePlaytest.PlaytestConsent // the answer in force
ProtokitePlaytest.SetPlaytestConsent(ProtokitePlaytestConsentChoice.VideoOnly); // answer it; NotAnswered asks again
ProtokitePlaytest.AskForPlaytestConsent(); // put the question on screen again
ProtokitePlaytest.IsConsentQuestionOpen // while it is on screen
```
Turn **Ask The Player For Playtest Consent** off in the playtest settings only where your players are
asked another way, or for a test run with nobody there to answer; the build then collects what the
playtest turns on, and every session says the player was never asked. An answer a player has already given
still counts.
### Watch it run
The Console says what the playtest is doing, each line starting `[Protokite Playtest]`:
```
[Protokite Playtest] The playtest's consent question is on screen; nothing is collected until the player answers it.
[Protokite Playtest] Playtesting is ready: playtest 01JABCDEFGHJKMNPQRSTVWXYZ0 is loaded, with features exception_capturing, heavy_analytics, video_recording and a feedback form.
[Protokite Playtest] Recording video for the playtest to …/recording-….mp4.part, at 1280x720 and 15 frames a second, as H.264. It stops for good after 60 minutes of play, before the file passes 1536 MB, or when the game stops it.
[Protokite Playtest] Protokite session 01J… started for this launch, with the player's device id and Flock session 01J….
```
The session then appears on the playtest's **Sessions** page. It ends when the game quits; quitting waits
up to 3 seconds for Protokite to take the end. A sign-out, a new Flock session or restarting the Flock SDK
neither ends nor restarts it: a launch is one session. In code, `ProtokitePlaytest.Status` answers the
same question the log does, and `ProtokitePlaytest.Describe(status)` gives the log's words.
## When something stops the playtest
The Console says why and what to change, once, as a warning.
| Status | Meaning | Fix |
| - | - | - |
| `TurnedOff` | **Playtesting Enabled** is off, or the project has no playtest settings | Turn it on in **Protokite → Playtest → Settings** |
| `ProtokiteApiUrlMissing` | The Protokite API URL is empty | Set **Protokite Api Url** (production is `https://api-protokite.qwacks.com`) |
| `ProtokiteApiUrlUnusable` | No `http://` or `https://`, no host, or a space in it | Correct the URL; it is never trimmed for you |
| `WaitingForFlock` | Everything is set; the Flock SDK has not started yet | Nothing — it goes on once the Flock SDK starts |
| `FetchingPlaytestConfig` | Asking Protokite for this build's playtest | Nothing |
| `PlaytestNotLinked` | Protokite has no playtest for this build: none for the version it asks for, or, with Playtest ID empty, none in the game | Paste the playtest's ID into **Playtest ID**, or create a playtest |
| `PlaytestIdNotResolved` | **Playtest ID** is set but not resolved to a playtest, so none is asked for | Open the playtest settings while Flock can be reached |
| `ProtokiteRefusedApiKey` | Protokite turned down the Flock API key | Check **Api Key** in **Flock → Settings** |
| `PlaytestConfigUnavailable` | Protokite or the network kept failing | Nothing — it is asked again when the next Flock session starts |
| `PlaytestConfigForAnotherVersion` | Protokite answered with another version's playtest, as a proxy dropping headers does | Check what sits between the game and Protokite |
| `PlaytestNoLongerCollecting` | The playtest has closed | Reopen it in Protokite, or set **Playtest ID** to a running one |
| `WaitingForPlayerConsent` | The playtest is loaded and the player has not said what it may collect | Nothing — the question is on screen |
| `PlayerRefusedPlaytest` | The player asked for nothing to be collected | Nothing — it is their answer |
A refusal (`PlaytestNotLinked`, `ProtokiteRefusedApiKey`, `PlaytestConfigForAnotherVersion`) is not asked
again until the Flock SDK starts again or the game is relaunched, since the answer would be the same.
## Before you ship
Turn **Playtesting Enabled** off before you build a release. With a **Playtest ID**, your Game Version is
already your game's; without one, point **Game Version** back at a release version too. A release build still uploads any recording an earlier playtest build on that machine could not
send, so a player's recording is never stranded; with playtesting off, nothing else is recorded or sent.
To take the package out, press **Remove** on the Playtesting tab. Your settings asset stays until you
delete it.
## Tell your players
A playtest build records the player's screen when the playtest turns video on, and sends what they type
into the feedback form. The build asks them first, and does what they answer, but tell your testers
before they play all the same: what is recorded, that it goes to your studio's Protokite playtest, and how
to reach you to have it removed. That question is about this playtest alone — anything your game collects
of its own is still yours to ask about.
Video, performance windows, exceptions and feedback forms, the C# calls for each, and how to test it all.
The Flock SDK the playtest runs on: setup, sign-in, and your game's data.
# What a playtest collects
Source: https://docs.qwacks.com/protokite/unreal-features
Video, performance windows, exceptions and feedback forms from an Unreal playtest build.
Each of these runs only while the playtest turns it on in Protokite **and the player has allowed it**,
and each has Blueprint nodes that are safe in every build: with no playtest running they answer false,
empty or Turned Off and change nothing. Setting the build up is on the
[Unreal plugin](/protokite/unreal-plugin) page.
## What the player allowed
The build asks its player what this playtest may collect before it collects anything, and their answer
decides what the rest of this page does:
| Their answer | Video recording | Heavy analytics and exceptions |
| - | - | - |
| Record my screen and collect play data | Runs | Runs |
| Record my screen only | Runs | Off |
| Collect play data only | Off | Runs |
| Collect nothing | Off | Off, and no session is started at all |
**Protokite Is Playtest Feature Enabled** already answers for both at once: a feature the playtest turns on
and the player left out reads as off, everywhere. **Protokite Get Playtest Consent** gives the answer
itself, and the question, how a game asks it in its own screens, and the setting that turns it off are
on the [Unreal plugin](/protokite/unreal-plugin#what-the-player-is-asked) page.
**The feedback form is not covered by that answer.** It is sent only when a player fills it in and
presses Send, which is their own doing either way — and a form an earlier launch could not send is
still sent later, whatever they answered since.
## Video recording
The game's screen is recorded from the moment the playtest loads, one recording per launch, into a
WebM file a browser plays. Background time is not recorded. The **Video Recording** settings in **Flock
Playtest Settings** set the size, frame rate and bitrate — 1280×720, 30 frames a second and
**Video Bitrate (kbps)** 2000 by default — and two limits, whichever comes first: **Recording Length
Limit (Minutes)**, 60, and **Recording Size Limit (MB)**, 1536.
A recording is uploaded to its session:
* when a length or size limit ends it;
* when the game asks, with **Protokite Stop And Upload Playtest Recording** or the feedback form's
**Upload your recording** button;
* when the game stops it for good, with **Protokite Stop Video Recording**;
* and at the start of a later launch, for anything an earlier launch could not send: a failed upload, a
quit, a crash.
**Quitting does not upload.** A whole recording cannot be sent inside a shutdown, so it is kept and
the next launch sends it. A recording is deleted once uploaded, and one that no playtest session ever
started for is deleted by the next launch, since it can never be sent.
Recordings wait under `Saved/ProtokitePlaytest/Recordings/`, inside **Recordings Disk Budget (MB)**, 4096.
When room is short the oldest test videos go first and recordings waiting to be uploaded go last.
Offer a player a way to send their recording only while **Protokite Can Send Playtest Recording** is true.
It is false when no session has started for the recording, and a button pressed then would stop the
recording and send nothing. To tell the player how it went, bind **On Recording Upload Finished**: it
is raised once when the launch's recording finishes — uploaded, or not uploaded with the reason,
including when the upload could not begin at all. It is not raised while the game is closing.
## Heavy analytics
The plugin sends these through the Flock SDK's analytics, under the `playtest` category, and they read
on **Dashboards → Game Metrics**:
* `performance_window` for every ten seconds of play: median, 95th and 99th percentile frame time,
hitches, and memory used and at its peak;
* `level_loaded` for every level this game instance loads, with the level before it and, for a blocking
load, how long it took;
* your own events, with **Protokite Record Playtest Event**. Build their properties with the Set Command
nodes.
Nothing is sent while the Flock SDK's **Analytics Enabled** is off; the log says so once. See
[Analytics & events](/guides/analytics-and-events) for how the category and the dashboards fit together.
## Exceptions
Exceptions stay the Flock SDK's. It captures error lines, failed ensures, Blueprint script errors such
as Accessed None, and crashes, with its own settings under **Analytics | Exceptions**, and reports them
on **Diagnostics → Errors**. A playtest's exception switch never turns that on or off; when a playtest
asks for exceptions and **Analytics Capture Exceptions** is off, the log warns once as the session
starts.
To check that exceptions reach the dashboard, type `Flock.RaiseTestException error` or
`Flock.RaiseTestException blueprint 100` in the console of a Development build. A hundred of the same
fault arrive as one report, then one repeat report counting the other ninety-nine once the repeat
window closes.
## The feedback form
When the playtest publishes a form, players open it with **Feedback Form Key** (F9 by default; set it
to none to turn the key off), or your game opens it with **Protokite Open Feedback Form** — from a pause
menu, for example. Check **Protokite Can Open Feedback Form** first: it is false when the playtest
published no form, so your game can leave its own entry out rather than offering nothing.
While the form is open the mouse shows and typing goes to it; closing or sending it puts both back as
they were. **Pause The Game While The Form Is Open** is off by default, because pausing does nothing in
a multiplayer game and loses what a single player was about to describe.
Every question comes from the playtest, so editing the form in Protokite changes what players see with
no new build. Answers are checked before they are sent, with every problem shown against its question.
A form that cannot be sent is kept and sent by a later launch, and sending again in the same session
replaces the earlier answers.
### A form of your own
**Protokite Get Feedback Form** hands your UI the questions: each one's id, kind, label, help text, options
and whether it needs an answer. Record answers with **Set Feedback Text Answer**, **Set Feedback Rating
Answer** (1 to 5), **Set Feedback Checkbox Answer** and **Set Feedback Chosen Option**, check them with
**Find Feedback Form Problems**, and send them with **Protokite Send Feedback Form Answers**.
Compare a question's kind with the **Protokite Feedback Question Kind** nodes rather than typing it, and
treat a kind you do not know as text, which is how the server reads it.
Two rules are easy to miss. **Set every checkbox**, ticked or not: the server counts an unticked box
as an answer, and a needed checkbox that was never set is missing. And **a chosen option must match
letter for letter** — the server compares options exactly, so `crash` is not `Crash`.
## Blueprint nodes
All under **Flock | Playtest**. Each finds the playtest from the calling graph.
| Node | Answers or does |
| - | - |
| Protokite Get Playtest Status, Protokite Is Playtest Ready, Protokite Describe Playtest Status | Whether playtest work may run, and why not |
| Protokite Is Playtest Feature Enabled, with Protokite Playtest Feature Video Recording / Exception Capturing / Heavy Analytics | Whether the playtest turns a feature on |
| Protokite Get Playtest Consent, Protokite Get Players Consent Answer, Protokite Describe Playtest Consent | What the player let this playtest collect |
| Protokite Set Playtest Consent, Protokite Ask For Playtest Consent, Protokite Is Consent Question Open | Answering it from your own screens, and asking again |
| Protokite Get Playtest Session Id, Protokite End Playtest Session | The launch's Protokite session |
| Protokite Record Playtest Event | Records one of your own playtest events |
| Protokite Is Recording Video, Protokite Stop Video Recording | The launch's recording |
| Protokite Can Send Playtest Recording, Protokite Stop And Upload Playtest Recording | Sending the recording now |
| Protokite Can Open Feedback Form, Protokite Open / Close Feedback Form, Protokite Is Feedback Form Open | The built-in form |
| Protokite Get Feedback Form, Set Feedback … Answer, Find Feedback Form Problems, Protokite Send Feedback Form Answers | A form of your own |
## Trying it in the editor
**Before any playtest exists**, tick **Record Video In Play In Editor** in **Project Settings → Plugins
→ Protokite Playtest Local Settings** and press Play, or type `ProtokitePlaytest.RecordTestVideo 30` in a
Development build's console. Test videos are saved under
`Saved/ProtokitePlaytest/Recordings/TestVideos/` and never uploaded.
**Against a real playtest**, press Play with the settings from the setup page and sign in with
`Flock.LoginWithDevice`. Then, in a Development build's console:
| Console command | Does |
| - | - |
| `ProtokitePlaytest.AnswerConsent ` | Answers what the playtest may collect: `video_and_play_data`, `video_only`, `play_data_only`, `nothing`, or `not_answered` to be asked again |
| `ProtokitePlaytest.OpenFeedbackForm [seconds]` | Opens the form, after a wait if given |
| `ProtokitePlaytest.SendTestFeedback [seconds]` | Fills in the published form and sends it, to check answers reach Protokite |
| `ProtokitePlaytest.StopVideoRecordingAndUploadIt [seconds]` | Stops the recording and uploads it |
| `ProtokitePlaytest.StopVideoRecording` | Stops the recording; like any finished one, it is uploaded when it has a session |
| `Flock.RaiseTestException [error\|blueprint] [times]` | Raises a fault the Flock SDK reports |
| `ProtokitePlaytest.SelfTest [closed playtest's Game Version ID]` | Checks the whole playtest against Protokite |
The waits exist because every `-ExecCmds` command runs on the first frame, before anything has been
recorded or a session has started.
## Checking a playtest build
`ProtokitePlaytest.SelfTest` checks everything a playtest build does against your real Protokite in one go,
and logs a line per step and a count at the end: `Playtest self-test finished: 16 passed, 0 failed, 1
skipped.` Development builds only.
Each check sits beside a request Protokite must refuse: a wrong API key, a missing one, a version no
playtest is linked to, a session start that names no player, a form missing a needed answer or choosing
an option that is not on the list, and a form, upload link and end for a session that does not exist. A
check that only ever succeeds cannot tell a working build from a broken one, so a refusal passes only
with its own HTTP status and, for a form, the question it names.
It signs nobody in, so sign in first. From a script:
```
UnrealEditor-Cmd.exe MyGame.uproject -game -windowed -ExecCmds="Flock.LoginWithDevice, ProtokitePlaytest.SelfTest, Flock.QuitAfterSeconds 90" -log
```
* **It ends the launch's session as its last step**, so run it in a launch of its own. A session a
refusal should have prevented is ended at once, so a run leaves nothing open.
* **It leaves a trace on your dashboards:** one filled-in form on its session, one `playtest_self_test`
event, one Blueprint fault raised twice so its repeat is counted, and the launch's recording.
* **A step is skipped, saying why,** when nobody has answered what the playtest may collect (answer it
first with `ProtokitePlaytest.AnswerConsent video_and_play_data`); when the playtest does not turn its feature on; when the build has
no video encoder, which is every platform but 64-bit Windows; when the launch cannot draw
(`-nullrhi`), since nothing is recorded; when **Analytics Cache Failed Events** is off, since a report
sent the moment it is made cannot be watched; and for a closed playtest unless you name one.
## What stays on the player's machine
| Folder under `Saved/ProtokitePlaytest/` | Holds | Until |
| - | - | - |
| `Recordings/Playtest/` | Playtest recordings, each with the session it belongs to | Uploaded, or deleted for disk room, oldest last |
| `Recordings/TestVideos/` | Test videos | Deleted for disk room, oldest first |
| `FeedbackForms/` | Answers that could not be sent | Sent, or refused by the server |
| `device_id.txt` | This install's device ID, when no Steam ID is available | Kept, so one install stays one player |
| `playtest_consent.json` | What the player let this playtest collect | Kept, so they are asked once and not every launch |
# Unreal plugin
Source: https://docs.qwacks.com/protokite/unreal-plugin
Turn a Development build of your Unreal game into a playtest build for Protokite.
Protokite Playtest is an optional Unreal plugin that turns a Development build into a **playtest build**.
While a playtest runs, every launch of the game becomes one session on the playtest's **Sessions**
page, and the playtest decides what that session collects.
`ProtokitePlaytest-.zip` ships beside the Flock SDK's own zip, in the same release and at the
same version.
**It runs on the Flock Unreal SDK.** The playtest plugin runs on top of the Flock Unreal SDK and sends
that SDK's API key.
## What a playtest collects
| Playtest feature | What the build sends | Where it shows |
| - | - | - |
| Always | One session per launch, named after the player's Steam ID or this install's device ID | Protokite **Sessions** |
| Video recording | A recording of the game's screen, uploaded when it ends | The session's recording player |
| Heavy analytics | A performance window every ten seconds of play, each level load, and your own playtest events | Flock **Dashboards → Game Metrics**, under the `playtest` category |
| Exception capturing | Nothing extra — the Flock SDK already reports exceptions, and the playtest only asks for them | Flock **Diagnostics → Errors** |
| A published feedback form | The player's answers | The session card, and the playtest's **Feedback form** page |
Every one of them is switched on the playtest's page in Protokite, so changing what a playtest
collects needs no new build — and the player is asked first, so a launch collects only what they
allowed. See [What the player is asked](#what-the-player-is-asked).
## Before you start
* **The Flock SDK is set up and working** in the project — API URL, API Key, Game Name and Game
Version, with the Flock panel showing nothing to fix. See the [Unreal SDK](/sdk/unreal) page.
* **A playtest exists in Protokite.** Creating one makes a Flock game version for it, named `pt-`
followed by the test's ID.
* **Playtest builds are Development builds.** A Shipping build compiles logging out, so the Flock SDK
cannot report error lines or failed ensures from it.
* **Video is recorded on 64-bit Windows only.** Every other platform runs the rest of the playtest and
logs once that it records no video.
## Install the plugin
Put it in your project's `Plugins/` folder, so `Plugins/ProtokitePlaytest/` sits beside
`Plugins/FlockUnrealSdk/`.
Turn on **Protokite Playtest** under **Edit → Plugins → Online Platform**, and restart the editor so
it builds.
Cloned the SDK repository instead? Copy or link `Plugins/FlockUnrealSdk/OptionalPlugins/ProtokitePlaytest/`
to `Plugins/ProtokitePlaytest/` — Unreal does not load a plugin from where that folder sits. Leave the
plugin out of projects that are not running playtests.
## Point the build at the playtest
Protokite finds the playtest from the Flock game version the build sends.
Open the test in Protokite. Its **SDK** block shows the playtest's Flock version **ID**.
In **Project Settings → Plugins → Flock SDK Settings**, set **Game Version** to `pt-` followed by
the test's ID, for example `pt-01JABCDEFGHJKMNPQRSTVWXYZ0`. The Flock SDK resolves that name to the ID
Protokite showed you.
In **Project Settings → Plugins → Protokite Playtest Settings**, turn on **Enable Playtesting**.
**Protokite API URL** is already set to production, `https://api-protokite.qwacks.com`; change it only to point
at a local Protokite such as `http://localhost:8020`. The URL must start with `http://` or `https://` and contain
no spaces; one that does not is refused rather than tidied up.
Set the version's **name**, not the ID Protokite displays. The Flock SDK resolves Game Version again
whenever Game Version, API URL or API Key changes, and replaces an ID written in by hand.
Pressing **Play** now lists in the Play message log whatever would stop the playtest or change it, each
with a link to the page that fixes it: an empty or unusable Protokite API URL, the Flock SDK's
analytics turned off, a Game Version that is not a playtest's, and the settings a playtest session
waits on. Nothing is said while **Enable Playtesting** is off.
## What the player is asked
**A playtest build asks its player what it may collect, and collects nothing until they answer.** The
question is drawn over the game as soon as this build's playtest loads, and offers four answers: record
the screen and collect play data, the screen only, play data only, or **nothing at all** — which leaves
the build behaving exactly as one with **Enable Playtesting** off.
**It is the playtest's own question.** The Flock SDK's **Analytics Require Explicit Consent** is a
different one, asked by your game in your game's words about your game's analytics. Neither answer
moves the other, and the question says so in front of the player.
Their answer is kept on their machine and used by every later launch, and each session start carries
`playtest_consent` and `playtest_consent_asked`, so a session with no recording reads as a player who
asked for none rather than a build that went wrong.
Your game can take the question over: **Protokite Get Playtest Consent** reads the answer, **Protokite Set
Playtest Consent** records one from your own screens, and **Protokite Ask For Playtest Consent** puts the
question again — what a "change what this playtest collects" entry in a menu calls.
Turn **Ask The Player For Playtest Consent** off in **Protokite Playtest Settings** only where your players
are asked another way, for an internal test on your own machines, or for an automated run with nobody
there to answer; the build then collects what the playtest turns on, and every session says the player
was never asked. An answer a player has already given is still honoured.
## Sign a player in
**A playtest never signs a player in.** The playtest session starts from the Flock session, and a Flock
session starts once your game signs a player in, with **Analytics Enabled** and **Analytics Auto Start
Session** on (or a Start Session call) and consent granted when **Analytics Require Explicit Consent**
is on. A device sign-in is enough and needs no account.
A playtest build with no sign-in screen of its own can sign in from the console of a Development build
with `Flock.LoginWithDevice`.
## Watch it run
The log, under `LogProtokitePlaytest`, says what the playtest is doing:
1. Playtesting is set up and waiting for the Flock SDK, then fetching this build's playtest from Protokite.
2. Playtesting is ready, naming the playtest and the features it turns on.
3. The Protokite session started for this launch, once a player is signed in and the Flock session has
reached the server.
The session then appears on the playtest's **Sessions** page. It ends when the game shuts down, or
earlier with **Protokite End Playtest Session**. A later Flock session, a sign-out, or the Flock SDK
initializing again neither ends nor restarts it: a launch is one session.
In Blueprint, **Protokite Get Playtest Status** answers the same question the log does, and **Protokite
Describe Playtest Status** turns it into the sentence the log uses.
## When something stops the playtest
The log says why and names the setting, once, as a warning.
| Status | Meaning | Fix |
| - | - | - |
| Protokite API URL missing | **Enable Playtesting** is on and the URL is empty | Set **Protokite API URL** (production is `https://api-protokite.qwacks.com`) |
| Protokite API URL unusable | No `http://` or `https://`, no host, or a space or line break in it | Correct the URL; it is never trimmed for you |
| Playtest not linked | Protokite has no playtest for this build's Game Version ID | Point **Game Version** at the playtest's `pt-` version |
| Protokite refused API key | Protokite turned down the Flock API key | Check **API Key** in the Flock SDK's settings |
| Playtest config unavailable | Protokite or the network kept failing | Nothing — it is fetched again when the next Flock session starts |
| Playtest no longer collecting | The playtest has closed | Reopen it in Protokite, or point Game Version at a running one |
| Waiting for player consent | The playtest is loaded and the player has not said what it may collect | Nothing — it is asked over the game |
| Player refused playtest | The player asked for nothing to be collected | Nothing — it is their answer |
## Shipping builds
A Shipping build that has **Enable Playtesting** on, or that carries a playtest's `pt-` Game Version,
gets a warning while it builds, and the build still succeeds. That warning comes from the plugin's
build rules, which a clean build reads and an unchanged rebuild skips — so package a release from a
clean build. Build playtests as Development, and point Game Version back at a release version before
you ship.
## Tell your players
A playtest build records the player's screen when the playtest turns video on, and sends what they type
into the feedback form. The build asks them first, and does what they answer, but tell your testers
before they play all the same: what is recorded, that it goes to your studio's Protokite playtest, and
how to reach you to have it removed. That question is about this playtest alone — anything your game
collects of its own is still yours to ask about.
Nothing is collected unless **Enable Playtesting** is on in the build *and* the playtest in Protokite
turns the feature on, and a build without the plugin collects none of it.
Video, performance windows, exceptions and feedback forms, and the Blueprint nodes for each.
The Flock SDK the playtest runs on: setup, auth, and your game's data.
# Quickstart
Source: https://docs.qwacks.com/quickstart
From zero to an authenticated player in five steps with the Flock SDK.
This guide takes you from a fresh game to your first signed-in player using the Flock SDK.
Sign in to the [Qwacks dashboard](https://flock-saas.qwacks.com), create a **studio**, then create a
**game** inside it. A game is the container for your players, economy, and config.
Open your game in the dashboard and copy its **API Key**, **Game ID**, and **Game Version**.
You'll plug these into the SDK so it can connect to your game.
Treat your API Key like a password. Use a key scoped to the right environment, keep dev and
production separate, and rotate it on the dashboard if it leaks.
Add the SDK for your engine — [Unity](/sdk/unity) or [Unreal](/sdk/unreal) — then open its
settings (**Flock → Settings** in Unity, **Project Settings → Plugins → Flock SDK** in Unreal)
and paste in your credentials. The window prepares the two things the SDK needs before it can start:
* **Resolve Game Version** — looks up your version's ID and bakes it into the config. Runtime init
uses that ID directly and never calls the server, so it must resolve before you play or build.
The window does this automatically as you fill in your credentials.
* **Verify** — confirms your credentials reach Flock (Setup checklist → **Verify**).
Then choose how the SDK starts:
* **Automatic (default):** with **Auto-Initialize On Load** on, the SDK initializes itself before
your first scene or level — nothing to call. Reach it through `FlockClient.Instance` in Unity, or
`UFlockSubsystem::Get(this)` in Unreal.
* **Manual:** turn it off and initialize it yourself — handy to wait for a splash screen or EULA.
Unity calls `FlockClient.Create(config.ToInitConfig())` (or uses the `FlockBootstrap` component);
Unreal calls `InitializeFromSettings()`.
Full details on the init styles are on the [Unity](/sdk/unity) and [Unreal](/sdk/unreal) SDK pages.
Sign the player in — the SDK manages the session from there.
```csharp Unity theme={null}
await FlockClient.Instance.Authentication.LoginWithEmailAsync("player@example.com", "hunter2");
Debug.Log($"Signed in as {FlockClient.Instance.CurrentPlayerId}");
```
```cpp Unreal theme={null}
UFlockSubsystem* Sdk = UFlockSubsystem::Get(this);
Sdk->GetAuthProvider()->LoginWithEmail(TEXT("player@example.com"), TEXT("hunter2"),
[](TFlockResult Result)
{
if (Result.bSuccess)
{
UE_LOG(LogTemp, Log, TEXT("Signed in as %s"), *Result.Value.PlayerId);
}
});
```
In Blueprint this is one `Flock Login With Email` node with success and failure pins.
Signing in does not create an account — registering is a separate call in both SDKs. Failures
surface differently: Unity **throws**, while Unreal returns a `TFlockResult` you check, because
Unreal builds with exceptions off.
Once a player is signed in, reach any feature through the SDK:
```csharp Unity theme={null}
var game = await FlockClient.Instance.Game.GetGameAsync();
Debug.Log($"Playing {game.Name}");
```
```cpp Unreal theme={null}
Sdk->GetGameProvider()->GetGame(
[](TFlockResult Game)
{
UE_LOG(LogTemp, Log, TEXT("Playing %s"), *Game.Value.Name);
});
```
That's it — you're live. 🎉
## Next steps
How the SDK manages game and player sessions.
Players, shops, leaderboards, remote config, and more.
# Track games
Source: https://docs.qwacks.com/qwacks/billing-games
See the games in your studio that count toward your plan.
The **Track Games** tab lists the games in your studio — the ones that count against your plan's
**games** limit. It's a quick read on what you're running and where each game stands.
Every game shows its name, a short description if it has one, its current **stage**, and when it
was created. As your studio grows, this is the list to weigh against the games quota on your
[plan](/qwacks/billing-plans) and in [Usage](/qwacks/billing-usage).
# Invoices
Source: https://docs.qwacks.com/qwacks/billing-invoices
Your studio's billing history, and how to pay an open invoice.
The **Invoices** tab is your studio's billing history — every charge for a plan or an on-demand
service, with its date, amount, and status.
## Statuses
An invoice is usually **paid** or **pending**. Pending means it's waiting on payment, most often a
new subscription or an automatic renewal that hasn't cleared yet. You may also see **cancelled** or
**expired** on charges that were never completed.
## Paying an invoice
A pending invoice has a **Pay Now** button that takes you to a secure **Paylink** payment page.
Pay there and you're returned to Qwacks, where the invoice flips to paid once it settles.
Renewals charge your default card automatically, so most invoices settle without you lifting a
finger. **Pay Now** is for the moments a charge needs you — a first payment, or a renewal your
bank asked to confirm.
# Payment methods
Source: https://docs.qwacks.com/qwacks/billing-methods
Save a card so your studio's plan renews without interruption.
The **Payment methods** tab holds the card your studio uses for **automatic plan renewals**. Save
one here and Qwacks charges it when your [plan](/qwacks/billing-plans) renews, so your limits never
lapse between months.
## Adding a card
Add a card by entering its details. They go straight to **Moyasar**, Qwacks's payment processor —
Qwacks never sees or stores your full card number, only the safe display details like the brand,
last four digits, and expiry. Moyasar may send you to your bank to confirm the card with 3-D
Secure; once you're back, the card is saved to the studio and set as its **default**.
Your **default** card is the one charged at renewal. Saving a new card makes it the default, and
you can remove a card whenever you like.
This card is for **recurring** renewals. Paying a one-off
[invoice](/qwacks/billing-invoices) — like your first subscription charge — happens on a separate
secure page at the time of payment and doesn't need a saved card.
# Plans & pricing
Source: https://docs.qwacks.com/qwacks/billing-plans
Choose a plan for your studio, and renew or cancel it anytime.
Your studio runs on a **plan**. It sets your limits across Flock, Protokite, and Data Duck —
games, monthly active users, team seats, storage, analytics retention, and more — and the price
you pay each month. Billing lives under **Payments & Billing** in the sidebar and is available to
studio **admins**.
## Choosing a plan
Each plan is a card showing its monthly price and what it includes: the headline limits (games,
MAU, team members) up top, and the finer print (storage, analytics events, multiplayer, analytics
retention, support level, and on-demand discounts) below. Pick the one that fits your studio's
scale — you can move up or down later.
Choosing a plan creates an invoice and sends you to a secure payment page to settle it. Once the
payment clears, the plan is active and its limits apply across your apps. A free plan, where one is
offered, activates without payment.
Need more than the listed plans, or a tailored mix across the three products? The **Custom /
Enterprise** option puts you in touch with the Qwacks team for bespoke limits, pricing, and
onboarding.
## Your current plan
While a plan is active, a banner at the top of Payments shows which plan you're on, when it was
purchased, and its status. You can **cancel** from there at any time, or **renew** one that has
lapsed. Plans renew automatically from your default saved card — see
[Payment methods](/qwacks/billing-methods) — so keeping one on file is what stops your limits from
lapsing between months.
## On-demand services
Subscriptions aside, you can buy **one-off services** as you need them — a playtest session, a
market report, QA testing, or a code review. These are charged per use, and your plan's on-demand
discount applies if it has one.
# Usage & limits
Source: https://docs.qwacks.com/qwacks/billing-usage
Track what your studio is using against your plan's limits.
The **Usage** tab shows what your studio is consuming this month against the limits your
[plan](/qwacks/billing-plans) allows — a running read on how much headroom you have before it's
time to upgrade.
## What's measured
Usage is broken down per product, with the metrics that matter for each:
* **Flock** — API calls and monthly active users (MAU)
* **Data Duck** — reports generated and shared reports accessed
* **Protokite** — playtests and QA sessions
There are studio-wide counters too, like the number of **games** and **analytics events**, each
shown against your plan's quota. Every metric sits next to its limit with a bar that turns amber as
you near the cap and red once you're almost there. Limits set to unlimited are labelled as such
rather than charted.
A metric creeping toward its limit is your cue to move to a larger
[plan](/qwacks/billing-plans) — you can upgrade whenever you like, and the new limits take effect
immediately.
## Renewals that need a hand
Most renewals charge your saved card quietly in the background. If your bank wants to confirm one,
Usage shows an **action required** notice with a link to finish the payment. Completing it there
keeps your plan from lapsing — it's the same as paying any [invoice](/qwacks/billing-invoices).
# Create your studio
Source: https://docs.qwacks.com/qwacks/create-studio
Set up the workspace that owns your team, billing, and games.
A **studio** is your workspace in Qwacks — your organization or team. It owns your members, your
billing and plan, and the games you ship, and it scopes everything you do across Flock, Protokite,
and Data Duck. Every new account sets up its first studio during onboarding.
## Your first studio
Straight after signing up you land on **Studio Setup**. Name the studio — usually after your
company or team — and create it; that name labels your workspace across every Qwacks app. Creating
it makes you the studio's first **admin** and drops you into the applications hub.
If you signed up with a work email on your own domain (rather than a generic provider like Gmail
or Outlook), Qwacks picks up that domain and shows it during setup. It's only there to help
identify your workspace later — nothing to configure.
## Adding more studios
You're not limited to one. From **Studio Management** you can create additional studios — separate
workspaces for different teams or clients — and switch between them whenever you like. How the
active studio shapes what you see across apps is covered in
[Studios & switching](/qwacks/studios).
Everything in Qwacks is scoped to the **active** studio — games, team, usage, and invoices all
belong to one studio at a time — so pick the right one before you start working.
# Sign in & accounts
Source: https://docs.qwacks.com/qwacks/sign-in
One Qwacks account signs you in to Flock, Protokite, and Data Duck.
Qwacks runs on one **single sign-on (SSO)** account — the same credentials get you into
[Flock](/introduction), Protokite, and Data Duck, with no separate login per app.
## Create an account
Choose **Sign Up** and enter your email. Qwacks sends a six-digit code to confirm the address;
enter it, set a password, and your account is ready. From there you'll
[create a studio](/qwacks/create-studio) — the workspace that holds your team, billing, and
games. If the code doesn't arrive within a minute, **Resend verification code** sends a fresh one.
## Signing in
When you sign in, where you land depends on your account: with a studio, you arrive at the
applications hub to open Flock, Protokite, or Data Duck; without one, Qwacks takes you to
[create your studio](/qwacks/create-studio) first.
## Signing out
Sign out from the **settings menu** at the bottom of the left sidebar. Your identity lives on your
Qwacks account rather than in any one app, so your studios, team, and billing are managed here in
the hub and apply everywhere you sign in.
# Single sign-on
Source: https://docs.qwacks.com/qwacks/sso
How one Qwacks login carries you into Flock, Protokite, and Data Duck.
Qwacks is the **identity provider** for everything it makes. You sign in once with your Qwacks
account, and that same identity opens [Flock](/introduction), Protokite, and Data Duck — there's no
separate username or password for each app.
## Opening an app
Launch an app from the Qwacks hub and it opens already signed in. Behind the scenes it's a standard
OAuth handoff: the app asks Qwacks to authorize you, Qwacks sends you back with a short-lived
authorization code, and the app trades that code for the tokens that keep you signed in. You see
none of it beyond a brief "completing sign-in" moment.
Go straight to an app without signing in first and it bounces you to Qwacks to authenticate, then
returns you to where you were headed — the same handoff, started from the app's side.
## Your studio comes with you
The handoff carries your **active studio** along with your identity, so an app opens in the same
studio you had selected in the hub — its games, data, and team, not another's. Switch studios in the
hub before opening an app and you'll land in the one you picked; [Studios &
switching](/qwacks/studios) covers how that works.
Your **role** travels too. What you can see and change inside an app follows the admin, manager,
editor, or viewer role you hold in that studio — and because roles are per-studio, the same account
can have more reach in one studio than in another. [Team & roles](/qwacks/team) has the details.
## Staying signed in
Once you're in, Qwacks keeps your session alive and refreshes it quietly in the background, so you're
not asked to log in again each time you move between apps. Signing out from the hub's settings menu
ends your session there.
# Studio settings
Source: https://docs.qwacks.com/qwacks/studio-settings
Rename the active studio and find its ID, from Studio Management.
**Studio Management** holds the settings for whichever studio is active. It's **admin-only** — it
appears in the menu for admins, and only an admin can change a studio's settings.
## Choosing which studio to manage
The page always acts on your **active** studio, shown at the top with the same switcher you'll
find in the sidebar. Switch there to manage a different studio, or use **Create Studio** to spin
up a new workspace (covered in [Create your studio](/qwacks/create-studio)).
## Studio details
This is where an admin renames the studio — the name that labels your workspace across every
Qwacks app — and where you'll find the **Studio ID**. That ID is the studio's permanent
identifier: it doesn't change when you rename, it's what support will ask for, and it's the value
the apps use behind the scenes to scope your data.
Renaming is an admin action. If your role can't manage the studio, you won't see Studio
Management in the menu in the first place.
# Studios & switching
Source: https://docs.qwacks.com/qwacks/studios
How studios scope your work, and how to switch between them.
A **studio** is the tenant for everything you do in Qwacks. Your games, team, plan, usage, and
invoices all belong to a studio — not to your personal account. Your account is just the identity
that can be a member of one or more studios.
## The active studio
You always have one **active** studio, and it scopes the whole platform: every app you open and
every page you load works against that studio's data. The choice is remembered across apps, so
moving from the Qwacks hub into Flock or Data Duck keeps you in the same studio without picking it
again.
Because membership is per-studio, your **role can differ between studios** — admin in your own,
maybe viewer in one you were invited to. Switching studios switches the permissions you're acting
with.
## Switching
Open the **settings menu** at the bottom of the left sidebar and choose a studio from the switcher
at the top. [Studio Management](/qwacks/studio-settings) has the same switcher under **Active
Studio** if you'd rather change it there. The switch is immediate — no reload — and whatever
studio-scoped page you're on refreshes to the studio you picked.
Belong to just one studio? There's nothing to switch — it's active everywhere by default.
## Belonging to more than one
You can be a member of several studios at once: your own, plus any you've been invited to. Create
more from [Studio Management](/qwacks/studio-settings), and manage who belongs to each from
[Team & roles](/qwacks/team).
# Team & roles
Source: https://docs.qwacks.com/qwacks/team
Invite people to your studio and control what they can do with roles.
A studio is rarely one person. **Team management** is where you bring people into the active
studio, set what they're allowed to do, and remove them when they leave. Everyone you add belongs
to *this* studio only — the same person can hold a different role in another studio.
Team management is available to **admins and managers** — the roles that can change who's in the
studio. Editors and viewers don't have access to it.
## Roles
A member's **role** decides what they can touch. There are four studio roles:
| Role | In the apps | Manage team | Studio settings | Billing |
| - | - | - | - | - |
| **Admin** | Read & write | ✓ | ✓ | ✓ |
| **Manager** | Read & write | ✓ | — | — |
| **Editor** | Read & write | — | — | — |
| **Viewer** | Read only | — | — | — |
Put simply: **admins** run the studio and are the only role that touches billing and studio
settings; **managers** handle the team and the day-to-day work; **editors** do the work but don't
manage people or the account; **viewers** look without changing anything.
Roles are per-studio. The same account can be an admin in one studio and a viewer in another —
switching studios switches the role you're acting with.
## Inviting people
Invite a member by email and choose the role they should start with. They receive an invitation;
once they accept — creating a Qwacks account first if they don't already have one — they appear in
your members list with that role. You can change someone's role or remove them from the studio
whenever you need, and the list can be searched by email or filtered by role as the studio grows.
## Specialized testing roles
Protokite playtesting adds two narrower roles — **QA** and **Play Tester** — for people who only
take part in tests. They're scoped to test participation rather than running the studio, so they
don't carry the studio-management permissions above. You'll come across them when assigning testers
in Protokite.
# Unity SDK
Source: https://docs.qwacks.com/sdk/unity
Add Flock to your Unity game with the official C# SDK.
The Flock Unity SDK connects your game to the Flock backend: player auth, save data, economy,
remote config, downloadable assets, and analytics — configured from one in-editor window, with
strongly typed C# generated from your own schemas.
Get the latest `.unitypackage` from the releases page.
## Requirements
* Unity **2021.3** or newer
* `com.unity.nuget.newtonsoft-json` **3.0.2+**
## Setup
Download the latest [release](https://github.com/QwackStack/FlockUnitySdk/releases) and import the
`.unitypackage` (**Assets → Import Package → Custom Package**).
Add Newtonsoft JSON to `Packages/manifest.json`:
```json theme={null}
{ "dependencies": { "com.unity.nuget.newtonsoft-json": "3.0.2" } }
```
Open **Flock → Settings** and fill in the four fields from your game's page on the
[dashboard](https://flock-saas.qwacks.com):
| Field | Value |
| - | - |
| **API URL** | Leave the default (`https://api-flock.qwacks.com`) unless told otherwise |
| **API Key** | Identifies your game — treat it like a password |
| **Game ID** | Your game's unique ID |
| **Game Version** | A version name that exists on the dashboard (e.g. `v1.0.0`) |
Values are saved to `Assets/Resources/FlockConfig.asset`. As you fill them in, the window prepares
the two things the SDK needs before it can start:
* **Resolve Game Version** — your version name is looked up and its **ID is baked into the
config**. Runtime init uses that baked ID directly and never calls the server, so it must
resolve before you play or build. The window resolves it automatically when your credentials
change (or click **Resolve Game Version**).
* **Verify** — the Setup checklist's **Verify** button confirms your credentials actually reach
Flock.
Green checks across the **Setup** checklist mean you're ready to initialize.
The API Key ships inside your build (it's loaded from `Resources` at runtime). Use a key scoped to
the right environment, and rotate it on the dashboard if it leaks.
## Your first call
With auto-init on, the SDK is already running — just use `FlockClient.Instance`.
```csharp theme={null}
using Flock;
// 1. Sign a player in. Auth methods throw on failure.
await FlockClient.Instance.Authentication.LoginWithDeviceAsync("device-uuid");
// 2. Call any service through the singleton.
var game = await FlockClient.Instance.Game.GetGameAsync();
Debug.Log($"Signed in as {FlockClient.Instance.CurrentPlayerId}, playing {game.Name}");
```
## Initialization
The SDK can start three ways. **Automatic** is the default and fits most games; the other two trade
a little setup for control over *when* init runs (after a splash screen, EULA, etc.).
Nothing to call — the SDK initializes from `FlockConfig.asset` before the first scene loads and
restores a saved session in the background. React to lifecycle events if you need to:
```csharp theme={null}
FlockEvents.OnInitialized += () => Debug.Log("Flock ready");
FlockEvents.OnSessionRestored += signedIn => Debug.Log(signedIn ? "Resumed" : "Show login");
```
Turn off **Auto-Initialize On Load**, then click **Add to Scene** in the Flock window (or add a
`FlockBootstrap` component yourself). It reads your config asset and initializes in `Awake`. Put
it in a boot scene with **Don't Destroy On Load** on.
Turn off **Auto-Initialize On Load** and create the client yourself — e.g. after a splash screen
or EULA. `Create` is synchronous and makes no network call.
```csharp theme={null}
var config = Resources.Load("FlockConfig");
FlockClient.Create(config.ToInitConfig());
// Manual init does NOT resume a saved session — do it yourself if you persist sessions:
bool signedIn = await FlockClient.Instance.Authentication.TryRestoreSessionAsync();
```
Init is fail-fast. A bad config throws (the auto-init path logs instead of crashing), and
`FlockClient.Instance` throws until init succeeds. Guard with `FlockClient.IsInitialized`, inspect
`FlockClient.InitializationError`, or handle `FlockEvents.OnInitializationFailed`.
## Authenticate a player
Signing in does not create an account — registering is a separate `RegisterWith…Async` call.
Registering an identity that already has one succeeds rather than failing, so you can send the player
to a login instead. Auth methods **throw** on failure, so wrap them in `try/catch`.
```csharp theme={null}
await FlockClient.Instance.Authentication.LoginWithDeviceAsync("device-uuid");
```
```csharp theme={null}
await FlockClient.Instance.Authentication.LoginWithEmailAsync(email, password);
```
Pass the token or ticket from the platform to the matching `LoginWith…Async` method.
Sessions refresh automatically. Subscribe to `FlockEvents.OnAuthenticated` and `OnAuthExpired` to
drive your UI.
## Work with your game data
Run **Code Generation → Sync Schemas** in the Flock window to turn your dashboard's player
templates, configs, and shops into typed C# accessors under `Assets/Flock/Generated/`.
```csharp theme={null}
// Player data — typed accessor generated from your "PlayerProgress" template.
var progress = await FlockClient.Instance.Player.GetPlayerProgressAsync();
progress.Level = 5;
await progress.UpdateAsync();
// Remote config — change values on the dashboard without rebuilding.
var gameplay = await FlockClient.Instance.Config.GetGameplayAsync();
float speed = gameplay.BaseMoveSpeed;
// Shop — enum-keyed purchase; the SDK resolves ids for you.
await FlockClient.Instance.Shop.PurchaseAsync(FlockShopItemId.GemPack);
```
**These accessors don't exist until you sync.** `GetPlayerProgressAsync` above is generated from a
`PlayerProgress` template on your dashboard. Write the call before the template exists — or before
the first **Sync Schemas** — and it fails to *compile*, not at runtime:
```text theme={null}
'PlayerProvider' does not contain a definition for 'GetPlayerProgressAsync'
```
The order is always: **author the schema on the dashboard → Sync Schemas → write the call.** Adding
or renaming a template later means syncing again.
Unity's own compiler error can't tell you any of that, so the SDK watches compilation and prints
the missing step in the Console when it recognises one of these. The **Code Generation** tab also
shows whether a sync has ever run, and which game version it last synced for.
Each sync also writes a read-only **FlockContentCatalog** asset — designers can browse every shop,
config, and template in the Inspector without touching code or the dashboard.
## Explore features
## Advanced settings
The **Advanced Settings** tab tunes analytics, HTTP retries, the asset and offline caches, and
editor tools (including **Auto-Initialize On Load**). The defaults are sensible — change them only
when you need to.
## Error handling
Every SDK failure raises `FlockException`. Its `Message` names the call that failed, the server's own
reason, the coded identifier, the HTTP status — and a `Fix:` line telling you what to do next.
```csharp theme={null}
try
{
await FlockClient.Instance.Authentication.LoginWithDeviceAsync(deviceId);
}
catch (FlockException ex)
{
Debug.LogError(ex.Message);
}
```
```text Console theme={null}
Device login failed: Invalid login credentials [player.invalid_login_credentials, HTTP 400]
Fix: This device is not registered yet. Call Authentication.RegisterWithDeviceAsync(deviceId) once to create the account, then Authentication.LoginWithDeviceAsync(deviceId) on later launches.
```
The same server code can mean different things depending on how the player signed in, so the fix is
specific to the credential — the message above says "register this device", while the identical code
on an email sign-in says the password was wrong.
### Reading the parts yourself
If you'd rather build your own text, every piece is on the exception:
| Property | What it holds |
| - | - |
| `Message` | Everything below, composed into the line shown above. |
| `ServerMessage` | The server's reason on its own (`"Invalid login credentials"`), or `null`. |
| `Hint` | The `Fix:` text on its own: the advice for the code, or for the kind of failure when the code has none. Never empty. |
| `Operation` | The call that failed (`"Device login"`). |
| `Code` | The coded identifier as a string (`"player.invalid_login_credentials"`). |
| `ErrorCode` | The same code as a `FlockErrorCode` enum value, for `switch` statements. |
| `StatusCode` | HTTP status, or `null` when the request never reached the server. |
| `Body` | The raw response body, for logs. |
Branch on `ErrorCode`, never on message text. Codes are a stable contract; wording is not and will
change between versions.
```csharp theme={null}
catch (FlockException ex) when (ex.ErrorCode == FlockErrorCode.ShopInsufficientFunds)
{
ShowNotEnoughCoinsDialog();
}
```
`Hint` is written for **you**, not your players — it names SDK methods and dashboard steps. Show it
in your own debug overlay or logs, and write separate player-facing copy for your UI. You can pull
the same text for any code with `FlockErrorHints.For(errorCode)`.
A failure with no `StatusCode` got no refusal from the server: the request never reached it (a
connectivity problem on the device, which `Hint` says), or the SDK refused the call itself, before
sending it (an empty argument, nobody signed in), and `Message` names what to change.
The full API reference, codegen details, offline cache, and analytics tuning live in the
[SDK README](https://github.com/QwackStack/FlockUnitySdk#readme).
# Unreal SDK
Source: https://docs.qwacks.com/sdk/unreal
Add Flock to your Unreal Engine game with the official plugin — from C++ or Blueprint.
The Flock Unreal SDK connects your game to the Flock backend: player auth, save data, economy,
remote config, and analytics — configured from Project Settings, with typed Blueprint nodes or C++
generated from your own schemas.
**Every feature works from Blueprint.** Each call is a self-contained async node with success and
failure pins, and no Target pin to wire — nodes resolve the SDK from the graph they sit in.
Get the latest plugin release from GitHub.
Everything below ships today. **Multiplayer** (parties, sessions, matchmaking and connecting players) is
not in the Unreal SDK yet — it lands in a later release.
## Requirements
* Unreal Engine **5.4 to 5.8**. Every version from 5.5 up is built and run against the full test suite before
a release; 5.4 builds and links in a real project, but its test suite has not been run on it yet.
* A C++ project. Blueprint-only projects need the C++ toolchain installed to compile the plugin.
**Known engine issue on 5.4 and 5.5:** a Development package ends with an access violation (`0xC0000005`)
about 60 seconds after it quits. The game has already closed by then, and its log is complete. This is an
engine bug, not the SDK's: Epic fixed it in 5.6, and it happens whether or not any Flock feature ran.
Shipping packages leave out the part involved, so they should not be affected, though that has not been
measured. If a script judges a packaged run, read the game's own exit lines in its log rather than the
process's exit code.
## Setup
Download the latest [release](https://github.com/QwackStack/FlockUnrealSdk/releases) and extract it
into your project's `Plugins/` folder, so you have `YourProject/Plugins/FlockUnrealSdk/`.
Then right-click your `.uproject` → **Generate Visual Studio project files**, and rebuild. The
plugin is enabled by default — verify under **Edit → Plugins → Online Platform → Flock SDK**.
Open **Project Settings → Plugins → Flock SDK Settings** and fill in the four fields from your game's page on
the [dashboard](https://flock-saas.qwacks.com):
| Field | Value |
| - | - |
| **API URL** | Leave the default (`https://api-flock.qwacks.com`) unless told otherwise |
| **API Key** | Identifies your game — treat it like a password |
| **Game Name** | Your game's name from the dashboard |
| **Game Version** | Your game version name, e.g. `1.0.0` |
Values are saved into your project's `DefaultGame.ini`.
The Game Version **ID** resolves and bakes itself as soon as those fields are valid, so startup
never makes a network call to find it. You can force it any time with
**Tools → Flock → Resolve Game Version**.
## Your first call
The SDK auto-initializes with your game, so there is nothing to start. Sign a player in, then read
something back.
```cpp theme={null}
UFlockSubsystem* Sdk = UFlockSubsystem::Get(this);
Sdk->GetAuthProvider()->LoginWithDevice(DeviceId,
[Sdk](TFlockResult Login)
{
if (!Login.bSuccess) { return; }
Sdk->GetGameProvider()->GetGame(
[](TFlockResult Game)
{
UE_LOG(LogTemp, Log, TEXT("Playing %s"), *Game.Value.Name);
});
});
```
## Initialization
**Auto-Initialize On Load** is on by default, so `UFlockSubsystem` is running before `BeginPlay`. To
defer past a splash screen or EULA, turn it off and call `InitializeFromSettings()` yourself.
Because init reads a baked version ID, it is synchronous and makes no network call — your game starts
the same offline as online.
## Authenticate a player
Email, device ID, Google, Apple, Steam, Facebook, and Discord. The SDK exchanges the credential your
platform SDK produced for a Flock session, stores the tokens encrypted between launches, restores the
session automatically on startup, and refreshes expired access tokens silently.
```cpp theme={null}
Sdk->GetAuthProvider()->LoginWithEmail(Email, Password,
[](TFlockResult Result)
{
if (Result.bSuccess)
{
UE_LOG(LogTemp, Log, TEXT("Signed in as %s"), *Result.Value.PlayerId);
}
});
```
You never pass a player ID. Reads and commands act for the signed-in player; the `Player Id` pin is
tucked into each node's advanced section for the rarer case of acting on someone else.
## Work with your game data
One menu click turns your backend's templates, configs, and shops into typed nodes. **Tools → Flock →
Sync Schemas** — no C++, no toolchain, no compile step.
Reading a config becomes **one node** that carries its own fetch and ID:
Changing a player's data is `Get` → the engine's **Set members in struct** → `Save`:
Tick the members you want to write. Unticked ones keep whatever `Get` fetched.
Shop purchases and currency grants take a generated dropdown rather than an ID you type:
### Prefer C++?
Switch **Codegen Target** to `C++` and a sync emits a generated module instead — a `USTRUCT` per
template and config, a `UENUM` per ID set, and a typed call surface.
```cpp theme={null}
FFlockGenerated::GetPlayerLevel(this,
[this](TFlockResult Result, const FString& RowId)
{
if (!Result.bSuccess) { return; }
FPlayerLevelTemplate Progress = Result.Value;
Progress.Level.Stage = 5;
FFlockGenerated::SavePlayerLevel(this, RowId, Progress, nullptr);
});
FFlockGenerated::Purchase(this, EFlockShopItemId::GemPack, OnBought);
FFlockGenerated::AddFunds(this, EFlockCurrencyId::Gold, 100, OnCredited);
```
A project uses one target or the other. Generated C++ structs are `BlueprintType`, so graphs still see
your types after switching.
## Explore features
## Advanced settings
Project Settings also tunes analytics, HTTP retries, the offline cache, and the codegen paths. The
defaults are sensible — change them only when you need to.
Turning on **Enable Debug Logs** traces every network call under the `LogFlock` category, and each line
names its caller so you can tell a Blueprint graph from C++:
```
[Flock SDK] -> GET .../v1/player_data?player_id=01KY... [Blueprint 'bpTest']
[Flock SDK] <- 200 GET .../v1/player_data?player_id=01KY... (34 ms, 2060 bytes) [Blueprint 'bpTest']
```
## Offline caching
Config, game, shop-catalog, and player-template reads are cached to disk, scoped to your game version,
and served when the server is unreachable — the server is always tried first, and there are no TTLs.
Player data writes queue offline and replay automatically when the connection returns. **Currency
grants never do:** a grant fails rather than being queued, and is never re-sent after an ambiguous
failure, so it cannot double-credit.
## Error handling
Every call answers with a `TFlockResult` rather than throwing — Unreal builds with exceptions off.
Check `bSuccess`, then read `Value` or `Error`.
```cpp theme={null}
if (!Result.bSuccess && Result.Error.ErrorCode == EFlockErrorCode::ShopInsufficientFunds)
{
// The server declined — not enough funds.
}
```
`Error.ToDisplayText()` says what went wrong and how to fix it: the call that failed, the server's own
reason, the coded identifier and the HTTP status, then a `Fix:` line. The Output Log carries the same
text for every failed call.
```cpp theme={null}
if (!Result.bSuccess)
{
UE_LOG(LogTemp, Warning, TEXT("%s"), *Result.Error.ToDisplayText());
}
```
```text Output Log theme={null}
Fetch shop item failed: Shop item with ID 'GoldItem' not found [shop_item.shop_item_not_found, HTTP 404]
Fix: No shop item has that id in this game. A purchase (Purchase, Flock Purchase) and GetItem (Flock Get Shop Item) take the shop item's Id, FFlockShopItem::Id, ...
```
`Error.Message` holds what went wrong on its own and `Error.Hint` the fix on its own; neither is ever
empty on a failure.
In Blueprint, every async node has an **On Failure** pin carrying the same `Error`, and
`UFlockErrorLibrary` turns it into display text (**To String (Flock Error)**) or a coded-error check without
matching strings.
The full API reference, per-feature guides, and codegen details live in the
[SDK README](https://github.com/QwackStack/FlockUnrealSdk#readme).
# Welcome
Source: https://docs.qwacks.com/welcome
Build, test, and grow your game on the Qwacks platform.
Welcome to the **Qwacks** developer documentation. Qwacks is a suite of products that span a game's
lifecycle — build the backend, test it, and understand its market. Pick a platform to dive in.
**Game backend** — players, save data, economy, remote config, and analytics, through native
SDKs. **Available now.**
**QA & playtesting** — builds, QA sprints, and live playtest streaming. *Coming soon.*
**Market intelligence** — competitor and store analytics, market reports. *Coming soon.*