This how-to implements a mutual Friends system in-game using LootLocker: sending friend requests, accepting or declining them, listing friends and pending requests, and handling blocking relationships. This creates opt-in, two-way connections between players.
Prerequisites
- Unity, Unreal, or Godot SDK installed and configured in your project (not required for REST).
- Multiple player profiles (e.g. created via separate sessions).
- An active Game Session.
- (Optional) Some player ULIDs collected from the Player Manager.
- (Optional) Familiarity with managing relationships in the Web Console.
Friends vs Followers
Friends are a two-way relationship requiring mutual consent. Player A sends a friend request to Player B, and they only become friends once B accepts. This powers you to build:
- Private messaging systems
- Co-op gameplay invitations
- Trusted player groups for guilds or parties
- Mutual achievement sharing and challenges
If you need one-way relationships that don't require approval, use Followers instead (see Use Followers in Game).
Core Concepts
- Friends list: players who have mutually accepted each other's friend requests.
- Incoming requests: friend requests sent to the current player by others.
- Outgoing requests: friend requests the current player has sent but not yet accepted/declined.
- Blocked players: players who can't send friend requests or interact with the current player.
- Offset pagination: lists use page numbers and per-page counts to paginate the data.
Typical UX Flow
- Open Social UI, then fetch friend counts for the header.
- Browse friends list, incoming requests, or outgoing requests in tabs.
- Send a friend request from a player profile — it appears in the target's incoming list.
- Accept/decline incoming requests — this moves the player to the friends list or removes the request.
- Cancel outgoing requests if you change your mind.
- Block/unblock players to manage harassment.
List Friends and Requests
The foundation of any friends system is showing existing relationships and pending requests.
// Listing Friends, Incoming/Outgoing Requests with Offset Pagination// Unity Friends API uses Page (0-indexed) and PerPage to paginate the data.// Assumptions:// - UI layer methods: UIFriendsList.SetLoading(bool), UIFriendsList.ReplaceItems(array), etc.// - Error handling via LogFailure(context, response)
using System.Collections.Generic;using LootLocker.Requests;
public class FriendsListManager{private readonly List<LootLockerAcceptedFriend> _friends = new();private readonly List<LootLockerFriend> _incomingRequests = new();private readonly List<LootLockerFriend> _outgoingRequests = new();private readonly List<LootLockerBlockedPlayer> _blockedPlayers = new();
// Page tracking (0-indexed)private int _friendsCurrentPage = 0;private int _incomingCurrentPage = 0;private int _outgoingCurrentPage = 0;private int _blockedCurrentPage = 0;
private const int PageSize = 20;
// Friends Listpublic void LoadFriendsPage(){_friends.Clear();_friendsCurrentPage = 0;UIFriendsList.SetLoading(true);
LootLockerSDKManager.ListFriendsPaginated(PageSize, _friendsCurrentPage, (resp) =>{UIFriendsList.SetLoading(false);if (!resp.success){LogFailure("Friends list", resp);return;}
if (resp.friends != null){_friends.AddRange(resp.friends);UIFriendsList.ReplaceItems(resp.friends);}
bool hasMore = resp.friends != null && resp.pagination.total > _friends.Length;UIFriendsList.SetHasMorePages(hasMore);});}
public void LoadMoreFriends(){_friendsCurrentPage++;UIFriendsList.SetLoading(true);
LootLockerSDKManager.ListFriendsPaginated(PageSize, _friendsCurrentPage, (resp) =>{UIFriendsList.SetLoading(false);if (!resp.success){LogFailure("Friends list (more)", resp);_friendsCurrentPage--; // Rollback page incrementreturn;}
if (resp.friends != null && resp.friends.Length > 0){_friends.AddRange(resp.friends);UIFriendsList.AppendItems(resp.friends);}
bool hasMore = resp.friends != null && resp.pagination.total > _friends.Length;UIFriendsList.SetHasMorePages(hasMore);});}
// Incoming Friend Requestspublic void LoadIncomingRequestsFirstPage(){_incomingRequests.Clear();_incomingCurrentPage = 0;UIIncomingRequestsList.SetLoading(true);
LootLockerSDKManager.ListIncomingFriendRequestsPaginated(PageSize, _incomingCurrentPage, (resp) =>{UIIncomingRequestsList.SetLoading(false);if (!resp.success){LogFailure("Incoming requests", resp);return;}
if (resp.incoming != null){_incomingRequests.AddRange(resp.incoming);UIIncomingRequestsList.ReplaceItems(resp.incoming);}
bool hasMore = resp.incoming != null && resp.pagination.total > _incomingRequests.Length;UIIncomingRequestsList.SetHasMorePages(hasMore);});}
public void LoadMoreIncomingRequests(){_incomingCurrentPage++;UIIncomingRequestsList.SetLoading(true);
LootLockerSDKManager.ListIncomingFriendRequestsPaginated(PageSize, _incomingCurrentPage, (resp) =>{UIIncomingRequestsList.SetLoading(false);if (!resp.success){LogFailure("Incoming requests (more)", resp);_incomingCurrentPage--; // Rollback page incrementreturn;}
if (resp.incoming != null && resp.incoming.Length > 0){_incomingRequests.AddRange(resp.incoming);UIIncomingRequestsList.AppendItems(resp.incoming);}
bool hasMore = resp.incoming != null && resp.pagination.total > _incomingRequests.Length;UIIncomingRequestsList.SetHasMorePages(hasMore);});}
// Outgoing Friend Requestspublic void LoadOutgoingRequestsFirstPage(){_outgoingRequests.Clear();_outgoingCurrentPage = 0;UIOutgoingRequestsList.SetLoading(true);
LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(PageSize, _outgoingCurrentPage, (resp) =>{UIOutgoingRequestsList.SetLoading(false);if (!resp.success){LogFailure("Outgoing requests", resp);return;}
if (resp.outgoing != null){_outgoingRequests.AddRange(resp.outgoing);UIOutgoingRequestsList.ReplaceItems(resp.outgoing);}
bool hasMore = resp.outgoing != null && resp.pagination.total > _outgoingRequests.Length;UIOutgoingRequestsList.SetHasMorePages(hasMore);});}
public void LoadMoreOutgoingRequests(){_outgoingCurrentPage++;UIOutgoingRequestsList.SetLoading(true);
LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(PageSize, _outgoingCurrentPage, (resp) =>{UIOutgoingRequestsList.SetLoading(false);if (!resp.success){LogFailure("Outgoing requests (more)", resp);_outgoingCurrentPage--; // Rollback page incrementreturn;}
if (resp.outgoing != null && resp.outgoing.Length > 0){_outgoingRequests.AddRange(resp.outgoing);UIOutgoingRequestsList.AppendItems(resp.outgoing);}
bool hasMore = resp.outgoing != null && resp.pagination.total > _outgoingRequests.Length;UIOutgoingRequestsList.SetHasMorePages(hasMore);});}
// Blocked Playerspublic void LoadBlockedPlayersFirstPage(){_blockedPlayers.Clear();_blockedCurrentPage = 0;UIBlockedPlayersList.SetLoading(true);
LootLockerSDKManager.ListBlockedPlayersPaginated(PageSize, _blockedCurrentPage, (resp) =>{UIBlockedPlayersList.SetLoading(false);if (!resp.success){LogFailure("Blocked players", resp);return;}
if (resp.blocked != null){_blockedPlayers.AddRange(resp.blocked);UIBlockedPlayersList.ReplaceItems(resp.blocked);}
bool hasMore = resp.blocked != null && resp.pagination.total > _blockedPlayers.Length;UIBlockedPlayersList.SetHasMorePages(hasMore);});}
public void LoadMoreBlockedPlayers(){_blockedCurrentPage++;UIBlockedPlayersList.SetLoading(true);
LootLockerSDKManager.ListBlockedPlayersPaginated(PageSize, _blockedCurrentPage, (resp) =>{UIBlockedPlayersList.SetLoading(false);if (!resp.success){LogFailure("Blocked players (more)", resp);_blockedCurrentPage--; // Rollback page incrementreturn;}
if (resp.blocked != null && resp.blocked.Length > 0){_blockedPlayers.AddRange(resp.blocked);UIBlockedPlayersList.AppendItems(resp.blocked);}
bool hasMore = resp.blocked != null && resp.pagination.total > _blockedPlayers.Length;UIBlockedPlayersList.SetHasMorePages(hasMore);});}
private void LogFailure(string context, LootLockerResponse resp){UnityEngine.Debug.LogWarning($"[FriendsListManager] {context} failed: {resp?.errorData?.message}");}}// Friends List Management - Unreal C++// Shows listing friends, incoming/outgoing requests, and blocked players using offset pagination.// Assumptions:// - UI layer methods for loading states and data display
#include "LootLockerManager.h"#include "LootLockerSDK/LLFriends.h"
class FFriendsListManager{public:TArray<FLootLockerAcceptedFriend> Friends;TArray<FLootLockerFriend> IncomingRequests;TArray<FLootLockerFriend> OutgoingRequests;TArray<FLootLockerBlockedPlayer> BlockedPlayers;
// Page tracking (0-indexed)int32 FriendsCurrentPage = 0;int32 IncomingCurrentPage = 0;int32 OutgoingCurrentPage = 0;int32 BlockedCurrentPage = 0;
static constexpr int32 PageSize = 20;
// Friends Listvoid LoadFriendsFirstPage(){Friends.Empty();FriendsCurrentPage = 0;UFriendsListWidget::SetLoading(true);
ULootLockerSDKManager::ListFriendsPaginated(PageSize, FriendsCurrentPage,FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnFriendsLoaded));}
void LoadMoreFriends(){FriendsCurrentPage++;UFriendsListWidget::SetLoading(true);
ULootLockerSDKManager::ListFriendsPaginated(PageSize, FriendsCurrentPage,FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreFriendsLoaded));}
// Incoming Requestsvoid LoadIncomingRequestsFirstPage(){IncomingRequests.Empty();IncomingCurrentPage = 0;UIncomingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(PageSize, IncomingCurrentPage,FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnIncomingRequestsLoaded));}
void LoadMoreIncomingRequests(){IncomingCurrentPage++;UIncomingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(PageSize, IncomingCurrentPage,FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreIncomingRequestsLoaded));}
// Outgoing Requestsvoid LoadOutgoingRequestsFirstPage(){OutgoingRequests.Empty();OutgoingCurrentPage = 0;UOutgoingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(PageSize, OutgoingCurrentPage,FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnOutgoingRequestsLoaded));}
void LoadMoreOutgoingRequests(){OutgoingCurrentPage++;UOutgoingRequestsWidget::SetLoading(true);
ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(PageSize, OutgoingCurrentPage,FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreOutgoingRequestsLoaded));}
// Blocked Playersvoid LoadBlockedPlayersFirstPage(){BlockedPlayers.Empty();BlockedCurrentPage = 0;UBlockedPlayersWidget::SetLoading(true);
ULootLockerSDKManager::ListBlockedPlayersPaginated(PageSize, BlockedCurrentPage,FLootLockerListBlockedPlayersResponseDelegate::CreateRaw(this, &FFriendsListManager::OnBlockedPlayersLoaded));}
void LoadMoreBlockedPlayers(){BlockedCurrentPage++;UBlockedPlayersWidget::SetLoading(true);
ULootLockerSDKManager::ListBlockedPlayersPaginated(PageSize, BlockedCurrentPage,FLootLockerListBlockedPlayersResponseDelegate::CreateRaw(this, &FFriendsListManager::OnMoreBlockedPlayersLoaded));}
private:void OnFriendsLoaded(const FLootLockerListFriendsResponse& Response){UFriendsListWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("Friends list failed: %s"), *Response.Error);return;}
Friends = Response.Friends;UFriendsListWidget::ReplaceItems(Friends);
bool bHasMore = Friends.Num < Response.Pagination.Total;UFriendsListWidget::SetHasMorePages(bHasMore);}
void OnMoreFriendsLoaded(const FLootLockerListFriendsResponse& Response){UFriendsListWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("More friends failed: %s"), *Response.Error);FriendsCurrentPage--; // Rollbackreturn;}
if (Response.Friends.Num() > 0){Friends.Append(Response.Friends);UFriendsListWidget::AppendItems(Response.Friends);}
bool bHasMore = Friends.Num < Response.Pagination.Total;UFriendsListWidget::SetHasMorePages(bHasMore);}
void OnIncomingRequestsLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response){UIncomingRequestsWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("Incoming requests failed: %s"), *Response.Error);return;}
IncomingRequests = Response.Incoming;UIncomingRequestsWidget::ReplaceItems(IncomingRequests);
bool bHasMore = IncomingRequests.Num() < Response.Pagination.Total;UIncomingRequestsWidget::SetHasMorePages(bHasMore);}
void OnMoreIncomingRequestsLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response){UIncomingRequestsWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("More incoming requests failed: %s"), *Response.Error);IncomingCurrentPage--; // Rollbackreturn;}
if (Response.Incoming.Num() > 0){IncomingRequests.Append(Response.Incoming);UIncomingRequestsWidget::AppendItems(Response.Incoming);}
bool bHasMore = IncomingRequests.Num() < Response.Pagination.Total;UIncomingRequestsWidget::SetHasMorePages(bHasMore);}
void OnOutgoingRequestsLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response){UOutgoingRequestsWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("Outgoing requests failed: %s"), *Response.Error);return;}
OutgoingRequests = Response.Outgoing;UOutgoingRequestsWidget::ReplaceItems(OutgoingRequests);
bool bHasMore = OutgoingRequests.Num() < Response.Pagination.Total;UOutgoingRequestsWidget::SetHasMorePages(bHasMore);}
void OnMoreOutgoingRequestsLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response){UOutgoingRequestsWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("More outgoing requests failed: %s"), *Response.Error);OutgoingCurrentPage--; // Rollbackreturn;}
if (Response.Outgoing.Num() > 0){OutgoingRequests.Append(Response.Outgoing);UOutgoingRequestsWidget::AppendItems(Response.Outgoing);}
bool bHasMore = OutgoingRequests.Num() < Response.Pagination.Total;UOutgoingRequestsWidget::SetHasMorePages(bHasMore);}
void OnBlockedPlayersLoaded(const FLootLockerListBlockedPlayersResponse& Response){UBlockedPlayersWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("Blocked players failed: %s"), *Response.Error);return;}
BlockedPlayers = Response.Blocked;UBlockedPlayersWidget::ReplaceItems(BlockedPlayers);
bool bHasMore = BlockedPlayers.Num() < Response.Pagination.Total;UBlockedPlayersWidget::SetHasMorePages(bHasMore);}
void OnMoreBlockedPlayersLoaded(const FLootLockerListBlockedPlayersResponse& Response){UBlockedPlayersWidget::SetLoading(false);if (!Response.success){UE_LOG(LogTemp, Warning, TEXT("More blocked players failed: %s"), *Response.Error);BlockedCurrentPage--; // Rollbackreturn;}
if (Response.Blocked.Num() > 0){BlockedPlayers.Append(Response.Blocked);UBlockedPlayersWidget::AppendItems(Response.Blocked);}
bool bHasMore = BlockedPlayers.Num() < Response.Pagination.Total;UBlockedPlayersWidget::SetHasMorePages(bHasMore);}};Blueprints: Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Offset Pagination Strategy
- Use page numbers (0-indexed) and
PerPagecount to paginate the data. - Track the current page for each list type separately.
- Consider a page "complete" if the returned items equal the requested
PerPage. - Reset to page 0 when refreshing lists.
Send and Cancel Friend Requests
Allow players to initiate and manage outgoing friend requests.
// Send and Cancel Friend Requests with Optimistic UI// Assumptions:// - _outgoingRequests list is maintained locally// - UI shows button states: Send Request / Pending / Friends// - ShowToast for user feedback
public class FriendRequestController{private readonly List<LootLockerFriend> _outgoingRequests;private readonly HashSet<string> _friendIds; // Track current friends for button state
public FriendRequestController(List<LootLockerFriend> outgoingRequests, HashSet<string> friendIds){_outgoingRequests = outgoingRequests;_friendIds = friendIds;}
public void SendFriendRequest(string targetPlayerULID, string displayName){if (string.IsNullOrEmpty(targetPlayerULID)) return;
// Check if already friends or request pendingif (_friendIds.Contains(targetPlayerULID)){ShowToast("Already friends");return;}
if (_outgoingRequests.Any(r => r.player_ulid == targetPlayerULID)){ShowToast("Request already sent");return;}
// Optimistic updatevar tempRequest = new LootLockerFriend{player_ulid = targetPlayerULID,player_name = displayName,created_at = DateTime.Now};_outgoingRequests.Add(tempRequest);UIProfileButton.SetState(FriendButtonState.Pending);UIOutgoingRequestsList.AddItem(tempRequest);
LootLockerSDKManager.SendFriendRequest(targetPlayerULID, (resp) =>{if (!resp.success){// Rollback optimistic update_outgoingRequests.RemoveAll(r => r.player_ulid == targetPlayerULID);UIProfileButton.SetState(FriendButtonState.SendRequest);UIOutgoingRequestsList.RemoveItem(targetPlayerULID);ShowToast($"Failed to send request: {resp.errorData?.message}");return;}
ShowToast($"Friend request sent to {displayName}");});}
public void CancelFriendRequest(string targetPlayerULID, string displayName){var request = _outgoingRequests.FirstOrDefault(r => r.player_ulid == targetPlayerULID);if (request == null) return;
// Optimistic removal_outgoingRequests.Remove(request);UIProfileButton.SetState(FriendButtonState.SendRequest);UIOutgoingRequestsList.RemoveItem(targetPlayerULID);
LootLockerSDKManager.CancelFriendRequest(targetPlayerULID, (resp) =>{if (!resp.success){// Rollback_outgoingRequests.Add(request);UIProfileButton.SetState(FriendButtonState.Pending);UIOutgoingRequestsList.AddItem(request);ShowToast($"Failed to cancel request: {resp.errorData?.message}");return;}
ShowToast($"Cancelled request to {displayName}");});}
private void ShowToast(string message){// Implementation depends on your UI frameworkDebug.Log(message);}}
public enum FriendButtonState{SendRequest,Pending,Friends}// Friend Request Management - Unreal C++// Handles sending and canceling friend requests with optimistic updates
class FFriendRequestController{public:TArray<FLootLockerFriend> OutgoingRequests;TSet<FString> FriendIds; // Track current friends for UI state
void SendFriendRequest(const FString& TargetPlayerULID, const FString& DisplayName){if (TargetPlayerULID.IsEmpty()) return;
if (FriendIds.Contains(TargetPlayerULID)){ShowToast(TEXT("Already friends"));return;}
// Check if request already existsif (OutgoingRequests.ContainsByPredicate([&](const FLootLockerFriend& Request) {return Request.Player_ulid == TargetPlayerULID;})){ShowToast(TEXT("Request already sent"));return;}
// Optimistic updateFLootLockerFriend TempRequest;TempRequest.Player_ulid = TargetPlayerULID;TempRequest.Player_name = DisplayName;TempRequest.Created_at = FDateTime::Now().ToString();
OutgoingRequests.Add(TempRequest);UProfileButtonWidget::SetState(EFriendButtonState::Pending);UOutgoingRequestsWidget::AddItem(TempRequest);
ULootLockerSDKManager::SendFriendRequest(TargetPlayerULID,FLootLockerFriendActionResponseDelegate::CreateLambda([this, TargetPlayerULID, DisplayName](const FLootLockerFriendActionResponse& Response){if (!Response.success){// RollbackOutgoingRequests.RemoveAll([&](const FLootLockerFriend& Request) {return Request.Player_ulid == TargetPlayerULID;});UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);UOutgoingRequestsWidget::RemoveItem(TargetPlayerULID);ShowToast(FString::Printf(TEXT("Failed to send request: %s"), *Response.Error));return;}
ShowToast(FString::Printf(TEXT("Friend request sent to %s"), *DisplayName));}));}
void CancelFriendRequest(const FString& TargetPlayerULID, const FString& DisplayName){// Find the requestFLootLockerFriend* RequestPtr = OutgoingRequests.FindByPredicate([&](const FLootLockerFriend& Request) {return Request.Player_ulid == TargetPlayerULID;});
if (!RequestPtr) return;FLootLockerFriend RequestCopy = *RequestPtr;
// Optimistic removalOutgoingRequests.RemoveAll([&](const FLootLockerFriend& Request) {return Request.Player_ulid == TargetPlayerULID;});UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);UOutgoingRequestsWidget::RemoveItem(TargetPlayerULID);
ULootLockerSDKManager::CancelFriendRequest(TargetPlayerULID,FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy, DisplayName](const FLootLockerFriendActionResponse& Response){if (!Response.success){// RollbackOutgoingRequests.Add(RequestCopy);UProfileButtonWidget::SetState(EFriendButtonState::Pending);UOutgoingRequestsWidget::AddItem(RequestCopy);ShowToast(FString::Printf(TEXT("Failed to cancel request: %s"), *Response.Error));return;}
ShowToast(FString::Printf(TEXT("Cancelled request to %s"), *DisplayName));}));}
private:void ShowToast(const FString& Message){UE_LOG(LogTemp, Log, TEXT("%s"), *Message);// Add your toast UI implementation}};
UENUM(BlueprintType)enum class EFriendButtonState : uint8{SendRequest,Pending,Friends};Blueprints: Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Accept and Decline Friend Requests
Handle incoming friend requests to build your friends network.
// Accept and Decline Incoming Friend Requests// Move requests between lists when processed
public class IncomingRequestsController{private readonly List<LootLockerFriend> _incomingRequests;private readonly List<LootLockerAcceptedFriend> _friends;
public IncomingRequestsController(List<LootLockerFriend> incomingRequests, List<LootLockerAcceptedFriend> friends){_incomingRequests = incomingRequests;_friends = friends;}
public void AcceptFriendRequest(string fromPlayerULID){var request = _incomingRequests.FirstOrDefault(r => r.player_ulid == fromPlayerULID);if (request == null) return;
// Optimistic update: move from incoming to friends_incomingRequests.Remove(request);var newFriend = new LootLockerAcceptedFriend{player_ulid = request.player_ulid,player_name = request.player_name,player_id = request.player_id,created_at = request.created_at,accepted_at = DateTime.Now};_friends.Add(newFriend);
UIIncomingRequestsList.RemoveItem(fromPlayerULID);UIFriendsList.AddItem(newFriend);ShowToast($"Now friends with {request.player_name}");
LootLockerSDKManager.AcceptFriendRequest(fromPlayerULID, (resp) =>{if (!resp.success){// Rollback: move back to incoming_friends.Remove(newFriend);_incomingRequests.Add(request);UIFriendsList.RemoveItem(fromPlayerULID);UIIncomingRequestsList.AddItem(request);ShowToast($"Failed to accept request: {resp.errorData?.message}");}});}
public void DeclineFriendRequest(string fromPlayerULID){var request = _incomingRequests.FirstOrDefault(r => r.player_ulid == fromPlayerULID);if (request == null) return;
// Optimistic removal_incomingRequests.Remove(request);UIIncomingRequestsList.RemoveItem(fromPlayerULID);ShowToast($"Declined request from {request.player_name}");
LootLockerSDKManager.DeclineFriendRequest(fromPlayerULID, (resp) =>{if (!resp.success){// Rollback_incomingRequests.Add(request);UIIncomingRequestsList.AddItem(request);ShowToast($"Failed to decline request: {resp.errorData?.message}");}});}
private void ShowToast(string message){Debug.Log(message);}}// Accept and Decline Friend Requests - Unreal C++
class FIncomingRequestsController{public:TArray<FLootLockerFriend> IncomingRequests;TArray<FLootLockerAcceptedFriend> Friends;
void AcceptFriendRequest(const FString& FromPlayerULID){// Find the requestFLootLockerFriend* RequestPtr = IncomingRequests.FindByPredicate([&](const FLootLockerFriend& Request) {return Request.Player_ulid == FromPlayerULID;});
if (!RequestPtr) return;FLootLockerFriend RequestCopy = *RequestPtr;
// Optimistic update: move from incoming to friendsIncomingRequests.RemoveAll([&](const FLootLockerFriend& Request) {return Request.Player_ulid == FromPlayerULID;});
FLootLockerAcceptedFriend NewFriend;NewFriend.Player_ulid = RequestCopy.Player_ulid;NewFriend.Player_name = RequestCopy.Player_name;NewFriend.Player_id = RequestCopy.Player_id;NewFriend.Created_at = RequestCopy.Created_at;NewFriend.Accepted_at = FDateTime::Now().ToString();
Friends.Add(NewFriend);UIncomingRequestsWidget::RemoveItem(FromPlayerULID);UFriendsListWidget::AddItem(NewFriend);ShowToast(FString::Printf(TEXT("Now friends with %s"), *RequestCopy.Player_name));
ULootLockerSDKManager::AcceptFriendRequest(FromPlayerULID,FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy, NewFriend, FromPlayerULID](const FLootLockerFriendActionResponse& Response){if (!Response.success){// Rollback: move back to incomingFriends.RemoveAll([&](const FLootLockerAcceptedFriend& Friend) {return Friend.Player_ulid == FromPlayerULID;});IncomingRequests.Add(RequestCopy);UFriendsListWidget::RemoveItem(FromPlayerULID);UIncomingRequestsWidget::AddItem(RequestCopy);ShowToast(FString::Printf(TEXT("Failed to accept request: %s"), *Response.Error));}}));}
void DeclineFriendRequest(const FString& FromPlayerULID){// Find the requestFLootLockerFriend* RequestPtr = IncomingRequests.FindByPredicate([&](const FLootLockerFriend& Request) {return Request.Player_ulid == FromPlayerULID;});
if (!RequestPtr) return;FLootLockerFriend RequestCopy = *RequestPtr;
// Optimistic removalIncomingRequests.RemoveAll([&](const FLootLockerFriend& Request) {return Request.Player_ulid == FromPlayerULID;});UIncomingRequestsWidget::RemoveItem(FromPlayerULID);ShowToast(FString::Printf(TEXT("Declined request from %s"), *RequestCopy.Player_name));
ULootLockerSDKManager::DeclineFriendRequest(FromPlayerULID,FLootLockerFriendActionResponseDelegate::CreateLambda([this, RequestCopy](const FLootLockerFriendActionResponse& Response){if (!Response.success){// RollbackIncomingRequests.Add(RequestCopy);UIncomingRequestsWidget::AddItem(RequestCopy);ShowToast(FString::Printf(TEXT("Failed to decline request: %s"), *Response.Error));}}));}
private:void ShowToast(const FString& Message){UE_LOG(LogTemp, Log, TEXT("%s"), *Message);}};Blueprints: Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Block and Unblock Players
Manage a player's blocked players list to prevent unwanted interactions.
// Block and Unblock Players// Blocking removes from friends and prevents future friend requests
public class BlockingController{private readonly List<LootLockerBlockedPlayer> _blockedPlayers;private readonly List<LootLockerAcceptedFriend> _friends;
public BlockingController(List<LootLockerBlockedPlayer> blockedPlayers, List<LootLockerAcceptedFriend> friends){_blockedPlayers = blockedPlayers;_friends = friends;}
public void BlockPlayer(string targetPlayerULID, string displayName){if (string.IsNullOrEmpty(targetPlayerULID)) return;
// Check if already blockedif (_blockedPlayers.Any(p => p.player_ulid == targetPlayerULID)){ShowToast("Player already blocked");return;}
// Optimistic update: remove from friends if present, add to blockedvar existingFriend = _friends.FirstOrDefault(f => f.player_ulid == targetPlayerULID);if (existingFriend != null){_friends.Remove(existingFriend);UIFriendsList.RemoveItem(targetPlayerULID);}
var blockedPlayer = new LootLockerBlockedPlayer{player_ulid = targetPlayerULID,player_name = displayName,blocked_at = DateTime.Now};_blockedPlayers.Add(blockedPlayer);UIBlockedPlayersList.AddItem(blockedPlayer);UIProfileButton.SetState(FriendButtonState.Blocked);
LootLockerSDKManager.BlockPlayer(targetPlayerULID, (resp) =>{if (!resp.success){// Rollback_blockedPlayers.Remove(blockedPlayer);UIBlockedPlayersList.RemoveItem(targetPlayerULID);if (existingFriend != null){_friends.Add(existingFriend);UIFriendsList.AddItem(existingFriend);UIProfileButton.SetState(FriendButtonState.Friends);}else{UIProfileButton.SetState(FriendButtonState.SendRequest);}ShowToast($"Failed to block player: {resp.errorData?.message}");return;}
ShowToast($"Blocked {displayName}");});}
public void UnblockPlayer(string targetPlayerULID, string displayName){var blockedPlayer = _blockedPlayers.FirstOrDefault(p => p.player_ulid == targetPlayerULID);if (blockedPlayer == null) return;
// Optimistic removal_blockedPlayers.Remove(blockedPlayer);UIBlockedPlayersList.RemoveItem(targetPlayerULID);UIProfileButton.SetState(FriendButtonState.SendRequest);
LootLockerSDKManager.UnblockPlayer(targetPlayerULID, (resp) =>{if (!resp.success){// Rollback_blockedPlayers.Add(blockedPlayer);UIBlockedPlayersList.AddItem(blockedPlayer);UIProfileButton.SetState(FriendButtonState.Blocked);ShowToast($"Failed to unblock player: {resp.errorData?.message}");return;}
ShowToast($"Unblocked {displayName}");});}
private void ShowToast(string message){Debug.Log(message);}}// Block and Unblock Players - Unreal C++
class FBlockingController{public:TArray<FLootLockerBlockedPlayer> BlockedPlayers;TArray<FLootLockerAcceptedFriend> Friends;
void BlockPlayer(const FString& TargetPlayerULID, const FString& DisplayName){if (TargetPlayerULID.IsEmpty()) return;
// Check if already blockedif (BlockedPlayers.ContainsByPredicate([&](const FLootLockerBlockedPlayer& Player) {return Player.Player_ulid == TargetPlayerULID;})){ShowToast(TEXT("Player already blocked"));return;}
// Remove from friends if presentFLootLockerAcceptedFriend* ExistingFriend = Friends.FindByPredicate([&](const FLootLockerAcceptedFriend& Friend) {return Friend.Player_ulid == TargetPlayerULID;});
FLootLockerAcceptedFriend FriendCopy;bool bWasFriend = false;if (ExistingFriend){FriendCopy = *ExistingFriend;bWasFriend = true;Friends.RemoveAll([&](const FLootLockerAcceptedFriend& Friend) {return Friend.Player_ulid == TargetPlayerULID;});UFriendsListWidget::RemoveItem(TargetPlayerULID);}
// Add to blockedFLootLockerBlockedPlayer BlockedPlayer;BlockedPlayer.Player_ulid = TargetPlayerULID;BlockedPlayer.Player_name = DisplayName;BlockedPlayer.Blocked_at = FDateTime::Now().ToString();
BlockedPlayers.Add(BlockedPlayer);UBlockedPlayersWidget::AddItem(BlockedPlayer);UProfileButtonWidget::SetState(EFriendButtonState::Blocked);
ULootLockerSDKManager::BlockPlayer(TargetPlayerULID,FLootLockerFriendActionResponseDelegate::CreateLambda([this, BlockedPlayer, FriendCopy, bWasFriend, TargetPlayerULID, DisplayName](const FLootLockerFriendActionResponse& Response){if (!Response.success){// RollbackBlockedPlayers.RemoveAll([&](const FLootLockerBlockedPlayer& Player) {return Player.Player_ulid == TargetPlayerULID;});UBlockedPlayersWidget::RemoveItem(TargetPlayerULID);
if (bWasFriend){Friends.Add(FriendCopy);UFriendsListWidget::AddItem(FriendCopy);UProfileButtonWidget::SetState(EFriendButtonState::Friends);}else{UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);}
ShowToast(FString::Printf(TEXT("Failed to block player: %s"), *Response.Error));return;}
ShowToast(FString::Printf(TEXT("Blocked %s"), *DisplayName));}));}
void UnblockPlayer(const FString& TargetPlayerULID, const FString& DisplayName){FLootLockerBlockedPlayer* BlockedPlayerPtr = BlockedPlayers.FindByPredicate([&](const FLootLockerBlockedPlayer& Player) {return Player.Player_ulid == TargetPlayerULID;});
if (!BlockedPlayerPtr) return;FLootLockerBlockedPlayer BlockedPlayerCopy = *BlockedPlayerPtr;
// Optimistic removalBlockedPlayers.RemoveAll([&](const FLootLockerBlockedPlayer& Player) {return Player.Player_ulid == TargetPlayerULID;});UBlockedPlayersWidget::RemoveItem(TargetPlayerULID);UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);
ULootLockerSDKManager::UnblockPlayer(TargetPlayerULID,FLootLockerFriendActionResponseDelegate::CreateLambda([this, BlockedPlayerCopy, DisplayName](const FLootLockerFriendActionResponse& Response){if (!Response.success){// RollbackBlockedPlayers.Add(BlockedPlayerCopy);UBlockedPlayersWidget::AddItem(BlockedPlayerCopy);UProfileButtonWidget::SetState(EFriendButtonState::Blocked);ShowToast(FString::Printf(TEXT("Failed to unblock player: %s"), *Response.Error));return;}
ShowToast(FString::Printf(TEXT("Unblocked %s"), *DisplayName));}));}
private:void ShowToast(const FString& Message){UE_LOG(LogTemp, Log, TEXT("%s"), *Message);}};Blueprints: Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Check Friendship Status
Determine the current relationship between players for UI state management.
// Check Friendship Status Using GetFriend// Returns specific friend data if they are friends, null/error otherwise
public static class FriendshipProbe{public static void CheckFriendshipStatus(string targetPlayerULID, System.Action<FriendshipStatus> onResult){if (string.IsNullOrEmpty(targetPlayerULID)){onResult?.Invoke(FriendshipStatus.None);return;}
LootLockerSDKManager.GetFriend(targetPlayerULID, (resp) =>{if (resp.success && !string.IsNullOrEmpty(resp.player_ulid)){// They are friends - check when friendship was establishedonResult?.Invoke(FriendshipStatus.Friends);}else{// Not friends - could be pending request, blocked, or no relationship// You might need additional checks to distinguish these statesonResult?.Invoke(FriendshipStatus.None);}});}}
public enum FriendshipStatus{None, // No relationshipFriends, // Mutual friendsIncomingRequest, // They sent us a requestOutgoingRequest, // We sent them a requestBlocked // One party blocked the other}
// Usage:// FriendshipProbe.CheckFriendshipStatus(otherPlayerUid, (status) => {// switch(status) {// case FriendshipStatus.Friends:// UIProfileButton.SetState(FriendButtonState.Friends);// break;// case FriendshipStatus.None:// UIProfileButton.SetState(FriendButtonState.SendRequest);// break;// }// });// Friendship Status Check - Unreal C++
UENUM(BlueprintType)enum class EFriendshipStatus : uint8{None, // No relationshipFriends, // Mutual friendsIncomingRequest, // They sent us a requestOutgoingRequest, // We sent them a requestBlocked // One party blocked the other};
class FFriendshipProbe{public:static void CheckFriendshipStatus(const FString& TargetPlayerULID, TFunction<void(EFriendshipStatus)> Callback){if (TargetPlayerULID.IsEmpty()){Callback(EFriendshipStatus::None);return;}
ULootLockerSDKManager::GetFriend(TargetPlayerULID,FLootLockerGetFriendResponseDelegate::CreateLambda([Callback](const FLootLockerGetFriendResponse& Response){if (Response.success && !Response.Player_ulid.IsEmpty()){// They are friendsCallback(EFriendshipStatus::Friends);}else{// Not friends - could be pending, blocked, or no relationship// Additional logic could check pending requests listsCallback(EFriendshipStatus::None);}}));}};
// Usage:// FFriendshipProbe::CheckFriendshipStatus(OtherPlayerUid, [](EFriendshipStatus Status) {// switch(Status) {// case EFriendshipStatus::Friends:// UProfileButtonWidget::SetState(EFriendButtonState::Friends);// break;// case EFriendshipStatus::None:// UProfileButtonWidget::SetState(EFriendButtonState::SendRequest);// break;// }// });Blueprints: Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Displaying Counts and Status
Show meaningful counts and relationship indicators in your UI.
// Friends Counts and Status Display// Use first page loads to estimate counts and provide status context
public class FriendsCountsManager{private int _friendsCount = 0;private int _incomingRequestsCount = 0;private int _outgoingRequestsCount = 0;
public void LoadAllCounts(){// Load first pages to get count estimatesLoadFriendsCount();LoadIncomingRequestsCount();LoadOutgoingRequestsCount();}
private void LoadFriendsCount(){LootLockerSDKManager.ListFriendsPaginated(1, 0, (resp) =>{if (resp.success){UIFriendsHeader.SetCount(resp.pagination.total.ToString());}});}
private void LoadIncomingRequestsCount(){LootLockerSDKManager.ListIncomingFriendRequestsPaginated(1, 0, (resp) =>{if (resp.success && resp.incoming != null){_incomingRequestsCount = resp.pagination.total;UIIncomingRequestsHeader.SetCount(_incomingRequestsCount);
// Show notification badge if there are pending requestsif (_incomingRequestsCount > 0){UIFriendsTabButton.ShowBadge(_incomingRequestsCount);}}});}
private void LoadOutgoingRequestsCount(){LootLockerSDKManager.ListOutGoingFriendRequestsPaginated(1, 0, (resp) =>{if (resp.success && resp.outgoing != null){UIOutgoingRequestsHeader.SetCount(resp.pagination.total);}});}}// Friends Counts Display - Unreal C++
class FFriendsCountsManager{public:int32 FriendsCount = 0;int32 IncomingRequestsCount = 0;int32 OutgoingRequestsCount = 0;
void LoadAllCounts(){LoadFriendsCount();LoadIncomingRequestsCount();LoadOutgoingRequestsCount();}
private:void LoadFriendsCount(){ULootLockerSDKManager::ListFriendsPaginated(1, 0,FLootLockerListFriendsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnFriendsCountLoaded));}
void LoadIncomingRequestsCount(){ULootLockerSDKManager::ListIncomingFriendRequestsPaginated(1, 0,FLootLockerListIncomingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnIncomingCountLoaded));}
void LoadOutgoingRequestsCount(){ULootLockerSDKManager::ListOutgoingFriendRequestsPaginated(1, 0,FLootLockerListOutgoingFriendRequestsResponseDelegate::CreateRaw(this, &FFriendsCountsManager::OnOutgoingCountLoaded));}
void OnFriendsCountLoaded(const FLootLockerListFriendsResponse& Response){if (Response.success){UFriendsHeaderWidget::SetCount(FString::FromInt(Response.Pagination.Total));}}
void OnIncomingCountLoaded(const FLootLockerListIncomingFriendRequestsResponse& Response){if (Response.success){IncomingRequestsCount = Response.Pagination.Total;UIncomingRequestsHeaderWidget::SetCount(FString::FromInt(IncomingRequestsCount));
// Show notification badge if there are pending requestsif (IncomingRequestsCount > 0){UFriendsTabButtonWidget::ShowBadge(IncomingRequestsCount);}}}
void OnOutgoingCountLoaded(const FLootLockerListOutgoingFriendRequestsResponse& Response){if (Response.success){OutgoingRequestsCount = Response.Pagination.Total;UOutgoingRequestsHeaderWidget::SetCount(FString::FromInt(OutgoingRequestsCount));}}};Blueprints: Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Coming soon — this sample hasn't been written yet.
Performance Tips
- Batch relationship checks: when showing many players (leaderboards, for example), only fetch data for visible items.
- Cache friend IDs locally: maintain a set of friend ULIDs for instant button state rendering.
- Moderate page sizes: 20–50 items per page balances responsiveness with network efficiency.
- Debounce rapid requests: prevent spam-clicking send/cancel buttons.
- Lazy load secondary lists: load friends first, then incoming/outgoing requests when tabs are opened.
Example Feature Ideas
- Mutual Friends: "You have 3 mutual friends with this player."
- Friend Recommendations: suggest friends-of-friends or players with similar interests.
- Private Groups: create invite-only spaces using a player's friends list.
- Co-op Quick Join: join friends' game sessions directly from the friends list.
Conclusion
You've implemented friend requests, list management, accept/decline workflows, blocking functionality, and relationship status checking. The friends system provides the foundation for deeper social features like messaging, co-op play, and community building.
Key differences from followers include the mutual consent model, request management workflow, and offset-based pagination. Next, consider adding Followers for asymmetric relationships, or explore Web Console management for administrative oversight.