Skip to content

Instantly share code, notes, and snippets.

@thsbrown
Last active August 5, 2025 04:21
Show Gist options
  • Select an option

  • Save thsbrown/633e524634a6b932ffcecd8d6da9b0a4 to your computer and use it in GitHub Desktop.

Select an option

Save thsbrown/633e524634a6b932ffcecd8d6da9b0a4 to your computer and use it in GitHub Desktop.
Example of a Service in Command Center Earth (Standard C# class)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BreakstepStudios.Scripts.Runtime.PlayFab;
using PlayFab;
using PlayFab.ClientModels;
using PlayFab.SharedModels;
using ThomasBrown.PlayFab;
using UniRx;
namespace _Game_Assets.Scripts.Runtime.Systems
{
public class PlayFabFriendsListService
{
/// <summary>
/// The tag in our friends list that denotes confirmed friends (from sent or received friend request)
/// </summary>
public const string FRIEND_CONFIRMED_TAG = "confirmed";
/// <summary>
/// The tag in our friends list that denotes we have sent a friend request to a specific player
/// </summary>
public const string FRIEND_REQUEST_SENT_TAG = "sent";
/// <summary>
/// The tag in our friends list that denotes we have received a friend request from a specific player
/// </summary>
public const string FRIEND_REQUEST_RECEIVED_TAG = "received";
/// <summary>
/// Behavior subject we will utilize to alert subscribers when our friends list has been modified
/// </summary>
private BehaviorSubject<List<FriendInfo>> friendsListModifiedBehaviorSubject;
/// <summary>
/// Behavior subject we will utilize to alert subscribers when our friend requests have been modified
/// </summary>
private BehaviorSubject<List<FriendInfo>> friendRequestsModifiedBehaviorSubject;
/// <summary>
/// The cloud script revision that our friends list service is operating on
/// </summary>
private CloudScriptRevision cloudScriptRevision;
/// <summary>
/// A callback used to allow modification of display names in friends list and friend requests before they are output via
/// <see cref="ObserveFriendsListModified"/>, <see cref="ObserveFriendRequestsModified"/> and <see cref="GetFriendsList"/>
/// </summary>
private Func<FriendInfo, string> displayNameModifierCallback;
public PlayFabFriendsListService()
{
cloudScriptRevision = new CloudScriptRevision();
//set values to null initially so we have an identifier of when initial values haven't yet loaded
friendsListModifiedBehaviorSubject = new BehaviorSubject<List<FriendInfo>>(null);
friendRequestsModifiedBehaviorSubject = new BehaviorSubject<List<FriendInfo>>(null);
}
/// <summary>
/// Initializes the friends list service with appropriate parameters
/// <para>In most cases initialization should occur once player is signed into PlayFab whereas instantiation can occur before hand.</para>
/// </summary>
/// <param name="cloudScriptRevision">
/// The cloud script revision that contains our backend leaderboards code.
/// </param>
/// <param name="displayNameModifierCallback">
/// A callback used to allow modification of display names in friends list and friend requests before they are output via
/// <see cref="ObserveFriendsListModified"/>, <see cref="ObserveFriendRequestsModified"/> and <see cref="GetFriendsList"/>
/// </param>
public void Initialize(CloudScriptRevision cloudScriptRevision = null, Func<FriendInfo, string> displayNameModifierCallback = null)
{
this.displayNameModifierCallback = displayNameModifierCallback ?? (info => info.TitleDisplayName ?? "");
this.cloudScriptRevision = cloudScriptRevision ?? new CloudScriptRevision();
}
/// <summary>
/// Retrieves friends list / request from PlayFab sorted as follows: 1. isFriendRequest, 2. isOutgoingRequest, 3.titleDisplayName
/// </summary>
/// <returns>List of our confirmed friends and friend requests sent and received</returns>
//TODO we should likely add a param to GetFriendsList that allows for specifying if we want confirmed friend, unconfirmed friends or both
//TODO we already have this functionality server side so it would just be a matter of specifying the right FunctionName below
public async Task<PlayFabCloudScriptResponse<GetFriendsListResult>> GetFriendsList()
{
var executeCloudScriptRequest = new ExecuteCloudScriptRequest
{
FunctionName = "getFriendsList",
FunctionParameter = new
{
GetFriendsListRequest = new GetFriendsListRequest
{
IncludeFacebookFriends = false,
IncludeSteamFriends = false
}
},
GeneratePlayStreamEvent = true,
RevisionSelection = cloudScriptRevision.RevisionOption,
SpecificRevision = cloudScriptRevision.RevisionNumber
};
var result =
await PlayFabClientAPIWrapper.ExecuteCloudScriptAsync<GetFriendsListResult>(executeCloudScriptRequest);
if (result.ContainsError)
{
return result;
}
friendsListModifiedBehaviorSubject.OnNext(result.FunctionResult.Friends.Where(x => !x.IsFriendRequest).ToList());
friendRequestsModifiedBehaviorSubject.OnNext(result.FunctionResult.Friends.Where(x => x.IsFriendRequest).ToList());
//Sort our combined friends / friends requests list and return it
var returnList = await ObserveFriendsListModified.Take(1).ToTask();
returnList.AddRange(await ObserveFriendRequestsModified.Take(1).ToTask());
result.FunctionResult.Friends = returnList;
return result;
}
/// <summary>
/// Sends a friend request to a player
/// </summary>
/// <param name="friendIdInfo">The type of friend id info we are sending to server</param>
/// <param name="friendId">The identification for the friend we are requesting (based on friendIdInfo above)</param>
/// <returns>The cloud script result containing the FriendInfo of the player friend request was sent to</returns>
public async Task<PlayFabCloudScriptResponse<FriendInfo>> SendFriendRequest(FriendIdInfo friendIdInfo, string friendId)
{
if (friendIdInfo != FriendIdInfo.PlayFabId)
{
var getAccountInfoRequest = new GetAccountInfoRequest();
switch (friendIdInfo)
{
case FriendIdInfo.TitleDisplayName:
getAccountInfoRequest.TitleDisplayName = friendId;
break;
case FriendIdInfo.Username:
getAccountInfoRequest.Username = friendId;
break;
case FriendIdInfo.Email:
getAccountInfoRequest.Email = friendId;
break;
case FriendIdInfo.PlayFabId:
default:
throw new ArgumentOutOfRangeException(nameof(friendIdInfo), friendIdInfo, null);
}
var getAccountInfoResult = await PlayFabClientAPIWrapper.GetAccountInfoAsync(getAccountInfoRequest);
if (getAccountInfoResult.ContainsError)
{
//convert our error here to a cloudscript response logs error to make error checking easier
return PlayFabCloudScriptResponse.ConvertToLogsError<FriendInfo>(getAccountInfoResult.Error);
}
friendId = getAccountInfoResult.Result.AccountInfo.PlayFabId;
}
var executeCloudScriptRequest = new ExecuteCloudScriptRequest
{
FunctionName = "sendFriendRequest",
GeneratePlayStreamEvent = true,
FunctionParameter = new
{
FriendPlayFabId = friendId
},
RevisionSelection = cloudScriptRevision.RevisionOption,
SpecificRevision = cloudScriptRevision.RevisionNumber
};
var result =
await PlayFabClientAPIWrapper.ExecuteCloudScriptAsync<FriendInfo>(executeCloudScriptRequest);
if (result.ContainsError)
{
return result;
}
//remove to ensure we always have latest info for the player and then pass along updated friend requests list
var friendRequests = friendRequestsModifiedBehaviorSubject.Value ?? new List<FriendInfo>();
friendRequests.RemoveAll(x => x.FriendPlayFabId == result.FunctionResult.FriendPlayFabId);
friendRequests.Add(result.FunctionResult);
friendRequestsModifiedBehaviorSubject.OnNext(friendRequests);
return result;
}
/// <summary>
/// Accepts an incoming friend request
/// </summary>
/// <param name="friendPlayFabId">The PlayFabId of the friend we want to accept friend request from</param>
/// <returns>The cloud script result containing the FriendInfo of the player that we accept friend reqeust for</returns>
public async Task<PlayFabCloudScriptResponse<FriendInfo>> AcceptFriendRequest(string friendPlayFabId)
{
var executeCloudScriptRequest = new ExecuteCloudScriptRequest
{
FunctionName = "acceptFriendRequest",
FunctionParameter = new
{
FriendPlayFabId = friendPlayFabId
},
GeneratePlayStreamEvent = true,
RevisionSelection = cloudScriptRevision.RevisionOption,
SpecificRevision = cloudScriptRevision.RevisionNumber
};
var result =
await PlayFabClientAPIWrapper.ExecuteCloudScriptAsync<FriendInfo>(executeCloudScriptRequest);
if (result.ContainsError)
{
return result;
}
//remove to ensure we always have latest info for the player and then pass along updated friends list
var friendsList = friendsListModifiedBehaviorSubject.Value ?? new List<FriendInfo>();
friendsList.RemoveAll(x => x.FriendPlayFabId == result.FunctionResult.FriendPlayFabId);
friendsList.Add(result.FunctionResult);
friendsListModifiedBehaviorSubject.OnNext(friendsList);
//ensure we remove the added friend from our friend requests list and then pass along updated friend requests list
var friendRequests = friendRequestsModifiedBehaviorSubject.Value ?? new List<FriendInfo>();
friendRequests.RemoveAll(x => x.FriendPlayFabId == result.FunctionResult.FriendPlayFabId);
friendRequestsModifiedBehaviorSubject.OnNext(friendRequests);
return result;
}
/// <summary>
/// Denys an incoming friend request
/// </summary>
/// <param name="friendPlayFabId">The playfab id of the friend we want to deny the friend request from</param>
/// <returns>The cloud script response containing the result of the operation</returns>
public async Task<PlayFabCloudScriptResponse> DenyFriendRequest(string friendPlayFabId)
{
var executeCloudScriptRequest = new ExecuteCloudScriptRequest
{
FunctionName = "denyFriendRequest",
FunctionParameter = new
{
FriendPlayFabId = friendPlayFabId
},
GeneratePlayStreamEvent = true,
RevisionSelection = cloudScriptRevision.RevisionOption,
SpecificRevision = cloudScriptRevision.RevisionNumber
};
var result =
await PlayFabClientAPIWrapper.ExecuteCloudScriptAsync(executeCloudScriptRequest);
if (result.ContainsError)
{
return result;
}
//ensure we remove the denied friend from our friend requests list and then pass along updated friend requests list
var friendRequests = friendRequestsModifiedBehaviorSubject.Value ?? new List<FriendInfo>();
friendRequests.RemoveAll(x => x.FriendPlayFabId == friendPlayFabId);
friendRequestsModifiedBehaviorSubject.OnNext(friendRequests);
return result;
}
/// <summary>
/// Cancels an outgoing friend request
/// </summary>
/// <param name="friendPlayFabId">The playfab id of the friend we want to cancel the friend request to</param>
/// <returns>The cloud script response containing the result of the operation</returns>
public async Task<PlayFabCloudScriptResponse> CancelFriendRequest(string friendPlayFabId)
{
var executeCloudScriptRequest = new ExecuteCloudScriptRequest
{
FunctionName = "cancelFriendRequest",
FunctionParameter = new
{
FriendPlayFabId = friendPlayFabId
},
GeneratePlayStreamEvent = true,
RevisionSelection = cloudScriptRevision.RevisionOption,
SpecificRevision = cloudScriptRevision.RevisionNumber
};
var result =
await PlayFabClientAPIWrapper.ExecuteCloudScriptAsync(executeCloudScriptRequest);
if (result.ContainsError)
{
return result;
}
//ensure we remove the cancelled friend from our friend requests list and then pass along updated friend requests list
var friendRequests = friendRequestsModifiedBehaviorSubject.Value ?? new List<FriendInfo>();
friendRequests.RemoveAll(x => x.FriendPlayFabId == friendPlayFabId);
friendRequestsModifiedBehaviorSubject.OnNext(friendRequests);
return result;
}
/// <summary>
/// Unfriends a player, removing them from friendsList and removing you from their friends list.
/// </summary>
/// <param name="friendPlayFabId">The playFabId of the player on our friends list we want to unfriend</param>
/// <returns>The cloud script response containing the result of the operation</returns>
public async Task<PlayFabCloudScriptResponse> UnFriend(string friendPlayFabId)
{
var executeCloudScriptRequest = new ExecuteCloudScriptRequest
{
FunctionName = "unFriend",
FunctionParameter = new
{
FriendPlayFabId = friendPlayFabId
},
GeneratePlayStreamEvent = true,
RevisionSelection = cloudScriptRevision.RevisionOption,
SpecificRevision = cloudScriptRevision.RevisionNumber
};
var result =
await PlayFabClientAPIWrapper.ExecuteCloudScriptAsync(executeCloudScriptRequest);
if (result.ContainsError)
{
return result;
}
//ensure we remove the unfriended friend from our friend list and then pass along updated friend list
var friendList = friendsListModifiedBehaviorSubject.Value ?? new List<FriendInfo>();
friendList.RemoveAll(x => x.FriendPlayFabId == friendPlayFabId);
friendsListModifiedBehaviorSubject.OnNext(friendList);
return result;
}
/// <summary>
/// Completes, disposes and then recreates <see cref="ObserveFriendsListModified"/> and <see cref="ObserveFriendRequestsModified"/>
/// initializing them with null values.
/// </summary>
public void ResetFriendsModifiedObservables()
{
//clear friendsList Observable
try
{
friendsListModifiedBehaviorSubject.OnCompleted();
}
finally
{
friendsListModifiedBehaviorSubject.Dispose();
friendsListModifiedBehaviorSubject = new BehaviorSubject<List<FriendInfo>>(null);
}
//clear friendRequests Observable
try
{
friendRequestsModifiedBehaviorSubject.OnCompleted();
}
finally
{
friendRequestsModifiedBehaviorSubject.Dispose();
friendRequestsModifiedBehaviorSubject = new BehaviorSubject<List<FriendInfo>>(null);
}
}
/// <summary>
/// Observable firing when our friends list is modified returns the latest friends list
/// </summary>
public IObservable<List<FriendInfo>> ObserveFriendsListModified
{
get
{
return friendsListModifiedBehaviorSubject
.Where(x => x != null)
.Select(x =>
{
return x
.Select(x => { x.TitleDisplayName = displayNameModifierCallback(x); return x; })
.OrderBy(x => x.TitleDisplayName)
.ToList();
});
}
}
/// <summary>
/// Observable firing when our friends requests are modified returns the latest friend requests (ordered by outgoing first)
/// </summary>
public IObservable<List<FriendInfo>> ObserveFriendRequestsModified
{
get
{
return friendRequestsModifiedBehaviorSubject
.Where(x => x != null)
.Select(x =>
{
return x
.Select(x => { x.TitleDisplayName = displayNameModifierCallback(x); return x; })
.OrderBy(x => x.IsOutgoingFriendRequest)
.ThenBy(x => x.TitleDisplayName).ToList();
});
}
}
/// <inheritdoc cref="PlayFab.ClientModels.GetFriendsListResult"/>
/// <remarks>
/// Mirrors <see cref="PlayFab.ClientModels.GetFriendsListResult"/> but uses
/// <see cref="FriendInfo"/> instead of <see cref="PlayFab.ClientModels.FriendInfo"/>
/// </remarks>
[Serializable]
public class GetFriendsListResult : PlayFabResultCommon
{
/// <summary>
/// Array of friends found.
/// </summary>
public List<FriendInfo> Friends;
}
/// <summary>
/// Extended FriendInfo used to derive additional data about a FriendInfo such as if it's apart of a friend request
/// </summary>
public class FriendInfo : PlayFab.ClientModels.FriendInfo
{
/// <summary>
/// Returns true if <see cref="IsIncomingFriendRequest"/> or <see cref="IsOutgoingFriendRequest"/> is true
/// <remarks>If this is false, then we have a confirmed friend that should be marked with a <see cref="PlayFabFriendsListService.FRIEND_CONFIRMED_TAG"/></remarks>
/// </summary>
public bool IsFriendRequest => IsIncomingFriendRequest || IsOutgoingFriendRequest;
/// <summary>
/// Returns true if we have a friend in our friends list marked with a <see cref="PlayFabFriendsListService.FRIEND_REQUEST_RECEIVED_TAG"/>
/// </summary>
public bool IsIncomingFriendRequest => Tags.Contains(FRIEND_REQUEST_RECEIVED_TAG);
/// <summary>
/// Returns true if we have a friend in our friends list marked with a <see cref="PlayFabFriendsListService.FRIEND_REQUEST_SENT_TAG"/>
/// </summary>
public bool IsOutgoingFriendRequest => Tags.Contains(FRIEND_REQUEST_SENT_TAG);
}
}
/// <summary>
/// Used for identifying type of f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment