Unreal Engine
A "keep the servers running" store in an Unreal game, from C++ or Blueprint, on the dependency-free C++ SDK.
Unreal uses Encore's C++ SDK: C++17 and the standard library, no exceptions, no
RTTI, no threads — the way Unreal builds — plus a complete sample
UGameInstanceSubsystem that wraps it for C++ and Blueprint.
The subsystem is a sample, written against UE 5.3–5.5, for you to copy into your
game and own. The handful of lines most likely to differ between engine versions
are marked VERIFY in the source.
Requirements
- Steamworks SDK 1.57 or later, for
ISteamUser::GetAuthTicketForWebApi. - The
OnlineSubsystemSteamplugin enabled and configured as usual. It is what initialises Steam and pumps its callbacks. - A Steam client signed in, and the game launched through Steam or with a
steam_appid.txtin development.
Install
- Copy the SDK and the sample
From
sdk/cpp/encoreinplany-cloud/apis: copyinclude/andsrc/into your game module — saySource/YourGame/ThirdParty/PlanyEncore/— and the two files insamples/unreal/intoSource/YourGame/. ReplaceYOURGAME_APIwith your module's API macro. - Add the modules
In
YourGame.Build.cs:C#PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "HTTP" });AddEngineThirdPartyPrivateStaticDependencies(Target, "Steamworks");PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "ThirdParty/PlanyEncore/include"));PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "ThirdParty/PlanyEncore/src")); - Configure the Encore key
In
Config/DefaultGame.ini:INI[/Script/YourGame.PlanyEncoreSubsystem]EncoreKey=ek-01j9…The key is not a secret — it ships in every copy of the game — but it is the game's, so it belongs in config. See The Encore key.
How the subsystem is put together
UPlanyEncoreSubsystem is a game instance subsystem: one Encore client for the
life of the running game, surviving level travel — which is exactly when a store
widget is most likely to be torn down with a purchase still in flight. It is not
created on a dedicated server.
When it initialises it builds:
- an
HttpClientoverFHttpModule, - a
ScheduleroverFTSTicker, which is what enables retries, - a request-id generator over
FGuid::NewGuid(), which is what makes opening a purchase safely retryable, and - a
SteamworksBridge— once, at start-up, because Steam can deliver the player's answer before the HTTP response naming the order has arrived, and a bridge created at the moment of purchase would miss it. Steam's callbacks are posted to the game thread.
If Steam is not running, it logs a warning and purchases report that Steam is unavailable; funding and offers still load.
A "keep the servers running" button
void UStoreWidget::NativeConstruct(){ Super::NativeConstruct(); UPlanyEncoreSubsystem* Encore = GetGameInstance()->GetSubsystem<UPlanyEncoreSubsystem>(); Encore->OnOffersLoaded.AddDynamic(this, &UStoreWidget::HandleOffers); Encore->OnFundingLoaded.AddDynamic(this, &UStoreWidget::HandleFunding); Encore->OnPurchaseFinished.AddDynamic(this, &UStoreWidget::HandlePurchase); Encore->RefreshOffers(); Encore->RefreshFunding();} void UStoreWidget::HandleOffers(bool bSuccess, const TArray<FPlanyEncoreOffer>& Offers, const FString& Error){ if (!bSuccess || Offers.Num() == 0) { StatusText->SetText(FText::FromString(TEXT("The store is unavailable right now."))); return; } Offer = Offers[0]; BuyLabel->SetText(FText::FromString(FString::Printf( TEXT("Keep the servers running — %s %s"), *Offer.Price, *Offer.CurrencyCode))); // Offers are listed before the game is verified. When they are not purchasable // yet, Error carries the reason — ENCORE_NOT_VERIFIED, ENCORE_NOT_READY — and // the button stays disabled. BuyButton->SetIsEnabled(Error.IsEmpty());} void UStoreWidget::OnBuyClicked(){ GetGameInstance()->GetSubsystem<UPlanyEncoreSubsystem>()->Purchase(Offer.OfferId);} void UStoreWidget::HandlePurchase(bool bSuccess, EPlanyEncorePurchaseState State, bool bConfirmed, const FString& Error){ if (!bSuccess) { // No order was opened, so nothing was charged. Safe to try again. StatusText->SetText(FText::FromString(TEXT("The store is unavailable right now. You have not been charged."))); return; } switch (State) { case EPlanyEncorePurchaseState::Credited: StatusText->SetText(FText::FromString(TEXT("Thank you — you are keeping the servers running."))); break; case EPlanyEncorePurchaseState::Authorized: // The player has paid; Encore finishes crediting it on its own. StatusText->SetText(FText::FromString(TEXT("Thank you! Your support will show up shortly."))); break; case EPlanyEncorePurchaseState::Cancelled: break; // the player declined — not an error default: StatusText->SetText(FText::FromString(TEXT("Steam did not complete the purchase. You have not been charged."))); break; }}The same three events and three functions are Blueprint-assignable and Blueprint-callable, under the Encore category.
OnPurchaseFinished is the one to read carefully:
| Parameter | Meaning |
|---|---|
bSuccess | false only when no order was opened — the ticket failed, or opening the purchase was refused. Nothing was charged. |
State | Once an order exists, always a value. A decline is Cancelled, not a failure. |
bConfirmed | false when Encore could not be told the player's answer. An approval still lands: Encore settles approved orders itself within about two minutes. |
Error | Why, when there is a reason to give. |
Purchase is one at a time: a second call while one is in flight is ignored,
because two overlays for two orders is how a player is charged twice.
IsPurchaseInFlight() tells a widget to disable its button.
The player's Steam language is passed to the overlay automatically.
Showing the runway
void UStoreWidget::HandleFunding(bool bSuccess, const FPlanyEncoreFunding& Funding, const FString& Error){ FString Text; if (!bSuccess) Text = TEXT(""); // unknown is not "unfunded" else if (!Funding.bFunded) Text = TEXT("The servers are asleep until somebody keeps them running."); else if (Funding.RunwayDays < 0) Text = TEXT("The servers are funded."); // nothing to project from else if (Funding.RunwayDays == 0) Text = TEXT("Less than a day of servers left."); // zero is real, and urgent else Text = FString::Printf(TEXT("%lld days of servers left."), Funding.RunwayDays); RunwayText->SetText(FText::FromString(Text));}In the sample, RunwayDays is -1 when there is no runway to show, and 0 means
less than a day. Blueprint has no optional integer, so absent is -1; never render
it as "0 days left". A successful purchase also broadcasts OnFundingLoaded with the
new figures, so the runway updates the moment a player's purchase lands.
What a funding response contains depends on what the studio chose to publish — see What players see.
Handling errors
The C++ Error carries a gRPC status, a typed reason, the raw reasonName, the
httpStatus (0 when no response arrived), and retryable. The sample turns it into
the Error string on each event; a game that needs to branch on the reason should
read reason in the sample's callbacks.
ErrorReason | What to do |
|---|---|
EncoreNotVerified | Not verified yet. Keep the buy button disabled. |
EncoreNotReady | Verified; servers not yet moved into their project. Same; it clears on its own. |
OfferInactive | The studio withdrew the offer. RefreshOffers(). |
PublisherLimitReached | The studio's line of credit with Plany is used up. Try later. |
PublisherUnavailable | The studio's Steam key is broken. Only the studio can fix it. |
OrderRefused | Steam would not open the order. Nothing was charged. |
PlayerNotAuthenticated | Steam did not vouch for the player. HTTP 401. |
ProjectCreditExhausted | Not sent by these calls; a store sees bFunded == false. |
Unrecognized | A reason newer than this SDK; reasonName has it. |
All named refusals but PlayerNotAuthenticated are HTTP 400, none improves by
retrying, and none is retried. Every refusal of a purchase happens before the player
is charged. The full list is in Refusals.
Retries. With the sample's scheduler, reads and CompletePurchase are retried on
transient failures with jittered backoff, and opening a purchase is retried only
under its request id — without one, a retry could open a second Steam order. While
an order settles, CompletePurchase is asked again up to settlementPolls times
(four by default).
Testing before launch
Test the whole store against a sandbox link: the
same overlay and the same calls, with nothing charged and nothing credited. Put the
sandbox Encore's key in a test build's DefaultGame.ini, launch it through a signed-in
Steam client, and buy — the purchase walks through every state a real one does and
ends in Credited, with nothing added to the project's balance.