GS2-Exchange

In-game resource exchange feature

This is one of the most widely used microservices provided by GS2. It is responsible for converting the resources of any microservice into the resources of a completely different microservice.

There are many game specifications related to resource exchange, and GS2-Exchange comes into play each time.

Examples of resource exchange

Conversion of enhancement materials

Exchange 10 ★1 enhancement materials managed by GS2-Inventory for 1 ★2 enhancement material also managed by GS2-Inventory.

Obtain an item once every 6 hours

Exchange 1 stamina, which recovers once every 6 hours, managed by GS2-Stamina, for an item managed by GS2-Inventory.

Sell items

Exchange items managed by GS2-Inventory for in-game currency also managed by GS2-Inventory.

Types of exchange

GS2-Exchange has three modes: direct exchange, where exchange happens immediately at the rate defined in master data; asynchronous exchange, where the exchange result is obtained after a certain real-world time has passed after executing the exchange; and cost-increasing exchange, where the cost rises with each purchase.

flowchart LR
  Verify["Verify cost<br/>(verifyActions)"] --> Consume["Consume cost<br/>(consumeActions)"]
  Consume -->|timingType=direct| Acquire["Grant reward<br/>(acquireActions)"]
  Consume -->|timingType=await| Await["Create Await object"]
  Await -. lockTime elapses .-> Acquire2["AcquireAsync receives reward"]

The exchange behavior can be enabled per mode through the namespace settings enableDirectExchange / enableAwaitExchange. By switching the timingType of a rate model to direct or await, you can choose whether each individual rate operates as an immediate exchange or as an asynchronous exchange.

Behavior of asynchronous exchange

When an asynchronous exchange is used, the cost is consumed at the time the exchange is executed, and an Await object is created instead of the reward being granted. Once the exchange wait time defined in master data (lockTime seconds) elapses from the time the Await object was created, the reward can be received.

State Field
Time the exchange was executed exchangedAt
Time at which the reward becomes acquirable acquirableAt
Quantity consumed as cost count
Seconds shortened by skipping skipSeconds

Strengthening bases

In a city-building game, after consuming resources to grow a base, the base’s experience is actually added after 8 hours have elapsed.

Expeditions

After organizing a party and going on an adventure, the reward of the adventure can be received 3 hours later.

Skipping asynchronous exchange

You can also allow the wait of asynchronous exchanges to be skipped by paying additional cost. This kind of specification is typically implemented as a monetization feature, but by consuming in-game currency managed by GS2-Money that has been purchased with real money, you can implement a specification where wait times can be shortened.

Forced acquisition and deletion of pending rewards

Through the management console or API (transaction actions), it is possible to forcibly acquire rewards ignoring the wait time, or to cancel an in-progress asynchronous exchange (Await object) by deleting it.

Examples of cost-increasing exchange

Enhancement (inflationary game)

The amount of gold consumed to enhance increases each time you enhance.

Stamina recovery cost increases

The amount of paid currency required to purchase stamina increases each time stamina is purchased. The purchase count is reset daily.

Calculation of cost increase

There are three modes for the amount of cost increase, specified by calculateType of IncrementalRateModel.

linear

baseValue + (coefficientValue * number of exchanges)

Example

baseValue = 100, coefficientValue = 50

Number of exchanges Cost
0 100
1 150
2 200
3 250
4 300

power

coefficientValue * (number of exchanges + 1) ^ 2

Example

coefficientValue = 50

Number of exchanges Cost
0 50
1 200
2 450
3 800
4 1250

gs2_script

Calculated based on the result of executing a GS2-Script. Useful when you want to calculate the cost based on complex conditions.

Example

currentExchangeCount = args.currentExchangeCount
quantity = quantity

cost = 100
for i = 1 , quantity do
	cost = cost + (i + currentExchangeCount - 1) * 50
end

result = {
    cost=cost
}
Number of exchanges Cost
0 100
1 150
2 200
3 250
4 300

Managing the number of exchanges

In IncrementalRateModel, by specifying exchangeCountId and maximumExchangeCount, you can integrate with GS2-Limit’s count restriction model to track the number of exchanges executed, or set an exchange limit within a specific period.

If you specify a GS2-Limit Counter model as exchangeCountId, the exchange count is tracked in GS2-Limit and can be reset daily, weekly, etc., according to GS2-Limit’s reset specification. Exchanges that exceed maximumExchangeCount are rejected, which makes it possible to implement gachas with a per-period purchase limit, or item shops where you can purchase only N times per day.

Master data operations

Registering master data allows you to configure data and behavior that can be used by the microservice.

Available master data types:

  • RateModel: Rate definitions for immediate or asynchronous exchanges
  • IncrementalRateModel: Rate definitions where cost increases with the number of exchanges

Main fields of RateModel

Field Description
name Rate name (unique)
verifyActions Verify actions performed before executing the exchange
consumeActions Actions consumed as the cost
acquireActions Actions granted as the reward
timingType direct (immediate exchange) / await (asynchronous exchange)
lockTime Wait seconds of asynchronous exchange

Main fields of IncrementalRateModel

Field Description
name Rate name (unique)
consumeAction Action consumed as cost (quantity is determined by the cost calculation)
acquireActions Actions granted as reward
calculateType Cost calculation method (linear / power / gs2_script)
baseValue / coefficientValue Coefficients used in cost calculation
calculateScriptId Script invoked when using gs2_script
exchangeCountId GS2-Limit counter that manages the exchange count
maximumExchangeCount Upper limit on the number of exchanges

JSON example of master data

{
  "version": "2019-08-19",
  "rateModels": [
    {
      "name": "material_n_to_r",
      "metadata": "N -> R enhancement material exchange",
      "consumeActions": [
        {
          "action": "Gs2Inventory:ConsumeItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"n-material\",\"userId\":\"#{userId}\",\"consumeCount\":10}"
        }
      ],
      "timingType": "await",
      "lockTime": 3600,
      "acquireActions": [
        {
          "action": "Gs2Inventory:AcquireItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"r-material\",\"userId\":\"#{userId}\",\"acquireCount\":1}"
        }
      ]
    }
  ]
}

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

Script Triggers

By adding the following script settings to a namespace, you can run custom scripts before and after exchange processing or pending reward retrieval.

Main event triggers and script setting names are:

  • exchangeScript (completion notification: exchangeDone): before and after exchange processing
  • incrementalExchangeScript (completion notification: incrementalExchangeDone): before and after cost-increasing exchange
  • acquireAwaitScript (completion notification: acquireAwaitDone): before and after acquiring a pending reward

These scripts can be executed synchronously or asynchronously, with asynchronous execution also supporting external integration via GS2-Script or Amazon EventBridge.

When calculateType gs2_script is selected on an IncrementalRateModel, the calculateScriptId configured per rate is invoked to calculate the cost.

Correction by Buff

When linked with GS2-Buff, you can dynamically override lockTime, acquireActions, verifyActions, and consumeActions of a RateModel, and acquireActions, consumeAction, and maximumExchangeCount of an IncrementalRateModel via the context stack. This allows flexible adjustment of rewards, wait times, and exchange limits for events or campaigns.

For example, you can implement measures such as “halving the wait time for enhancement material exchanges for a limited period” or “raising the daily gacha purchase limit” by simply applying a buff, without rewriting master data.

Implementation Example

Executing an exchange (direct)

The config parameter accepts context values used in the transactions of the cost/reward actions associated with the exchange.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).ExchangeAsync(
        rateName: "rate-0001",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->Exchange(
        "rate-0001", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Starting an asynchronous exchange

When ExchangeAsync is called on a rate with timingType=await, the cost is consumed immediately and the reward is held in an Await object.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).ExchangeAsync(
        rateName: "material_n_to_r",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->Exchange(
        "material_n_to_r", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Get a list of Awaits

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

Receiving the reward of an Await after the wait time has elapsed

Receive the reward from an Await whose wait time has elapsed. The receive process issues a transaction, and the reward-granting processes defined in acquireActions are executed against other microservices.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Await(
        rateName: "material_n_to_r",
        awaitName: "await-0001"
    ).AcquireAsync(
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Await(
        "await-0001", // awaitName
        "material_n_to_r" // rateName
    );
    const auto Future = Domain->Acquire(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Deleting an Await (cancellation)

Discards a pending Await and cancels the exchange. Note that resources consumed as cost are not returned. If a refund is necessary, compensate via a server-side script using transaction actions.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Await(
        rateName: "material_n_to_r",
        awaitName: "await-0001"
    ).DeleteAwaitAsync(
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Await(
        "await-0001", // awaitName
        "material_n_to_r" // rateName
    );
    const auto Future = Domain->DeleteAwait(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Skipping wait time

To skip the wait time by paying an additional cost, configure the cost required to skip via skipByConfig, then call the server-side API (via GS2-JobQueue or GS2-Script). The skip process can also be used as a transaction action, so it can be combined with shop designs such as skip items or skip tickets.

Executing a cost-increasing exchange

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).IncrementalExchangeAsync(
        rateName: "rate-0001",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->IncrementalExchange(
        "rate-0001", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Transaction Actions

GS2-Exchange provides the following transaction actions:

  • Consume action: Delete an in-progress exchange wait (Await)
  • Acquire action: Immediately execute an exchange (Exchange), execute a cost-increasing exchange (IncrementalExchange), create an exchange wait (Await), forcibly acquire a pending reward, skip wait time

By using “Forcibly acquire pending reward” as an acquire action, you can immediately complete an in-progress construction or expedition (asynchronous exchange) as a reward for acquiring a specific item or achieving a mission. Also, by using “Delete exchange wait” as a consume action, you can interrupt (cancel) an in-progress process.

Detailed Reference