game(scripts): add backend and adjacent scripts
This commit is contained in:
parent
9149887e4e
commit
9cc96fdcc6
8
Game/Assets/Scripts.meta
Normal file
8
Game/Assets/Scripts.meta
Normal file
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b2fa2ae79460cd147a2f52ae9879eb9f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
690
Game/Assets/Scripts/Backend.cs
Normal file
690
Game/Assets/Scripts/Backend.cs
Normal file
|
@ -0,0 +1,690 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Firebase;
|
||||
using Firebase.Auth;
|
||||
using Firebase.Database;
|
||||
using Firebase.Extensions;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// the general managing class for handling communication with the firebase backend
|
||||
/// (to be initialised by GameManager)
|
||||
/// </summary>
|
||||
public class Backend
|
||||
{
|
||||
/// <summary>
|
||||
/// enum for the result of the authentication process
|
||||
/// </summary>
|
||||
public enum AuthenticationResult
|
||||
{
|
||||
Ok,
|
||||
AlreadyAuthenticated,
|
||||
NonExistentUser,
|
||||
AlreadyExistingUser,
|
||||
UsernameAlreadyTaken,
|
||||
InvalidEmail,
|
||||
InvalidCredentials,
|
||||
GenericError
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// enum for the connection status of the firebase backend
|
||||
/// </summary>
|
||||
public enum FirebaseConnectionStatus
|
||||
{
|
||||
NotConnected,
|
||||
Connected,
|
||||
UpdateRequired, // "a required system component is out of date"
|
||||
Updating, // "a required system component is updating, retrying in a bit..."
|
||||
ExternalError, // "a system component is disabled, invalid, missing, or permissions are insufficient"
|
||||
InternalError // "an unknown error occurred"
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// generic enum for the result of a database transaction
|
||||
/// </summary>
|
||||
public enum TransactionResult
|
||||
{
|
||||
Ok,
|
||||
Unauthenticated,
|
||||
Error
|
||||
}
|
||||
|
||||
public enum UserAccountDetailTargetEnum
|
||||
{
|
||||
Username,
|
||||
Email,
|
||||
Password
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// callback functions to be invoked when the connection status changes
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private readonly List<Action<FirebaseConnectionStatus>> _onConnectionStatusChangedCallbacks = new();
|
||||
|
||||
/// <summary>
|
||||
/// callback functions to be invoked when the user signs in
|
||||
/// </summary>
|
||||
private readonly List<Action<FirebaseUser>> _onSignInCallbacks = new();
|
||||
|
||||
/// <summary>
|
||||
/// callback functions to be invoked when the user signs out
|
||||
/// </summary>
|
||||
private readonly List<Action> _onSignOutCallbacks = new();
|
||||
|
||||
/// <summary>
|
||||
/// the firebase authentication object
|
||||
/// </summary>
|
||||
private FirebaseAuth _auth;
|
||||
|
||||
/// <summary>
|
||||
/// the firebase database reference
|
||||
/// </summary>
|
||||
private DatabaseReference _db;
|
||||
|
||||
/// <summary>
|
||||
/// the current user object, if authenticated
|
||||
/// </summary>
|
||||
private FirebaseUser _user;
|
||||
|
||||
/// <summary>
|
||||
/// the current user's username, if authenticated
|
||||
/// </summary>
|
||||
private string _username;
|
||||
|
||||
/// <summary>
|
||||
/// whether the user is signed in
|
||||
/// </summary>
|
||||
public bool IsSignedIn;
|
||||
|
||||
/// <summary>
|
||||
/// whether the backend is connected to the firebase backend
|
||||
/// </summary>
|
||||
public FirebaseConnectionStatus Status = FirebaseConnectionStatus.NotConnected;
|
||||
|
||||
/// <summary>
|
||||
/// variable initialisation function
|
||||
/// </summary>
|
||||
public void Initialise(Action<FirebaseConnectionStatus> callback)
|
||||
{
|
||||
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task =>
|
||||
{
|
||||
// cher is this robust enough
|
||||
switch (task.Result)
|
||||
{
|
||||
case DependencyStatus.Available:
|
||||
_auth = FirebaseAuth.GetAuth(FirebaseApp.DefaultInstance);
|
||||
_auth.StateChanged += AuthStateChanged;
|
||||
_db = FirebaseDatabase.DefaultInstance.RootReference;
|
||||
Status = FirebaseConnectionStatus.Connected;
|
||||
callback(Status);
|
||||
FireOnConnectionStatusChangedCallbacks();
|
||||
break;
|
||||
|
||||
case DependencyStatus.UnavailableDisabled:
|
||||
case DependencyStatus.UnavailableInvalid:
|
||||
case DependencyStatus.UnavilableMissing:
|
||||
case DependencyStatus.UnavailablePermission:
|
||||
Status = FirebaseConnectionStatus.ExternalError;
|
||||
callback(Status);
|
||||
FireOnConnectionStatusChangedCallbacks();
|
||||
break;
|
||||
|
||||
case DependencyStatus.UnavailableUpdating:
|
||||
Status = FirebaseConnectionStatus.Updating;
|
||||
callback(Status);
|
||||
FireOnConnectionStatusChangedCallbacks();
|
||||
RetryInitialiseAfterDelay(callback);
|
||||
break;
|
||||
|
||||
case DependencyStatus.UnavailableUpdaterequired:
|
||||
Status = FirebaseConnectionStatus.UpdateRequired;
|
||||
FireOnConnectionStatusChangedCallbacks();
|
||||
callback(Status);
|
||||
break;
|
||||
|
||||
case DependencyStatus.UnavailableOther:
|
||||
default:
|
||||
Status = FirebaseConnectionStatus.InternalError;
|
||||
Debug.LogError("firebase ??? blew up or something," + task.Result);
|
||||
FireOnConnectionStatusChangedCallbacks();
|
||||
callback(Status);
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.Log("firebase status is" + Status);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// async function to retry initialisation after a delay
|
||||
/// </summary>
|
||||
private async void RetryInitialiseAfterDelay(Action<FirebaseConnectionStatus> callback)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Initialise(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// cleanup function
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
SignOutUser();
|
||||
_auth.StateChanged -= AuthStateChanged;
|
||||
_auth = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to register a callback for when the user signs in
|
||||
/// </summary>
|
||||
/// <param name="callback">callback function that takes in a <c>FirebaseUser</c> object</param>
|
||||
public void RegisterOnSignInCallback(Action<FirebaseUser> callback)
|
||||
{
|
||||
_onSignInCallbacks.Add(callback);
|
||||
Debug.Log($"registering OnSignInCallback ({_onSignInCallbacks.Count})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to register a callback for when the user signs out
|
||||
/// </summary>
|
||||
/// <param name="callback">callback function</param>
|
||||
public void RegisterOnSignOutCallback(Action callback)
|
||||
{
|
||||
_onSignOutCallbacks.Add(callback);
|
||||
Debug.Log($"registering OnSignOutCallback ({_onSignOutCallbacks.Count})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to register a callback for when the connection status changes
|
||||
/// </summary>
|
||||
/// <param name="callback">callback function that takes in a <c>FirebaseConnectionStatus</c> enum</param>
|
||||
public void RegisterOnConnectionStatusChangedCallback(Action<FirebaseConnectionStatus> callback)
|
||||
{
|
||||
_onConnectionStatusChangedCallbacks.Add(callback);
|
||||
Debug.Log($"registering ConnectionStatusChangedCallback ({_onConnectionStatusChangedCallbacks.Count})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to fire all on sign in callbacks
|
||||
/// </summary>
|
||||
private void FireOnSignInCallbacks()
|
||||
{
|
||||
Debug.Log($"firing OnSignInCallbacks ({_onSignInCallbacks.Count})");
|
||||
foreach (var callback in _onSignInCallbacks)
|
||||
try
|
||||
{
|
||||
callback.Invoke(_user);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"error invoking OnSignInCallback: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to fire all on sign-out callbacks
|
||||
/// </summary>
|
||||
private void FireOnSignOutCallbacks()
|
||||
{
|
||||
Debug.Log($"firing OnSignOutCallbacks ({_onSignOutCallbacks.Count})");
|
||||
foreach (var callback in _onSignOutCallbacks)
|
||||
try
|
||||
{
|
||||
callback.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"error invoking OnSignOutCallback: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to fire all on connection status changed callbacks
|
||||
/// </summary>
|
||||
private void FireOnConnectionStatusChangedCallbacks()
|
||||
{
|
||||
Debug.Log($"firing OnConnectionStatusChangedCallbacks ({_onConnectionStatusChangedCallbacks.Count})");
|
||||
foreach (var callback in _onConnectionStatusChangedCallbacks)
|
||||
try
|
||||
{
|
||||
callback.Invoke(Status);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"error invoking OnConnectionStatusChangedCallback: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to handle the authentication state change event
|
||||
/// </summary>
|
||||
/// <param name="sender">the object that triggered the event</param>
|
||||
/// <param name="eventArgs">the event arguments</param>
|
||||
private void AuthStateChanged(object sender, EventArgs eventArgs)
|
||||
{
|
||||
// if the user hasn't changed, do nothing
|
||||
if (_auth.CurrentUser == _user) return;
|
||||
|
||||
// if the user has changed, check if they've signed in or out
|
||||
IsSignedIn = _user != _auth.CurrentUser && _auth.CurrentUser != null;
|
||||
|
||||
// if we're not signed in, but we still hold _user locally, we've signed out
|
||||
if (!IsSignedIn && _user != null)
|
||||
{
|
||||
Debug.Log("moi-moi");
|
||||
FireOnSignOutCallbacks();
|
||||
}
|
||||
|
||||
// they have signed in, update _user
|
||||
_user = _auth.CurrentUser;
|
||||
if (!IsSignedIn) return;
|
||||
|
||||
Debug.Log($"signed in successfully as {_user?.UserId}");
|
||||
RetrieveUsernameWithCallback((_, _) => { FireOnSignInCallbacks(); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to authenticate the user
|
||||
/// </summary>
|
||||
/// <param name="email">email string</param>
|
||||
/// <param name="password">user raw password string</param>
|
||||
/// <param name="callback">callback function that takes in an <c>AuthenticationResult</c> enum</param>
|
||||
/// <param name="registerUser">whether to treat authentication as registration</param>
|
||||
/// <param name="registeringUsername">username string if registering</param>
|
||||
public void AuthenticateUser(
|
||||
string email,
|
||||
string password,
|
||||
Action<AuthenticationResult> callback,
|
||||
bool registerUser = false,
|
||||
string registeringUsername = "")
|
||||
{
|
||||
if (GameManager.Instance.Backend.GetUser() != null)
|
||||
{
|
||||
callback(AuthenticationResult.AlreadyAuthenticated);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registerUser)
|
||||
{
|
||||
// check if the username is already taken
|
||||
_db.Child("users")
|
||||
.OrderByChild("username")
|
||||
.EqualTo(registeringUsername)
|
||||
.GetValueAsync()
|
||||
.ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.Exception != null)
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(AuthenticationResult.GenericError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task.IsCompletedSuccessfully || task.Result.ChildrenCount > 0)
|
||||
{
|
||||
callback(AuthenticationResult.UsernameAlreadyTaken);
|
||||
return;
|
||||
}
|
||||
|
||||
// register user
|
||||
_auth.CreateUserWithEmailAndPasswordAsync(email, password)
|
||||
.ContinueWithOnMainThread(createTask =>
|
||||
{
|
||||
if (createTask.IsCompletedSuccessfully)
|
||||
{
|
||||
// store username
|
||||
_db.Child("users")
|
||||
.Child(_user.UserId)
|
||||
.Child("username")
|
||||
.SetValueAsync(registeringUsername)
|
||||
.ContinueWithOnMainThread(setUsernameTask =>
|
||||
{
|
||||
if (setUsernameTask.IsCompletedSuccessfully)
|
||||
{
|
||||
_username = registeringUsername;
|
||||
callback(AuthenticationResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(setUsernameTask.Exception);
|
||||
callback(AuthenticationResult.GenericError);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (createTask.Exception?.InnerException == null)
|
||||
{
|
||||
callback(AuthenticationResult.GenericError);
|
||||
return;
|
||||
}
|
||||
|
||||
var error = (AuthError)((FirebaseException)createTask.Exception.InnerException).ErrorCode;
|
||||
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (error)
|
||||
{
|
||||
case AuthError.UserNotFound:
|
||||
callback(AuthenticationResult.NonExistentUser);
|
||||
return;
|
||||
|
||||
case AuthError.InvalidEmail:
|
||||
callback(AuthenticationResult.InvalidEmail);
|
||||
return;
|
||||
|
||||
case AuthError.WeakPassword:
|
||||
case AuthError.InvalidCredential:
|
||||
callback(AuthenticationResult.InvalidCredentials);
|
||||
return;
|
||||
|
||||
case AuthError.AccountExistsWithDifferentCredentials:
|
||||
case AuthError.EmailAlreadyInUse:
|
||||
callback(AuthenticationResult.AlreadyExistingUser);
|
||||
return;
|
||||
|
||||
case AuthError.Failure:
|
||||
default:
|
||||
Debug.LogError(error);
|
||||
Debug.LogError(createTask.Exception);
|
||||
callback(AuthenticationResult.GenericError);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// logging in
|
||||
_auth.SignInWithEmailAndPasswordAsync(email, password)
|
||||
.ContinueWithOnMainThread(signInTask =>
|
||||
{
|
||||
if (signInTask.IsCompletedSuccessfully)
|
||||
{
|
||||
RetrieveUsername();
|
||||
callback(AuthenticationResult.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signInTask.Exception?.InnerException == null)
|
||||
{
|
||||
callback(AuthenticationResult.GenericError);
|
||||
return;
|
||||
}
|
||||
|
||||
var error = (AuthError)((FirebaseException)signInTask.Exception.InnerException).ErrorCode;
|
||||
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (error)
|
||||
{
|
||||
case AuthError.UserNotFound:
|
||||
callback(AuthenticationResult.NonExistentUser);
|
||||
return;
|
||||
case AuthError.InvalidEmail:
|
||||
callback(AuthenticationResult.InvalidEmail);
|
||||
return;
|
||||
case AuthError.WeakPassword:
|
||||
case AuthError.InvalidCredential:
|
||||
callback(AuthenticationResult.InvalidCredentials);
|
||||
return;
|
||||
case AuthError.AccountExistsWithDifferentCredentials:
|
||||
case AuthError.EmailAlreadyInUse:
|
||||
callback(AuthenticationResult.AlreadyExistingUser);
|
||||
return;
|
||||
case AuthError.Failure:
|
||||
default:
|
||||
Debug.LogError(error);
|
||||
Debug.LogError(signInTask.Exception);
|
||||
callback(AuthenticationResult.GenericError);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// helper function to run RetrieveUsername with no callback
|
||||
/// </summary>
|
||||
private void RetrieveUsername()
|
||||
{
|
||||
RetrieveUsernameWithCallback((_, _) => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to retrieve the user's username from the database
|
||||
/// </summary>
|
||||
private void RetrieveUsernameWithCallback(Action<TransactionResult, string> callback)
|
||||
{
|
||||
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
|
||||
|
||||
if (_user == null)
|
||||
{
|
||||
Debug.LogError("receiving username post-authentication but user is null (should be unreachable)");
|
||||
callback(TransactionResult.Unauthenticated, "Unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
_db.Child("users")
|
||||
.Child(_user.UserId)
|
||||
.Child("username")
|
||||
.GetValueAsync()
|
||||
.ContinueWithOnMainThread(task =>
|
||||
{
|
||||
TransactionResult result;
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
result = TransactionResult.Ok;
|
||||
_username = task.Result.Value.ToString();
|
||||
Debug.Log($"our username is {_username}");
|
||||
}
|
||||
else
|
||||
{
|
||||
result = TransactionResult.Error;
|
||||
_username = "Unknown";
|
||||
Debug.LogError("failed to get username");
|
||||
}
|
||||
|
||||
callback(result, _username);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to retrieve the user
|
||||
/// </summary>
|
||||
/// <returns>the firebase user object</returns>
|
||||
public FirebaseUser GetUser()
|
||||
{
|
||||
return _user;
|
||||
}
|
||||
|
||||
public string GetUsername()
|
||||
{
|
||||
return _username;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to sign out the user
|
||||
/// </summary>
|
||||
public void SignOutUser()
|
||||
{
|
||||
_auth.SignOut();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to delete the user
|
||||
/// </summary>
|
||||
public void DeleteUser(Action<TransactionResult> callback)
|
||||
{
|
||||
if (!Status.Equals(FirebaseConnectionStatus.Connected))
|
||||
{
|
||||
callback(TransactionResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_user == null)
|
||||
{
|
||||
callback(TransactionResult.Unauthenticated);
|
||||
return;
|
||||
}
|
||||
|
||||
_user.DeleteAsync().ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
Debug.Log("user deleted");
|
||||
_user = null;
|
||||
FireOnSignOutCallbacks();
|
||||
callback(TransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"error deleting user: {task.Exception}");
|
||||
callback(TransactionResult.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function for the user to reset their password
|
||||
/// </summary>
|
||||
/// <param name="email">the forgetful user's email lol</param>
|
||||
/// <param name="callback">callback function to be invoked after the password reset email is sent</param>
|
||||
public void ResetUserPassword(string email, Action<bool> callback)
|
||||
{
|
||||
_auth.SendPasswordResetEmailAsync(email)
|
||||
.ContinueWithOnMainThread(resetTask =>
|
||||
{
|
||||
if (resetTask.IsCompletedSuccessfully)
|
||||
{
|
||||
callback(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(resetTask.Exception);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to get the leaderboard from the database
|
||||
/// </summary>
|
||||
/// <param name="callback">
|
||||
/// callback function that takes in a <c>TransactionResult</c> enum and a <c>List<LeaderboardEntry></c>
|
||||
/// </param>
|
||||
public void GetLeaderboard(
|
||||
Action<TransactionResult, List<LeaderboardEntry>> callback)
|
||||
{
|
||||
Debug.Log("getting leaderboard");
|
||||
|
||||
_db.Child("users")
|
||||
.OrderByChild("rating")
|
||||
.LimitToLast(Leaderboard.MaxEntries)
|
||||
.GetValueAsync()
|
||||
.ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (!task.IsCompletedSuccessfully)
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(TransactionResult.Error, new List<LeaderboardEntry>(0));
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = new List<LeaderboardEntry>();
|
||||
foreach (var child in task.Result.Children)
|
||||
try
|
||||
{
|
||||
var entry = new LeaderboardEntry(child.Value as Dictionary<string, object>);
|
||||
entries.Add(entry);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError(e);
|
||||
}
|
||||
|
||||
callback(TransactionResult.Ok, entries);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// abstraction function to update the user's account details in the database
|
||||
/// </summary>
|
||||
/// <param name="target">the target account detail to update</param>
|
||||
/// <param name="newValue">the new value for the target account detail</param>
|
||||
/// <param name="callback">callback function that takes in a <c>TransactionResult</c> enum</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">thrown when the target is not a valid UserAccountDetailTargetEnum</exception>
|
||||
public void UpdateUserAccountDetail(
|
||||
UserAccountDetailTargetEnum target,
|
||||
string newValue,
|
||||
Action<TransactionResult> callback)
|
||||
{
|
||||
if (!Status.Equals(FirebaseConnectionStatus.Connected)) callback(TransactionResult.Unauthenticated);
|
||||
|
||||
if (_user == null)
|
||||
{
|
||||
callback(TransactionResult.Unauthenticated);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target)
|
||||
{
|
||||
case UserAccountDetailTargetEnum.Email:
|
||||
_user.SendEmailVerificationBeforeUpdatingEmailAsync(newValue).ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
GameManager.Instance.Data.LastKnownEmail = newValue;
|
||||
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
|
||||
callback(TransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(TransactionResult.Error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case UserAccountDetailTargetEnum.Username:
|
||||
_db.Child("users")
|
||||
.Child(_user.UserId)
|
||||
.Child("username")
|
||||
.SetValueAsync(newValue)
|
||||
.ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
_username = newValue;
|
||||
GameManager.Instance.Data.LastKnownUsername = newValue;
|
||||
GameManager.Instance.FireLocalPlayerDataChangeCallbacks(GameManager.Instance.Data);
|
||||
callback(TransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(TransactionResult.Error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case UserAccountDetailTargetEnum.Password:
|
||||
_user.UpdatePasswordAsync(newValue).ContinueWithOnMainThread(task =>
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
callback(TransactionResult.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError(task.Exception);
|
||||
callback(TransactionResult.Error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(target), target, null);
|
||||
}
|
||||
}
|
||||
}
|
11
Game/Assets/Scripts/Backend.cs.meta
Normal file
11
Game/Assets/Scripts/Backend.cs.meta
Normal file
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d4abe93509ef91f47a2e59dd474a7137
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
121
Game/Assets/Scripts/GameManager.cs
Normal file
121
Game/Assets/Scripts/GameManager.cs
Normal file
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// singleton for a single source of truth game state and flow management
|
||||
/// </summary>
|
||||
public class GameManager : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// singleton pattern: define instance field for accessing the singleton elsewhere
|
||||
/// </summary>
|
||||
public static GameManager Instance;
|
||||
|
||||
/// <summary>
|
||||
/// list of callbacks to call when the local player data changes
|
||||
/// </summary>
|
||||
private readonly List<Action<LocalPlayerData>> _onLocalPlayerDataChangeCallbacks = new();
|
||||
|
||||
/// <summary>
|
||||
/// the local player data object for storing player data
|
||||
/// </summary>
|
||||
private LocalPlayerData _data;
|
||||
|
||||
/// <summary>
|
||||
/// backend object for handling communication with the firebase backend
|
||||
/// </summary>
|
||||
public Backend Backend;
|
||||
|
||||
/// <summary>
|
||||
/// read-only property for accessing the local player data outside this class
|
||||
/// </summary>
|
||||
public LocalPlayerData Data => _data;
|
||||
|
||||
/// <summary>
|
||||
/// enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
// check if instance hasn't been set yet
|
||||
if (Instance == null)
|
||||
{
|
||||
Debug.Log("awake as singleton instance, setting self as the forever-alive instance");
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
// check if instance is already set and it's not this instance
|
||||
else if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.Log("awake as non-singleton instance, destroying self");
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// start modifying state
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
Debug.Log("GameManager starts here");
|
||||
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// initialise variables and ui elements
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
// load the local player data and refresh the ui
|
||||
_data = new LocalPlayerData();
|
||||
|
||||
Backend = new Backend();
|
||||
Backend.Initialise(status => { Debug.Log("initialised backend"); });
|
||||
|
||||
// register a callback to refresh the ui when the player signs in
|
||||
Backend.RegisterOnSignInCallback(_ =>
|
||||
{
|
||||
Debug.Log("sign in callback, refreshing GameManager-controlled SideView UI");
|
||||
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
|
||||
});
|
||||
|
||||
Backend.RegisterOnConnectionStatusChangedCallback(status => { Debug.Log($"post-fcStatus change: {status}"); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// called when the application is quitting, saves the local player data
|
||||
/// </summary>
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
Debug.Log("running deferred cleanup/save functions");
|
||||
Backend.Cleanup();
|
||||
_data.SaveToTheWorld();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to register a callback to be called when the local player data changes
|
||||
/// </summary>
|
||||
/// <param name="callback">callback function that takes a <c>LocalPlayerData</c> object</param>
|
||||
public void RegisterOnLocalPlayerDataChangeCallback(Action<LocalPlayerData> callback)
|
||||
{
|
||||
_onLocalPlayerDataChangeCallbacks.Add(callback);
|
||||
Debug.Log($"registering LocalPlayerDataChangeCallback ({_onLocalPlayerDataChangeCallbacks.Count})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// function to fire all local player data change callbacks
|
||||
/// </summary>
|
||||
public void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data)
|
||||
{
|
||||
Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})");
|
||||
foreach (var callback in _onLocalPlayerDataChangeCallbacks)
|
||||
try
|
||||
{
|
||||
callback.Invoke(data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"error invoking LocalPlayerDataChangeCallback: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
3
Game/Assets/Scripts/GameManager.cs.meta
Normal file
3
Game/Assets/Scripts/GameManager.cs.meta
Normal file
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: deaef88acf07406db5e0c354472ae40c
|
||||
timeCreated: 1732641033
|
32
Game/Assets/Scripts/Leaderboard.cs
Normal file
32
Game/Assets/Scripts/Leaderboard.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// class that loads leaderboard data and displays it in the UI
|
||||
/// </summary>
|
||||
public class Leaderboard : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// maximum number of entries to display in the leaderboard
|
||||
/// </summary>
|
||||
public const int MaxEntries = 50;
|
||||
|
||||
// /// <summary>
|
||||
// /// register callbacks
|
||||
// /// </summary>
|
||||
// private void OnEnable() {}
|
||||
}
|
||||
|
||||
public readonly struct LeaderboardEntry
|
||||
{
|
||||
public readonly string Username;
|
||||
|
||||
public LeaderboardEntry(Dictionary<string, object> data)
|
||||
{
|
||||
if (!data.ContainsKey("username") || data["username"] is not string username)
|
||||
throw new ArgumentException("data['username'] not found or invalid");
|
||||
|
||||
Username = username;
|
||||
}
|
||||
}
|
3
Game/Assets/Scripts/Leaderboard.cs.meta
Normal file
3
Game/Assets/Scripts/Leaderboard.cs.meta
Normal file
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 827c6e446cfa4da886052fe3934bdc1f
|
||||
timeCreated: 1732641719
|
123
Game/Assets/Scripts/LocalPlayerData.cs
Normal file
123
Game/Assets/Scripts/LocalPlayerData.cs
Normal file
|
@ -0,0 +1,123 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// local player data structure/class
|
||||
/// </summary>
|
||||
public class LocalPlayerData
|
||||
{
|
||||
/// <summary>
|
||||
/// last known email used
|
||||
/// </summary>
|
||||
public string LastKnownEmail = "";
|
||||
|
||||
/// <summary>
|
||||
/// last known username used
|
||||
/// </summary>
|
||||
public string LastKnownUsername = "Guest";
|
||||
|
||||
/// <summary>
|
||||
/// loads player data from player prefs and database
|
||||
/// </summary>
|
||||
public void LoadFromTheWorld(Action<LocalPlayerData> callback)
|
||||
{
|
||||
// load user data, possibly from the backend
|
||||
var possibleUser = GameManager.Instance.Backend.GetUser();
|
||||
var currentKnownEmail = string.Empty;
|
||||
var currentKnownUsername = string.Empty;
|
||||
if (possibleUser != null)
|
||||
{
|
||||
currentKnownEmail = possibleUser.Email;
|
||||
currentKnownUsername = GameManager.Instance.Backend.GetUsername();
|
||||
}
|
||||
|
||||
var lastStoredEmail = PlayerPrefs.GetString("LastKnownEmail", "");
|
||||
var lastStoredUsername = PlayerPrefs.GetString("LastKnownUsername", "Guest");
|
||||
LastKnownEmail = string.IsNullOrEmpty(currentKnownEmail) ? lastStoredEmail : currentKnownEmail;
|
||||
LastKnownUsername = string.IsNullOrEmpty(currentKnownUsername) ? lastStoredUsername : currentKnownUsername;
|
||||
|
||||
Debug.Log($"loaded lpdata from the local world ({LastKnownUsername} <{LastKnownEmail}>");
|
||||
callback(this);
|
||||
|
||||
// TODO: potentially load data from the backend
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// saves player data to player prefs
|
||||
/// </summary>
|
||||
public void SaveToTheWorld()
|
||||
{
|
||||
PlayerPrefs.SetString("LastKnownEmail", LastKnownEmail);
|
||||
PlayerPrefs.SetString("LastKnownUsername", LastKnownUsername);
|
||||
// online scores should have already saved in the backend
|
||||
Debug.Log("saved lpdata to player prefs");
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// safely get a float value from a dictionary
|
||||
// /// </summary>
|
||||
// /// <param name="data">the dictionary to get the value from</param>
|
||||
// /// <param name="key">the key to get the value from</param>
|
||||
// /// <returns>the float value</returns>
|
||||
// /// <exception cref="ArgumentException">thrown if the key is not found, or the value is not a valid float</exception>
|
||||
// public static float GetFloatyKey(Dictionary<string, object> data, string key)
|
||||
// {
|
||||
// if (!data.TryGetValue(key, out var possibleFloat)) throw new ArgumentException($"{key} not found");
|
||||
// return possibleFloat switch
|
||||
// {
|
||||
// double f => (float)f,
|
||||
// long l => l,
|
||||
// _ => throw new ArgumentException($"data['{key}'] not a valid float")
|
||||
// };
|
||||
// }
|
||||
|
||||
public struct Score
|
||||
{
|
||||
/// <summary>
|
||||
/// timestamp of the score
|
||||
/// </summary>
|
||||
public DateTime Timestamp;
|
||||
|
||||
/// <summary>
|
||||
/// constructor for the score struct
|
||||
/// </summary>
|
||||
/// <param name="timestamp">timestamp of the score</param>
|
||||
public Score(DateTime timestamp = new())
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// dict-based constructor for the score struct
|
||||
/// </summary>
|
||||
/// <param name="data">dictionary of the score data</param>
|
||||
/// <exception cref="ArgumentException">thrown if the dictionary is malformed or missing data</exception>
|
||||
public Score(Dictionary<string, object> data)
|
||||
{
|
||||
// try to safely construct the score from a backend-provided dictionary
|
||||
// for each value, if it's not found, or not a valid value, throw an exception
|
||||
|
||||
if (!data.ContainsKey("timestamp") || data["timestamp"] is not long timestamp)
|
||||
throw new ArgumentException("data['timestamp'] not found or invalid");
|
||||
|
||||
Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// converts the score struct to a dictionary safe for the backend
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Dictionary<string, object> ToDictionary(string userId = "")
|
||||
{
|
||||
var dict = new Dictionary<string, object>
|
||||
{
|
||||
{ "timestamp", new DateTimeOffset(Timestamp).ToUnixTimeSeconds() }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(userId)) dict.Add("userId", userId);
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
3
Game/Assets/Scripts/LocalPlayerData.cs.meta
Normal file
3
Game/Assets/Scripts/LocalPlayerData.cs.meta
Normal file
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 51a8ca9c59474177bbfaab42d698cd3c
|
||||
timeCreated: 1732641111
|
Loading…
Reference in a new issue