GS2-Quest

Progress tracking feature

Manages game progression and quest progress.

GS2-Quest is a microservice that manages on the server only the “entry” and “exit” of the in-game portion of the game (battles, stages, etc.). It is not involved in the internal logic of the in-game; it is responsible for processing that should be trusted by the server, such as cost consumption at start, granting rewards on clear, and evaluation of prerequisites.

graph LR
  Start["Quest start<br/>StartAsync"] --> Battle["Run the in-game<br/>(client / dedicated server)"]
  Battle -- Success --> End1["Report quest end<br/>EndAsync(isComplete:true)"]
  Battle -- Failure --> End2["Report quest end<br/>EndAsync(isComplete:false)"]
  End1 --> Reward["Grant clear rewards"]
  End2 --> FailedReward["Grant failure rewards"]

Quests

A quest is the basic unit of in-game play and is the entity you select when starting the in-game portion. A quest can have a cost required to attempt it and rewards earned by attempting it, and GS2-Quest accepts the start and end of a quest as APIs. In other words, GS2-Quest is not involved in the in-game content itself.

Quest Challenge Cost

You set the cost required to put a quest into the started state. Typically, you configure costs such as consuming stamina managed by GS2-Stamina or consuming items managed by GS2-Inventory.

By configuring consume actions in consumeActions on the QuestModel, the costs are processed atomically as a transaction when Start is executed.

Quest Verify Conditions

By configuring verify actions in verifyActions on the QuestModel, you can perform additional condition checks when the quest starts. For example, you can express conditions that only check state without consuming anything, such as “the player must possess a specific item” or “a specific entry must be registered in GS2-Dictionary”.

Quest Clear Rewards

You can configure rewards obtained when the player attempts and clears a quest. Multiple types (Contents) can be prepared for rewards. Each Contents can have a weight for the lottery, and the reward pattern applied is determined according to that probability. Using this feature, you can set up a version of the quest where a rare monster appears with a certain probability and the rewards are more luxurious than usual.

First Clear Reward

You can configure an additional reward that is granted only the first time the quest is cleared. Set acquire actions in firstCompleteAcquireActions on the QuestModel.

Reducing Clear Rewards

If a monster that appeared in the quest was not defeated, or a treasure chest was missed, you can reduce the quest’s rewards.

The quest start API responds with the maximum rewards obtainable within the quest, and the quest completion API reports the quantities actually obtained. By reporting fewer rewards in this report, the reward amount is reduced. If you attempt to report more rewards than the maximum, an error is returned.

Quest Failure Rewards

You can configure rewards obtained when the player attempts a quest but fails to clear it. They are configured in failedAcquireActions on the QuestModel. On failure, you can achieve processing such as refunding the stamina paid when starting the quest.

Quest Prerequisites

You can set the prerequisite that other quests must have been cleared before attempting a quest. By setting an array of quest names in premiseQuestNames on the QuestModel, the quest cannot be attempted unless all of the specified quests have been cleared. This allows you to chain quests together.

Quest Challenge Period

You can associate a GS2-Schedule event with a quest as its challenge period. The challengeable period is judged when the start API is executed; it is not judged at completion processing.

Therefore, even if the period passes before the end is reported, there will be no situation where the quest reward cannot be received.

Quest Group

An entity that bundles multiple quests together. It is useful for managing quests grouped by chapter or world.

Quest Group Challenge Period

A quest group can also be associated with a GS2-Schedule event as its challenge period. When a challenge period is set on the quest group, the condition is applied to all quests within the group.

When a challenge period is set on both the quest group and the quest, the quest can only be attempted when both events are active.

Quest in Progress (Progress)

When a quest starts, exactly one in-progress Progress is recorded on the server per user. The Progress holds the maximum rewards already determined by the lottery and a random seed generated on the server, which are used to verify the validity of the reward quantities reported.

Since only one in-progress Progress can be held per user, if completion cannot be reported due to a disconnection or similar, you must discard it with DeleteProgress before starting another quest. When starting another quest, you can also start a new one while discarding the in-progress Progress by specifying force: true to StartAsync.

Managing Clear Status

When a quest is cleared, the name of that quest is recorded in CompletedQuestList. This is managed as a separate entity per quest group, allowing you to retrieve the clear status of a particular quest group in one go.

The clear status is also used on the server to judge prerequisites, so when you need to modify it from outside, you must go through transaction actions.

Script Triggers

By configuring startQuestScript, completeQuestScript, and failedQuestScript on the namespace, you can invoke custom scripts at the timing of quest start, clear, and failure.

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

  • startQuestScript: When a quest starts
  • completeQuestScript: When a quest is cleared
  • failedQuestScript: When a quest fails

Scripts can make decisions such as overriding rewards or rejecting a quest start.

Master Data Management

By registering master data, you can configure the data and behaviors available in the microservice.

The types of master data are as follows.

  • QuestGroupModel: Quest groupings and their challenge periods
  • QuestModel: Definition of costs and rewards

In addition to registering master data from the management console, you can also reflect data from GitHub or build a workflow that registers it from CI using GS2-Deploy.

The main configuration items of the quest model are as follows.

Item Description
name Quest name
contents Reward patterns selected by lottery (completeAcquireActions and weight)
firstCompleteAcquireActions Reward granted only on the first clear
failedAcquireActions Reward granted on failure
consumeActions Resources consumed when starting
verifyActions Prerequisite checks performed when starting
premiseQuestNames Cleared quests that are required as prerequisites
challengePeriodEventId GS2-Schedule event representing the challenge period

Example master data in JSON:

{
  "version": "2022-02-15",
  "questGroupModels": [
    {
      "name": "main",
      "metadata": "main-story",
      "quests": [
        {
          "name": "quest-0001",
          "metadata": "intro",
          "contents": [
            {
              "metadata": "normal",
              "completeAcquireActions": [
                {
                  "action": "Gs2Inventory:AcquireItemSetByUserId",
                  "request": "{...}"
                }
              ],
              "weight": 9
            },
            {
              "metadata": "rare",
              "completeAcquireActions": [
                {
                  "action": "Gs2Inventory:AcquireItemSetByUserId",
                  "request": "{...}"
                }
              ],
              "weight": 1
            }
          ],
          "consumeActions": [
            {
              "action": "Gs2Stamina:ConsumeStaminaByUserId",
              "request": "{...}"
            }
          ],
          "premiseQuestNames": []
        }
      ]
    }
  ]
}

Buff-Based Adjustment

By integrating with GS2-Buff, you can apply buffs to a quest model’s completeAcquireActions, firstCompleteAcquireActions, failedAcquireActions, verifyActions, and consumeActions. You can flexibly adjust rewards, participation conditions, and consumption costs according to events and campaigns.

You can implement experiences such as “1.5x quest rewards during the login campaign” or “half the challenge cost while a specific piece of equipment is equipped” without rewriting the master data.

Transaction Actions

GS2-Quest provides the following transaction actions.

  • Consume action: Delete a quest progress (Progress)
  • Acquire action: Create a quest progress (Progress)

By using “Create quest progress” as an acquire action, you can safely run processes within a transaction that directly put a specific quest into a started state as a reward for a shop purchase or as a reward for completing a specific mission. This makes it easy to provide a seamless play experience such as immediately guiding the player to a special quest after they purchase a particular item.

Implementation Examples

Retrieving the List of Quest Groups

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

Retrieving the List of Quest Models

    var items = await gs2.Quest.Namespace(
        namespaceName: "namespace-0001"
    ).QuestGroupModel(
        questGroupName: "quest-group-0001"
    ).QuestModelsAsync(
    ).ToListAsync();
    const auto Domain = Gs2->Quest->Namespace(
        "namespace-0001" // namespaceName
    )->QuestGroupModel(
        "quest-group-0001" // questGroupName
    );
    const auto It = Domain->QuestModels(
    );
    TArray<Gs2::UE5::Quest::Model::FEzQuestModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Retrieving Quest Clear Status

    var item = await gs2.Quest.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).CompletedQuestList(
        questGroupName: "main"
    ).ModelAsync();

    var completedQuestNames = item.CompleteQuestNames;
    const auto Domain = Gs2->Quest->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->CompletedQuestList(
        "main" // questGroupName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

Starting a Quest

    var result = await gs2.Quest.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).StartAsync(
        questGroupName: "group-0001",
        questName: "quest-0001"
    );

    await result.WaitAsync();
    const auto Domain = Gs2->Quest->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->Start(
        "group-0001", // questGroupName
        "quest-0001" // questName
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;

Retrieving the In-Progress Quest

You can retrieve the maximum rewards determined when the quest started, which can be used for in-game clear presentation.

    var item = await gs2.Quest.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).ModelAsync();

    var rewards = item.Rewards;
    const auto Domain = Gs2->Quest->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

Reporting the End of a Quest

Report whether the quest was cleared with isComplete, and the actual quantities of rewards obtained with rewards. As long as the reported values are within the maximums returned by the in-progress Progress, those quantities of rewards are granted.

    var result = await gs2.Quest.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).EndAsync(
        isComplete: true,
        rewards: new [] {
            new Gs2.Unity.Gs2Quest.Model.EzReward {
                Action = "Gs2Inventory:AcquireItemSetByUserId",
                ItemId = "grn:gs2:{region}:{ownerId}:inventory:namespace-0001:model:item-0001",
                Value = 3,
            },
        },
        config: null
    );

    await result.WaitAsync();
    const auto Domain = Gs2->Quest->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    );
    const auto Future = Domain->End(
        true,
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Quest::Model::FReward>>>();
            v->Add(MakeShared<Gs2::Quest::Model::FReward>()
                ->WithAction(TOptional<FString>("Gs2Inventory:AcquireItemSetByUserId"))
                ->WithItemId(TOptional<FString>("grn:gs2:{region}:{ownerId}:inventory:namespace-0001:model:item-0001"))
                ->WithValue(TOptional<int32>(3)));
            return v;
        }(), // rewards
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;

Discarding the In-Progress Quest

Use this for recovery when End could not be called due to a disconnection or similar.

    await gs2.Quest.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).DeleteProgressAsync();
    const auto Future = Gs2->Quest->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->DeleteProgress();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Other Features

Branching Quests

Under the normal specification, quests cannot be branched. If you want to implement a quest with two exits and which subsequent quest can be attempted depends on which exit was used, consider the following data structure.

Quest Name Prerequisite Quest
Quest1
Quest1a Phantom
Quest1b Phantom
Quest2a Quest1a
Quest2b Quest1b

This is a bit tricky, but a quest’s prerequisite quest can be set to a quest name that does not exist in the master data.

Here, Quest1a / Quest1b have a quest named “Phantom” as their prerequisite, but the Phantom quest does not exist in the master data. Therefore, Quest1a / Quest1b are quests that can never be attempted directly.

Quest2a / Quest2b have Quest1a / Quest1b as their prerequisite quests. In this state, configure the clear rewards of Quest1 to include “Mark Quest1a as cleared” and “Mark Quest1b as cleared”, and decide which of these rewards to give to the player based on which exit they used.

The reason Quest1a / Quest1b exist is that quests that do not exist in the master data cannot be marked as cleared.

graph TD
  Quest1 -- if use exit A --> Quest1a
  Quest1 -- if use exit B --> Quest1b
  phantom --- Quest1a
  phantom --- Quest1b
  Quest1a --> Quest2a
  Quest1b --> Quest2b
  linkStyle 2 stroke:#ccc,stroke-dasharray:4
  linkStyle 3 stroke:#ccc,stroke-dasharray:4
  class phantom pale
  class Quest1a pale
  class Quest1b pale

Passing Parameters to Scripts via Config

StartAsync / EndAsync accept a config parameter, which lets you pass arbitrary key-value pairs to the script when a script trigger runs. You can convey game-specific context such as the difficulty the player selected or which items they used.

Resetting Clear Status

By deleting CompletedQuestList, you can reset the clear status of a specific quest group. This is useful for implementing event re-runs or chapter new-game+ flows.

Detailed Reference