# Unreal

**URL:** https://heroiclabs.com/docs/nakama/client-libraries/unreal/
**Summary:** The official C++ client handles all communication in real-time with the server. It implements all features in the server. This guide will show you how to use the core Nakama features in Unreal by showing you how to develop the Nakama specific parts of a game.
**Keywords:** unreal client guide, unreal client, nakama unreal, authentication, user accounts, persistence, storage, wallet, status, groups, chat, dynamic chat, real-world example unreal
**Categories:** nakama, unreal, client-libraries

---


# Nakama Unreal Client Guide

This client library guide will show you how to use the core Nakama features in **Unreal** by showing you how to develop the Nakama specific parts (without full game logic or UI) of an [Among Us (external)](https://www.innersloth.com/games/among-us/) inspired game called Sagi-shi (Japanese for "Imposter").

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/gameplay.png" >}} alt="Sagi-shi gameplay screen">
  <figcaption>Sagi-shi gameplay</figcaption>
</figure>

{{< note "important" "Console Support" >}}
The Unreal SDK includes [console support](../consoles/) for Sony, Microsoft, and Nintendo platforms.
![]({{< fingerprint_image "/images/pages/index/console_logos_min.png" >}})
{{< /note >}}


## Prerequisites

Before proceeding ensure that you have:

* [Installed Nakama server](../../getting-started/install/docker/)
* Installed the [Nakama Unreal SDK](#installation)

### Installation

The client is available from the:

* [Heroic Labs GitHub releases page](https://github.com/heroiclabs/nakama-unreal/releases)

For full instructions on how to setup your Unreal project and install the Nakama Unreal SDK please view the [Nakama Unreal SDK README](https://github.com/heroiclabs/nakama-unreal) on GitHub.

#### Updates

New versions of the Nakama Unreal Client and the corresponding improvements are documented in the [Changelog](https://github.com/heroiclabs/nakama-unreal/blob/master/CHANGELOG.md).

{{< note "important" "TFunction Support" >}}
As of Nakama Unreal `v2.9.0`, [`TFunction` support](https://docs.unrealengine.com/5.3/en-US/API/Runtime/Core/GenericPlatform/TFunction/) is available, meaning you can now use lambdas as callbacks for all API calls.
{{< /note >}}

### Asynchronous programming

Many of the Nakama APIs are asynchronous and non-blocking.

Sagi-shi calls these methods in a way which does not block the calling thread so that the game is responsive and efficient.

```cpp
FOnAuthUpdate AuthenticationSuccessDelegate;
AuthenticationSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAuthenticationSuccess);

FOnError AuthenticationErrorDelegate;
AuthenticationErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAuthenticationError);

FString DeviceId = TEXT("<DeviceId>");
FString Username = TEXT("NewUser");
bool bCreate = true;
TMap<FString, FString> Vars;
Client->AuthenticateDevice(DeviceId, Username, Create, Vars, AuthenticationSuccessDelegate, AuthenticationErrorDelegate);

/* Define callbacks */

void ASagiShiActor::OnAuthenticationSuccess(UNakamaSession* LoginData)
{
    UE_Log(LogTemp, Log, TEXT("Login successful: %s"), *LoginData->SessionData.Username);
}

void ASagiShiActor::OnAuthenticationError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("Failed to login: %s"), *Error.Message);
}
```

### Handling exceptions

Network programming requires additional safeguarding against connection and payload issues.

API calls in Sagi-shi gracefully handle errors:

```cpp
// Define the error handler function
void ASagiShiActor::OnError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("An error occurred: %s"), *Error.Message);

    if (Error == ENakamaErrorCode::ConnectionError)
    {
        UE_LOG(LogTemp, Error, TEXT("The server is currently unavailable. Check internet connection"));
    }
}

// Declare an error handler delegate and bind it to the error handler function
FOnError ErrorDelegate;
ErrorDelegate.AddDynamic(this, &ASagiShiActor::OnError);

// Pass the error delegate to the API function call
Client->GetUserAccount(Session, SuccessDelegate, ErrorDelegate);
```


### Serializing and deserializing data

This guide makes use of JSON serialization and deserialization using the built in JSON utilities inside Unreal Engine.

To use them you must ensure you have included them in your `*.build.cs` file as follows:

```cpp
PublicDependencyModuleNames.AddRange(new string[] { "Json", "JsonUtilities" });
```

First define a `struct` for your data (You can do this in the `.h` file for your Actor):

```cpp
USTRUCT()
struct FPlayer
{
    GENERATED_BODY()

public:
    UPROPERTY()
    FString Username;

    UPROPERTY()
    uint8 Level;
}
```

To write the `struct` to an `FString` (be sure to `#include "JsonObjectConverter.h"`):

```cpp
FPlayer Player { "PlayerOne", 99 };
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(Player, Json);
```

To read from a JSON string into a `struct`:

```cpp
FString Json = TEXT("{\"username\": \"PlayerTwo\", \"level\": 10}");
FPlayer PlayerTwo;
if (!FJsonObjectConverter::JsonObjectStringToUStruct(Json, &PlayerTwo))
{
    // Error deserializing
}
```

## Getting started

Learn how to get started using the Nakama Client and Socket objects to start building Sagi-shi and your own game.


### Nakama Client

The Nakama Client connects to a Nakama Server and is the entry point to access Nakama features. It is recommended to have one client per server per game.

To create a client for Sagi-shi pass in your server connection details:

```cpp
FString ServerKey = TEXT("defaultkey");
FString Host = TEXT("127.0.0.1");
int32 Port = 7350;

Client = UNakamaClient::CreateDefaultClient(ServerKey, Host, Port);
```

### Nakama Socket

The Nakama Socket is used for gameplay and real-time latency-sensitive features such as chat, parties, matches and RPCs.

From the client create a real time client:

```cpp
RealtimeClient = Client->SetupRealtimeClient(Session);

FOnRealtimeClientConnected ConnectionSuccessDelegate;
ConnectionSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnRealtimeClientConnectionSuccess);

FOnRealtimeClientError ConnectionErrorDelegate;
ConnectionErrorDelegate.AddDynamic(this, &ASagiShiActor::OnRealtimeClientConnectionError);

RealtimeClient->Connect(ConnectionSuccessDelegate, ConnectionErrorDelegate);

/* Define callbacks */

void ASagiShiActor::OnRealtimeClientConnectionSuccess()
{
    UE_LOG(LogTemp, Log, TEXT("Socket connection succeeded."));
}

void ASagiShiActor::OnRealtimeClientConnectionError()
{
    UE_LOG(LogTemp, Log, TEXT("Socket connection failed."));
}
```

## Authentication

Nakama has many [authentication methods](../../concepts/authentication/) and supports creating [custom authentication](../../concepts/authentication/#custom) on the server.

Sagi-shi will use device and Facebook authentication, linked to the same user account so that players can play from multiple devices.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/login.png" >}} alt="Sagi-shi login screen">
  <figcaption>Login screen and Authentication options</figcaption>
</figure>


### Device authentication

Nakama [Device Authentication](../../concepts/authentication/#device) uses the physical device's unique identifier to easily authenticate a user and create an account if one does not exist.

When using only device authentication, you don't need a login UI as the player can automatically authenticate when the game launches.

Authentication is an example of a Nakama feature accessed from a Nakama Client instance.

```cpp
// Typically you would get the system's unique device identifier here.
FString DeviceId = TEXT("<DeviceId>");
FString Username = TEXT("<Username>");
bool bCreate = true;
TMap<FString, FString> Vars;

FOnAuthUpdate AuthenticationSuccessDelegate;
AuthenticationSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAuthenticationSuccess);

FOnError AuthenticationErrorDelegate;
AuthenticationErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAuthenticationError);

// Authenticate with the Nakama server using Device Authentication.
Client->AuthenticateDevice(DeviceId, Username, bCreate, Vars, AuthenticationSuccessDelegate, AuthenticationErrorDelegate);
```


### Facebook authentication

Nakama [Facebook Authentication](../../concepts/authentication/#facebook) is an easy to use authentication method which lets you optionally import the player's Facebook friends and add them to their Nakama Friends list.

```cpp
// Authenticate with the Nakama server using Facebook Authentication.
FString AccessToken = TEXT("<Token>");
FString Username = TEXT("<Username>");
bool bCreate = true;
bool bImportFriends = true;
TMap<FString, FString> Vars;

Client->AuthenticateFacebook(AccessToken, Username, bCreate, bImportFriends, Vars, AuthenticationSuccessDelegate, AuthenticationErrorDelegate);
```

### Console authentication

See the [console authentication](../../concepts/authentication/#console) documentation for more information on how to authenticate players on consoles.

### Custom authentication

Nakama supports [Custom Authentication](../../concepts/authentication/#custom) methods to integrate with additional identity services.

See the [Itch.io custom authentication](../../client-libraries/snippets/custom-authentication/) recipe for an example.

### Linking authentication

Nakama allows players to [Link Authentication](../../concepts/authentication/#link-or-unlink) methods to their account once they have authenticated.


**Linking Device ID authentication**

```cpp
FOnLinkSuccess LinkSuccessDelegate;
LinkSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnLinkSuccess);

FOnError LinkErrorDelegate;
LinkErrorDelegate.AddDynamic(this, &ASagiShiActor::OnLinkError);

// Link Device Authentication to existing player account.
Client->LinkDevice(Session, DeviceId, LinkSuccessDelegate, LinkErrorDelegate);
```


**Linking Facebook authentication**

```cpp
FOnLinkSuccess LinkSuccessDelegate;
LinkSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnLinkSuccess);

FOnError LinkErrorDelegate;
LinkErrorDelegate.AddDynamic(this, &ASagiShiActor::OnLinkError);

// Link Facebook Authentication to existing player account.
Client->LinkFacebook(Session, AccessToken, bImportFriends, LinkSuccessDelegate, LinkErrorDelegate);
```


### Session variables

Nakama [Session Variables](../../concepts/session/#session-variables) can be stored when authenticating and will be available on the client and server as long as the session is active.

Sagi-shi uses session variables to implement analytics, referral and rewards programs and more.

Store session variables by passing them as an argument when authenticating:

```cpp
TMap<FString, FString> Vars = {
    { "DeviceOS", "<OperatingSystem>" },
    { "DeviceModel", "<DeviceModel>" },
    { "GameVersion", "<GameVersion>" },
    { "InviterUserId", "<SomeUserId>" },
};

/// ...

Client->AuthenticateDevice(DeviceId, Username, bCreate, Vars, AuthenticationSuccessDelegate, AuthenticationErrorDelegate);
```

To access session variables:

```cpp
FString DeviceOs = Session->SessionData.Variables["DeviceOS"];
```


### Session lifecycle

Nakama [Sessions](../../concepts/session/) expire after a time set in your server [configuration](../../getting-started/configuration/#session). Expiring inactive sessions is a good security practice.

Nakama provides ways to restore sessions, for example when Sagi-shi players re-launch the game, or refresh tokens to keep the session active while the game is being played.

Use the auth and refresh tokens on the session object to restore or refresh sessions.

Store the tokens for use later:

```cpp
FString AuthToken = Session->SessionData.AuthToken;
FString RefreshToken = Session->SessionData.RefreshToken;
```

Restore a session without having to re-authenticate:

```cpp
Session->RestoreSession(authToken, refreshToken);
```

Check if a session has expired or is close to expiring and refresh it to keep it alive:

```cpp
// Check whether a session has expired or is close to expiry.
FDateTime Now = FDateNow::Now();
FDateTime In1Day = FDateTime::FromUnixTimestamp(Now.ToUnixTimestamp() + 24*60*60);
if (Session->IsExpired(FDateTime::Now()) || Session->IsExpired(In1Day))
{
    // Refresh the existing session (uses Nakama Core API)
    Client->Client->AuthenticateRefresh(Session->UserSession);
}
```

## User accounts

Nakama [User Accounts](../../concepts/user-accounts/) store user information defined by Nakama and custom developer metadata.

Sagi-shi allows players to edit their accounts and stores metadata for things like game progression and in-game items.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/profile.png" >}} alt="Sagi-shi player profile screen">
  <figcaption>Player profile</figcaption>
</figure>


### Get the user account

Many of Nakama's features are accessible with an authenticated session, like [fetching a user account](../../concepts/user-accounts/#fetch-account).

Get a Sagi-shi player's full user account with their basic [user information](../../concepts/user-accounts/#fetch-account) and user id:

```cpp
FOnUserAccountInfo GetAccountSuccessDelegate;
GetAccountSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetAccountSuccess);

FOnError GetAccountErrorDelegate;
GetAccountErrorDelegate.AddDynamic(this, &ASagiShiActor::OnGetAccountError);

Client->GetUserAccount(Session, GetAccountSuccessDelegate, GetAccountErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnGetAccountSuccess(const FNakamaAccount& AccountData)
{
    FString Username = AccountData.User.Username;
    FString AvatarUrl = AccountData.User.AvatarUrl;
    FString UserId = AccountData.User.Id;
}

void ASagiShiActor::OnGetAccountError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("Failed to get user account: %s"), *Error.Message);
}
```


### Update the user account

Nakama provides easy methods to update server stored resources like user accounts.

Sagi-shi players need to be able to update their public profiles:

```cpp
FOnUpdateAccount UpdateAccountSuccessDelegate;
UpdateAccountSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnUpdateAccountSuccess);

FOnError UpdateAccountErrorDelegate;
UpdateAccountErrorDelegate.AddDynamic(this, &ASagiShiActor::OnUpdateAccountError);

FString Username = Text("<NewUsername>");
FString DisplayName = Text("<NewDisplayName>");
FString AvatarUrl = Text("<NewAvatarUrl>");
FString LanguageTag = Text("<NewLanguageTag>");
FString Location = Text("<NewLocation>");
FString Timezone = Text("<NewTimezone>");
Client->UpdateAccount(Session, Username, DisplayName, AvatarUrl, LanguageTag, Location, Timezone, UpdateAccountSuccessDelegate, UpdateAccountErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnUpdateAccountSuccess()
{
    UE_LOG(LogTemp, Log, TEXT("Account successfully updated"));
}

void ASagiShiActor::OnUpdateAccountError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("Failed to get user account: %s"), *Error.Message);
}
```


### Getting users

In addition to getting the current authenticated player's user account, Nakama has a convenient way to get a list of other players' public profiles from their ids or usernames.

Sagi-shi uses this method to display player profiles when engaging with other Nakama features:

```cpp
FOnGetUsers GetUsersSuccessDelegate;
GetUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetUsersSuccess);

FOnError GetUsersErrorDelegate;
GetUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnGetUsersError);

TArray<FString> UserIds = { TEXT("<AnotherUserId>") };
TArray<FString> Usernames = { TEXT("<AnotherUsername>") };
TArray<FString> FacebookIds = { TEXT("<AnotherFacebookId>") };

Client->GetUsers(Session, UserIds, Usernames, FacebookIds, GetUsersSuccessDelegate, GetUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnGetUsersSuccess(const TArray<FNakamaUser>& Users)
{
    UE_LOG(LogTemp, Log, TEXT("Successfully retrieved %d users", Users.Num()));
}

void ASagiShiActor::OnGetUsersError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("Error retrieving users: %s"), *Error.Message);
}
```


### Storing metadata

Nakama [User Metadata](../../concepts/user-accounts/#user-metadata) allows developers to extend user accounts with public user fields.

User metadata can only be updated on the server. See the [updating user metadata](../snippets/user-metadata/) recipe for an example.

Sagi-shi will use metadata to store what in-game items players have equipped:


### Reading metadata

Define a class that describes the metadata and parse the JSON metadata:

```cpp
USTRUCT()
struct FUserMetadata
{
    GENERATED_BODY()

public:
    UPROPERTY()
    FString Title;

    UPROPERTY()
    FString Hat;

    UPROPERTY()
    FString Skin;
}

FOnUserAccountInfo GetUserAccountSuccessDelegate;
GetUserAccountSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetUserAccountSuccess);

FOnError GetUserAccountErrorDelegate;
GetUserAccountErrorDelegate.AddDynamic(this, &ASagiShiActor::OnGetUserAccountError);

Client->GetUserAccount(Session, GetUserAccountSuccessDelegate, GetUserAccountErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnGetUserAccountSuccess(const FNakamaAccount& AccountData)
{
    FUserMetadata UserMetadata;
    FJsonObjectConverter::JsonObjectStringToUStruct(AccountData.User.Metadata, &UserMetadata);
    UE_LOG(LogTemp, Log, TEXT("Title: %s"), *UserMetadata.Title);
    UE_LOG(LogTemp, Log, TEXT("Hat: %s"), *UserMetadata.Hat);
    UE_LOG(LogTemp, Log, TEXT("Skin: %s"), *UserMetadata.Skin);
}

void ASagiShiActor::OnGetUserAccountError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("Error retrieving metadata: %s"), *Error.Message);
}
```


### Wallets

Nakama [User Wallets](../../concepts/user-accounts/#virtual-wallet) can store multiple digital currencies as key/value pairs of strings/integers.

Players in Sagi-shi can unlock or purchase titles, skins and hats with a virtual in-game currency.


#### Accessing wallets

Parse the JSON wallet data from the user account:

```cpp
USTRUCT()
struct FUserWallet
{
    GENERATED_BODY()

public:
    int32 Coins;
}

void ASagiShiActor::OnGetUserAccountSuccess(const FNakamaAccount& AccountData)
{
    FUserWallet UserWallet;
    FJsonObjectConverter::JsonObjectStringToUStruct(AccountData.User.Wallet, &UserWallet);
    UE_LOG(LogTemp, Log, TEXT("Coins: %d"), UserWallet.Coins);
}
```


#### Updating wallets

Wallets can only be updated on the server. See the [user account virtual wallet](../../concepts/user-accounts/#virtual-wallet) documentation for an example.


## Storage Engine

The Nakama [Storage Engine](../../concepts/storage/collections/) is a distributed and scalable document-based storage solution for your game.

The Storage Engine gives you more control over how data can be [accessed](../../concepts/storage/permissions/#object-permissions) and [structured](../../concepts/storage/collections/#collections) in collections.

Collections are named, and store JSON data under a unique key and the user id.

By default, the player has full permission to create, read, update and delete their own storage objects.

Sagi-shi players can unlock or purchase many items, which are stored in the Storage Engine.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/player-items.png" >}} alt="Sagi-shi player items screen">
  <figcaption>Player items</figcaption>
</figure>


### Reading storage objects

Define a class that describes the storage object and create a new storage object id with the collection name, key and user id. Finally, read the storage objects and parse the JSON data:

```cpp
USTRUCT()
struct FUnlocksStorageObject
{
    GENERATED_BODY()

public:
    UPROPERTY()
    TArray<FString> Hats;
}

FOnStorageObjectsRead ReadStorageObjectsSuccessDelegate;
ReadStorageObjectsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnReadStorageObjectsSuccess);

FOnError ReadStorageObjectsErrorDelegate;
ReadStorageObjectsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnReadStorageObjectsError);

FNakamaReadStorageObjectId Id;
Id.Collection = TEXT("Unlocks");
Id.Key = TEXT("Hats");
Id.UserId = Session->SessionData.UserId;
TArray<FNakamaReadStorageObjectId> StorageObjectIds = { Id };

Client->ReadStorageObjects(Session, StorageObjectIds, ReadStorageObjectsSuccessDelegate, ReadStorageObjectsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnReadStorageObjectsSuccess(const FNakamaStorageObjectList& StorageObjects)
{
	if (StorageObjects.Num() != 1)
    {
		UE_LOG(LogTemp, Warning, TEXT("Incorrect amount of storage objects %d."), StorageObjects.Num());	// Withoug opening the code I don't recall if this was the correct way of formatting strings, I think it is.
		return;
    }
	FUnlocksStorageObject HatsStorageObject;
	if (!FJsonObjectConverter::JsonObjectStringToUStruct(StorageObjects[0].Value, &HatsStorageObject, 0, 0))
	{
		UE_LOG(LogTemp, Warning, TEXT("Failed to deserialize storage object."));
	}
	UE_LOG(LogTemp, Log, TEXT("Unlocked Hats:"));
	for (int32 i = 0; i < HatsStorageObject.Items.Num(); i++)
	{
		UE_LOG(LogTemp, Log, TEXT("%s"), *HatsStorageObject.Items[i]);
	}
}

void ASagiShiActor::OnReadStorageObjectsError(const FNakamaError& Error)
{
    UE_LOG(LogTemp, Error, TEXT("Error reading storage objects: %s"), *Error.Message);
}
```

To read other players' public storage objects use their `UserId` instead. Remember that players can only read storage objects they own or that are public (`PermissionRead` value of `2`).

### Writing storage objects

Nakama allows developers to write to the Storage Engine from the client and server.

Consider what adverse effects a malicious user can have on your game and economy when deciding where to put your write logic, for example data that should only be written authoritatively (i.e. game unlocks or progress).

Sagi-shi allows players to favorite items for easier access in the UI and it is safe to write this data from the client.

Create a write storage object with the collection name, key and JSON encoded data. Finally, write the storage objects to the Storage Engine:

```cpp
FUnlocksStorageObject HatsStorageObject;
HatsStorageObject.Items.Add(TEXT("cowboy"));
HatsStorageObject.Items.Add(TEXT("alien"));

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(HatsStorageObject, Json);

FNakamaStoreObjectWrite WriteObject;
WriteObject.Collection = "Unlocks";
WriteObject.Key = "Hats";
WriteObject.PermissionWrite = ENakamaStoragePermissionWrite::OWNER_WRITE;
WriteObject.PermissionRead = ENakamaStoragePermissionRead::OWNER_READ;
WriteObject.Value = Json;

TArray<FNakamaStoreObjectWrite> StorageObjectsData = { WriteObject };

FOnStorageObjectAcks WriteStorageObjectsSuccessDelegate;
WriteStorageObjectsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnWriteStorageObjectsSuccess);

FOnError WriteStorageObjectsErrorDelegate;
WriteStorageObjectsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnWriteStorageObjectsError);

Client->WriteStorageObjects(Session, StorageObjectsData, WriteStorageObjectsSuccessDelegate, WriteStorageObjectsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnWriteStorageObjectsSuccess(const FNakamaStoreObjectAcks& StorageObjectAcks)
{
	UE_LOG(LogTemp, Log, TEXT("Success writing storage object"));
}

void ASagiShiActor::OnWriteStorageObjectsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error writing storage object: %s"), *Error.Message);
}
```

You can also pass multiple write objects:

```cpp
TArray<FNakamaStoreObjectWrite> StorageObjectsData = { WriteObject1, WriteObject2, WriteObject3 };
Client->WriteStorageObjects(Session, StorageObjectsData, WriteStorageObjectsSuccessDelegate, WriteStorageObjectsErrorDelegate);
```


### Conditional writes

Storage Engine [Conditional Writes](../../concepts/storage/collections/#conditional-writes) ensure that write operations only happen if the object hasn't changed since you accessed it.

This gives you protection from overwriting data, for example the Sagi-shi server could have updated an object since the player last accessed it.

To perform a conditional write, add a version to the write storage object with the most recent object version:

```cpp
// Assuming we already have a storage object (WriteObject).
FNakamaStoreObjectWrite WriteObject2;
WriteObject2.Collection = "Unlocks";
WriteObject2.Key = "Hats";
WriteObject2.PermissionWrite = ENakamaStoragePermissionWrite::OWNER_WRITE;
WriteObject2.PermissionRead = ENakamaStoragePermissionRead::OWNER_READ;
WriteObject2.Value = Json;
WriteObject2.Version = WriteObject.Version

TArray<FNakamaStoreObjectWrite> StorageObjectsData = { WriteObject2 };

Client->WriteStorageObjects(Session, StorageObjectsData, WriteStorageObjectsSuccessDelegate, WriteStorageObjectsErrorDelegate);
```


### Listing storage objects

Instead of doing multiple read requests with separate keys you can list all the storage objects the player has access to in a collection.

Sagi-shi lists all the player's unlocked or purchased titles, hats and skins:

```cpp
FString Collection = TEXT("Unlocks");
int32 Limit = 3;
FString Cursor = TEXT("");

FOnStorageObjectsListed ListStorageObjectsSuccessDelegate;
ListStorageObjectsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListStorageObjectsSuccess);

FOnError ListStorageObjectsErrorDelegate;
ListStorageObjectsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListStorageObjectsError);

Client->ListStorageObjects(Session, Collection, Session->SessionData.UserId, Limit, Cursor, ListStorageObjectsSuccessDelegate, ListStorageObjectsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListStorageObjectsSuccess(const FNakamaStorageObjectList& StorageObjects)
{
	for (int16 i = 0; i < StorageObjects.Objects.Num(); i++)
	{
		FUnlocksStorageObject StorageObject;
		FJsonObjectConverter::JsonObjectStringToUStruct(StorageObjects.Objects[i].Value, &StorageObject);

		UE_LOG(LogTemp, Log, TEXT("%s Unlocks:"), *StorageObjects.Objects[i].Key);
		for (int16 x = 0; x < StorageObject.Items.Num(); x++)
		{
			UE_LOG(LogTemp, Log, TEXT("%s"), *StorageObject.Items[x]);
		}
	}
}

void ASagiShiActor::OnListStorageObjectsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error reading storage objects: %s"), *Error.Message);
}
```


### Paginating results

Nakama methods that list results return a cursor which can be passed to subsequent calls to Nakama to indicate where to start retrieving objects from in the collection.

For example:
- If the cursor has a value of 5, you will get results from the fifth object.
- If the cursor is `null`, you will get results from the first object.

```cpp
Client->ListStorageObjects(Session, Collection, Session->SessionData.UserId, Limit, StorageObjects.Cursor, ListStorageObjectsSuccessDelegate, ListStorageObjectsErrorDelegate);
```


### Protecting storage operations on the server

Nakama Storage Engine operations can be protected on the server to protect data the player shouldn't be able to modify (i.e.  game unlocks or progress). See the [writing to the Storage Engine authoritatively](../snippets/authoritative-write/) recipe.


## Remote Procedure Calls

The Nakama [Server](../../server-framework/) allows developers to write custom logic and expose it to the client as [RPCs](../../server-framework/introduction/#functionality).

Sagi-shi contains various logic that needs to be protected on the server, like checking if the player owns equipment before equipping it.


### Creating server logic

See the [handling player equipment authoritatively](../snippets/authoritative-read/) recipe for an example of creating a remote procedure to check if the player owns equipment before equipping it.


### Client RPCs

Nakama Remote Procedures can be called from the client and take optional JSON payloads.

The Sagi-shi client makes an RPC to securely equip a hat:

```cpp
FOnRPC RpcSuccessDelegate;
RpcSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnRpcSuccess);

FOnError RpcErrorDelegate;
RpcErrorDelegate.AddDynamic(this, &ASagiShiActor::OnRpcError);

Client->RPC(Session, "EquipHat", "<PayloadString>", RpcSuccessDelegate, RpcErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnRpcSuccess(const FNakamaRPC& RPC)
{
	UE_LOG(LogTemp, Log, TEXT("New hat successfully equipped"));
}

void ASagiShiActor::OnRpcError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error reading storage objects: %s"), *Error.Message);
}
```

## Friends

Nakama [Friends](../../concepts/friends/) offers a complete social graph system to manage friendships amongst players.

Sagi-shi allows players to add friends, manage their relationships and play together.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/friends.png" >}} alt="Sagi-shi Friends screen">
  <figcaption>Friends screen</figcaption>
</figure>


### Adding friends

Adding a friend in Nakama does not immediately add a mutual friend relationship. An outgoing friend request is created to each user, which they will need to accept.

Sagi-shi allows players to add friends by their usernames or user ids:


```cpp
auto successCallback = []()
{
    cout << "Successfully added friends" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error adding friends " << error.message << endl;
};

TArray<FString> Ids = { "<SomeUserId>", "<AnotherUserId>" };
TArray<FString> Usernames = { "AlwaysTheImposter21", "SneakyBoi" };

FOnAddedFriend AddFriendsSuccessDelegate;
AddFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAddFriendsSuccess);

FOnError AddFriendsErrorDelegate;
AddFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddFriendsError);

Client->AddFriends(Session, Ids, Usernames, AddFriendsSuccessDelegate, AddFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnAddFriendsSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully added friends"));
}

void ASagiShiActor::OnAddFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Log, TEXT("Error adding friends: %s"), *Error.Message);
}
```


### Friendship states

Nakama friendships are categorized with the following states:

| Value | State |
| 
----- | ----- |
| 0 | Mutual friends |
| 1 | An outgoing friend request pending acceptance |
| 2 | An incoming friend request pending acceptance |
| 3 | Blocked by the user |


### Listing friends

Nakama allows developers to list the player's friends based on their friendship state.

Sagi-shi lists the 20 most recent mutual friends:

```cpp
int32 Limit = 20;
ENakamaFriendState FriendshipState = ENakamaFriendState::FRIEND;
FString Cursor = TEXT("");

FOnFriendsList GetFriendsSuccessDelegate;
GetFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsSuccess);

FOnError GetFriendsErrorDelegate;
GetFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddFriendsError);

Client->GetFriends(Session, Limit, FriendshipState, Cursor, GetFriendsSuccessDelegate, GetFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnGetFriendsSuccess(FNakamaFriendList Friends)
{
	UE_LOG(LogTemp, Log, TEXT("Friends:"));
	for (int16 i = 0; i < Friends.NakamaUsers.Num(); i++)
	{
		UE_LOG(LogTemp, Log, TEXT("ID: %s"), *Friends.NakamaUsers[i].NakamaUser.Id);
	}
}

void ASagiShiActor::OnGetFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error retrieving friends: %s"), *Error.Message);
}
```


### Accepting friend requests

When accepting a friend request in Nakama the player adds a [bi-directional friend relationship](../../concepts/friends/best-practices/#modeling-relationships).

Nakama takes care of changing the state from pending to mutual for both.

In a complete game you would allow players to accept individual requests.

Sagi-shi just fetches and accepts all the incoming friend requests:

```cpp
int32 Limit = 1000;
ENakamaFriendState FriendshipState = ENakamaFriendState::INVITE_RECEIVED;
FString Cursor = TEXT("");

FOnFriendsList GetFriendsSuccessDelegate;
GetFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsSuccess);

FOnError GetFriendsErrorDelegate;
GetFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddFriendsError);

Client->GetFriends(Session, Limit, FriendshipState, Cursor, GetFriendsSuccessDelegate, GetFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnGetFriendsSuccess(FNakamaFriendList Friends)
{
	for (int16 i = 0; i < Friends.NakamaUsers.Num(); i++)
	{
    // Subsequent delegate handlers omitted for brevity
		FOnAddedFriend OnAddFriendSuccessDelegate;
		FOnError OnAddFriendErrorDelegate;

		Client->AddFriends(Session, { Friends.NakamaUsers[i].NakamaUser.Id }, {}, OnAddFriendSuccessDelegate, OnAddFriendErrorDelegate);
	}
}

void ASagiShiActor::OnGetFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Log, TEXT("Error retrieving friends: %s"), *Error.Message);
}
```


### Deleting friends

Sagi-shi players can decline friend requests and remove friends by their username or user id:

```cpp
FOnRemovedFriends RemoveFriendsSuccessDelegate;
RemoveFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnRemoveFriendsSuccess);

FOnError RemoveFriendsErrorDelegate;
RemoveFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnRemoveFriendsError);

// Delete friends by User ID
Client->RemoveFriends(Session, { "<SomeUserId>", "<AnotherUserId>" }, {}, RemoveFriendsSuccessDelegate, RemoveFriendsErrorDelegate);

// Delete friends by Username
Client->RemoveFriends(Session, {}, { "<SomeUsername>", "<AnotherUsername>" }, RemoveFriendsSuccessDelegate, RemoveFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnRemoveFriendsSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully deleted friends"));
}

void ASagiShiActor::OnRemoveFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error deleting friends: %s"), *Error.Message);
}
```


### Blocking users

Sagi-shi players can block others by their username or user id:


```cpp
FOnBlockedFriends BlockFriendsSuccessDelegate;
BlockFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnBlockedFriendsSuccess);

FOnError BlockFriendsErrorDelegate;
BlockFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnBlockedFriendsError);

// Block friends by User ID
Client->BlockFriends(Session, { "<SomeUserId>", "<AnotherUserId>" }, {}, BlockFriendsSuccessDelegate, BlockFriendsErrorDelegate);

// Block friends by Username
Client->BlockFriends(Session, {}, { "<SomeUsername>", "<AnotherUsername>" }, BlockFriendsSuccessDelegate, BlockFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnBlockedFriendsSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully blocked friends"));
}

void ASagiShiActor::OnBlockedFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error blocking friends: %s"), *Error.Message);
}
```


## Status & Presence

Nakama [Status](../../concepts/status/) is a real-time status and presence service that allows users to set their online presence, update their status message and follow other user's updates.

Players don't have to be friends with others they want to follow.

Sagi-shi uses status messages and online presences to notify players when their friends are online and share matches.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/status.png" >}} alt="Sagi-shi status update screen">
  <figcaption>Updating player status</figcaption>
</figure>


### Follow users

The Nakama real-time APIs allow developers to subscribe to events on the socket, like a status presence change, and receive them in real-time.

The method to follow users also returns the current online users, known as presences, and their status.

Sagi-shi follows a player's friends and notifies them when they are online:

```cpp
// Subscribe to the Status Presence event
FOnReceivedStatusPresence ReceivedStatusPresenceDelegate;
ReceivedStatusPresenceDelegate.AddDynamic(this, &ASagiShiActor::OnReceivedStatusPresence);

RealtimeClient->PresenceStatusReceived = ReceivedStatusPresenceDelegate;
RealtimeClient->SetListenerStatusPresenceCallback();

// Get mutual friends
int32 Limit = 1000;
ENakamaFriendState FriendshipState = ENakamaFriendState::FRIEND;
FString Cursor = TEXT("");

FOnFriendsList GetFriendsSuccessDelegate;
GetFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsSuccess);

FOnError GetFriendsErrorDelegate;
GetFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddFriendsError);

Client->GetFriends(Session, Limit, FriendshipState, Cursor, GetFriendsSuccessDelegate, GetFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnReceivedStatusPresence(const FNakamaStatusPresenceEvent& UserPresenceData)
{
	for (int16 i = 0; i < UserPresenceData.Leaves.Num(); i++)
	{
		FNakamaUserPresence User = UserPresenceData.Leaves[i];
		UE_LOG(LogTemp, Log, TEXT("%s (%s) left status: %s"), *User.Username, *User.UserID, *User.Status);
	}

	for (int16 i = 0; i < UserPresenceData.Joins.Num(); i++)
	{
		FNakamaUserPresence User = UserPresenceData.Joins[i];
		UE_LOG(LogTemp, Log, TEXT("%s (%s) new status: %s"), *User.Username, *User.UserID, *User.Status);
	}
}

void ASagiShiActor::OnGetFriendsSuccess(FNakamaFriendList Friends)
{
	for (int16 i = 0; i < Friends.NakamaUsers.Num(); i++)
	{
		FOnFollowUsers FollowUsersSuccessDelegate;
		FollowUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnFollowUsersSuccess);

		FOnRtError FollowUsersErrorDelegate;
		FollowUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnFollowUsersError);

		RealtimeClient->FollowUsers({ Friends.NakamaUsers[i].NakamaUser.Id }, FollowUsersSuccessDelegate, FollowUsersErrorDelegate);
	}
}

void ASagiShiActor::OnGetFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error retrieving friends: %s"), *Error.Message);
}

void ASagiShiActor::OnFollowUsersSuccess(const FNakamaStatus& Status)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully followed users"));

	// Get the initial Status of any that are currently online.
	for (int16 i = 0; i < Status.Presences.Num(); i++)
	{
		FNakamaUserPresence User = Status.Presences[i];
		UE_LOG(LogTemp, Log, TEXT("%s (%s) current status: %s"), *User.Username, *User.UserID, *User.Status);
	}
}

void ASagiShiActor::OnFollowUsersError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error following users: %s"), *Error.Message);
}
```


### Unfollow users

Sagi-shi players can unfollow others:

```cpp
FOnUnFollowUsers UnfollowUsersSuccessDelegate;
UnfollowUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnUnfollowUsersSuccess);

FOnRtError UnfollowUsersErrorDelegate;
UnfollowUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnUnfollowUsersError);


RealtimeClient->UnFollowUsers({ "<UserId>" }, UnfollowUsersSuccessDelegate, UnfollowUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnUnfollowUsersSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully unfollowed users"));
}

void ASagiShiActor::OnUnfollowUsersError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error unfollowing users: %s"), *Error.Message);
}
```


### Updating player status

Sagi-shi players can change and publish their status to their followers:

```cpp
FOnSetStatus SetStatusSuccessDelegate;
SetStatusSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnSetStatusSuccess);

FOnRtError SetStatusErrorDelegate;
SetStatusErrorDelegate.AddDynamic(this, &ASagiShiActor::OnSetStatusError);

RealtimeClient->UpdateStatus(TEXT("Viewing the Main Menu"), SetStatusSuccessDelegate, SetStatusErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnSetStatusSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully updated status"));
}

void ASagiShiActor::OnSetStatusError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error updating status: %s"), *Error.Message);
}
```


## Groups

Nakama [Groups](../../concepts/groups/) is a group or clan system with public/private visibility, user memberships and permissions, metadata and group chat.

Sagi-shi allows players to form and join groups to socialize and compete.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/groups-list.png" >}} alt="Sagi-shi groups screen">
  <figcaption>Groups list screen</figcaption>
</figure>


### Creating groups

Groups have a public or private "open" visibility. Anyone can join public groups, but they must request to join and be accepted by a superadmin/admin of a private group.

Sagi-shi players can create groups around common interests:

```cpp
FString Name = TEXT("Imposters R Us");
FString Description = TEXT("A group for people who love playing the imposters.");
FString AvatarUrl = TEXT("");
FString LangTag = TEXT("");
bool bOpen = true;
int32 MaxSize = 100;

FOnCreateGroup CreateGroupSuccessDelegate;
CreateGroupSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnCreateGroupSuccess);

FOnError CreateGroupErrorDelegate;
CreateGroupErrorDelegate.AddDynamic(this, &ASagiShiActor::OnCreateGroupError);

Client->CreateGroup(Session, Name, Description, AvatarUrl, LangTag, bOpen, MaxSize, CreateGroupSuccessDelegate, CreateGroupErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnCreateGroupSuccess(FNakamaGroup Group)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully created group: %s"), *Group.Id);
}

void ASagiShiActor::OnCreateGroupError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error creating group: %s"), *Error.Message);
}
```


### Update group visibility

Nakama allows group superadmin or admin members to update some properties from the client, like the open visibility:

```cpp
FString GroupId = TEXT("<GroupId>");
FString Name = TEXT("New Name");
FString Description = TEXT("New description");
FString AvatarUrl = TEXT("");
FString LangTag = TEXT("");
bool bOpen = true;

FOnUpdateGroup UpdateGroupSuccessDelegate;
UpdateGroupSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnUpdateGroupSuccess);

FOnError UpdateGroupErrorDelegate;
UpdateGroupErrorDelegate.AddDynamic(this, &ASagiShiActor::OnUpdateGroupError);

Client->UpdateGroup(Session, GroupId, Name, Description, AvatarUrl, LangTag, bOpen, UpdateGroupSuccessDelegate, UpdateGroupErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnUpdateGroupSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully updated group"));
}

void ASagiShiActor::OnUpdateGroupError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error updating group: %s"), *Error.Message);
}
```


### Update group size

Other properties, like the group's maximum member size, can only be changed on the server.

See the [updating group size](../../concepts/groups/#updating-group-size) recipe for an example, and the [Groups server function reference](../../server-framework/typescript-runtime/function-reference/#Groups) to learn more about updating groups on the server.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/group-edit.png" >}} alt="Sagi-shi group edit screen">
  <figcaption>Sagi-shi group edit</figcaption>
</figure>


### Listing and filtering groups

Groups can be listed like other Nakama resources and also [filtered](../../concepts/groups/#list-and-filter-groups) with a wildcard group name.

Sagi-shi players use group listing and filtering to search for existing groups to join:


```cpp
FString GroupNameFilter = TEXT("imposter%");
int32 Limit = 20;
FString Cursor = TEXT("");

FOnGroupsList ListGroupsSuccessDelegate;
ListGroupsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListGroupsSuccess);

FOnError ListGroupsErrorDelegate;
ListGroupsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListGroupsError);

Client->ListGroups(Session, GroupNameFilter, Limit, Cursor, ListGroupsSuccessDelegate, ListGroupsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListGroupsSuccess(const FNakamaGroupList& Groups)
{
	for (int16 i = 0; i < Groups.Groups.Num(); i++)
	{
		UE_LOG(LogTemp, Log, TEXT("%s: %s"), *Groups.Groups[i].Name, Groups.Groups[i].open ? TEXT("Public") : TEXT("Private"));
	}
}

void ASagiShiActor::OnListGroupsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing groups: %s"), *Error.Message);
}
```


### Deleting groups

Nakama allows group superadmins to delete groups.

Developers can disable this feature entirely, see the [Guarding APIs guide](../../guides/server-framework/guarding-apis/) for an example on how to protect various Nakama APIs.

Sagi-shi players can delete groups which they are superadmins for:

```cpp
FString GroupId = TEXT("<GroupId>");
FOnRemoveGroup DeleteGroupSuccessDelegate;
DeleteGroupSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnDeleteGroupSuccess);

FOnError DeleteGroupErrorDelegate;
DeleteGroupErrorDelegate.AddDynamic(this, &ASagiShiActor::OnDeleteGroupError);

Client->DeleteGroup(Session, GroupId, DeleteGroupSuccessDelegate, DeleteGroupErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnDeleteGroupSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully deleted group"));
}

void ASagiShiActor::OnDeleteGroupError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error deleting groups: %s"), *Error.Message);
}
```


### Group metadata

Like Users Accounts, Groups can have public metadata.

Sagi-shi uses group metadata to store the group's interests, active player times and languages spoken.

Group metadata can only be updated on the server. See the [updating group metadata](../snippets/group-metadata/) recipe for an example.

The Sagi-shi client makes an RPC with the group metadata payload:

```cpp
/* Define a metadata struct */
USTRUCT()
struct FGroupMetadata
{
	GENERATED_BODY()

public:
	UPROPERTY()
	TArray<FString> Interests;

	UPROPERTY()
	TArray<FString> ActiveTimes;

	UPROPERTY()
	TArray<FString> Languages;
};

FString GroupId = TEXT("<GroupId>");
FGroupMetadata GroupMetadata;
GroupMetadata.Interests = { "Deception", "Sabotage", "Cute Furry Bunnies" };
GroupMetadata.ActiveTimes = { "9am-2pm Weekdays", "9am-10am Weekends" };
GroupMetadata.Languages = { "English", "German" };

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(GroupMetadata, Json);

FOnRPC RpcSuccessDelegate;
RpcSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnRpcSuccess);

FOnError RpcErrorDelegate;
RpcErrorDelegate.AddDynamic(this, &ASagiShiActor::OnRpcError);

Client->RPC(Session, TEXT("UpdateGroupMetadata"), Json, RpcSuccessDelegate, RpcErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnRpcSuccess(const FNakamaRPC& RPC)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully updated group metadata"));
}

void ASagiShiActor::OnRpcError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error updating group metadata: %s"), *Error.Message);
}
```


### Group membership states

Nakama group memberships are categorized with the following states:

| Code | Purpose | |
| ---- | ------- | - |
|    0 | Superadmin | There must at least be 1 superadmin in any group. The superadmin has all the privileges of the admin and can additionally delete the group and promote admin members. |
|    1 | Admin | There can be one of more admins. Admins can update groups as well as accept, kick, promote, demote, ban or add members. |
|    2 | Member | Regular group member. They cannot accept join requests from new users. |
|    3 | Join request | A new join request from a new user. This does not count towards the maximum group member count. |


### Joining a group

If a player joins a public group they immediately become a member, but if they try and join a private group they must be accepted by a group admin.

Sagi-shi players can join a group:

```cpp
FString GroupId = TEXT("<GroupId>");

FOnJoinedGroup JoinGroupSuccessDelegate;
JoinGroupSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinGroupSuccess);

FOnError JoinGroupErrorDelegate;
JoinGroupErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinGroupError);

Client->JoinGroup(Session, GroupId, JoinGroupSuccessDelegate, JoinGroupErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnJoinGroupSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully joined group"));
}

void ASagiShiActor::OnJoinGroupError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error joining group: %s"), *Error.Message);
}
```


### Listing the user's groups

Sagi-shi players can list groups they are a member of:

```cpp
int32 Limit = 100;
ENakamaGroupState State = ENakamaGroupState::MEMBER;
FString Cursor = TEXT("");

FOnUserGroups ListUserGroupsSuccessDelegate;
ListUserGroupsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListUserGroupsSuccess);

FOnError ListUserGroupsErrorDelegate;
ListUserGroupsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListUserGroupsError);

Client->ListUserGroups(Session, Session->SessionData.UserId, Limit, State, Cursor, ListUserGroupsSuccessDelegate, ListUserGroupsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListUserGroupsSuccess(const FNakamaUserGroupList& UserGroups)
{
	for (int16 i = 0; i < UserGroups.UserGroups.Num(); i++)
	{
		UE_LOG(LogTemp, Log, TEXT("%s"), *UserGroups.UserGroups[i].Group.Name);
	}
}

void ASagiShiActor::OnListUserGroupsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing user groups: %s"), *Error.Message);
}
```


### Listing members

Sagi-shi players can list a group's members:

```cpp
FString GroupId = TEXT("<GroupId>");
int32 Limit = 100;
ENakamaGroupState State = ENakamaGroupState::MEMBER;
FString Cursor = TEXT("");

FOnListGroupMembers ListGroupUsersSuccessDelegate;
ListGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListGroupUsersSuccess);

FOnError ListGroupUsersErrorDelegate;
ListGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListGroupUsersError);

Client->ListGroupUsers(Session, GroupId, Limit, State, Cursor, ListGroupUsersSuccessDelegate, ListGroupUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListGroupUsersSuccess(const FNakamaGroupUsersList& GroupUsers)
{
	for (int16 i = 0; i < GroupUsers.GroupUsers.Num(); i++)
	{
		UE_LOG(LogTemp, Log, TEXT("%s"), *GroupUsers.GroupUsers[i].User.Username);
	}
}

void ASagiShiActor::OnListGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing group users: %s"), *Error.Message);
}
```


### Accepting join requests

Private group admins or superadmins can accept join requests by re-adding the user to the group.

Sagi-shi first lists all the users with a join request state and then loops over and adds them to the group:


```cpp
FString GroupId = TEXT("<GroupId>");
int32 Limit = 100;
ENakamaGroupState State = ENakamaGroupState::JOIN_REQUEST;
FString Cursor = TEXT("");

FOnListGroupMembers ListGroupUsersSuccessDelegate;
ListGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListGroupUsersSuccess);

FOnError ListGroupUsersErrorDelegate;
ListGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListGroupUsersError);

Client->ListGroupUsers(Session, GroupId, Limit, State, Cursor, ListGroupUsersSuccessDelegate, ListGroupUsersErrorDelegate);

void ASagiShiActor::OnListGroupUsersSuccess(const FNakamaGroupUsersList& GroupUsers)
{
	for (int16 i = 0; i < GroupUsers.GroupUsers.Num(); i++)
	{
		FOnAddGroupUsers AddGroupUsersSuccessDelegate;
		AddGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAddGroupUsersSuccess);

		FOnError AddGroupUsersErrorDelegate;
		AddGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddGroupUsersError);

		Client->AddGroupUsers(Session, TEXT("<GroupId>"), {GroupUsers.GroupUsers[i].User.Id}, AddGroupUsersSuccessDelegate, AddGroupUsersErrorDelegate);
		UE_LOG(LogTemp, Log, TEXT("%s"), *GroupUsers.GroupUsers[i].User.Username);
	}
}

void ASagiShiActor::OnListGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing group users: %s"), *Error.Message);
}

void ASagiShiActor::OnAddGroupUsersSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully added group users"));
}

void ASagiShiActor::OnAddGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error adding group users: %s"), *Error.Message);
}
```


### Promoting members

Nakama group members can be promoted to admin or superadmin roles to help manage a growing group or take over if members leave.

Admins can promote other members to admins, and superadmins can promote other members up to superadmins.

The members will be promoted up one level. For example:

- Promoting a member will make them an admin
- Promoting an admin will make them a superadmin

```cpp
FString GroupId = TEXT("<GroupId>");

FOnPromoteGroupUsers PromoteGroupUsersSuccessDelegate;
PromoteGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnPromoteGroupUsersSuccess);

FOnError PromoteGroupUsersErrorDelegate;
PromoteGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnPromoteGroupUsersError);

Client->PromoteGroupUsers(Session, GroupId, { TEXT("<UserId>") }, PromoteGroupUsersSuccessDelegate, PromoteGroupUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnPromoteGroupUsersSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully promoted group users"));
}

void ASagiShiActor::OnPromoteGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error promoting group users: %s"), *Error.Message);
}
```


### Demoting members

Sagi-shi group admins and superadmins can demote members:

```cpp
FString GroupId = TEXT("<GroupId>");

FOnDemoteGroupUsers DemoteGroupUsersSuccessDelegate;
DemoteGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnDemoteGroupUsersSuccess);

FOnError DemoteGroupUsersErrorDelegate;
DemoteGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnDemoteGroupUsersError);

Client->DemoteGroupUsers(Session, GroupId, { TEXT("<UserId>") }, DemoteGroupUsersSuccessDelegate, DemoteGroupUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnDemoteGroupUsersSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully demoted group users"));
}

void ASagiShiActor::OnDemoteGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error demoting group users: %s"), *Error.Message);
}
```


### Kicking members

Sagi-shi group admins and superadmins can remove group members:

```cpp
FString GroupId = TEXT("<GroupId>");

FOnKickGroupUsers KickGroupUsersSuccessDelegate;
KickGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnKickGroupUsersSuccess);

FOnError KickGroupUsersErrorDelegate;
KickGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnKickGroupUsersError);

Client->KickGroupUsers(Session, GroupId, { TEXT("<UserId>") }, KickGroupUsersSuccessDelegate, KickGroupUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnKickGroupUsersSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully kicked group users"));
}

void ASagiShiActor::OnKickGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error kicking group users: %s"), *Error.Message);
}
```

### Banning members

Sagi-shi group admins and superadmins can ban a user when demoting or kicking is not severe enough:

```cpp
FString GroupId = TEXT("<GroupId>");

FOnBanGroupUsers BanGroupUsersSuccessDelegate;
BanGroupUsersSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnBanGroupUsersSuccess);

FOnError BanGroupUsersErrorDelegate;
BanGroupUsersErrorDelegate.AddDynamic(this, &ASagiShiActor::OnBanGroupUsersError);

Client->BanGroupUsers(Session, GroupId, { TEXT("<UserId>") }, BanGroupUsersSuccessDelegate, BanGroupUsersErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnBanGroupUsersSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully banned group users"));
}

void ASagiShiActor::OnBanGroupUsersError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error banning group users: %s"), *Error.Message);
}
```

### Leaving groups

Sagi-shi players can leave a group:

```cpp
FString GroupId = TEXT("<GroupId>");

FOnLeaveGroup LeaveGroupSuccessDelegate;
LeaveGroupSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnLeaveGroupSuccess);

FOnError LeaveGroupErrorDelegate;
LeaveGroupErrorDelegate.AddDynamic(this, &ASagiShiActor::OnLeaveGroupError);

Client->LeaveGroup(Session, GroupId, LeaveGroupSuccessDelegate, LeaveGroupErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnLeaveGroupSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully left group"));
}

void ASagiShiActor::OnLeaveGroupError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error leaving group: %s"), *Error.Message);
}
```


## Chat

Nakama [Chat](../../concepts/chat/) is a real-time chat system for groups, private/direct messages and dynamic chat rooms.

Sagi-shi uses dynamic chat during matches, for players to mislead each other and discuss who the imposters are, group chat and private/direct messages.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/chat.png" >}} alt="Sagi-shi chat screen">
  <figcaption>Sagi-shi Chat</figcaption>
</figure>


### Joining dynamic rooms

Sagi-shi matches have a non-persistent chat room for players to communicate in:

```cpp
FString RoomName = TEXT("<MatchId>");
bool Persistence = false;
bool Hidden = false;

FOnJoinChat JoinChatSuccessDelegate;
JoinChatSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinChatSuccess);

FOnRtError JoinChatErrorDelegate;
JoinChatErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinChatError);

RealtimeClient->JoinChat(RoomName, ENakamaChannelType::ROOM, Persistence, Hidden, JoinChatSuccessDelegate, JoinChatErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnJoinChatSuccess(FNakamaChannel Channel)
{
	UE_LOG(LogTemp, Log, TEXT("Connected to channel: %s"), *Channel.Id);
}

void ASagiShiActor::OnJoinChatError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error connecting to channel: %s"), *Error.Message);
}
```


### Joining group chat

Sagi-shi group members can have conversations that span play sessions in a persistent group chat channel:

```cpp
FString GroupId = TEXT("<GroupId>");
bool Persistence = false;
bool Hidden = false;

FOnJoinChat JoinChatSuccessDelegate;
JoinChatSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinChatSuccess);

FOnRtError JoinChatErrorDelegate;
JoinChatErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinChatError);

RealtimeClient->JoinChat(GroupId, ENakamaChannelType::GROUP, Persistence, Hidden, JoinChatSuccessDelegate, JoinChatErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnJoinChatSuccess(FNakamaChannel Channel)
{
	UE_LOG(LogTemp, Log, TEXT("Connected to channel: %s"), *Channel.Id);
}

void ASagiShiActor::OnJoinChatError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error connecting to channel: %s"), *Error.Message);
}
```


### Joining direct chat

Sagi-shi players can also chat privately one-to-one during or after matches and view past messages:

```cpp
FString UserId = TEXT("<UserId>");
bool Persistence = true;
bool Hidden = false;

FOnJoinChat JoinChatSuccessDelegate;
JoinChatSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinChatSuccess);

FOnRtError JoinChatErrorDelegate;
JoinChatErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinChatError);

RealtimeClient->JoinChat(UserId, ENakamaChannelType::DIRECT_MESSAGE, Persistence, Hidden, JoinChatSuccessDelegate, JoinChatErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnJoinChatSuccess(FNakamaChannel Channel)
{
	UE_LOG(LogTemp, Log, TEXT("Connected to channel: %s"), *Channel.Id);
}

void ASagiShiActor::OnJoinChatError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error connecting to channel: %s"), *Error.Message);
}
```


### Sending messages

Sending messages is the same for every type of chat channel. Messages contain chat text and emotes and are sent as JSON serialized data:

```cpp
USTRUCT()
struct FChatMessage
{
	GENERATED_BODY()

public:
	UPROPERTY()
	FString Message;

	UPROPERTY()
	FString Emote;

	UPROPERTY()
	FString EmoteTarget;

  UPROPERTY()
  FString MatchId;

  UPROPERTY()
  FString PartyId;
};

FString ChannelId = TEXT("<ChannelId>");

FOnWriteChatMessage SendMessageSuccessDelegate;
SendMessageSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnSendMessageSuccess);

FOnRtError SendMessageErrorDelegate;
SendMessageErrorDelegate.AddDynamic(this, &ASagiShiActor::OnSendMessageError);

FChatMessage ChatMessage;
ChatMessage.Message = TEXT("I think Red is the imposter!");

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(ChatMessage, Json);

RealtimeClient->SendMessage(ChannelId, Json, SendMessageSuccessDelegate, SendMessageErrorDelegate);

FChatMessage ChatMessage2;
ChatMessage2.Emote = TEXT("point");
ChatMessage2.EmoteTarget = TEXT("<RedPlayerUserId");

FString Json2;
FJsonObjectConverter::UStructToJsonObjectString(ChatMessage2, Json2);

RealtimeClient->SendMessage(ChannelId, Json2, SendMessageSuccessDelegate, SendMessageErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnSendMessageSuccess(FNakamaChannelMessageAck ChannelMessage)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully sent message: %s"), *ChannelMessage.MessageId);
}

void ASagiShiActor::OnSendMessageError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error sending message: %s"), *Error.Message);
}
```


### Listing message history

Message listing takes a parameter which indicates if messages are received from oldest to newest (forward) or newest to oldest.

Sagi-shi players can list a group's message history:

```cpp
FString ChannelId = TEXT("<ChannelId>");
int32 Limit = 100;
bool Forward = false;
FString Cursor = TEXT("");

FOnListChannelMessages ListChannelMessagesSuccessDelegate;
ListChannelMessagesSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListChannelMessagesSuccess);

FOnError ListChannelMessagesErrorDelegate;
ListChannelMessagesErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListChannelMessagesError);

Client->ListChannelMessages(Session, ChannelId, Limit, Cursor, Forward, ListChannelMessagesSuccessDelegate, ListChannelMessagesErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListChannelMessagesSuccess(const FNakamaChannelMessageList& MessageList)
{
	for (int16 i = 0; i < MessageList.Messages.Num(); i++)
	{
		UE_LOG(LogTemp, Log, TEXT("%s: %s"), *MessageList.Messages[i].Username, *MessageList.Messages[i].Content);
	}
}

void ASagiShiActor::OnListChannelMessagesError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error retrieving messages: %s"), *Error.Message);
}
```

Chat also has cacheable cursors to fetch the most recent messages. Read more about cacheable cursors in the [listing notifications](../../concepts/notifications/#list-notifications) documentation.

```cpp
FString Cursor = MessageList.NextCursor;
Client->ListChannelMessages(Session, ChannelId, Limit, Cursor, Forward, ListChannelMessagesSuccessDelegate, ListChannelMessagesErrorDelegate);
```


<!-- ### Updating messages

Nakama also supports updating messages. It is up to you whether you want to use this feature, but in a game of deception like Sagi-shi it can add an extra element of deception.

For example a player sends the following message:

```cpp
string channelId = "<ChannelId>";
json j;
j["message"] = "I think Red is the imposter!";

rtClient->writeChatMessage(channelId, j.dump(), successCallback, errorCallback);
```

They then quickly edit their message to confuse others:

```cpp
auto successCallback = [&rtClient](const NChannelMessageAck& messageAck)
{
    json j;
    j["message"] = "I think BLUE is the imposter!";
    rtClient->updateChatMessage(messageAck.channelId, messageAck.messageId, j.dump(), nullptr, nullptr);
};
``` -->


## Matches

Nakama supports [Server Authoritative](../../concepts/multiplayer/authoritative/) and [Server Relayed](../../concepts/multiplayer/relayed/) multiplayer matches.

In server authoritative matches the server controls the gameplay loop and must keep all clients up to date with the current state of the game.

In server relayed matches the client is in control, with the server only relaying information to the other connected clients.

In a competitive game such as Sagi-shi, server authoritative matches would likely be used to prevent clients from interacting with your game in unauthorized ways.

For the simplicity of this guide, the server relayed model is used.

### Creating matches

Sagi-shi players can create their own matches and invite their online friends to join:

```cpp
FOnCreateMatch CreateMatchSuccessDelegate;
CreateMatchSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnCreateMatchSuccess);

FOnRtError CreateMatchErrorDelegate;
CreateMatchErrorDelegate.AddDynamic(this, &ASagiShiActor::OnCreateMatchError);

RealtimeClient->CreateMatch(CreateMatchSuccessDelegate, CreateMatchErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnCreateMatchSuccess(FNakamaMatch Match)
{
	CurrentMatch = Match;

	UE_LOG(LogTemp, Log, TEXT("Created match: %s"), *Match.MatchId);

	// Get the users friends so we can send them invites to the match
	FOnFriendsList GetFriendsSuccessDelegate;
	GetFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsSuccess);

	FOnError GetFriendsErrorDelegate;
	GetFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsError);

	Client->GetFriends(Session, 100, ENakamaFriendState::FRIEND, TEXT(""), GetFriendsSuccessDelegate, GetFriendsErrorDelegate);
}

void ASagiShiActor::OnCreateMatchError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error creating match: %s"), *Error.Message);
}

void ASagiShiActor::OnGetFriendsSuccess(FNakamaFriendList Friends)
{
	for (int16 i = 0; i < Friends.NakamaUsers.Num(); i++)
	{
		FOnWriteChatMessage SendDirectMessageSuccessDelegate;
		SendDirectMessageSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnSendMessageSuccess);

		FOnRtError SendDirectMessageErrorDelegate;
		SendDirectMessageErrorDelegate.AddDynamic(this, &ASagiShiActor::OnSendMessageError);

		FChatMessage ChatMessage;
		ChatMessage.Message = FString::Printf(TEXT("Hey %s, join me for a match!"), *Friends.NakamaUsers[i].NakamaUser.Username);
		ChatMessage.MatchId = CurrentMatch.MatchId;

		FString Json;
		FJsonObjectConverter::UStructToJsonObjectString(ChatMessage, Json);

		RealtimeClient->SendDirectMessage(Friends.NakamaUsers[i].NakamaUser.Id, Json, SendDirectMessageSuccessDelegate, SendDirectMessageErrorDelegate);
	}
}

void ASagiShiActor::OnGetFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Log, TEXT("Error retrieving friends: %s"), *Error.Message);
}
```

### Joining matches

Sagi-shi players can try to join existing matches if they know the id:
```cpp
FString MatchId = "<MatchId>";
TMap<FString, FString> Metadata = {
  { "Region", "EU" }
};

FOnCreateMatch JoinMatchSuccessDelegate;
JoinMatchSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinMatchSuccess);

FOnRtError JoinMatchErrorDelegate;
JoinMatchErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinMatchError);

RealtimeClient->JoinMatch(MatchId, Metadata, JoinMatchSuccessDelegate, JoinMatchErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnJoinMatchSuccess(FNakamaMatch Match)
{
	CurrentMatch = Match;
	UE_LOG(LogTemp, Log, TEXT("Successfully joined match: %s"), *Match.MatchId);
}

void ASagiShiActor::OnJoinMatchError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error joining match: %s"), *Error.Message);
}
```

Or set up a real-time matchmaker listener and add themselves to the matchmaker:

```cpp
FOnReceivedMatchmakerMatched MatchmakerMatchedSuccessDelegate;
MatchmakerMatchedSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnMatchmakerMatchedSuccess);

RealtimeClient->MatchmakerMatchMatched = MatchmakerMatchedSuccessDelegate;
RealtimeClient->SetListenerMatchmakerMatchedCallback();

int32 MinPlayers = 2;
int32 MaxPlayers = 10;
FString Query = TEXT("");
TMap<FString, FString> StringProperties;
TMap<FString, int32> NumericProperties;
int32 CountMultiple = 0;
bool IgnoreCountMultiple = true;

FOnMatchmakerTicket AddMatchmakerSuccessDelegate;
AddMatchmakerSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAddMatchmakerSuccess);

FOnRtError AddMatchmakerErrorDelegate;
AddMatchmakerErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddMatchmakerError);

RealtimeClient->AddMatchmaker(MinPlayers, MaxPlayers, Query, StringProperties, NumericProperties, CountMultiple, IgnoreCountMultiple, AddMatchmakerSuccessDelegate, AddMatchmakerErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnAddMatchmakerSuccess(FString Ticket)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully joined matchmaker: %s"), *Ticket);
}

void ASagiShiActor::OnAddMatchmakerError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error adding matchmaker: %s"), *Error.Message);
}

void ASagiShiActor::OnMatchmakerMatchedSuccess(const FNakamaMatchmakerMatched& Match)
{
	TMap<FString, FString> Metadata;
	FOnCreateMatch JoinMatchSuccessDelegate;
	FOnRtError JoinMatchErrorDelegate;

	RealtimeClient->JoinMatch(Match.MatchId, Metadata, JoinMatchSuccessDelegate, JoinMatchErrorDelegate);
}
```

**Joining matches from player status**

Sagi-shi players can update their status when they join a new match:

```cpp
USTRUCT()
struct FUserStatus
{
	GENERATED_BODY()

public:
	UPROPERTY()
	FString Status;
	
	UPROPERTY()
	FString MatchId;
};

FUserStatus UserStatus;
UserStatus.Status = TEXT("Playing a match");
UserStatus.MatchId = TEXT("<MatchId>");

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(UserStatus, Json);

FOnSetStatus UpdateStatusSuccessDelegate;
FOnRtError UpdateStatusErrorDelegate;

RealtimeClient->UpdateStatus(Json, UpdateStatusSuccessDelegate, UpdateStatusErrorDelegate);
```

When their followers receive the real-time status event they can try and join the match:

```cpp
void ASagiShiActor::OnReceivedStatusPresence(const FNakamaStatusPresenceEvent& UserPresenceData)
{
	for (int16 i = 0; i < UserPresenceData.Joins.Num(); i++)
	{
		FNakamaUserPresence User = UserPresenceData.Joins[i];

		FUserStatus UserStatus;
		FJsonObjectConverter::JsonObjectStringToUStruct(User.Status, &UserStatus);

		if (UserStatus.MatchId != "")
		{
			FOnCreateMatch JoinMatchSuccessDelegate;
			FOnRtError JoinMatchErrorDelegate;

			RealtimeClient->JoinMatch(UserStatus.MatchId, {}, JoinMatchSuccessDelegate, JoinMatchErrorDelegate);
		}
	}
}
```

### Listing matches

[Match Listing](../../concepts/multiplayer/match-listing/) takes a number of criteria to filter matches by including player count, a match label and an option to provide a more complex [search query](../../concepts/multiplayer/query-syntax/).

Sagi-shi matches start in a lobby state. The match exists on the server but the actual gameplay doesn't start until enough players have joined.

Sagi-shi can then list matches that are waiting for more players:

```cpp
int32 MinPlayers = 2;
int32 MaxPlayers = 10;
int32 Limit = 10;
bool Authoritative = true;
FString Label = TEXT("");

FOnMatchlist ListMatchesSuccessDelegate;
ListMatchesSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListMatchesSuccess);

FOnError ListMatchesErrorDelegate;
ListMatchesErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListMatchesError);

Client->ListMatches(Session, MinPlayers, MaxPlayers, Limit, Label, Authoritative, ListMatchesSuccessDelegate, ListMatchesErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListMatchesSuccess(const FNakamaMatchList& MatchList)
{
	for (int16 i = 0; i < MatchList.Matches.Num(); i++)
	{
		FNakamaMatch Match = MatchList.Matches[i];
		UE_LOG(LogTemp, Log, TEXT("%s:%d/10 players"), *Match.MatchId, Match.Size);
	}
}

void ASagiShiActor::OnListMatchesError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing matches: %s"), *Error.Message);
}
```

To find a match that has a label of `"AnExactMatchLabel"`:

```cpp
FString Label = TEXT("AnExactMatchLabel");
```


### Spawning players

The match object has a list of current online users, known as presences.

Sagi-shi uses the match presences to spawn players on the client:

```cpp
// Assuming an FGameObject struct

TMap<FString, FGameObject> Players;
for (int16 i = 0; i < Match.Presences.Num(); i++)
{
  FNakamaUserPresence Presence = Presences[i];
  FGameObject GameObject = SpawnPlayer(); // Instantiate player object
  Players.Add(Presence.SessionID, GameObject);
}
```

Sagi-shi keeps the spawned players up-to-date as they leave and join the match using the match presence received event:

```cpp
FOnReceivedMatchPresenceCallback ReceivedMatchPresenceDelegate;
ReceivedMatchPresenceDelegate.AddDynamic(this, &ASagiShiActor::OnReceivedMatchPresence);

RealtimeClient->MatchmakerPresenceCallback = ReceivedMatchPresenceDelegate;
RealtimeClient->SetListenerMatchPresenceCallback();

/* Delegate handlers */

void ASagiShiActor::OnReceivedMatchPresence(const FNakamaMatchPresenceEvent& PresenceEvent)
{
	for (int16 i = 0; i < PresenceEvent.Joins.Num(); i++)
	{
		FNakamaUserPresence Presence = PresenceEvent.Joins[i];
		FGameObject GameObject = SpawnPlayer(); // Instantiate player object
		Players.Add(Presence.SessionID, GameObject);
	}

	for (int16 i = 0; i < PresenceEvent.Leaves.Num(); i++)
	{
		FNakamaUserPresence Presence = PresenceEvent.Leaves[i];
		if (Players.Contains(Presence.SessionID))
		{
			Players.Remove(Presence.SessionID);
		}
	}
}
```


### Sending match state

Nakama has real-time networking to [send](../../concepts/multiplayer/relayed/#send-data-messages) and [receive](../../concepts/multiplayer/relayed/#receive-data-messages) match state as players move and interact with the game world.

During the match, each Sagi-shi client sends match state to the server to be relayed to the other clients.

Match state contains an op code that lets the receiver know what data is being received so they can deserialize it and update their view of the game.

Example op codes used in Sagi-shi:
- 1: player position
- 2: player calling vote


**Sending player position**

Define a class to represent Sagi-shi player position states:

```cpp
USTRUCT()
struct FPositionState
{
	GENERATED_BODY()

public:
	UPROPERTY()
	float X;

	UPROPERTY()
	float Y;

	UPROPERTY()
	float Z;
};
```

Create an instance from the player's transform, set the op code and send the JSON encoded state:

```cpp
// Assuming a Position variable
FPositionState PositionState;
PositionState.X = Position.X;
PositionState.Y = Position.Y;
PositionState.Z = Position.Z;

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(PositionState, Json);

int64 OpCode = 1;
RealtimeClient->SendMatchData(CurrentMatch.MatchId, OpCode, Json, {});
```


**Op Codes as a static class**

Sagi-shi has many networked game actions. Using defines for op codes will keep your code easier to follow and maintain:

```cpp
#define OPCODE_POSITION 1
#define OPCODE_VOTE 1

// ...

RealtimeClient->SendMatchData(CurrentMatch.MatchId, OPCODE_POSITION, Json, {});
```


### Receiving match state

Sagi-shi players can receive match data from the other connected clients by subscribing to the match state received event:

```cpp
FOnReceivedMatchData ReceivedMatchDataDelegate;
ReceivedMatchDataDelegate.AddDynamic(this, &ASagiShiActor::OnReceivedMatchData);

RealtimeClient->MatchDataCallback = ReceivedMatchDataDelegate;
RealtimeClient->SetListenerMatchDataCallback();

/* Delegate handlers */

void ASagiShiActor::OnReceivedMatchData(const FNakamaMatchData& MatchData)
{
	switch (MatchData.OpCode)
	{
	case OPCODE_POSITION:
		FPositionState PositionState;
		FJsonObjectConverter::JsonObjectStringToUStruct(MatchData.Data, &PositionState);

		// Update the GameObject associated with that player.
		if (Players.Contains(MatchData.Presence.SessionID))
		{
			// Here we would normally do something like smoothly interpolate to the new position, but for this example let's just set the position directly.
			FVector NewPosition = FVector(PositionState.X, PositionState.Y, PositionState.Z);
			Players[MatchData.Presence.SessionID].Position = NewPosition;
		}
		break;
	default:
		UE_LOG(LogTemp, Error, TEXT("Unsupported opcode"));
		break;
	}
}
```


## Matchmaker

Developers can find matches for players using Match Listing or the Nakama [Matchmaker](../../concepts/multiplayer/matchmaker/), which enables players join the real-time matchmaking pool and be notified when they are matched with other players that match their specified criteria.

Matchmaking helps players find each other, it does not create a match. This decoupling is by design, allowing you to use matchmaking for more than finding a game match. For example, if you were building a social experience you could use matchmaking to find others to chat with.


### Add matchmaker

Matchmaking criteria can be simple, find 2 players, or more complex, find 2-10 players with a minimum skill level interested in a specific game mode.

Sagi-shi allows players to join the matchmaking pool and have the server match them with other players:

```cpp
FOnReceivedMatchmakerMatched MatchmakerMatchedSuccessDelegate;
MatchmakerMatchedSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnMatchmakerMatchedSuccess);

RealtimeClient->MatchmakerMatchMatched = MatchmakerMatchedSuccessDelegate;
RealtimeClient->SetListenerMatchmakerMatchedCallback();

int32 MinPlayers = 2;
int32 MaxPlayers = 10;
FString Query = TEXT("+skill:>100 mode:sabotage");
TMap<FString, FString> StringProperties = { { "mode", "sabotage" } };
TMap<FString, int32> NumericProperties = { { "skill", 125 }};
int32 CountMultiple = 0;
bool IgnoreCountMultiple = true;

FOnMatchmakerTicket AddMatchmakerSuccessDelegate;
AddMatchmakerSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAddMatchmakerSuccess);

FOnRtError AddMatchmakerErrorDelegate;
AddMatchmakerErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddMatchmakerError);

RealtimeClient->AddMatchmaker(MinPlayers, MaxPlayers, Query, StringProperties, NumericProperties, CountMultiple, IgnoreCountMultiple, AddMatchmakerSuccessDelegate, AddMatchmakerErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnAddMatchmakerSuccess(FString Ticket)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully joined matchmaker: %s"), *Ticket);
}

void ASagiShiActor::OnAddMatchmakerError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error adding matchmaker: %s"), *Error.Message);
}

void ASagiShiActor::OnMatchmakerMatchedSuccess(const FNakamaMatchmakerMatched& Match)
{
	TMap<FString, FString> Metadata;
	FOnCreateMatch JoinMatchSuccessDelegate;
	FOnRtError JoinMatchErrorDelegate;

	RealtimeClient->JoinMatch(Match.MatchId, Metadata, JoinMatchSuccessDelegate, JoinMatchErrorDelegate);
}
```

## Parties

Nakama [Parties](../../concepts/parties/) is a real-time system that allows players to form short lived parties that don't persist after all players have disconnected.

Sagi-shi allows friends to form a party and matchmake together.


### Creating parties

The player who creates the party is the party's leader. Parties have maximum number of players and can be open to automatically accept players or closed so that the party leader can accept incoming join requests.

Sagi-shi uses closed parties with a maximum of 4 players:

```cpp
bool Open = false;
int32 MaxPlayers = 4;

FOnCreateParty CreatePartySuccessDelegate;
CreatePartySuccessDelegate.AddDynamic(this, &ASagiShiActor::OnCreatePartySuccess);

FOnRtError CreatePartyErrorDelegate;
CreatePartyErrorDelegate.AddDynamic(this, &ASagiShiActor::OnCreatePartyError);

RealtimeClient->CreateParty(Open, MaxPlayers, CreatePartySuccessDelegate, CreatePartyErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnCreatePartySuccess(FNakamaParty Party)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully created party: %s"), *Party.Id);
}

void ASagiShiActor::OnCreatePartyError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error creating party: %s"), *Error.Message);
}
```

Sagi-shi shares party ids with friends via private/direct messages:

```cpp
// Get the users friends so we can send them invites to the party
FOnFriendsList GetFriendsSuccessDelegate;
GetFriendsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsSuccess);

FOnError GetFriendsErrorDelegate;
GetFriendsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnGetFriendsError);

Client->GetFriends(Session, 100, ENakamaFriendState::FRIEND, TEXT(""), GetFriendsSuccessDelegate, GetFriendsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnGetFriendsSuccess(FNakamaFriendList Friends)
{
	for (int16 i = 0; i < Friends.NakamaUsers.Num(); i++)
	{
		FOnWriteChatMessage SendDirectMessageSuccessDelegate;
		SendDirectMessageSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnSendMessageSuccess);

		FOnRtError SendDirectMessageErrorDelegate;
		SendDirectMessageErrorDelegate.AddDynamic(this, &ASagiShiActor::OnSendMessageError);

		FChatMessage ChatMessage;
		ChatMessage.Message = FString::Printf(TEXT("Hey %s, wanna join the party?"), *Friends.NakamaUsers[i].NakamaUser.Username);
		ChatMessage.PartyId = CurrentParty.PartyId;

		FString Json;
		FJsonObjectConverter::UStructToJsonObjectString(ChatMessage, Json);

		RealtimeClient->SendDirectMessage(Friends.NakamaUsers[i].NakamaUser.Id, Json, SendDirectMessageSuccessDelegate, SendDirectMessageErrorDelegate);
	}
}

void ASagiShiActor::OnGetFriendsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Log, TEXT("Error retrieving friends: %s"), *Error.Message);
}
```


### Joining parties

Safi-shi players can join parties from chat messages by checking for the party id in the message:

```cpp
FOnReceivedChannelMessage ReceivedChannelMessageDelegate;
ReceivedChannelMessageDelegate.AddDynamic(this, &ASagiShiActor::OnReceivedChannelMessage);

RealtimeClient->ChannelMessageReceived = ReceivedChannelMessageDelegate;
RealtimeClient->SetListenerChannelMessageCallback();

/* Delegate handlers */

void ASagiShiActor::OnReceivedChannelMessage(const FNakamaChannelMessage& ChannelMessage)
{
	FChatMessage ChatMessage;
	FJsonObjectConverter::JsonObjectStringToUStruct(ChannelMessage.Content, &ChatMessage);

	if (ChatMessage.PartyId != "")
	{
		FOnJoinParty JoinPartySuccessDelegate;
		JoinPartySuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinPartySuccess);

		FOnRtError JoinPartyErrorDelegate;
		JoinPartyErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinPartyError);

		RealtimeClient->JoinParty(ChatMessage.PartyId, JoinPartySuccessDelegate, JoinPartyErrorDelegate);
	}
}

void ASagiShiActor::OnJoinPartySuccess(FString PartyId)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully joined party: %s"), *PartyId);
}

void ASagiShiActor::OnJoinPartyError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error joining party: %s"), *Error.Message);
}
```


### Promoting a member

Sagi-shi party members can be promoted to the party leader:

```cpp
FOnPromotePartyMember PromotePartyMemberSuccessDelegate;
PromotePartyMemberSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnPromotePartyMemberSuccess);

FOnRtError PromotePartyMemberErrorDelegate;
PromotePartyMemberErrorDelegate.AddDynamic(this, &ASagiShiActor::OnPromotePartyMemberError);

for (int16 i = 0; i < Party.Presences.Num(); i++)
{
  RealtimeClient->PromotePartyMember(Party.Id, Party.Presences[i], PromotePartyMemberSuccessDelegate, PromotePartyMemberErrorDelegate);
}

/* Delegate handlers */

void ASagiShiActor::OnPromotePartyMemberSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully promoted party member"));
}

void ASagiShiActor::OnPromotePartyMemberError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error promoting party member: %s"), *Error.Message);
}
```

### Leaving parties

Sagi-shi players can leave parties:

```cpp
FOnLeaveParty LeavePartySuccessDelegate;
LeavePartySuccessDelegate.AddDynamic(this, &ASagiShiActor::OnLeavePartySuccess);

FOnRtError LeavePartyErrorDelegate;
LeavePartyErrorDelegate.AddDynamic(this, &ASagiShiActor::OnLeavePartyError);

RealtimeClient->LeaveParty(Party.Id, LeavePartySuccessDelegate, LeavePartyErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnLeavePartySuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully left party"));
}

void ASagiShiActor::OnLeavePartyError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error leaving party: %s"), *Error.Message);
}
```


### Matchmaking with parties

One of the main benefits of joining a party is that all the players can join the matchmaking pool together.

Sagi-shi players can listen to the the matchmaker matched event and join the match when one is found:

```cpp
FOnReceivedMatchmakerMatched MatchmakerMatchedSuccessDelegate;
MatchmakerMatchedSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnMatchmakerMatchedSuccess);

RealtimeClient->MatchmakerMatchMatched = MatchmakerMatchedSuccessDelegate;
RealtimeClient->SetListenerMatchmakerMatchedCallback();

/* Delegate handlers */

void ASagiShiActor::OnMatchmakerMatchedSuccess(const FNakamaMatchmakerMatched& Match)
{
	TMap<FString, FString> Metadata;
	FOnCreateMatch JoinMatchSuccessDelegate;
	FOnRtError JoinMatchErrorDelegate;

	RealtimeClient->JoinMatch(Match.MatchId, Metadata, JoinMatchSuccessDelegate, JoinMatchErrorDelegate);
}
```

The party leader will start the matchmaking for their party:

```cpp
int32 MinPlayers = 2;
int32 MaxPlayers = 10;
FString Query = TEXT("");
TMap<FString, FString> StringProperties;
TMap<FString, int32> NumericProperties;
int32 CountMultiple = 0;
bool IgnoreCountMultiple = true;

FOnAddMatchmakerParty AddMatchmakerPartySuccessDelegate;
AddMatchmakerPartySuccessDelegate.AddDynamic(this, &ASagiShiActor::OnAddMatchmakerPartySuccess);

FOnRtError AddMatchmakerPartyErrorDelegate;
AddMatchmakerPartyErrorDelegate.AddDynamic(this, &ASagiShiActor::OnAddMatchmakerPartyError);

RealtimeClient->AddMatchmakerParty(Party.Id, Query, MinPlayers, MaxPlayers, StringProperties, NumericProperties, CountMultiple, IgnoreCountMultiple, AddMatchmakerPartySuccessDelegate, AddMatchmakerPartyErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnAddMatchmakerPartySuccess(FNakamaPartyMatchmakerTicket Ticket)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully joined matchmaker as party: %s"), *Ticket.Ticket);
}

void ASagiShiActor::OnAddMatchmakerPartyError(const FNakamaRtError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error matchmaking as party: %s"), *Error.Message);
}
```

### Sending party data

Sagi-shi players can send data to other members of their party to indicate they wish to start a vote.

```cpp
USTRUCT()
struct FVoteState
{
	GENERATED_BODY()

public:
	UPROPERTY()
	FString Username;

	UPROPERTY()
	FString Reason
};

// ...

#define OPCODE_CALL_VOTE 6

FVoteState VoteState;
VoteState.Username = TEXT("<Username>");
VoteState.Reason = TEXT("Emergency");

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(VoteState, Json);

RealtimeClient->SendPartyData(Party.Id, OPCODE_CALL_VOTE, Json);
```


### Receiving party data

Sagi-shi players can receive party data from other party members by subscribing to the party data event.

```cpp
FOnReceivedPartyData ReceivedPartyDataDelegate;
ReceivedPartyDataDelegate.AddDynamic(this, &ASagiShiActor::OnReceivedPartyData);

RealtimeClient->PartyDataCallback = ReceivedPartyDataDelegate;
RealtimeClient->SetListenerPartyDataCallback();

/* Delegate handlers */

void ASagiShiActor::OnReceivedPartyData(const FNakamaPartyData& PartyData)
{
	switch (PartyData.OpCode)
	{
	case OPCODE_CALL_VOTE:
		FVoteState VoteState;
		FJsonObjectConverter::JsonObjectStringToUStruct(PartyData.Data, &VoteState);

		// Show a UI dialogue - "<username> has proposed to call a vote for <reason>. Do you agree? Yes/No"
		break;
	default:
		UE_LOG(LogTemp, Error, TEXT("Unsupported opcode"));
		break;
	}
}
```


## Leaderboards

Nakama [Leaderboards](../../concepts/leaderboards/) introduce a competitive aspect to your game and increase player engagement and retention.

Sagi-shi has a leaderboard of weekly imposter wins, where player scores increase each time they win, and similarly a leaderboard for weekly crew member wins.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/leaderboard.png" >}} alt="Sagi-shi leaderboard screen">
  <figcaption>Sagi-shi Leaderboard</figcaption>
</figure>


### Creating leaderboards

Leaderboards have to be created on the server, see the [leaderboard](../../concepts/leaderboards/#create-a-leaderboard) documentation for details on creating leaderboards.


### Submitting scores

When players submit scores, Nakama will increment the player's existing score by the submitted score value.

Along with the score value, Nakama also has a subscore, which can be used for ordering when the scores are the same.

Sagi-shi players can submit scores to the leaderboard with contextual metadata, like the map the score was achieved on:

```cpp
USTRUCT()
struct FLeaderboardMetadata
{
	GENERATED_BODY()

public:
	UPROPERTY()
	FString Map;
};

int32 Score = 1;
int32 SubScore = 0;

FLeaderboardMetadata LeaderboardMetadata;
LeaderboardMetadata.Map = "space_station";

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(LeaderboardMetadata, Json);

FOnWriteLeaderboardRecord WriteLeaderboardRecordSuccessDelegate;
WriteLeaderboardRecordSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnWriteLeaderboardRecordSuccess);

FOnError WriteLeaderboardRecordErrorDelegate;
WriteLeaderboardRecordErrorDelegate.AddDynamic(this, &ASagiShiActor::OnWriteLeaderboardRecordError);

Client->WriteLeaderboardRecord(Session, TEXT("for_all"), Score, SubScore, Json, WriteLeaderboardRecordSuccessDelegate, WriteLeaderboardRecordErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnWriteLeaderboardRecordSuccess(const FNakamaLeaderboardRecord& Record)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully wrote leaderboard record"));
}

void ASagiShiActor::OnWriteLeaderboardRecordError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error writing leaderboard record: %s"), *Error.Message);
}
```

### Listing the top records

Sagi-shi players can list the top records of the leaderboard:

```cpp
int32 Limit = 20;
FString LeaderboardId = "weekly_imposter_wins";
TArray<FString> OwnerIds = {};
FString Cursor = TEXT("");

FOnListLeaderboardRecords ListLeaderboardRecordsSuccessDelegate;
ListLeaderboardRecordsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsSuccess);

FOnError ListLeaderboardRecordsErrorDelegate;
ListLeaderboardRecordsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsError);

Client->ListLeaderboardRecords(Session, LeaderboardId, OwnerIds, Limit, Cursor, ENakamaLeaderboardListBy::BY_SCORE, ListLeaderboardRecordsSuccessDelegate, ListLeaderboardRecordsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListLeaderboardRecordsSuccess(const FNakamaLeaderboardRecordList& RecordsList)
{
	for (int16 i = 0; i < RecordsList.Records.Num(); i++)
	{
		FNakamaLeaderboardRecord Record = RecordsList.Records[i];
		UE_LOG(LogTemp, Log, TEXT("%s: %d"), *Record.OwnerId, Record.Score);
	}
}

void ASagiShiActor::OnListLeaderboardRecordsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing leaderboard records: %s"), *Error.Message);
}
```


**Listing records around the user**

Nakama allows developers to list leaderboard records around a player.

Sagi-shi gives players a snapshot of how they are doing against players around them:

```cpp
int32 Limit = 5;
FString LeaderboardId = "weekly_imposter_wins";
FString Cursor = TEXT("");

FOnListLeaderboardRecords ListLeaderboardRecordsSuccessDelegate;
ListLeaderboardRecordsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsSuccess);

FOnError ListLeaderboardRecordsErrorDelegate;
ListLeaderboardRecordsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsError);

Client->ListLeaderboardRecordsAroundOwner(Session, LeaderboardId, Session->SessionData.UserId, Limit, ListLeaderboardRecordsSuccessDelegate, ListLeaderboardRecordsErrorDelegate);
```

For example, if the leaderboard contains 100 records and the ownerId = "player123" with limit = 5, the result will include the specified user along with nearby records:

```json
{
  "records": [
    { "ownerId": "player120", "rank": 48, "score": 1500 },
    { "ownerId": "player121", "rank": 49, "score": 1480 },
    { "ownerId": "player123", "rank": 50, "score": 1450 },  // Your ownerId
    { "ownerId": "player125", "rank": 51, "score": 1430 },
    { "ownerId": "player127", "rank": 52, "score": 1400 }
  ]
}
```

**Listing records for a list of users**

Sagi-shi players can get their friends' scores by supplying their user ids to the owner id parameter:

```cpp
void ASagiShiActor::OnGetFriendsSuccess(FNakamaFriendList Friends)
{
	TArray<FString> FriendIds;

	for (int16 i = 0; i < Friends.NakamaUsers.Num(); i++)
	{
		FriendIds.Add(Friends.NakamaUsers[i].NakamaUser.Id);
	}

	int32 Limit = 20;
	FString LeaderboardId = "weekly_imposter_wins";
	FString Cursor = TEXT("");

	FOnListLeaderboardRecords ListLeaderboardRecordsSuccessDelegate;
	ListLeaderboardRecordsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsSuccess);

	FOnError ListLeaderboardRecordsErrorDelegate;
	ListLeaderboardRecordsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsError);

	Client->ListLeaderboardRecords(Session, LeaderboardId, FriendIds, Limit, Cursor, ENakamaLeaderboardListBy::BY_SCORE, ListLeaderboardRecordsSuccessDelegate, ListLeaderboardRecordsErrorDelegate);
}
```

The same approach can be used to get group member's scores by supplying their user ids to the owner id parameter:

```cpp
void ASagiShiActor::OnListGroupUsersSuccess(const FNakamaGroupUsersList& GroupUsers)
{
	TArray<FString> MemberIds;

	for (int16 i = 0; i < GroupUsers.GroupUsers.Num(); i++)
	{
		MemberIds.Add(GroupUsers.GroupUsers[i].User.Id);
	}

	int32 Limit = 20;
	FString LeaderboardId = "weekly_imposter_wins";
	FString Cursor = TEXT("");

	FOnListLeaderboardRecords ListLeaderboardRecordsSuccessDelegate;
	ListLeaderboardRecordsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsSuccess);

	FOnError ListLeaderboardRecordsErrorDelegate;
	ListLeaderboardRecordsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListLeaderboardRecordsError);

	Client->ListLeaderboardRecords(Session, LeaderboardId, MemberIds, Limit, Cursor, ENakamaLeaderboardListBy::BY_SCORE, ListLeaderboardRecordsSuccessDelegate, ListLeaderboardRecordsErrorDelegate);
}
```


### Deleting records

Sagi-shi players can delete their own leaderboard records:

```cpp
FOnDeletedLeaderboardRecord DeleteLeaderboardRecordSuccessDelegate;
DeleteLeaderboardRecordSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnDeleteLeaderboardRecordSuccess);

FOnError DeleteLeaderboardRecordErrorDelegate;
DeleteLeaderboardRecordErrorDelegate.AddDynamic(this, &ASagiShiActor::OnDeleteLeaderboardRecordError);

Client->DeleteLeaderboardRecord(Session, TEXT("<LeaderboardId>"), DeleteLeaderboardRecordSuccessDelegate, DeleteLeaderboardRecordErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnDeleteLeaderboardRecordSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully deleted leaderboard record"));
}

void ASagiShiActor::OnDeleteLeaderboardRecordError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error deleting leaderboard record: %s"), *Error.Message);
}
```


## Tournaments

Nakama [Tournaments](../../concepts/tournaments/) are short lived competitions where players compete for a prize.

Sagi-shi players can view, filter and join running tournaments.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/tournaments.png" >}} alt="Sagi-shi tournaments screen">
  <figcaption>Sagi-shi Tournaments</figcaption>
</figure>


### Creating tournaments

Tournaments have to be created on the server, see the [tournament](../../concepts/tournaments/#create-tournament) documentation for details on how to create a tournament.

Sagi-shi has a weekly tournament which challenges players to get the most correct imposter votes. At the end of the week the top players receive a prize of in-game currency.


### Joining tournaments

By default in Nakama players don't have to join tournaments before they can submit a score, but Sagi-shi makes this mandatory:

```cpp
FOnJoinedTournament JoinTournamentSuccessDelegate;
JoinTournamentSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnJoinTournamentSuccess);

FOnError JoinTournamentErrorDelegate;
JoinTournamentErrorDelegate.AddDynamic(this, &ASagiShiActor::OnJoinTournamentError);

Client->JoinTournament(Session, TEXT("<TournamentId>"), JoinTournamentSuccessDelegate, JoinTournamentErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnJoinTournamentSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully joined tournament"));
}

void ASagiShiActor::OnJoinTournamentError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error joining tournament: %s"), *Error.Message);
}
```

### Listing tournaments

Sagi-shi players can list and filter tournaments with various criteria:

```cpp
int32 CategoryStart = 1;
int32 CategoryEnd = 2;
int32 StartTime = 0;
int32 EndTime = 0;
int32 Limit = 100;
FString Cursor = TEXT("");

FOnListTournaments ListTournamentsSuccessDelegate;
ListTournamentsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListTournamentsSuccess);

FOnError ListTournamentsErrorDelegate;
ListTournamentsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListTournamentsError);

Client->ListTournaments(Session, CategoryStart, CategoryEnd, StartTime, EndTime, Limit, Cursor, ListTournamentsSuccessDelegate, ListTournamentsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListTournamentsSuccess(const FNakamaTournamentList& Tournaments)
{
	for (int16 i = 0; i < Tournaments.Tournaments.Num(); i++)
	{
		FNakamaTournament Tournament = Tournaments.Tournaments[i];
		UE_LOG(LogTemp, Log, TEXT("%s: %s"), *Tournament.Id, *Tournament.Title);
	}
}

void ASagiShiActor::OnListTournamentsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing tournaments: %s"), *Error.Message);
}
```

For performance reasons categories are filtered using a range, not individual numbers. Structure your categories to take advantage of this (e.g. all PVE tournaments in the 1XX range, all PVP tournaments in the 2XX range, etc.).

### Listing records

Sagi-shi players can list tournament records:

```cpp
int32 Limit = 20;
FString TournamentId = TEXT("weekly_top_detectives");
FString Cursor = TEXT("");
TArray<FString> OwnerIds = {};

FOnListTournamentRecords ListTournamentRecordsSuccessDelegate;
ListTournamentRecordsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListTournamentRecordsSuccess);

FOnError ListTournamentRecordsErrorDelegate;
ListTournamentRecordsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListTournamentRecordsError);

Client->ListTournamentRecords(Session, TournamentId, Limit, Cursor, OwnerIds, ENakamaLeaderboardListBy::BY_SCORE, ListTournamentRecordsSuccessDelegate, ListTournamentRecordsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListTournamentRecordsSuccess(const FNakamaTournamentRecordList& RecordsList)
{
	for (int16 i = 0; i < RecordsList.Records.Num(); i++)
	{
		FNakamaLeaderboardRecord Record = RecordsList.Records[i];
		UE_LOG(LogTemp, Log, TEXT("%s: %d"), *Record.OwnerId, Record.Score);
	}
}

void ASagiShiActor::OnListTournamentRecordsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing tournament records: %s"), *Error.Message);
}
```


**Listing records around a user**

Similarly to leaderboards, Sagi-shi players can get other player scores around them:

```cpp
int32 Limit = 20;
FString TournamentId = TEXT("weekly_top_detectives");
FString Cursor = TEXT("");
TArray<FString> OwnerIds = {};

FOnListTournamentRecords ListTournamentRecordsSuccessDelegate;
ListTournamentRecordsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListTournamentRecordsSuccess);

FOnError ListTournamentRecordsErrorDelegate;
ListTournamentRecordsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListTournamentRecordsError);

Client->ListTournamentRecordsAroundOwner(Session, TournamentId, Session->SessionData.UserId, Limit, ListTournamentRecordsSuccessDelegate, ListTournamentRecordsErrorDelegate);
```


### Submitting scores

Sagi-shi players can submit scores, subscores and metadata to the tournament:

```cpp
int32 Score = 1;
int32 SubScore = 0;

FLeaderboardMetadata LeaderboardMetadata;
LeaderboardMetadata.Map = "space_station";

FString Json;
FJsonObjectConverter::UStructToJsonObjectString(LeaderboardMetadata, Json);

FOnWriteLeaderboardRecord WriteTournamentRecordSuccessDelegate;
WriteTournamentRecordSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnWriteTournamentRecordSuccess);

FOnError WriteTournamentRecordErrorDelegate;
WriteTournamentRecordErrorDelegate.AddDynamic(this, &ASagiShiActor::OnWriteTournamentRecordError);

Client->WriteTournamentRecord(Session, TEXT("weekly_top_detective"), Score, SubScore, Json, WriteTournamentRecordSuccessDelegate, WriteTournamentRecordErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnWriteTournamentRecordSuccess(const FNakamaLeaderboardRecord& Record)
{
	UE_LOG(LogTemp, Log, TEXT("Successfully wrote tournament record"));
}

void ASagiShiActor::OnWriteTournamentRecordError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error writing tournament record: %s"), *Error.Message);
}
```


## Notifications

Nakama [Notifications](../../concepts/notifications/) can be used for the game backend to broadcast real-time messages to players.

Notifications can be either persistent (remaining until a player has viewed it) or transient (received only if the player is currently online).

Sagi-shi uses Notifications to notify tournament winners about their winnings.

<figure>
  <img src={{< fingerprint_image "/images/pages/nakama/client-libraries/notifications.png" >}} alt="Sagi-shi notification screen">
  <figcaption>Sagi-shi notifications</figcaption>
</figure>


### Receiving notifications

Notifications have to be sent from the server.

Nakama uses a code to differentiate notifications. Codes of `0` and below are [system reserved](../../concepts/notifications/#notification-codes) for Nakama internals.

Sagi-shi players can subscribe to the notification received event. Sagi-shi uses a code of `100` for tournament winnings:

```cpp
FOnReceivedNotification NotificationReceivedDelegate;
NotificationReceivedDelegate.AddDynamic(this, &ASagiShiActor::OnReceivedNotification);

RealtimeClient->NotificationReceived = NotificationReceivedDelegate;
RealtimeClient->SetListenerNotificationsCallback();

/* Delegate handlers */

void ASagiShiActor::OnReceivedNotification(const FNakamaNotificationList& NotificationList)
{
	const int32 RewardCode = 100;

	for (int16 i = 0; i < NotificationList.Notifications.Num(); i++)
	{
		FNakamaNotification Notification = NotificationList.Notifications[i];
		switch (Notification.Code)
		{
		case RewardCode:
			UE_LOG(LogTemp, Log, TEXT("Congratulations, you won the tournament!\n%s"), *Notification.Subject, *Notification.Content);
			break;
		default:
			UE_LOG(LogTemp, Log, TEXT("Other notification: %s\n%s"), *Notification.Subject, *Notification.Content);
			break;
		}
	}
}
```


### Listing notifications

Sagi-shi players can list the notifications they received while offline:

```cpp
int32 Limit = 100;
FString Cursor = TEXT("");

FOnListNotifications ListNotificationsSuccessDelegate;
ListNotificationsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnListNotificationsSuccess);

FOnError ListNotificationsErrorDelegate;
ListNotificationsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnListNotificationsError);

Client->ListNotifications(Session, Limit, Cursor, ListNotificationsSuccessDelegate, ListNotificationsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnListNotificationsSuccess(FNakamaNotificationList NotificationList)
{
	for (int16 i = 0; i < NotificationList.Notifications.Num(); i++)
	{
		FNakamaNotification Notification = NotificationList.Notifications[i];
		UE_LOG(LogTemp, Log, TEXT("Notification: %s\n%s"), *Notification.Subject, *Notification.Content);
	}
}

void ASagiShiActor::OnListNotificationsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error listing notifications: %s"), *Error.Message);
}
```


**Pagination and cacheable cursors**

Like other listing methods, notification results can be paginated using a cursor or cacheable cursor from the result.

```cpp
FString CacheableCursor = NotificationList.CacheableCursor;
```

The next time the player logs in the cacheable cursor can be used to list unread notifications.

```cpp
Client->ListNotifications(Session, Limit, CacheableCursor, ListNotificationsSuccessDelegate, ListNotificationsErrorDelegate);
```


### Deleting notifications

Sagi-shi players can delete notifications once they've read them:

```cpp
TArray<FString> NotificationIds;

FOnDeleteNotifications DeleteNotificationsSuccessDelegate;
DeleteNotificationsSuccessDelegate.AddDynamic(this, &ASagiShiActor::OnDeleteNotificationsSuccess);

FOnError DeleteNotificationsErrorDelegate;
DeleteNotificationsErrorDelegate.AddDynamic(this, &ASagiShiActor::OnDeleteNotificationsError);

Client->DeleteNotifications(Session, NotificationIds, DeleteNotificationsSuccessDelegate, DeleteNotificationsErrorDelegate);

/* Delegate handlers */

void ASagiShiActor::OnDeleteNotificationsSuccess()
{
	UE_LOG(LogTemp, Log, TEXT("Successfully deleted notifications"));
}

void ASagiShiActor::OnDeleteNotificationsError(const FNakamaError& Error)
{
	UE_LOG(LogTemp, Error, TEXT("Error deleting notifications: %s"), *Error.Message);
}
```
