Managing Player Balances
In this section, we will cover one possible solution for retrieving a given player's asset balances and maintaining said balances as a collection that is cached locally. Before reading this section, we recommend reading the introductory sections for SDKs, particularly setting up an event service and interacting with cloud events.
What we will be managing throughout this section is a cache of balances for assets owned by a player in our project.
Why we want to manage this is to reduce the number of API calls we make to the platform and to reduce the time spent idling waiting for a response back. Overall this will help improve our project's user experience by allowing us to display our player's asset balance sooner rather than later and free up network resources with few platform queries.
How we may perform this is through the utilization of a platform client, an event service, and an event listener from the SDK. The platform client will be what we use to make the initial request to fetch the player's balances. The event service is what we will use to subscribe to receive events related to our player's wallet. And last but not least, the event listener is what will be processing the cloud events we receive.
For the purposes of the example, we will only focus on the aspects unique to balance management. Therefore, we will assume that the client and event service have already been setup and that the event listener has been registered with the service. Furthermore, we will also assume that the player has already been created for the project and is linked to a wallet.
The following class lays out the fields and methods we will be using to maintain our cache of balances for the player. Key parts relevant for caching balances are the collections for storing fungible and non-fungible balances and the various methods to add to and subtract from the balances.
Java
C# | Unity
C++
Unreal
import com.enjin.sdk.PlayerClient;
import com.enjin.sdk.events.PusherEventService;
import java.util.Map;
import java.util.Set;
class MyPlayer {
// Data
String ethAddress;
Map<String, Integer> fungibleBalances;
Map<String, Set<String>> nonFungibleBalances;
// Client and service
PlayerClient client;
PusherEventService eventService;
// Mutexes
Object mutex = new Object();
public String getEthAddress() { /* ... */ } // Synchronizes on mutex
public void addBalance(String id, Integer value) { /* ... */ }
void addBalanceImpl(String id, Integer value) { /* ... */ }
public void addBalance(String id, String index) { /* ... */ }
void addBalanceImpl(String id, String index) { /* ... */ }
public void subtractBalance(String id, Integer value) { /* ... */ }
public void subtractBalance(String id, String index) { /* ... */ }
void retrievePlayerData() { /* ... */ }
}
using System.Collections.Generic;
using Enjin.SDK;
using Enjin.SDK.Events;
public class MyPlayer
{
// Data
string ethAddress;
Dictionary<string, int> fungibleBalances;
Dictionary<string, ISet<string>> nonFungibleBalances;
// Client and event service
PlayerClient client;
PusherEventService eventService;
// Mutexes
object mutex = new object();
public string GetAddress() { /* ... */ } // Locks on mutex
public void AddBalance(string id, int value) { /* ... */ }
void AddBalanceImpl(string id, int value) { /* ... */ }
public void AddBalance(string id, string index) { /* ... */ }
void AddBalanceImpl(string id, string index) { /* ... */ }
public void SubtractBalance(string id, int value) { /* ... */ }
public void SubtractBalance(string id, string index) { /* ... */ }
void RetrievePlayerData() { /* ... */ }
}
MyPlayer.h
#include "enjinsdk/PlayerClient.hpp"
#include "enjinsdk/PusherEventService.hpp"
#include <future>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
class MyPlayer {
// Data
std::string eth_address;
std::map<std::string, int> fungible_balances;
std::map<std::string, std::set<std::string>> non_fungible_balances;
// Client and event service
std::unique_ptr<enjin::sdk::PlayerClient> client;
std::unique_ptr<enjin::sdk::events::PusherEventService> event_service;
// Mutexes
mutable std::mutex mutex;
void add_balance_impl(std::string id, int value);
void add_balance_impl(std::string id, std::string index);
std::future<void> retrieve_player_data();
public:
std::string get_eth_address() const; // Locks on mutex
void add_balance(std::string id, int value);
void add_balance(std::string id, std::string index);
void subtract_balance(std::string id, int value);
void subtract_balance(std::string id, std::string index);
};
MyPlayer.h
#include "PlayerClient.h"
#include "PusherEventService.h"
#include <mutex>
class FMyPlayer
{
// Data
FString EthAddress;
TMap<FString, int32> FungibleBalances;
TMap<FString, TSet<FString>> NonFungibleBalances;
// Client and event service
TUniquePtr<Enjin::Sdk::FPlayerClient> Client;
TUniquePtr<Enjin::Sdk::Event::FPusherEventService> EventService;
// Mutexes
mutable std::mutex Mutex;
void AddBalanceImpl(FString Id, int32 Value);
void AddBalanceImpl(FString Id, FString Index);
void RetrievePlayerData();
public:
FString GetEthAddress() const; // Locks on mutex
void AddBalance(FString Id, int32 Value);
void AddBalance(FString Id, FString Index);
void SubtractBalance(FString Id, int32 Value);
void SubtractBalance(FString Id, FString Index);
};
For adding balances, it may be desirable to separate the implementation into two methods. One being the public interface which locks the mutex, ensuring our data is atomic when called outside our player class. And another being the private implementation we may call internally when we already control the lock on the mutex. This will be useful later on when we make the initial query for our player's data.
The code-block below shows the add balance methods we may use for fungible balances. Essentially, if we already created an entry for the given asset ID, then we will add to it and if not, then we will create an entry and assign the initial value.
Java
C# | Unity
C++
Unreal
void addBalance(String id, Integer value) {
synchronized (mutex) {
addBalanceImpl(id, value);
}
}
void addBalanceImpl(String id, Integer value) {
if (fungibleBalances.containsKey(id)) {
fungibleBalances.put(id, fungibleBalances.get(id) + value);
} else {
fungibleBalances.put(id, value);
}
}
public void AddBalance(string id, int value)
{
lock (mutex)
{
AddBalanceImpl(id, value);
}
}
void AddBalanceImpl(string id, int value)
{
if (fungibleBalances.ContainsKey(id))
{
fungibleBalances[id] += value;
}
else
{
fungibleBalances.Add(id, value);
}
}
MyPlayer.cpp
#include <mutex>
#include <string>
void MyPlayer::add_balance(std::string id, int value) {
std::lock_guard<std::mutex> guard(mutex);
add_balance_impl(id, value);
}
void MyPlayer::add_balance_impl(std::string id, int value) {
auto key_iter = fungible_balances.find(id);
if (key_iter == fungible_balances.end()) {
fungible_balances.emplace(id, value);
} else {
key_iter->second += value;
}
}
MyPlayer.cpp
#include <mutex>
void FMyPlayer::AddBalance(FString Id, int32 Value)
{
std::lock_guard<std::mutex> Guard(Mutex);
AddBalanceImpl(Id, Value);
}
void FMyPlayer::AddBalanceImpl(FString Id, int32 Value)
{
if (FungibleBalances.Contains(Id))
{
FungibleBalances.Add(Id, FungibleBalances[Id] + Value);
}
else
{
FungibleBalances.Add(Id, Value);
}
}
For adding non-fungible assets, the code-block below shows how we may add a balance for an instance of such assets. If there is already an entry for the given asset, then we will add the index to the set of indices for the asset. If there is no entry, then we will create a new set, add the index to the set, and add the entry for the asset with the set of indices.
Java
C# | Unity
C++
Unreal
import java.util.Set;
void addBalance(String id, String index) {
synchronized (mutex) {
addBalanceImpl(id, index);
}
}
void addBalanceImpl(String id, String index) {
Set<String> indices;
if (nonFungibleBalances.containsKey(id)) {
indices = nonFungibleBalances.get(id);
} else {
indices = new HashSet<>();
nonFungibleBalances.put(id, indices);
}
indices.add(index);
}
using System.Collections.Generic;
public void AddBalance(string id, string index)
{
lock (mutex)
{
AddBalanceImpl(id, index);
}
}
void AddBalanceImpl(string id, string index)
{
ISet<string> indices;
if (nonFungibleBalances.ContainsKey(id))
{
indices = nonFungibleBalances[id];
}
else
{
indices = new HashSet<string>();
nonFungibleBalances.Add(id, indices);
}
indices.Add(index);
}
MyPlayer.cpp
#include <mutex>
#include <string>
#include <utility>
void MyPlayer::add_balance(std::string id, std::string index) {
std::lock_guard<std::mutex> guard(mutex);
add_balance_impl(id, index);
}
void MyPlayer::add_balance_impl(std::string id, std::string index) {
auto key_iter = non_fungible_balances.find(id);
if (key_iter == non_fungible_balances.end()) {
std::set<std::string> indices;
indices.emplace(index);
non_fungible_balances.emplace(id, std::move(indices));
} else {
key_iter->second.emplace(index);
}
}
MyPlayer.cpp
#include <mutex>
void UEnjPlayer::AddBalance(FString Id, FString Index)
{
std::lock_guard Guard(PlayerMutex);
AddBalanceImpl(Id, Index);
}
void UEnjPlayer::AddBalanceImpl(FString Id, FString Index)
{
if (NonFungibleBalances.Contains(Id))
{
TSet<FString>& Indices = NonFungibleBalances[Id];
Indices.Add(Index);
}
else
{
TSet<FString> Indices;
Indices.Add(Index);
NonFungibleBalances.Add(Id, MoveTemp(Indices));
}
}
For subtracting a fungible balance, we will want to first check if we have a balance for the given asset. We will also want to check if the reduction would result in having a non-positive balance. If it does, then we will want to remove the entry for the asset and if not, then we will assign the new value for the balance.
Java
C# | Unity
C++
Unreal
void subtractBalance(String id, Integer value) {
synchronized (mutex) {
if (!fungibleBalances.containsKey(id)) {
return;
}
Integer newValue = fungibleBalances.get(id) - value;
if (newValue > 0) {
fungibleBalances.put(id, newValue);
} else {
fungibleBalances.remove(id);
}
}
}
public void SubtractBalance(string id, int value)
{
lock (mutex)
{
if (!fungibleBalances.ContainsKey(id))
{
return;
}
int newValue = fungibleBalances[id] - value;
if (newValue > 0)
{
fungibleBalances[id] = newValue;
}
else
{
fungibleBalances.Remove(id);
}
}
}
MyPlayer.cpp
#include <mutex>
#include <string>
void MyPlayer::subtract_balance(std::string id, int value) {
std::lock_guard<std::mutex> guard(mutex);
auto key_iter = fungible_balances.find(id);
if (key_iter == fungible_balances.end()) {
return;
}
int new_value = key_iter->second - value;
if (new_value > 0) {
fungible_balances.emplace(id, new_value);
} else {
fungible_balances.erase(id);
}
}
MyPlayer.cpp
#include <mutex>
void FMyPlayer::SubtractBalance(FString Id, int32 Value)
{
std::lock_guard<std::mutex> Guard(Mutex);
if (!FungibleBalances.Contains(Id))
{
return;
}
int32 NewValue = FungibleBalances[Id] - Value;
if (NewValue > 0)
{
FungibleBalances.Add(Id, NewValue);
}
else
{
FungibleBalances.Remove(Id);
}
}
For non-fungible balances we will want to check if there is an entry for the given asset ID. If there is an entry then we will try to remove the given index from the set of indices we are tracking.
Java
C# | Unity
C++
Unreal
void subtractBalance(String id, String index) {
synchronized (mutex) {
if (nonFungibleBalances.containsKey(id)) {
nonFungibleBalances.get(id).remove(index);
}
}
}
public void SubtractBalance(string id, string index)
{
lock (mutex)
{
if (nonFungibleBalances.ContainsKey(id))
{
nonFungibleBalances[id].Remove(index);
}
}
}
MyPlayer.cpp
#include <mutex>
#include <string>
void MyPlayer::subtract_balance(std::string id, std::string index) {
std::lock_guard<std::mutex> guard(mutex);
auto key_iter = non_fungible_balances.find(id);
if (key_iter != non_fungible_balances.end()) {
key_iter->second.erase(index);
}
}
MyPlayer.cpp
#include <mutex>
void FMyPlayer::SubtractBalance(FString Id, FString Index)
{
std::lock_guard<std::mutex> Guard(Mutex);
if (NonFungibleBalances.Contains(Id))
{
NonFungibleBalances[Id].Remove(Index);
}
}
Now we will want to retrieve the data for our player from the platform, including the balances for their wallet. To get started, we will want to use the
GetPlayer
request and make calls to its methods to also request the player's wallet and the balances from their wallet. Once we have the response with our player's data we will want to do three things. Store the address of their wallet, subscribe to the event channel for their wallet, and create our initial cache from balances in their wallet.Java
C# | Unity
C++
Unreal
import com.enjin.sdk.models.Balance;
import com.enjin.sdk.models.Player;
import com.enjin.sdk.models.Wallet;
import com.enjin.sdk.schemas.player.queries.GetPlayer;
void retrievePlayerData() {
GetPlayer req = new GetPlayer()
.withWallet()
.withWalletBalances();
client.getPlayer(req).thenAccept(res -> {
if (!res.isSuccess()) {
// Handle unsuccessful response
return;
}
Player player = res.getData();
Wallet wallet = player.getWallet();
if (wallet == null) {
// Handle unlinked player
return;
}
synchronized (mutex) {
ethAddress = wallet.getEthAddress();
eventService.subscribeToWallet(ethAddress);
for (Balance balance : wallet.getBalances()) {
String id = balance.getId();
String index = balance.getIndex();
Integer value = balance.getValue();
// Using impl methods since we still control the mutex
if (index.equals("0000000000000000")) {
addBalanceImpl(id, value);
} else {
addBalanceImpl(id, index);
}
}
}
});
}
using Enjin.SDK.Graphql;
using Enjin.SDK.Models;
using Enjin.SDK.PlayerSchema;
void RetrievePlayerData()
{
GetPlayer req = new GetPlayer()
.WithWallet()
.WithWalletBalances();
client.GetPlayer(req).ContinueWith(task =>
{
if (!task.IsCompletedSuccessfully)
{
// Handle unsuccessful task
return;
}
GraphqlResponse<Player> res = task.Result;
if (!res.IsSuccess)
{
// Handle unsuccessful response
return;
}
Player player = res.Result;
Wallet wallet = player.Wallet;
if (wallet == null)
{
// Handle unlinked player
return;
}
lock (mutex)
{
ethAddress = wallet.EthAddress;
eventService.SubscribeToWallet(ethAddress);
foreach (Balance balance in wallet.Balances)
{
string id = balance.Id;
string index = balance.Index;
int? value = balance.Value;
if (index.Equals("0000000000000000"))
{
AddBalanceImpl(id, value.Value);
}
else
{
AddBalanceImpl(id, index);
}
}
}
});
}
MyPlayer.cpp
#include "enjinsdk/GraphqlResponse.hpp"
#include "enjinsdk/models/Balance.hpp"
#include "enjinsdk/models/Player.hpp"
#include "enjinsdk/models/Wallet.hpp"
#include "enjinsdk/player/GetPlayer.hpp"
#include <future>
#include <mutex>
#include <string>
using namespace enjin::sdk::graphql;
using namespace enjin::sdk::models;
using namespace enjin::sdk::player;
std::future<void> MyPlayer::retrieve_player_data() {
return std::async([this]() {
GetPlayer req = GetPlayer()
.set_with_wallet()
.set_with_wallet_balances();
GraphqlResponse<Player> res = client->get_player(req).get();
if (!res.is_successful()) {
// Handle unsuccessful task
return;
}
const Player& player = res.get_result().value();
if (!player.get_wallet().has_value()) {
// Handle unlinked player
return;
}
const Wallet& wallet = player.get_wallet().value();
std::lock_guard<std::mutex> guard(mutex);
eth_address = wallet.get_eth_address().value();
event_service->subscribe_to_wallet(eth_address);
for (const Balance& balance : wallet.get_balances().value()) {
const std::string& id = balance.get_id().value();
const std::string& index = balance.get_index().value();
int value = balance.get_value().value();
if (index == "0000000000000000") {
add_balance(id, value);
} else {
add_balance(id, index);
}
}
});
}
MyPlayer.cpp
#include "GraphQlResponse.h"
#include "Model/Balance.h"
#include "Model/Player.h"
#include "Model/Wallet.h"
#include "Player/GetPlayer.h"
using namespace Enjin::Sdk::GraphQl;
using namespace Enjin::Sdk::Model;
using namespace Enjin::Sdk::Player;
void FMyPlayer::RetrievePlayerData()
{
FGetPlayer Req = FGetPlayer()
.SetWithPlayerWallet()
.SetWithWalletBalances();
Client->GetPlayer(Req)
.Next([this](const TGraphQlResponseForOnePtr<FPlayer>& Res)
{
if (!Res.IsValid() || !Res->IsSuccessful())
{
// Handle unsuccessful response
return;
}
const FPlayer& Player = Res->GetResult().GetValue();
if (!Player.GetWallet().IsSet())
{
// Handle unlinked player
return;
}
const FWallet& Wallet = Player.GetWallet().GetValue();
std::lock_guard<std::mutex> Guard(Mutex);
EthAddress = Wallet.GetEthAddress().GetValue();
EventService->SubscribeToWallet(EthAddress);
for (const FBalance& Balance : Wallet.GetBalances().GetValue())
{
const FString& Id = Balance.GetId().GetValue();
const FString& Index = Balance.GetIndex().GetValue();
int32 Value = Balance.GetValue().Get(0);
if (Index.Equals(TEXT("0000000000000000")))
{
AddBalanceImpl(Id, Value);
}
else
{
AddBalanceImpl(Id, Index);
}
}
});
}
To process events broadcasted by the cloud we will want to create an event listener class which implements the
IEventListener
interface. Our listener will need a reference to our player to make calls to add and subtract. We will also want separate methods to process each asset event that affects our player's balances.Java
C# | Unity
C++
Unreal
import com.enjin.sdk.events.IEventListener;
import com.enjin.sdk.models.NotificationEvent;
class MyEventListener implements IEventListener {
MyPlayer player;
MyEventListener(MyPlayer player) { /* Assign field `player` */ }
@Override
public void notificationReceived(NotificationEvent event) { /* ... */ }
void processMelt(NotificationEvent event) { /* ... */ }
void processMint(NotificationEvent event) { /* ... */ }
void processTransfer(NotificationEvent event) { /* ... */ }
}
using Enjin.SDK.Events;
using Enjin.SDK.Models;
public class MyEventListener : IEventListener
{
MyPlayer player;
public MyEventListener(MyPlayer player) { /* Assign field `player` */ }
public void NotificationReceived(NotificationEvent e) { /* ... */ }
void ProcessMelt(NotificationEvent e) { /* ... */ }
void ProcessMint(NotificationEvent e) { /* ... */ }
void ProcessTransferred(NotificationEvent e) { /* ... */ }
}
MyEventListener.h
#include "MyPlayer.h"
#include "enjinsdk/IEventListener.hpp"
#include "enjinsdk/models/NotificationEvent.hpp"
class MyEventListener : public enjin::sdk::events::IEventListener {
MyPlayer& player;
public:
MyEventListener(MyPlayer& player);
void notification_received(
const enjin::sdk::models::NotificationEvent& event) override;
private:
void process_melt(enjin::sdk::models::NotificationEvent event);
void process_mint(enjin::sdk::models::NotificationEvent event);
void process_transferred(enjin::sdk::models::NotificationEvent event);
};