Skip to main content

Implement Subsystem - Entitlements Essentials - (Unreal Engine module)

Last updated on December 17, 2025
note

Make sure you have completed and activated the In-Game Store Essentials module Starter mode before proceeding with this page.

Unwrap the subsystem

Byte Wars uses the Game Instance Subsystem called EntitlementsEssentialsSubsystem to wrap the AccelByte Gaming Services (AGS) Online Subsystem (OSS). This subsystem uses FOnlineEntitlementsAccelByte, which is AccelByte's implementation of Unreal Engine’s IOnlineEntitlements interface. In this tutorial, you'll be working with a starter version of the subsystem, allowing you to implement the required functions from scratch.

What's in the Starter Pack

To follow along with this tutorial, a starter subsystem class named EntitlementsEssentialsSubsystem_Starter has been prepared. You can find it in the Resources section. It includes the following files:

  • Header file: Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsSubsystem_Starter.h
  • CPP file: Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsSubsystem_Starter.cpp

The EntitlementsEssentialsSubsystem_Starter class contains several helpful components:

  • Declaration and initialization of the AGS OSS interfaces, which provides access to AGS Software Development Kit (SDK) features.

    private:
    FOnlineIdentityAccelBytePtr IdentityInterface;
    FOnlineCloudSaveAccelBytePtr CloudSaveInterface;
    FOnlineEntitlementsAccelBytePtr EntitlementsInterface;
    void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    Super::Initialize(Collection);

    const FOnlineSubsystemAccelByte* Subsystem = static_cast<const FOnlineSubsystemAccelByte*>(Online::GetSubsystem(GetWorld()));
    if (!ensure(Subsystem))
    {
    return;
    }

    IdentityInterface = StaticCastSharedPtr<FOnlineIdentityAccelByte>(Subsystem->GetIdentityInterface());
    CloudSaveInterface = StaticCastSharedPtr<FOnlineCloudSaveAccelByte>(Subsystem->GetCloudSaveInterface());
    EntitlementsInterface = StaticCastSharedPtr<FOnlineEntitlementsAccelByte>(Subsystem->GetEntitlementsInterface());
    if (!ensure(IdentityInterface) || !ensure(CloudSaveInterface) || !ensure(EntitlementsInterface))
    {
    return;
    }
    // ...
    }
  • Variable to store in-game store items.

    private:
    // ...
    UPROPERTY()
    TArray<UStoreItemDataObject*> StoreOffers;
  • Functions to convert the FOnlineEntitlement data struct (used by the interface) to UStoreItemDataObject (used by the game). These also consolidate data from both entitlement and store item sources.

    private:
    // ...
    TArray<UStoreItemDataObject*> EntitlementsToDataObjects(TArray<TSharedRef<FOnlineEntitlement>> Entitlements) const;
    UStoreItemDataObject* EntitlementToDataObject(TSharedRef<FOnlineEntitlement> Entitlement) const;

In addition to the subsystem class, there is a file named EntitlementsEssentialsModel.h that contains helper classes, delegates, and constants. This file is located at:

/Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsModel.h

This file includes the following helpers:

  • Helper delegates to handle events and callbacks.

    DECLARE_DELEGATE_TwoParams(FOnGetOrQueryUserEntitlementsComplete, const FOnlineError& /*Error*/, const TArray<UStoreItemDataObject*> /*Entitlements*/)
    DECLARE_DELEGATE_TwoParams(FOnGetOrQueryUserItemEntitlementComplete, const FOnlineError& /*Error*/, const UStoreItemDataObject* /*Entitlement*/)
    DECLARE_DELEGATE_TwoParams(FOnConsumeUserEntitlementComplete, const FOnlineError& /*Error*/, const UStoreItemDataObject* /*Entitlement*/)
    DECLARE_DELEGATE_TwoParams(FOnUpdateUserEquipmentsComplete, const FOnlineError& /*Error*/, const FPlayerEquipments& /*Equipments*/)

    /**
    * Delegate for tracking when a player purchased an in-game item.
    * @param BuyingPlayerNetId Unique Net ID of the player doing the purchase.
    * @param TransactionId Unity ID of the transaction.
    * @param ItemName In-game item name of the purchased item.
    * @param Amount The quantity of the item purchased.
    * @param EndAmount Total quantity of the item that the player owned.
    */
    DECLARE_MULTICAST_DELEGATE_FiveParams(
    FOnItemPurchased,
    const FString& /*BuyingPlayerNetId*/,
    const FString& /*TransactionId*/,
    const FString& /*ItemName*/,
    const int /*Amount*/,
    const int /*EndAmount*/)
  • Helper structs to store entitlement requests and player equipments.

    struct FUserItemEntitlementRequest
    {
    const FUniqueNetIdRef UserId;
    FOnGetOrQueryUserItemEntitlementComplete OnComplete;
    };
    struct FConsumeEntitlementRequest
    {
    const FUniqueNetIdRef UserId;
    FOnConsumeUserEntitlementComplete OnComplete;
    };
    USTRUCT(BlueprintType)
    struct FPlayerEquipments
    {
    GENERATED_BODY()

    UPROPERTY(BlueprintReadWrite)
    FString SkinId;

    UPROPERTY(BlueprintReadWrite)
    FString ColorId;

    UPROPERTY(BlueprintReadWrite)
    FString ExplosionFxId;

    UPROPERTY(BlueprintReadWrite)
    FString MissileTrailFxId;

    UPROPERTY(BlueprintReadWrite)
    FString PowerUpId;
    };
  • String constants to display messages.

    #define USER_EQUIPMENT_KEY FString(TEXT("UserEqipment"))
    #define TEXT_SAVING_EQUIPMENTS NSLOCTEXT("AccelByteWars", "Saving Equipments", "Saving Equipments")
    #define TEXT_OWNED NSLOCTEXT("AccelByteWars", "Owned", "Owned")

Implement query entitlements

When implementing entitlement queries, it's important to understand that entitlements returned from the backend only indicate ownership of a store item, they do not contain the full metadata or configuration associated with that item. This means that simply knowing which item a player owns isn't enough; the game also needs to retrieve detailed store item data (such as SKU) separately. Because of this, the game must perform two requests in parallel: one to fetch the player's entitlements, and another to fetch the relevant store items. Only once both responses are available can the game accurately interpret and act on the entitlements. The diagram below shows how Byte Wars handles this logic in practice.

  1. Open EntitlementsEssentialsSubsystem_Starter header file and declare the following functions to retrieve the entitlements.

    public:
    void GetOrQueryUserEntitlements(
    const FUniqueNetIdPtr UserId,
    const FOnGetOrQueryUserEntitlementsComplete& OnComplete,
    const bool bForceRequest = false);
    void GetOrQueryUserItemEntitlement(
    const FUniqueNetIdPtr UserId,
    const FUniqueOfferId& StoreItemId,
    const FOnGetOrQueryUserItemEntitlementComplete& OnComplete,
    const bool bForceRequest = false);
  2. Declare the following variables to store completion delegates while a query request is in progress.

    private:
    // ...
    TMultiMap<const FUniqueNetIdRef, FOnGetOrQueryUserEntitlementsComplete> UserEntitlementsParams;
    TMultiMap<const FUniqueOfferId /*OfferId*/, FUserItemEntitlementRequest> UserItemEntitlementParams;
  3. Declare the following variables and functions to trigger and handle the queries.

    • QueryProcess: tracks how many query responses has been executed.
    • QueryResultUserId: tracks the player the current entitlement query is for. Useful when multiple accounts are logged in.
    • QueryResultError: stores errors from the entitlement query response.
    • QueryUserEntitlement(): triggers both entitlement and store item queries.
    • OnQueryEntitlementComplete(): handles the entitlement query response.
    • OnQueryStoreOfferComplete(): handles the store item query response.
    • GetItemEntitlement(): retrieves a cached entitlement for a given store item ID.
    • CompleteQuery(): called when both query responses have been received.
    private:
    // ...
    uint8 QueryProcess = 0;
    FUniqueNetIdPtr QueryResultUserId;
    FOnlineError QueryResultError;

    void QueryUserEntitlement(const FUniqueNetIdPtr UserId);
    void OnQueryEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const FString& Namespace,
    const FString& ErrorMessage);
    void OnQueryStoreOfferComplete(TArray<UStoreItemDataObject*> Offers);
    UStoreItemDataObject* GetItemEntitlement(const FUniqueNetIdPtr UserId, const FUniqueOfferId OfferId) const;
    void CompleteQuery();
  4. Open the EntitlementsEssentialsSubsystem_Starter CPP file and implement the GetOrQueryUserEntitlements() function to fetch all owned items. It checks the cache, calls the completion delegate if available, or stores it for later and starts the query.

    void UEntitlementsEssentialsSubsystem_Starter::GetOrQueryUserEntitlements(
    const FUniqueNetIdPtr UserId,
    const FOnGetOrQueryUserEntitlementsComplete& OnComplete,
    const bool bForceRequest)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get or query user entitlement. User ID is invalid.");
    OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), {});
    return;
    }

    // Check overall cache.
    TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
    if (!bForceRequest)
    {
    EntitlementsInterface->GetAllEntitlements(UserId.ToSharedRef().Get(), FString(), Entitlements);
    }

    // If empty, trigger query.
    if (Entitlements.IsEmpty() || bForceRequest)
    {
    UserEntitlementsParams.Add(UserId.ToSharedRef(), OnComplete);
    QueryUserEntitlement(UserId);
    }
    // If not, trigger OnComplete immediately.
    else
    {
    OnComplete.Execute(FOnlineError::Success(), EntitlementsToDataObjects(Entitlements));
    }
    }
  5. Implement the GetOrQueryUserItemEntitlement() to fetch an entitlement for a specific store item ID.

    void UEntitlementsEssentialsSubsystem_Starter::GetOrQueryUserItemEntitlement(
    const FUniqueNetIdPtr UserId,
    const FUniqueOfferId& StoreItemId,
    const FOnGetOrQueryUserItemEntitlementComplete& OnComplete,
    const bool bForceRequest)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get or query user entitlement. User ID is invalid.");
    OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), nullptr);
    return;
    }

    // Check overall cache.
    TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
    if (!bForceRequest)
    {
    EntitlementsInterface->GetAllEntitlements(UserId.ToSharedRef().Get(), FString(), Entitlements);
    }

    // If empty, trigger query.
    if (Entitlements.IsEmpty() || bForceRequest)
    {
    UserItemEntitlementParams.Add(StoreItemId, {UserId.ToSharedRef(), OnComplete});
    QueryUserEntitlement(UserId);
    }
    // If not, trigger OnComplete immediately.
    else
    {
    OnComplete.ExecuteIfBound(FOnlineError::Success(), GetItemEntitlement(UserId, StoreItemId));
    }
    }
  6. Implement the QueryUserEntitlement() to trigger both entitlement and store item queries. This function adds QueryProcess twice to track the two expected responses.

    void UEntitlementsEssentialsSubsystem_Starter::QueryUserEntitlement(const FUniqueNetIdPtr UserId)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to query user entitlement. User ID is invalid.");
    return;
    }

    if (QueryProcess > 0)
    {
    return;
    }
    QueryProcess++;

    // Trigger query entitlements.
    EntitlementsInterface->QueryEntitlements(UserId.ToSharedRef().Get(), TEXT(""), FPagedQuery());

    // Trigger query offers.
    const UTutorialModuleDataAsset* StoreDataAsset = UTutorialModuleUtility::GetTutorialModuleDataAsset(
    FPrimaryAssetId{ "TutorialModule:INGAMESTOREESSENTIALS" },
    this,
    true);
    if (StoreDataAsset)
    {
    STORE_SUBSYSTEM_CLASS* StoreSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<STORE_SUBSYSTEM_CLASS>();
    if (!ensure(StoreSubsystem))
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "INGAMESTOREESSENTIALS module is inactive or its starter mode doesn't match entitlement module's starter mode. Treating as failed.")
    OnQueryStoreOfferComplete({});
    return;
    }

    QueryProcess++;
    StoreSubsystem->GetOrQueryOffersByCategory(
    UserId,
    TEXT("/ingamestore/item"),
    FOnGetOrQueryOffersByCategory::CreateUObject(this, &ThisClass::OnQueryStoreOfferComplete));
    }
    else
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "INGAMESTOREESSENTIALS module is inactive or its starter mode doesn't match entitlement module's starter mode. Treating as failed.")
    OnQueryStoreOfferComplete({});
    }
    }
  7. Implement the OnQueryEntitlementComplete() to handle the entitlement query response. It stores the response status, decreases QueryProcess, and calls CompleteQuery() if both responses are received. The actual entitlement data isn't in the response, it’s stored locally in the OSS and retrieved separately in the next step.

    void UEntitlementsEssentialsSubsystem_Starter::OnQueryEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const FString& Namespace,
    const FString& ErrorMessage)
    {
    QueryProcess--;

    FOnlineError Error;
    Error.bSucceeded = bWasSuccessful;
    Error.ErrorRaw = ErrorMessage;

    QueryResultUserId = UserId.AsShared();
    QueryResultError = Error;

    if (QueryProcess <= 0)
    {
    CompleteQuery();
    }
    }
  8. Implement the OnQueryStoreOfferComplete() to handle the store item query response.

    void UEntitlementsEssentialsSubsystem_Starter::OnQueryStoreOfferComplete(TArray<UStoreItemDataObject*> Offers)
    {
    QueryProcess--;
    StoreOffers = Offers;

    if (QueryProcess <= 0)
    {
    CompleteQuery();
    }
    }
  9. Implement the GetItemEntitlement() to retrieve an entitlement for a specific store item ID from the cache.

    UStoreItemDataObject* UEntitlementsEssentialsSubsystem_Starter::GetItemEntitlement(
    const FUniqueNetIdPtr UserId,
    const FUniqueOfferId OfferId) const
    {
    if (!UserId)
    {
    return nullptr;
    }

    UStoreItemDataObject* Item = nullptr;
    if (const TSharedPtr<FOnlineEntitlement> Entitlement =
    EntitlementsInterface->GetItemEntitlement(UserId.ToSharedRef().Get(), OfferId);
    Entitlement.IsValid())
    {
    Item = EntitlementToDataObject(Entitlement.ToSharedRef());
    }

    return Item;
    }
  10. Implement the CompleteQuery() to finalize the query process and trigger all stored completion delegates.

    void UEntitlementsEssentialsSubsystem_Starter::CompleteQuery()
    {
    // Trigger on complete delegate of GetOrQueryUserEntitlements function.
    for (TMultiMap<const FUniqueNetIdRef, FOnGetOrQueryUserEntitlementsComplete>::TIterator It = UserEntitlementsParams.CreateIterator(); It; ++It)
    {
    const FUniqueNetIdRef& Key = It.Key();
    if (!QueryResultUserId || Key.Get() != QueryResultUserId.ToSharedRef().Get())
    {
    continue;
    }

    TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
    if (QueryResultError.bSucceeded)
    {
    EntitlementsInterface->GetAllEntitlements(Key.Get(), FString(), Entitlements);
    }

    It.Value().Execute(QueryResultError, EntitlementsToDataObjects(Entitlements));
    It.RemoveCurrent();
    }

    // Trigger on complete delegate of GetOrQueryUserItemEntitlement function.
    for (TMultiMap<const FUniqueOfferId, FUserItemEntitlementRequest>::TIterator It = UserItemEntitlementParams.CreateIterator(); It; ++It)
    {
    const FUniqueOfferId& OfferId = It.Key();
    const FUserItemEntitlementRequest& Request = It.Value();
    if (!QueryResultUserId || Request.UserId.Get() != QueryResultUserId.ToSharedRef().Get())
    {
    continue;
    }

    Request.OnComplete.Execute(QueryResultError, GetItemEntitlement(Request.UserId, OfferId));
    It.RemoveCurrent();
    }
    }
  11. Still in the CPP file, locate the Initialize() function and replace the existing implementation with the code below. It binds OnQueryEntitlementComplete() to the OSS delegate and QueryUserEntitlement() to the shop widget's OnActivatedMulticastDelegate so entitlements are refreshed when the shop opens.

    void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    EntitlementsInterface->AddOnQueryEntitlementsCompleteDelegate_Handle(FOnQueryEntitlementsCompleteDelegate::CreateUObject(this, &ThisClass::OnQueryEntitlementComplete));
    // ...
    UShopWidget::OnActivatedMulticastDelegate.AddWeakLambda(this, [this](const APlayerController* PC)
    {
    if (const ULocalPlayer* LocalPlayer = PC ? PC->GetLocalPlayer() : nullptr)
    {
    QueryUserEntitlement(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId());
    }
    });
    // ...
    }
  12. Navigate to the Deinitialize() function and replace the existing implementation with the code below. It unbinds the OnQueryEntitlementComplete() and QueryUserEntitlement() functions.

    void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
    {
    // ...
    if (EntitlementsInterface.IsValid())
    {
    EntitlementsInterface->ClearOnQueryEntitlementsCompleteDelegates(this);
    // ...
    }
    // ...
    UShopWidget::OnActivatedMulticastDelegate.RemoveAll(this);
    // ...
    }

Implement consume entitlement

Byte Wars provide two ways to consume entitlement: via entitlement ID, which is straight forward; and via in-game item ID, which is more complicated. The diagram below shows how Byte Wars handles the consume entitlement via in-game item ID.

  1. Open EntitlementsEssentialsSubsystem_Starter header file and declare the following functions to trigger the consume entitlement logic.

    public:
    // ...
    void ConsumeItemEntitlementByInGameId(
    const FUniqueNetIdPtr UserId,
    const FString& InGameItemId,
    const int32 UseCount = 1,
    const FOnConsumeUserEntitlementComplete& OnComplete = FOnConsumeUserEntitlementComplete());
    void ConsumeEntitlementByEntitlementId(
    const FUniqueNetIdPtr UserId,
    const FString& EntitlementId,
    const int32 UseCount = 1,
    const FOnConsumeUserEntitlementComplete& OnComplete = FOnConsumeUserEntitlementComplete());
  2. Declare the following function and variable to handle the response.

    private:
    // ...
    TMultiMap<const FString /*InGameItemId*/, FConsumeEntitlementRequest> ConsumeEntitlementParams;
    private:
    // ...
    void OnConsumeEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const TSharedPtr<FOnlineEntitlement>& Entitlement,
    const FOnlineError& Error);
  3. Open the EntitlementsEssentialsSubsystem_Starter CPP file and implement the ConsumeItemEntitlementByInGameId() to perform the sequence shown in the flowchart. It stores delegates to be triggered later in OnConsumeEntitlementComplete().

    void UEntitlementsEssentialsSubsystem_Starter::ConsumeItemEntitlementByInGameId(
    const FUniqueNetIdPtr UserId,
    const FString& InGameItemId,
    const int32 UseCount,
    const FOnConsumeUserEntitlementComplete& OnComplete)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to consume item entitlement. User ID is invalid.");
    OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), nullptr);
    return;
    }

    // Get item's AB SKU.
    UInGameItemDataAsset* Item = UInGameItemUtility::GetItemDataAsset(InGameItemId);
    if (!ensure(Item))
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to consume item entitlement. Item is invalid.");
    OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("Item is invalid."))), nullptr);
    return;
    }

    const FString ItemSku = Item->SkuMap[EItemSkuPlatform::AccelByte];

    // Construct delegate to consume item.
    FOnGetOrQueryUserItemEntitlementComplete OnItemEntitlementComplete =
    FOnGetOrQueryUserItemEntitlementComplete::CreateWeakLambda(this, [this, UserId, OnComplete, UseCount]
    (const FOnlineError& Error, const UStoreItemDataObject* Entitlement)
    {
    if (Entitlement)
    {
    ConsumeEntitlementParams.Add(Entitlement->GetEntitlementId(), {UserId.ToSharedRef(), OnComplete});
    EntitlementsInterface->ConsumeEntitlement(UserId.ToSharedRef().Get(), Entitlement->GetEntitlementId(), UseCount);
    }
    });

    // Construct delegate to get store Item ID by SKU, then get entitlement ID.
    const FOnGetOrQueryOffersByCategory OnStoreOfferComplete = FOnGetOrQueryOffersByCategory::CreateWeakLambda(
    this, [UserId, this, OnItemEntitlementComplete, ItemSku](TArray<UStoreItemDataObject*> Offers)
    {
    for (const UStoreItemDataObject* Offer : Offers)
    {
    if (Offer->GetSkuMap()[EItemSkuPlatform::AccelByte].Equals(ItemSku))
    {
    GetOrQueryUserItemEntitlement(UserId, Offer->GetStoreItemId(), OnItemEntitlementComplete);
    break;
    }
    }
    });

    // Trigger query store item.
    UTutorialModuleDataAsset* StoreDataAsset = UTutorialModuleUtility::GetTutorialModuleDataAsset(
    FPrimaryAssetId{ "TutorialModule:INGAMESTOREESSENTIALS" },
    this,
    true);
    if (StoreDataAsset && !StoreDataAsset->IsStarterModeActive())
    {
    STORE_SUBSYSTEM_CLASS* StoreSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<STORE_SUBSYSTEM_CLASS>();
    if (!ensure(StoreSubsystem))
    {
    return;
    }

    StoreSubsystem->GetOrQueryOffersByCategory(UserId, TEXT("/ingamestore/item"), OnStoreOfferComplete);
    }
    else
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "INGAMESTOREESSENTIALS module is inactive or not in the same starter mode as this module. Treating as failed.")

    // Store Essential module is inactive or not in the same starter mode as this module. Treat as failed.
    OnStoreOfferComplete.ExecuteIfBound({});
    }
    }
  4. Implement the ConsumeEntitlementByEntitlementId() to directly consume an entitlement by its ID.

    void UEntitlementsEssentialsSubsystem_Starter::ConsumeEntitlementByEntitlementId(
    const FUniqueNetIdPtr UserId,
    const FString& EntitlementId,
    const int32 UseCount,
    const FOnConsumeUserEntitlementComplete& OnComplete)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to consume item entitlement. User ID is invalid.");
    OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), nullptr);
    return;
    }

    ConsumeEntitlementParams.Add(EntitlementId, {UserId.ToSharedRef(), OnComplete});
    EntitlementsInterface->ConsumeEntitlement(UserId.ToSharedRef().Get(), EntitlementId, UseCount);
    }
  5. Implement the OnConsumeEntitlementComplete() to handle the backend response and trigger stored delegates.

    void UEntitlementsEssentialsSubsystem_Starter::OnConsumeEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const TSharedPtr<FOnlineEntitlement>& Entitlement,
    const FOnlineError& Error)
    {
    TArray<FString> ConsumeEntitlementParamToDelete;
    for (const TTuple<const FString /*InGameItemId*/, FConsumeEntitlementRequest>& Param : ConsumeEntitlementParams)
    {
    if (Entitlement->Id.Equals(Param.Key))
    {
    if (Param.Value.UserId.Get() == UserId)
    {
    Param.Value.OnComplete.ExecuteIfBound(
    Error,
    bWasSuccessful ? EntitlementToDataObject(MakeShared<FOnlineEntitlement>(*Entitlement.Get())) : nullptr);
    }
    ConsumeEntitlementParamToDelete.Add(Param.Key);
    }
    }

    for (const FString& Param : ConsumeEntitlementParamToDelete)
    {
    ConsumeEntitlementParams.Remove(Param);
    }
    }
  6. In the Initialize() function, add the code below to bind OnConsumeEntitlementComplete() to the OSS delegate, and ConsumeItemEntitlementByInGameId() to the pawn’s OnActivatedMulticastDelegate to ensure that the consume entitlement is called when the player uses their power-up.

    void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    EntitlementsInterface->AddOnConsumeEntitlementCompleteDelegate_Handle(FOnConsumeEntitlementCompleteDelegate::CreateUObject(this, &ThisClass::OnConsumeEntitlementComplete));
    // ...
    AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.AddWeakLambda(this, [this](const APlayerController* PC, const FString& ItemId)
    {
    if (const ULocalPlayer* LocalPlayer = PC ? PC->GetLocalPlayer() : nullptr)
    {
    ConsumeItemEntitlementByInGameId(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), ItemId, 1);
    }
    });
    }
  7. Navigate to the Deinitialize() function and add the code below to unbind the OnConsumeEntitlementComplete() and ConsumeItemEntitlementByInGameId() functions.

    void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
    {
    // ...
    if (EntitlementsInterface.IsValid())
    {
    // ...
    EntitlementsInterface->ClearOnConsumeEntitlementCompleteDelegates(this);
    }
    // ...
    AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.RemoveAll(this);
    }

Implement store equipments

Byte Wars uses in-game item IDs to reference the player’s equipped items. To make these equipments persistent, we use Cloud Save to store the player’s equipped item IDs.

  1. Open the EntitlementsEssentialsSubsystem_Starter header file and declare the following functions.

    private:
    // ...
    void UpdateUserEquipments(
    const int32 LocalUserNum,
    const FUniqueNetIdPtr UserId,
    const FPlayerEquipments& Equipments,
    const FOnUpdateUserEquipmentsComplete& OnComplete);
    public:
    // ...
    void GetUserEquipments(
    const int32 LocalUserNum,
    const FUniqueNetIdPtr UserId,
    const FOnUpdateUserEquipmentsComplete& OnComplete = FOnUpdateUserEquipmentsComplete());
    public:
    // ...
    void SetUserEquipments(
    const int32 LocalUserNum,
    const FUniqueNetIdPtr UserId,
    const FPlayerEquipments& Equipments,
    const FOnUpdateUserEquipmentsComplete& OnComplete = FOnUpdateUserEquipmentsComplete());
  2. Next, declare these callbacks to handle when the equipment data is stored and fetched.

    private:
    // ...
    void OnGetUserEquipmentsComplete(
    const int32 LocalUserNum,
    const FOnlineError& Result,
    const FString& Key,
    const FAccelByteModelsUserRecord& Record,
    const FUniqueNetIdRef UserId,
    const FOnUpdateUserEquipmentsComplete OnComplete);

    void OnSetUserEquipmentsComplete(
    int32 LocalUserNum,
    const FOnlineError& Result,
    const FString& Key,
    const FUniqueNetIdRef UserId,
    const FPlayerEquipments Equipments,
    const FOnUpdateUserEquipmentsComplete OnComplete);

    FDelegateHandle OnGetUserEquipmentsCompleteDelegateHandle, OnSetUserEquipmentsCompleteDelegateHandle;
  3. Open the EntitlementsEssentialsSubsystem_Starter CPP file and define the UpdateUserEquipments() function. This function takes the equipped item IDs and applies them locally after validating that the player actually owns these items based on their entitlement records.

    void UEntitlementsEssentialsSubsystem_Starter::UpdateUserEquipments(
    const int32 LocalUserNum,
    const FUniqueNetIdPtr UserId,
    const FPlayerEquipments& Equipments,
    const FOnUpdateUserEquipmentsComplete& OnComplete)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipments. User ID is invalid.");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))),
    FPlayerEquipments());
    return;
    }

    GetOrQueryUserEntitlements(
    UserId,
    FOnGetOrQueryUserEntitlementsComplete::CreateWeakLambda(this, [this, LocalUserNum, Equipments, OnComplete]
    (const FOnlineError& Error, const TArray<UStoreItemDataObject*> Entitlements)
    {
    if (!Error.bSucceeded)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipment. Error %s: %s", *Error.ErrorCode, *Error.ErrorMessage.ToString());
    OnComplete.ExecuteIfBound(Error, FPlayerEquipments());
    return;
    }

    if (!GetWorld())
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipment. World is invalid");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("World is invalid."))),
    FPlayerEquipments());
    return;
    }

    UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetWorld()->GetGameInstance());
    if (!GameInstance)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipment. Game instance is invalid");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Game instance is invalid."))),
    FPlayerEquipments());
    return;
    }

    const TArray<FString> ItemIds =
    {
    Equipments.SkinId,
    Equipments.ColorId,
    Equipments.ExplosionFxId,
    Equipments.MissileTrailFxId,
    Equipments.PowerUpId
    };

    // Check whether the items are owned.
    TMap<FString, int32> EquippedItems;
    const EItemSkuPlatform PlatformSku = EItemSkuPlatform::AccelByte;
    for (const FString& ItemId : ItemIds)
    {
    const UInGameItemDataAsset* ItemDataAsset = UInGameItemUtility::GetItemDataAsset(ItemId);
    if (!ItemDataAsset || !ItemDataAsset->SkuMap.Contains(PlatformSku))
    {
    continue;
    }

    for (const UStoreItemDataObject* Entitlement : Entitlements)
    {
    const bool bIsOwned = Entitlement && Entitlement->GetSku(PlatformSku).Equals(ItemDataAsset->SkuMap[PlatformSku]);
    if (bIsOwned)
    {
    EquippedItems.Add(ItemDataAsset->Id, Entitlement->GetIsConsumable() ? Entitlement->GetCount() : 1);
    break;
    }
    }
    }

    // Update equipped items.
    CurrentEquipments = Equipments;
    GameInstance->UnEquipAll(LocalUserNum);
    GameInstance->UpdateEquippedItemsByInGameItemId(LocalUserNum, EquippedItems);

    UE_LOG_ENTITLEMENTS_ESSENTIALS(Log, "Success to update user equipment");
    OnComplete.ExecuteIfBound(FOnlineError::Success(), Equipments);
    }));
    }
  4. Next, define the GetUserEquipments() function. This function retrieves the player’s equipped item IDs from Cloud Save using the provided record key.

    void UEntitlementsEssentialsSubsystem_Starter::GetUserEquipments(
    const int32 LocalUserNum,
    const FUniqueNetIdPtr UserId,
    const FOnUpdateUserEquipmentsComplete& OnComplete)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipments. User ID is invalid.");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))),
    FPlayerEquipments());
    return;
    }

    OnGetUserEquipmentsCompleteDelegateHandle =
    CloudSaveInterface->AddOnGetUserRecordCompletedDelegate_Handle(
    LocalUserNum,
    FOnGetUserRecordCompletedDelegate::CreateUObject(
    this,
    &ThisClass::OnGetUserEquipmentsComplete,
    UserId.ToSharedRef(), OnComplete));
    CloudSaveInterface->GetUserRecord(LocalUserNum, USER_EQUIPMENT_KEY);
    }
  5. Then, define the callback function to handle when the equipment data is received. This function parses the record before attempting to equip the items in-game.

    void UEntitlementsEssentialsSubsystem_Starter::OnGetUserEquipmentsComplete(
    const int32 LocalUserNum,
    const FOnlineError& Result,
    const FString& Key,
    const FAccelByteModelsUserRecord& Record,
    const FUniqueNetIdRef UserId,
    const FOnUpdateUserEquipmentsComplete OnComplete)
    {
    CloudSaveInterface->ClearOnGetUserRecordCompletedDelegate_Handle(LocalUserNum, OnGetUserEquipmentsCompleteDelegateHandle);

    if (!Result.bSucceeded)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipment. Error %s: %s", *Result.ErrorCode, *Result.ErrorMessage.ToString());
    OnComplete.ExecuteIfBound(Result, FPlayerEquipments());
    return;
    }

    TSharedPtr<FJsonObject> JsonObject = Record.Value.JsonObject;
    if (JsonObject.IsValid() == false)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipment. Record object is invalid.");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Record object is invalid."))),
    FPlayerEquipments());
    return;
    }

    FPlayerEquipments Equipments;
    if (!FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), FPlayerEquipments::StaticStruct(), &Equipments, 0, 0))
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipment. Unable to parse record to PlayerEquipments struct.");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Unable to parse record."))),
    FPlayerEquipments());
    return;
    }

    UE_LOG_ENTITLEMENTS_ESSENTIALS(Log, "Success to get user equipment");
    UpdateUserEquipments(LocalUserNum, UserId, Equipments, OnComplete);
    }
  6. Define the SetUserEquipments() function. This function stores the equipped item IDs back into Cloud Save to keep the data persistent.

    void UEntitlementsEssentialsSubsystem_Starter::SetUserEquipments(
    const int32 LocalUserNum,
    const FUniqueNetIdPtr UserId,
    const FPlayerEquipments& Equipments,
    const FOnUpdateUserEquipmentsComplete& OnComplete)
    {
    if (!UserId)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to set user equipments. User ID is invalid.");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))),
    FPlayerEquipments());
    return;
    }

    TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();
    if (!FJsonObjectConverter::UStructToJsonObject(FPlayerEquipments::StaticStruct(), &Equipments, JsonObject.ToSharedRef(), 0, 0))
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to set user equipment. Unable to parse record to Json object.");
    OnComplete.ExecuteIfBound(
    FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Unable to parse record."))),
    FPlayerEquipments());
    return;
    }

    OnSetUserEquipmentsCompleteDelegateHandle =
    CloudSaveInterface->AddOnReplaceUserRecordCompletedDelegate_Handle(
    LocalUserNum,
    FOnReplaceUserRecordCompletedDelegate::CreateUObject(
    this,
    &ThisClass::OnSetUserEquipmentsComplete,
    UserId.ToSharedRef(),
    FPlayerEquipments(Equipments),
    OnComplete));
    CloudSaveInterface->ReplaceUserRecord(LocalUserNum, USER_EQUIPMENT_KEY, JsonObject.ToSharedRef().Get());
    }
  7. Then, define the callback function to handle when the equipment data is saved. If successful, it will attempt to equip the items in-game.

    void UEntitlementsEssentialsSubsystem_Starter::OnSetUserEquipmentsComplete(
    int32 LocalUserNum,
    const FOnlineError& Result,
    const FString& Key,
    const FUniqueNetIdRef UserId,
    const FPlayerEquipments Equipments,
    const FOnUpdateUserEquipmentsComplete OnComplete)
    {
    CloudSaveInterface->ClearOnReplaceUserRecordCompletedDelegate_Handle(LocalUserNum, OnSetUserEquipmentsCompleteDelegateHandle);

    if (!Result.bSucceeded)
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to set user equipment. Error %s: %s", *Result.ErrorCode, *Result.ErrorMessage.ToString());
    OnComplete.ExecuteIfBound(Result, FPlayerEquipments());
    return;
    }

    UE_LOG_ENTITLEMENTS_ESSENTIALS(Log, "Success to set user equipment");
    UpdateUserEquipments(LocalUserNum, UserId, Equipments, OnComplete);
    }
  8. In the Initialize() function, add the code below to call GetUserEquipments() when the lobby is connected. This ensures that whenever the player logs in or reconnects, their equipment is refreshed properly.

    void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    // ...
    IdentityInterface->AddOnConnectLobbyCompleteDelegate_Handle(0, FOnConnectLobbyCompleteDelegate::CreateWeakLambda(this, [this]
    (int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error)
    {
    if (bWasSuccessful)
    {
    GetUserEquipments(LocalUserNum, UserId.AsShared());
    }
    }));
    // ...
    }
  9. In the Deinitialize() function, add the code below to unbind all lobby connection events from this class.

    void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
    {
    // ...
    if (IdentityInterface.IsValid())
    {
    IdentityInterface->ClearOnConnectLobbyCompleteDelegates(0, this);
    }
    // ...
    }

Resources