GS2-Dictionary

Dictionary feature

GS2-Dictionary provides a dictionary (compendium) function for items and characters acquired in the game.

Basically, you can think of it as a simple-implementation version of GS2-Inventory, which manages a two-state (acquired / not acquired) ownership status.

It can also be used for purposes other than a dictionary. For example, the ownership status of avatar parts is a good example. Whether you own a particular avatar part is sufficient to manage as a two-state per part, so GS2-Dictionary is well suited for this. It can also be applied broadly to any element that can be expressed as a binary state, such as managing completed tutorial steps, unlocked cutscenes, or a list of unlocked BGM tracks.

graph TD
  Boss["Defeat a boss"] -- Add entry as reward --> Dictionary["Record in GS2-Dictionary"]
  Shop["Purchase from shop"] -- Add entry as reward --> Dictionary
  Dictionary --> Browse["Player browses the dictionary"]
  Dictionary -- Possession-proof signature --> Formation["Used by GS2-Formation, etc."]

Differences from GS2-Inventory

Presence of the stack concept

GS2-Inventory has the concept of stacking when holding multiple copies of the same item. For example, potions can be stacked up to 99, and a second stack is created when more than 99 are held.

Because of this specification, in GS2-Inventory the return value of the API for “get the quantity of potions held” is a list. This is because there may be multiple stacks of potions. This specification is convenient for managing data such as potions, but it is over-spec for managing a simple possession status like a dictionary, and handling lists also increases the amount of code written.

Multiple entries can be registered in a single acquisition

GS2-Inventory requires acquiring a potion and acquiring an elixir to be handled in separate API requests. GS2-Dictionary can register up to 100 entries in a single API request.

This allows operations such as “register all defeated monster species in the dictionary at once” as a quest clear reward to be efficiently performed in a single request.

Verification and deletion of entries

GS2-Dictionary allows you to delete entries that have been recorded, or to verify whether a specific entry has been acquired (or not acquired). This enables operations such as resetting the possession status of limited-time event items, or restricting quests so that they can only be started when a specific item has been acquired.

Feature comparison

Item GS2-Dictionary GS2-Inventory
State management Two-state: owned / not owned Quantity / stack management
Entries per request Up to 100 One at a time
Favorites Yes No
Possession-proof signature Yes Yes (per ItemSet)
Main use Dictionary, avatar parts, unlock state Ordinary item possession

Master data (EntryModel)

Entries that can be registered in the dictionary are defined as master data. The main configuration items of EntryModel are as follows.

Item Description
name Entry name (record key per player)
metadata Arbitrary metadata used by the client (display name, image reference, rarity, etc.)

Example master data JSON:

{
  "version": "2020-04-30",
  "entryModels": [
    {
      "name": "monster-0001",
      "metadata": "{\"displayName\":\"Slime\",\"rarity\":1}"
    },
    {
      "name": "monster-0002",
      "metadata": "{\"displayName\":\"Goblin\",\"rarity\":2}"
    }
  ]
}

Master data can be registered via the management console, imported from GitHub, or registered from CI using GS2-Deploy or various language CDKs.

Favorites feature

GS2-Dictionary allows you to mark recorded entries as favorites. Use AddLikes or DeleteLikes to manage the favorites list, and use the Likes API to retrieve them or subscribe to change notifications. By using favorite information, you can implement filtering on the dictionary screen or prioritized display in the UI.

Favorite entries are internally stored in a unit called LikeToc, so a user can efficiently retrieve a large number of favorites.

Possession-proof signature

GS2-Dictionary can issue signed data for the possession status of an entry. When integrating with other GS2 microservices, this allows you to guarantee “the player really does own that entry” without server-to-server communication.

sequenceDiagram
  participant Player
  participant Dictionary as GS2-Dictionary
  participant Formation as GS2-Formation
  Player ->> Dictionary: GetEntryWithSignature(entry, keyId)
  Dictionary -->> Player: Body + Signature
  Player ->> Formation: SetForm(itemId, body, signature)
  Formation ->> Formation: Verify signature
  Formation -->> Player: Success

The verifying service can validate the information contained in body (such as the entry name and user ID) using signature.

Script triggers

By setting entryScript or duplicateEntryScript in the namespace, you can invoke custom scripts before/after entry registration or when a duplicate registration occurs. Scripts can be executed synchronously or asynchronously; asynchronous execution also enables external integration via GS2-Script or Amazon EventBridge.

Main event triggers and script setting names are:

  • entryScript (completion notification: entryDone): before and after entry registration
  • duplicateEntryScript: when re-registering an already registered entry

Using duplicateEntryScript, you can implement conversion logic such as “when a rare monster that is already registered in the dictionary is dropped again, grant a different currency instead”.

Transaction actions

GS2-Dictionary provides the following transaction actions:

  • Verify action: Verify entry possession status
  • Consume action: Delete entries
  • Acquire action: Add entries

By using “Verify entry possession status” as a verify action, you can impose restrictions such as a quest that only players who have defeated a specific monster (and thus registered it in the dictionary) can attempt, or a product that can only be purchased by players who have acquired a specific item at least once.

How to record in the dictionary

Recording in the dictionary (adding an Entry) cannot be called directly from the game-engine SDKs. This is by design of the GS2 security model: arbitrary client-side rewrites are prohibited, and entries are only added through paths trusted by the server.

Specifically, it is incorporated as a transaction action in forms such as the following:

  • Set an acquire action that “registers the defeated monster in the dictionary” as a clear reward of a GS2-Quest quest
  • Set an acquire action that “registers the corresponding entry in the dictionary” as a reward for an item purchased in GS2-Showcase
  • Set an acquire action that “registers a specific entry” as a mission completion reward of GS2-Mission

Implementation Example

Get a list of master data that can be recorded in the dictionary

    var items = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).EntryModelsAsync(
    ).ToListAsync();
    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    );
    const auto It = Domain->EntryModels(
    );
    TArray<Gs2::UE5::Dictionary::Model::FEzEntryModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Get the list of entries recorded in the dictionary

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

Get a specific entry

    var item = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Entry(
        entryModelName: "entry-0001"
    ).ModelAsync();

    var acquiredAt = item.AcquiredAt;
    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Entry(
        "entry-0001" // entryModelName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();
    const auto AcquiredAt = Result->GetAcquiredAt();

Obtaining a possession-proof signature

When linking with other microservices within GS2, you may be required to provide data that guarantees an entry really has been recorded in GS2-Dictionary.

For example, suppose GS2-Dictionary manages the possession status of avatar parts and GS2-Formation manages how avatar parts are equipped. When setting a hairstyle in GS2-Formation, an API request is made to set the part “hair-0001”, but GS2-Formation requires that “hair-0001” be specified together with a possession-proof signature.

This way, GS2-Formation does not need to communicate with GS2-Dictionary behind the scenes to determine whether the part is really owned.

    var result = await gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Entry(
        entryModelName: "entry-0001"
    ).GetEntryWithSignatureAsync(
        keyId: "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001"
    );

    var body = result.Body;
    var signature = result.Signature;
    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Entry(
        "entry-0001" // entryModelName
    );
    const auto Future = Domain->GetEntryWithSignature(
        "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001" // keyId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
    const auto Result = Future2->GetTask().Result();
    const auto Body = Result->Body;
    const auto Signature = Result->Signature;

Adding favorite entries

    var domain = gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    );
    var result = await domain.AddLikesAsync(
        entryModelNames: new List<string> {
            "entry-0001",
        }
    );
    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->AddLikes(
        {
            "entry-0001",
        }
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Listing favorite entries

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

Deleting favorite entries

    var domain = gs2.Dictionary.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    );

    await domain.DeleteLikesAsync(
        entryModelNames: new List<string> {
            "entry-0001",
        }
    );
    const auto Domain = Gs2->Dictionary->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );

    const auto Future = Domain->DeleteLikes(
        {
            "entry-0001",
        }
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Detailed Reference