GS2-Inbox

Present box feature

Implements a mechanism for delivering messages and gifts from the system or operators to players.

In games, situations frequently arise where rewards are delivered asynchronously to players, such as “apology distributions”, “login bonuses”, “purchase bonuses”, and “event rewards”. GS2-Inbox provides message queuing, unread/read management, reward attachment, and automatic deletion by expiration date, serving as the present box feature throughout the lifecycle of the game.

sequenceDiagram
  participant Ops as Operator / Server
  participant Inbox as GS2-Inbox
  participant Player as Player
  Ops->>Inbox: Send message (with reward)
  Inbox-->>Player: receiveNotification
  Player->>Inbox: Get message list
  Player->>Inbox: Open message (Read)
  Inbox->>Player: Grant attached reward (transaction)
  Player->>Inbox: Delete read message

Messages

Read management

Messages have a read status. You can configure whether to keep read messages in the list or remove them from the list.

When isAutomaticDeletingEnabled is enabled in the namespace settings, messages are automatically deleted at the timing of being marked as read. You can implement a specification where only unreceived messages remain in the present box list, without any additional server-side implementation.

Attach reward

Rewards can be attached to messages. Instead of just marking a message as read, the player can receive the reward attached to the message.

Since attachable rewards are expressed as acquire actions (readAcquireActions) via the GS2 transaction mechanism, you can distribute resources of any microservice such as GS2-Inventory, GS2-Money, or GS2-Experience as rewards.

Expiration date

Messages can have an expiration date (expiresAt). Messages that reach their expiration date are automatically deleted, regardless of whether they are unread or have been read.

They are deleted even if the attached reward has not been received.

Message lifecycle

stateDiagram-v2
  [*] --> Unread: Message received
  Unread --> Read: Open (Read)
  Unread --> Expired: Expiration
  Read --> Deleted: Delete
  Read --> AutoDeleted: isAutomaticDeletingEnabled
  Read --> Expired: Expiration
  Deleted --> [*]
  AutoDeleted --> [*]
  Expired --> [*]

Global Message

When you want to distribute the same message to all players rather than to a specific player, use a “global message”. A global message is defined as master data, and when each player calls ReceiveGlobalMessageAsync, any messages they have not yet received are copied into their inbox.

Feature Description
expiresAt The message expiration date at an absolute time
expiresTimeSpan Determines the expiration date as a relative period from receipt (e.g., 3 days from receipt)
messageReceptionPeriodEventId Specifies a GS2-Schedule event ID, making the global message receivable only during a specific period
{
  "version": "2018-04-20",
  "globalMessages": [
    {
      "name": "welcome",
      "metadata": "Gift for new users",
      "readAcquireActions": [
        {
          "action": "Gs2Money:DepositByUserId",
          "request": "{\"namespaceName\":\"money-0001\",\"slot\":1,\"userId\":\"#{userId}\",\"price\":0,\"count\":100}"
        }
      ],
      "expiresTimeSpan": { "days": 7 }
    }
  ]
}

The types of master data are as follows:

  • GlobalMessage: Message definition delivered to all players

Master data can be registered from the Management Console, or you can set up a workflow that reflects data from GitHub or registers via CI using GS2-Deploy.

Script Triggers

Setting receiveMessageScript, readMessageScript, and deleteMessageScript in the namespace allows custom scripts to be executed before and after message reception, opening, and deletion. Scripts support both synchronous and asynchronous execution, with asynchronous execution supporting external integration via GS2-Script or Amazon EventBridge.

The main event triggers and script setting names that can be configured are as follows:

  • receiveMessageScript (completion notification: receiveMessageDone): before and after message reception
  • readMessageScript (completion notification: readMessageDone): before and after message opening
  • deleteMessageScript (completion notification: deleteMessageDone): before and after message deletion

Push Notifications

The main push notifications and their configuration names are as follows:

  • receiveNotification: notifies when a message is received

You can also specify settings to forward notifications to mobile push notifications when the notification device is offline, ensuring reliable delivery.

You can implement a delivery strategy without any additional implementation, such as sending instant notifications to players who have a GS2-Gateway WebSocket connection, and automatically sending iOS/Android mobile push notifications to players who are not connected.

Transaction Actions

GS2-Inbox provides the following transaction actions:

  • Consume Action: Open message, delete message
  • Acquire Action: Send message

By using “Send message” as an acquire action, it is possible to directly deliver messages (with items attached) to a player’s present box as a reward for actions like purchasing a product at a shop or completing a quest, all within a single transaction. This allows for flexible control over the timing of reward distribution and makes it easy to handle situations where a player’s inventory is full by temporarily storing rewards in the present box.

Example implementation

Sending messages

Sending messages cannot be directly called from the SDK for the game engine. It is normally executed via server-side scripts or via the “Transaction Actions” mentioned above.

For bulk distribution from operators, messages can be sent through the Management Console or GS2-Deploy. To trigger “message send” as a reward or production element in-game, incorporate Gs2Inbox:SendMessageByUserId into the acquireActions of GS2-Exchange or GS2-Quest.

Get list of received messages

    var items = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).MessagesAsync(
    ).ToListAsync();
    const auto It = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Messages(
    );
    TArray<Gs2::UE5::Inbox::Model::FEzMessagePtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Get list of unread messages only

    var items = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).MessagesAsync(
        isRead: false
    ).ToListAsync();
    const auto It = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Messages(
        false // isRead
    );
    TArray<Gs2::UE5::Inbox::Model::FEzMessagePtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Get a list of only read messages

    var items = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).MessagesAsync(
        isRead: true
    ).ToListAsync();
    const auto It = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Messages(
        true // isRead
    );
    TArray<Gs2::UE5::Inbox::Model::FEzMessagePtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Open a message

When a message is opened, it transitions to the Read state, and at the same time, the reward-granting process defined in readAcquireActions is executed as a transaction. When isAutomaticDeletingEnabled is enabled in the namespace settings, the message is deleted simultaneously with opening.

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Message(
        messageName: "message-0001"
    ).ReadAsync(
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Message(
        "message-0001" // messageName
    )->Read(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Open messages in bulk

Opens multiple messages at once and receives their attached rewards as a single transaction. Since the reward-granting process is executed sequentially internally, you can avoid update rate-limit issues that may occur when opening messages with large numbers of identical items attached in parallel.

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).BatchReadAsync(
        messageNames: new [] {
            "message-0001",
            "message-0002",
            "message-0003",
        }
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->BatchRead(
        []
        {
            const auto v = MakeShared<TArray<FString>>();
            v->Add("message-0001");
            v->Add("message-0002");
            v->Add("message-0003");
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Delete a message

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Message(
        messageName: "message-0001"
    ).DeleteAsync(
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Message(
        "message-0001" // messageName
    )->Delete(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Receive global messages

If there are global messages you have not yet received, this copies the messages into your own inbox. The timing can be controlled by the game, such as at login or after the title screen finishes loading.

    var result = await gs2.Inbox.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).ReceiveGlobalMessageAsync(
    );
    const auto Future = Gs2->Inbox->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->ReceiveGlobalMessage(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Frequently Asked Questions

Can I open messages in bulk?

Yes. You can use BatchReadAsync to open multiple messages collectively as a single transaction. See Open messages in bulk for details.

You can also call individual ReadAsync in parallel, but you should carefully consider whether the reward-granting process may exceed the limits defined by each microservice.

For example, GS2-Inventory defines that the held quantity of the same item can be updated no more than three times per second. Reading 10 messages with the same item attached in parallel may exceed this limit.

In principle, no results are lost even if this limit is exceeded. However, after calling the read API, the reward-granting process, which runs asynchronously, may take some time to complete. Even in such cases, with BatchReadAsync the operations are executed sequentially internally, so rewards can be received reliably without hitting rate limits.

I want to display an unread-count badge for messages

You can display the number of unread messages from the list count obtained by MessagesAsync(isRead: false). By subscribing to the receive notification (receiveNotification), the client can re-fetch the list when a new message arrives.

Detailed Reference