EOS platform services
Add EOS progression, files, commerce, and player protection features.
Complete EOS setup and login first. Configure each service in the target deployment and allow it in your client policy. Use the linked Crossplay guides when the same gameplay must work across stores.
Progression
Create your stats, achievement definitions, and leaderboards in the Developer Portal. EOS aggregates stat samples using each stat’s configured rule; a SUM stat takes the amount earned, not the player’s new total.
Loading the interactive viewer.
Read achievement definitions
Loading the interactive viewer.
Read achievement progress
Loading the interactive viewer.
Unlock a first-win achievement
Loading the interactive viewer.
Loading the interactive viewer.
Game module dependencies: EIKCore, BetideCore, Engine.
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKStats.h"
#include "Interfaces/IEIKAchievements.h"
bool RecordEosMatchWin(UGameInstance &GameInstance, const FEIKAsyncCallbackVoid &Completion)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
if (!Services)
return false;
const auto Stats = Services->GetStatsInterface();
if (!Stats)
return false;
FEIKUpdateStatsParams Params;
Params.Stats.Emplace(TEXT("MatchesWon"), 1);
Stats->UpdateStats(0, Params, Completion);
return true;
}
void ReadEosAchievementDefinitions(
IEIKAchievements &Achievements,
const FEIKAsyncCallback<FEIKQueryAchievementDefinitionsResult> &Completion)
{
Achievements.QueryAchievementDefinitions(0, Completion);
}
void ReadEosAchievementProgress(
IEIKAchievements &Achievements,
const FEIKAsyncCallback<FEIKQueryAchievementStatesResult> &Completion)
{
Achievements.QueryAchievementStates(0, Completion);
}
void UnlockEosFirstWin(IEIKAchievements &Achievements, const FEIKAsyncCallbackVoid &Completion)
{
FEIKUnlockAchievementsParams Params;
Params.AchievementIds.Add(TEXT("FIRST_WIN"));
Achievements.UnlockAchievements(0, Params, Completion);
}Bind completion callbacks with CreateWeakLambda. A true return means dispatched, not saved. Resolve IEIKAchievements through Services->GetAchievementsInterface() and check it before calling. Successful queries return Result.Result->Definitions or Result.Result->States. IEIKLeaderboards exposes leaderboard reads.
Create MatchesWon as a SUM stat for this example. Submit once per earned win; repeating an accepted write adds another win.
Match definitions and states by AchievementId. Progress runs from 0 to 1; use locked text and icons until unlocked, and honor bIsHidden in your UI. Refresh states after an unlock.
Replace FIRST_WIN with an achievement ID from your deployment. Direct unlocks require the client-policy permission; stat-linked achievements use their configured thresholds instead. Leaderboards are derived from their configured stats.
The global-board example uses direct EOS SDK nodes. Replace MostWins with your portal leaderboard ID and pass the signed-in player’s PUID. Read records only after the query succeeds. These nodes use the default EOS platform and a shared record cache; finish copying before starting another board query. The copy helper already releases native records.
Read a rank window
Replace MostWins with your portal leaderboard ID. Rank is 1-based: center rank 10 with limit 5 requests ranks 8–12. The returned window can be shorter near the end of the board. This flow supports OSSv1 and OSSv2.
Loading the interactive viewer.
Game Instance variable: ReadingRanks (Boolean, false).
Game module dependencies: EIKCore, BetideCore, Engine.
// EosRankWindowGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "EosRankWindowGameInstance.generated.h"
UCLASS()
class UEosRankWindowGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void ReadRanksNearPosition(int32 CenterRank);
UPROPERTY(BlueprintReadOnly)
bool ReadingRanks = false;
};// EosRankWindowGameInstance.cpp
#include "EosRankWindowGameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKLeaderboards.h"
#include "Logging/LogMacros.h"
DEFINE_LOG_CATEGORY_STATIC(LogEosRankWindow, Log, All);
void UEosRankWindowGameInstance::ReadRanksNearPosition(int32 CenterRank)
{
if (ReadingRanks)
{
UE_LOG(LogEosRankWindow, Warning, TEXT("A leaderboard read is already running"));
return;
}
const auto Services = FEIKOnlineServicesFactory::Get(GetWorld());
const auto Leaderboards = Services ? Services->GetLeaderboardsInterface() : nullptr;
if (!Leaderboards)
{
UE_LOG(LogEosRankWindow, Warning, TEXT("EOS leaderboards are unavailable"));
return;
}
FEIKReadLeaderboardAroundRankParams Params;
Params.LeaderboardId = TEXT("MostWins");
Params.Rank = CenterRank;
Params.Limit = 5;
ReadingRanks = true;
Leaderboards->ReadEntriesAroundRank(
0, Params,
FEIKAsyncCallback<FEIKLeaderboardResult>::CreateWeakLambda(
this, [this](const TEIKAsyncResult<FEIKLeaderboardResult> &Result) {
ReadingRanks = false;
if (!Result.IsSuccess() || !Result.Result.IsSet())
{
UE_LOG(LogEosRankWindow, Warning, TEXT("%s"), *Result.Error.Message);
return;
}
const auto &Entries = Result.Result->Entries;
for (const FEIKLeaderboardEntry &Entry : Entries)
{
UE_LOG(LogEosRankWindow, Log, TEXT("%d. %s: %lld"), Entry.Rank,
*Entry.DisplayName, Entry.Score);
}
UE_LOG(LogEosRankWindow, Log, TEXT("Loaded %d ranks"), Entries.Num());
}));
}Shared examples: achievements and stats, leaderboards.
C++: direct EOS leaderboard records
Add EIKBlueprints and EIKSDK to your module dependencies.
#include "Engine/GameInstance.h"
#include "SdkFunctions/LeaderboardsInterface/EIK_Leaderboards_QueryLeaderboardRanks.h"
#include "SdkFunctions/LeaderboardsInterface/EIK_LeaderboardsSubsystem.h"
#include "Logging/LogMacros.h"
DEFINE_LOG_CATEGORY_STATIC(LogEosLeaderboardExample, Log, All);
void QueryGlobalEosBoard(UGameInstance &GameInstance, FEIK_ProductUserId LocalUserId,
const FScriptDelegate &OnComplete)
{
FEIK_Leaderboards_QueryLeaderboardRanksOptions Options;
Options.LeaderboardId = TEXT("MostWins");
Options.LocalUserId = LocalUserId;
auto *Request =
UEIK_Leaderboards_QueryLeaderboardRanks::EIK_Leaderboards_QueryLeaderboardRanks(Options);
Request->RegisterWithGameInstance(&GameInstance);
Request->OnCallback.Add(OnComplete);
Request->Activate();
}
void PrintQueriedEosBoard(UGameInstance &GameInstance, EEIK_Result QueryResult)
{
if (QueryResult != EEIK_Result::EOS_Success)
{
UE_LOG(LogEosLeaderboardExample, Warning, TEXT("Leaderboard query failed: %d"),
static_cast<int32>(QueryResult));
return;
}
auto *Boards = GameInstance.GetSubsystem<UEIK_LeaderboardsSubsystem>();
if (!Boards)
return;
const int32 Count = Boards->EIK_Leaderboards_GetLeaderboardRecordCount();
for (int32 Index = 0; Index < Count; ++Index)
{
FEIK_Leaderboards_LeaderboardRecord Record;
const auto Result = Boards->EIK_Leaderboards_CopyLeaderboardRecordByIndex(Index, Record);
if (Result != EEIK_Result::EOS_Success)
{
UE_LOG(LogEosLeaderboardExample, Warning, TEXT("Leaderboard record %d failed: %d"),
Index, static_cast<int32>(Result.GetValue()));
continue;
}
UE_LOG(LogEosLeaderboardExample, Log, TEXT("Rank %d: %s - %d"), Record.Rank,
*Record.UserDisplayName, Record.Score);
}
}Bind OnComplete to a UFUNCTION taking (const TEnumAsByte<EEIK_Result>& Result, const FString& LeaderboardId), then call PrintQueriedEosBoard with that result. The query returning does not mean records are ready.
Player files and title files
Player Data Storage holds per-player saves. Title Storage holds files published for your game, such as shared configuration. Generate your project’s encryption key before using either.
Upload player settings
Loading the interactive viewer.
Download player settings
Loading the interactive viewer.
List player files
Loading the interactive viewer.
List title files
Loading the interactive viewer.
Download game rules
Loading the interactive viewer.
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKUserFile.h"
#include "Interfaces/IEIKTitleFile.h"
bool UploadEosSettings(UGameInstance &GameInstance, const FString &SettingsJson,
const FEIKAsyncCallbackVoid &Completion)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
if (!Services)
return false;
const auto Files = Services->GetUserFileInterface();
if (!Files)
return false;
FEIKUserFileContents Contents;
Contents.SetDataFromString(SettingsJson);
Files->WriteFile(0, FEIKWriteUserFileParams(TEXT("settings.json"), Contents.Data), Completion);
return true;
}
void DownloadEosSettings(IEIKUserFile &Files,
const FEIKAsyncCallback<FEIKReadUserFileResult> &Completion)
{
Files.ReadFile(0, FEIKReadUserFileParams(TEXT("settings.json")), Completion);
}
void ListEosPlayerFiles(IEIKUserFile &Files,
const FEIKAsyncCallback<FEIKEnumerateUserFilesResult> &Completion)
{
Files.EnumerateFiles(0, {}, Completion);
}
void DownloadEosRules(IEIKTitleFile &Files,
const FEIKAsyncCallback<FEIKReadTitleFileResult> &Completion)
{
Files.ReadFile(0, FEIKReadTitleFileParams(TEXT("rules.json")), Completion);
}
void ListEosTitleFiles(IEIKTitleFile &Files,
const FEIKAsyncCallback<FEIKEnumerateTitleFilesResult> &Completion)
{
Files.EnumerateFiles(0, {}, Completion);
}Resolve the interfaces from the current world using GetUserFileInterface() or GetTitleFileInterface(). Bind completions with CreateWeakLambda and check the result. Successful reads return Result.Result->FileContents; use GetDataAsString() for these UTF-8 files. Enumeration returns Result.Result->Files, which can be empty.
Pass valid JSON such as {"version":1,"musicVolume":0.8} to the upload example; string conversion does not validate JSON. Upload replaces settings.json. Use EIK Save Game to Bytes / Bytes to Save Game for Unreal SaveGame objects instead.
Use the Async user-file nodes for remote transfers. Cache helpers read or change local cached bytes; they are not proof of a completed cloud transfer. Version your save payload and decide how to handle simultaneous writes from different devices.
Upload rules.json in the Developer Portal before reading it. OSSv1 requires Title Storage Tags in EOS settings and ignores the node’s Tag input; this wrapper uses the first configured tag page. OSSv2 uses the Tag input, with an empty value requesting all files. Shared examples: cloud saves, title files.
Epic friends and presence
These features require Epic-account authentication and the matching application permissions. Connect-only login supplies a PUID but no Epic social account.
Refresh Epic friends
Loading the interactive viewer.
Publish lobby presence
Loading the interactive viewer.
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKFriends.h"
#include "Interfaces/IEIKPresence.h"
bool QueryEpicFriends(UGameInstance &GameInstance,
const FEIKAsyncCallback<FEIKQueryFriendsResult> &Completion)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
if (!Services)
return false;
const auto Friends = Services->GetFriendsInterface();
if (!Friends)
return false;
Friends->QueryFriends(0, Completion);
return true;
}
bool PublishEpicLobbyPresence(UGameInstance &GameInstance, const FEIKAsyncCallbackVoid &Completion)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
if (!Services)
return false;
const auto Presence = Services->GetPresenceInterface();
if (!Presence)
return false;
Presence->SetRichPresenceText(0, TEXT("Waiting in the lobby"), Completion);
return true;
}Bind completions with CreateWeakLambda and handle failures there. A true return only means dispatched. Successful friend queries return Result.Result->Friends.
The query returns Friends; an empty list can be a successful result. Refresh after relationship changes. Set EIK Rich Presence Text also sets your status to Online in both backends. Update the text when leaving the lobby; it does not make a game joinable. Use Crossplay friends and presence to combine providers.
Offers and ownership
Sign in with an Epic account and configure offers for your application and sandbox. Connect-only login is not enough. Use the returned FormattedPrice for display and the selected OfferId for checkout. Entitlements contain a separate catalog ProductId and a unique EntitlementId.
List store offers
Loading the interactive viewer.
Buy an offer and refresh ownership
Loading the interactive viewer.
List owned content
Loading the interactive viewer.
Redeem fulfilled content (OSSv2)
Loading the interactive viewer.
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKCommerce.h"
bool QueryEosOwnership(UGameInstance &GameInstance,
const FEIKAsyncCallback<FEIKQueryEntitlementsResult> &Completion)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
if (!Services)
return false;
const auto Commerce = Services->GetCommerceInterface();
if (!Commerce)
return false;
Commerce->QueryEntitlements(0, false, Completion);
return true;
}
void QueryEosOffers(IEIKCommerce &Commerce,
const FEIKAsyncCallback<FEIKQueryOffersResult> &Completion)
{
Commerce.QueryOffers(0, Completion);
}
bool BuyEosOffer(UGameInstance &GameInstance, const FString &OfferId,
const FEIKAsyncCallback<FEIKQueryEntitlementsResult> &Ownership)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
const auto Commerce = Services ? Services->GetCommerceInterface() : nullptr;
if (!Commerce || OfferId.IsEmpty())
return false;
FEIKPurchaseOffer Offer;
Offer.OfferId = OfferId;
Offer.Quantity = 1;
Commerce->Checkout(
0, {Offer},
FEIKAsyncCallback<FEIKCheckoutResult>::CreateWeakLambda(
&GameInstance,
[Commerce, Ownership](const TEIKAsyncResult<FEIKCheckoutResult> &Result) {
if (!Result.bWasSuccessful)
{
Ownership.ExecuteIfBound(
TEIKAsyncResult<FEIKQueryEntitlementsResult>::Failure(Result.Error));
return;
}
Commerce->QueryEntitlements(0, false, Ownership);
}));
return true;
}
bool RedeemFulfilledEosEntitlementV2(
UGameInstance &GameInstance, const FString &EntitlementId,
const FEIKAsyncCallback<FEIKQueryEntitlementsResult> &Ownership)
{
const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
const auto Commerce = Services ? Services->GetCommerceInterface() : nullptr;
if (!Commerce || EntitlementId.IsEmpty())
return false;
Commerce->RedeemEntitlement(
0, EntitlementId,
FEIKAsyncCallbackVoid::CreateWeakLambda(
&GameInstance, [Commerce, Ownership](const TEIKAsyncResult<void> &Result) {
if (!Result.bWasSuccessful)
{
Ownership.ExecuteIfBound(
TEIKAsyncResult<FEIKQueryEntitlementsResult>::Failure(Result.Error));
return;
}
Commerce->QueryEntitlements(0, false, Ownership);
}));
return true;
}Resolve IEIKCommerce through GetCommerceInterface(). Bind callbacks with CreateWeakLambda; check bWasSuccessful and Result.IsSet() before reading Offers or Entitlements. The bool returns mean dispatched; checkout and redemption finish through the ownership callback after refreshing entitlements.
Pass an offer returned by the query to Buy Selected Offer. A failed or canceled checkout grants nothing; if a purchase is still processing, refresh ownership later instead of starting another purchase. The refreshed list contains all unredeemed entitlements, not just this purchase. Use server-verified ownership for backend-controlled rewards.
Redemption example: OSSv2 only. Redeem a consumable’s entitlement after your fulfillment flow records its grant, then refresh ownership. The current OSSv1 wrapper calls an unsupported finalization operation and reports success without redeeming. OSSv1 also ignores Include Redeemed when querying entitlements. Do not use these results to confirm consumption or retrieve redeemed history on OSSv1.
Reports and sanctions
A report submits a complaint; it does not ban the target. Query sanctions after login and apply the returned Action in your game’s access checks. Keep privileged sanction-management credentials on your backend.
Use the signed-in reporter’s Product User ID and the selected player’s PUID. Submit only after the player confirms the report. Messages may contain up to 512 UTF-8 bytes. The node uses the configured EOS platform with either OSSv1 or OSSv2.
Loading the interactive viewer.
Game Instance variable: ReportInProgress (Boolean, false).
Game module dependencies: EIKBlueprints, EIKCore, BetideCore, Engine.
// EosReportsGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "AsyncFunctions/Reports/EIK_SendReport_AsyncFunction.h"
#include "EosReportsGameInstance.generated.h"
UCLASS()
class UEosReportsGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void ReportSelectedPlayer(const FString &TargetPuid, EEIKPlayerReportCategory Category,
const FString &Message);
UPROPERTY(BlueprintReadOnly)
bool ReportInProgress = false;
private:
UFUNCTION()
void ReportSucceeded(const FBetideError &Error);
UFUNCTION()
void ReportFailed(const FBetideError &Error);
};// EosReportsGameInstance.cpp
#include "EosReportsGameInstance.h"
#include "Logging/LogMacros.h"
DEFINE_LOG_CATEGORY_STATIC(LogEosReportExample, Log, All);
void UEosReportsGameInstance::ReportSelectedPlayer(const FString &TargetPuid,
EEIKPlayerReportCategory Category,
const FString &Message)
{
if (ReportInProgress)
{
UE_LOG(LogEosReportExample, Warning, TEXT("Wait for the current report to finish"));
return;
}
auto *Request = UEIK_SendReport_AsyncFunction::SendEIKPlayerReportAsyncFunction(
this, 0, TargetPuid, Category, Message, FString());
Request->Success.AddDynamic(this, &ThisClass::ReportSucceeded);
Request->Failure.AddDynamic(this, &ThisClass::ReportFailed);
ReportInProgress = true;
Request->Activate();
}
void UEosReportsGameInstance::ReportSucceeded(const FBetideError &Error)
{
ReportInProgress = false;
UE_LOG(LogEosReportExample, Log, TEXT("Player report submitted"));
}
void UEosReportsGameInstance::ReportFailed(const FBetideError &Error)
{
ReportInProgress = false;
UE_LOG(LogEosReportExample, Warning, TEXT("%s"), *Error.Message);
}Read player sanctions
Loading the interactive viewer.
Submit a sanction appeal
Loading the interactive viewer.
Add EIKBlueprints and EIKSDK for these SDK-facing headers.
#include "Engine/GameInstance.h"
#include "Logging/LogMacros.h"
#include "SdkFunctions/SanctionsInterface/EIK_Sanctions_QueryActivePlayerSanctions.h"
#include "SdkFunctions/SanctionsInterface/EIK_Sanctions_CreatePlayerSanctionAppeal.h"
#include "SdkFunctions/SanctionsInterface/EIK_SanctionsSubsystem.h"
DEFINE_LOG_CATEGORY_STATIC(LogEosSanctionExample, Log, All);
void QueryEosSanctions(UGameInstance &GameInstance, FEIK_ProductUserId LocalUserId,
FEIK_ProductUserId TargetUserId, const FScriptDelegate &OnComplete)
{
auto *Request =
UEIK_Sanctions_QueryActivePlayerSanctions::EIK_Sanctions_QueryActivePlayerSanctions(
LocalUserId, TargetUserId);
if (!Request)
return;
Request->RegisterWithGameInstance(&GameInstance);
Request->OnCallback.Add(OnComplete);
Request->Activate();
}
void ReadQueriedEosSanctions(FEIK_ProductUserId TargetUserId, TEnumAsByte<EEIK_Result> QueryResult)
{
if (QueryResult != EEIK_Result::EOS_Success)
{
UE_LOG(LogEosSanctionExample, Warning, TEXT("Sanctions query failed: %d"),
int32(QueryResult));
return;
}
const int32 Count = UEIK_SanctionsSubsystem::EIK_Sanctions_GetPlayerSanctionCount(TargetUserId);
if (Count < 0)
{
UE_LOG(LogEosSanctionExample, Warning, TEXT("Unable to read cached sanctions"));
return;
}
for (int32 Index = 0; Index < Count; ++Index)
{
FEIK_Sanctions_PlayerSanction Sanction;
const auto Result = UEIK_SanctionsSubsystem::EIK_Sanctions_CopyPlayerSanctionByIndex(
TargetUserId, Index, Sanction);
if (Result != EEIK_Result::EOS_Success)
{
UE_LOG(LogEosSanctionExample, Warning, TEXT("Sanction copy failed: %d"), int32(Result));
continue;
}
UE_LOG(LogEosSanctionExample, Log, TEXT("%s | Ref %s | Expires %lld"), *Sanction.Action,
*Sanction.ReferenceId, Sanction.TimeExpires);
}
UE_LOG(LogEosSanctionExample, Log, TEXT("Finished reading %d cached sanctions"), Count);
}
void SubmitEosSanctionAppeal(UGameInstance &GameInstance, FEIK_ProductUserId LocalUserId,
const FString &ReferenceId,
TEnumAsByte<EEIK_ESanctionAppealReason> Reason,
const FScriptDelegate &OnComplete)
{
auto *Request =
UEIK_Sanctions_CreatePlayerSanctionAppeal::EIK_Sanctions_CreatePlayerSanctionAppeal(
LocalUserId, Reason, ReferenceId);
if (!Request)
return;
Request->RegisterWithGameInstance(&GameInstance);
Request->OnCallback.Add(OnComplete);
Request->Activate();
}Bind OnComplete to a UFUNCTION before dispatch. The query handler takes const FEIK_ProductUserId& LocalUserId, const FEIK_ProductUserId& TargetUserId, and const TEnumAsByte<EEIK_Result>& Result; call ReadQueriedEosSanctions(TargetUserId, Result) there. The appeal handler takes only LocalUserId and Result; check for EOS_Success.
Use the logged-in player’s PUID for LocalUserId. On the count/copy nodes, the pin named Local User Id actually selects the target whose sanctions were queried. These SDK nodes use the default EOS platform. The copy wrapper releases native memory itself. TimeExpires = 0 means permanent; other values are Unix timestamps.
An appeal uses a ReferenceId returned by the query for the local player. Pass the reason the player selected, such as Incorrect Sanction. Submission does not remove the sanction.
For unified projects, see Crossplay moderation.
Anti-cheat lifecycle
This example protects a dedicated-server match. Enable the EOS anti-cheat service and launch packaged clients through the protected bootstrapper. See Epic’s service and packaging guidance. Editor compilation does not test protection.
- Use
BP_Docs_EosAntiCheatServeras the Game Mode andBP_Docs_EosAntiCheatRelayas its Player Controller Class. Keep one local player per client process. - Call Start Protection once after EOS initializes, before admitting players.
- After your server authenticates a connected player, call Admit Authenticated Player with that controller, the verified PUID, and platform. Never accept a client-supplied PUID as proof of identity.
EOS anti-cheat player relay
Loading the interactive viewer.
EOS anti-cheat dedicated server
Loading the interactive viewer.
Controller variables: ClientProtectionStarted, ServerPlayerRegistered (Boolean). Game Mode variables: SessionBound, SessionRunning (Boolean). All default to false. Keep both graphs together: the Game Mode calls events on the controller class.
EosProtection.h
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/GameModeBase.h"
#include "AntiCheat/AntiCheatClient.h"
#include "AntiCheat/AntiCheatServer.h"
#include "EosProtection.generated.h"
UCLASS()
class AEosProtectionController : public APlayerController
{
GENERATED_BODY()
public:
void RegisterAuthenticatedPlayer(const FString &VerifiedPuid,
TEnumAsByte<EUserPlatform> Platform);
void UnregisterProtectedPlayer();
UFUNCTION(Client, Reliable) void ClientBeginProtection(const FString &VerifiedPuid);
UFUNCTION(Client, Reliable) void ClientAntiCheatMessage(const TArray<uint8> &Message);
UFUNCTION(Server, Reliable) void ServerAntiCheatMessage(const TArray<uint8> &Message);
UFUNCTION(Server, Reliable) void ServerProtectionFailed();
protected:
void EndPlay(const EEndPlayReason::Type Reason) override;
private:
UPROPERTY() TObjectPtr<UAntiCheatClient> Client;
UPROPERTY() TObjectPtr<UAntiCheatServer> Server;
bool bClientProtectionStarted = false;
bool bServerPlayerRegistered = false;
void StopClientProtection();
UFUNCTION() void SendToServer(const TArray<uint8> &Message);
UFUNCTION()
void IntegrityViolated(const FEIK_AntiCheatClientIntegrityViolatedCallbackInfo &Data);
};
UCLASS()
class AEosProtectionGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AEosProtectionGameMode();
UFUNCTION(BlueprintCallable) void StartProtection(const FString &ServerName);
UFUNCTION(BlueprintCallable) void StopProtection();
UFUNCTION(BlueprintCallable)
void AdmitAuthenticatedPlayer(APlayerController *Player, const FString &VerifiedPuid,
TEnumAsByte<EUserPlatform> Platform);
protected:
void EndPlay(const EEndPlayReason::Type Reason) override;
private:
UPROPERTY() TObjectPtr<UAntiCheatServer> Server;
bool bSessionBound = false;
bool bSessionRunning = false;
UFUNCTION()
void SendToClient(APlayerController *ControllerRef, const TArray<uint8> &ClientData);
UFUNCTION() void ActionRequired(APlayerController *ControllerRef, bool bRemoveFromSession);
};EosProtection.cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
#include "EosProtection.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
DEFINE_LOG_CATEGORY_STATIC(LogEosProtection, Log, All);
void AEosProtectionController::RegisterAuthenticatedPlayer(const FString &VerifiedPuid,
TEnumAsByte<EUserPlatform> Platform)
{
if (!HasAuthority() || bServerPlayerRegistered)
return;
Server = GetGameInstance()->GetSubsystem<UAntiCheatServer>();
if (!Server)
{
ServerProtectionFailed();
return;
}
// Keep startup ahead of protection packets on this controller's reliable channel.
ClientBeginProtection(VerifiedPuid);
bServerPlayerRegistered =
Server->RegisterClientForAntiCheat(VerifiedPuid, this, Platform, EOS_ProtectedClient);
if (!bServerPlayerRegistered)
ServerProtectionFailed();
}
void AEosProtectionController::ClientBeginProtection_Implementation(const FString &VerifiedPuid)
{
if (bClientProtectionStarted)
return;
Client = GetGameInstance()->GetSubsystem<UAntiCheatClient>();
if (!Client)
{
ServerProtectionFailed();
return;
}
Client->OnSendMessageToServer.AddUniqueDynamic(this, &ThisClass::SendToServer);
Client->OnClientIntegrityViolated.AddUniqueDynamic(this, &ThisClass::IntegrityViolated);
// The wrapper installs notifications before BeginSession, even when BeginSession fails.
bClientProtectionStarted = true;
if (!Client->RegisterAntiCheatClient(VerifiedPuid))
{
StopClientProtection();
ServerProtectionFailed();
return;
}
UE_LOG(LogEosProtection, Log, TEXT("Client session started; authentication is still pending"));
}
void AEosProtectionController::SendToServer(const TArray<uint8> &Message)
{
ServerAntiCheatMessage(Message);
}
void AEosProtectionController::ClientAntiCheatMessage_Implementation(const TArray<uint8> &Message)
{
if (!bClientProtectionStarted)
return;
if (!Client || !Client->RecievedMessageFromServer(Message))
{
StopClientProtection();
ServerProtectionFailed();
}
}
void AEosProtectionController::ServerAntiCheatMessage_Implementation(const TArray<uint8> &Message)
{
if (!bServerPlayerRegistered || Message.IsEmpty() || Message.Num() > 512 || !Server ||
!Server->RecievedMessageFromClient(this, Message))
{
ServerProtectionFailed();
}
}
void AEosProtectionController::IntegrityViolated(
const FEIK_AntiCheatClientIntegrityViolatedCallbackInfo &Data)
{
StopClientProtection();
ServerProtectionFailed();
}
void AEosProtectionController::StopClientProtection()
{
if (!bClientProtectionStarted)
return;
const bool bEnded = Client && Client->UnregisterAntiCheatClient();
bClientProtectionStarted = false;
if (Client)
{
Client->OnSendMessageToServer.RemoveDynamic(this, &ThisClass::SendToServer);
Client->OnClientIntegrityViolated.RemoveDynamic(this, &ThisClass::IntegrityViolated);
}
if (!bEnded)
UE_LOG(LogEosProtection, Error, TEXT("Client protection cleanup failed"));
}
void AEosProtectionController::UnregisterProtectedPlayer()
{
if (!HasAuthority() || !bServerPlayerRegistered)
return;
const bool bRemoved = Server && Server->UnregisterClientFromAntiCheat(this);
bServerPlayerRegistered = false;
if (!bRemoved)
UE_LOG(LogEosProtection, Error, TEXT("Player protection cleanup failed"));
}
void AEosProtectionController::ServerProtectionFailed_Implementation()
{
if (!HasAuthority())
return;
UnregisterProtectedPlayer();
Destroy();
}
void AEosProtectionController::EndPlay(const EEndPlayReason::Type Reason)
{
if (HasAuthority())
UnregisterProtectedPlayer();
else
StopClientProtection();
Super::EndPlay(Reason);
}
AEosProtectionGameMode::AEosProtectionGameMode()
{
PlayerControllerClass = AEosProtectionController::StaticClass();
}
void AEosProtectionGameMode::StartProtection(const FString &ServerName)
{
if (GetNetMode() != NM_DedicatedServer || bSessionBound)
return;
Server = GetGameInstance()->GetSubsystem<UAntiCheatServer>();
if (!Server)
{
UE_LOG(LogEosProtection, Error, TEXT("Anti-cheat server subsystem is unavailable"));
return;
}
Server->OnAntiCheatRegisterClient.AddUniqueDynamic(this, &ThisClass::SendToClient);
Server->OnAntiCheatActionRequired.AddUniqueDynamic(this, &ThisClass::ActionRequired);
bSessionBound = true;
bSessionRunning = Server->RegisterAntiCheatServer(ServerName, FString());
if (!bSessionRunning)
{
StopProtection();
UE_LOG(LogEosProtection, Error, TEXT("Server protection failed; do not admit players"));
}
}
void AEosProtectionGameMode::AdmitAuthenticatedPlayer(APlayerController *Player,
const FString &VerifiedPuid,
TEnumAsByte<EUserPlatform> Platform)
{
if (!IsValid(Player))
return;
auto *ProtectedPlayer = Cast<AEosProtectionController>(Player);
if (!ProtectedPlayer)
{
Player->Destroy();
return;
}
if (!bSessionRunning)
{
ProtectedPlayer->ServerProtectionFailed();
return;
}
ProtectedPlayer->RegisterAuthenticatedPlayer(VerifiedPuid, Platform);
}
void AEosProtectionGameMode::SendToClient(APlayerController *ControllerRef,
const TArray<uint8> &ClientData)
{
if (auto *Player = Cast<AEosProtectionController>(ControllerRef); IsValid(Player))
Player->ClientAntiCheatMessage(ClientData);
}
void AEosProtectionGameMode::ActionRequired(APlayerController *ControllerRef,
bool bRemoveFromSession)
{
if (!bRemoveFromSession)
return;
if (auto *Player = Cast<AEosProtectionController>(ControllerRef); IsValid(Player))
Player->ServerProtectionFailed();
}
void AEosProtectionGameMode::StopProtection()
{
if (!bSessionBound)
return;
bSessionRunning = false;
TArray<AActor *> Players;
UGameplayStatics::GetAllActorsOfClass(this, AEosProtectionController::StaticClass(), Players);
for (AActor *Actor : Players)
if (auto *Player = Cast<AEosProtectionController>(Actor); IsValid(Player))
Player->ServerProtectionFailed();
const bool bEnded = Server && Server->UnregisterAntiCheatServer();
bSessionBound = false;
if (Server)
{
Server->OnAntiCheatRegisterClient.RemoveDynamic(this, &ThisClass::SendToClient);
Server->OnAntiCheatActionRequired.RemoveDynamic(this, &ThisClass::ActionRequired);
}
if (!bEnded)
UE_LOG(LogEosProtection, Error, TEXT("Server protection cleanup failed"));
}
void AEosProtectionGameMode::EndPlay(const EEndPlayReason::Type Reason)
{
StopProtection();
Super::EndPlay(Reason);
}Add EIKBlueprints, EIKSDK, EOSIntegrationKit, OnlineSubsystem, and Engine to your module dependencies.
ClientProductID means the player’s PUID, not the Product ID from Project Settings. Leave it empty when starting the dedicated server session. A successful registration starts the exchange; it does not mean client authentication has finished.
Keep the relay RPCs Reliable on the same Player Controller. The start RPC runs before that player’s first server packet, using Unreal’s reliable RPC ordering. On Anti Cheat Register Client carries outgoing bytes, despite its name.
Removal requests disconnect the addressed controller. Stop Protection disconnects the match’s protected players, removes notifications, and ends the server session; End Play also calls it. End this match before changing worlds. Peer-to-peer and seamless-travel protection need a different session owner and lifecycle.
SDK nodes, Web APIs, and uploads
SDK nodes expose EOS operations directly and may require an explicit query before a copy/get call. Check their result code and release SDK handles where the API requires it. Web API credentials for administrative operations belong on a trusted backend.
Use Betide → Tools for build upload settings. BuildPatchTool credentials are separate from runtime SDK credentials. Uploading a game build does not publish title files, define achievements, or enable service permissions.