This repository has been archived on 2024-11-20. You can view files and clone it, but cannot push or open issues or pull requests.
colourmeok/ColourMeOKGame/Assets/Scripts/Backend.cs

621 lines
21 KiB
C#
Raw Normal View History

2024-11-15 04:15:45 +08:00
using System;
2024-11-17 07:29:22 +08:00
using System.Collections.Generic;
2024-11-15 04:15:45 +08:00
using System.Threading.Tasks;
using Firebase;
using Firebase.Auth;
using Firebase.Database;
2024-11-17 07:29:22 +08:00
using Firebase.Extensions;
2024-11-15 04:15:45 +08:00
using UnityEngine;
/// <summary>
/// the general managing class for handling communication with the firebase backend
/// (to be initialised by GameManager)
/// </summary>
2024-11-17 07:29:22 +08:00
public class Backend
2024-11-15 04:15:45 +08:00
{
/// <summary>
/// enum for the result of the authentication process
/// </summary>
public enum AuthenticationResult
{
Ok,
AlreadyAuthenticated,
NonExistentUser,
AlreadyExistingUser,
2024-11-17 07:29:22 +08:00
InvalidEmail,
InvalidCredentials,
GenericError
}
/// <summary>
/// generic enum for the result of a database transaction
/// </summary>
public enum DatabaseTransactionResult
{
Ok,
Unauthenticated,
Error
}
/// <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"
}
2024-11-17 19:10:01 +08:00
public enum UserAccountDetailTargetEnum
{
Username,
Email,
Password
}
2024-11-15 04:15:45 +08:00
/// <summary>
/// callback functions to be invoked when the user signs in
2024-11-15 04:15:45 +08:00
/// </summary>
private readonly List<Action<FirebaseUser>> _onSignInCallback = new();
2024-11-17 07:29:22 +08:00
/// <summary>
/// callback functions to be invoked when the user signs out
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 23:17:24 +08:00
private readonly List<Action> _onSignOutCallback = new();
2024-11-15 04:15:45 +08:00
/// <summary>
/// the firebase authentication object
2024-11-15 04:15:45 +08:00
/// </summary>
private FirebaseAuth _auth;
2024-11-15 04:15:45 +08:00
/// <summary>
/// the firebase database reference
2024-11-15 04:15:45 +08:00
/// </summary>
private DatabaseReference _db;
2024-11-15 04:15:45 +08:00
/// <summary>
/// the current user object, if authenticated
/// </summary>
private FirebaseUser _user;
2024-11-17 23:17:24 +08:00
/// <summary>
/// whether the user is signed in
/// </summary>
public bool IsSignedIn;
2024-11-15 04:15:45 +08:00
/// <summary>
2024-11-17 07:29:22 +08:00
/// the current user's username, if authenticated
2024-11-15 04:15:45 +08:00
/// </summary>
2024-11-17 07:29:22 +08:00
private string _username;
2024-11-15 04:15:45 +08:00
/// <summary>
2024-11-17 07:29:22 +08:00
/// whether the backend is connected to the firebase backend
2024-11-15 04:15:45 +08:00
/// </summary>
2024-11-17 07:29:22 +08:00
public FirebaseConnectionStatus Status = FirebaseConnectionStatus.NotConnected;
2024-11-15 04:15:45 +08:00
/// <summary>
/// variable initialisation function
/// </summary>
public void Initialise()
2024-11-15 04:15:45 +08:00
{
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
2024-11-15 04:15:45 +08:00
{
// cher is this robust enough
2024-11-15 04:15:45 +08:00
switch (task.Result)
{
case DependencyStatus.Available:
2024-11-15 07:54:43 +08:00
_auth = FirebaseAuth.GetAuth(FirebaseApp.DefaultInstance);
2024-11-15 04:15:45 +08:00
_auth.StateChanged += AuthStateChanged;
_db = FirebaseDatabase.DefaultInstance.RootReference;
2024-11-17 07:29:22 +08:00
Status = FirebaseConnectionStatus.Connected;
2024-11-15 04:15:45 +08:00
break;
case DependencyStatus.UnavailableDisabled:
case DependencyStatus.UnavailableInvalid:
case DependencyStatus.UnavilableMissing:
case DependencyStatus.UnavailablePermission:
2024-11-17 07:29:22 +08:00
Status = FirebaseConnectionStatus.ExternalError;
2024-11-15 04:15:45 +08:00
break;
case DependencyStatus.UnavailableUpdating:
2024-11-17 07:29:22 +08:00
Status = FirebaseConnectionStatus.Updating;
RetryInitialiseAfterDelay();
2024-11-15 04:15:45 +08:00
break;
case DependencyStatus.UnavailableUpdaterequired:
2024-11-17 07:29:22 +08:00
Status = FirebaseConnectionStatus.UpdateRequired;
break;
2024-11-15 04:15:45 +08:00
case DependencyStatus.UnavailableOther:
default:
2024-11-17 07:29:22 +08:00
Status = FirebaseConnectionStatus.InternalError;
2024-11-15 04:15:45 +08:00
Debug.LogError("firebase ??? blew up or something," + task.Result);
break;
}
2024-11-17 07:29:22 +08:00
Debug.Log("firebase status is" + Status);
2024-11-15 04:15:45 +08:00
});
}
2024-11-17 23:17:24 +08:00
/// <summary>
/// function to fire all on sign in callbacks
/// </summary>
private void FireOnSignInCallbacks()
{
Debug.Log($"firing on sign in callbacks ({_onSignInCallback.Count})");
foreach (var callback in _onSignInCallback)
{
try
{
callback.Invoke(_user);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
/// <summary>
/// function to fire all on sign out callbacks
/// </summary>
private void FireOnSignOutCallbacks()
{
Debug.Log($"firing on sign out callbacks ({_onSignOutCallback.Count})");
foreach (var callback in _onSignOutCallback)
{
try
{
callback.Invoke();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
2024-11-15 04:15:45 +08:00
2024-11-17 07:29:22 +08:00
/// <summary>
/// async function to retry initialisation after a delay
/// </summary>
private async void RetryInitialiseAfterDelay()
2024-11-17 07:29:22 +08:00
{
await Task.Delay(TimeSpan.FromSeconds(10));
Initialise();
2024-11-17 07:29:22 +08:00
}
/// <summary>
/// cleanup function
/// </summary>
public void Cleanup()
{
SignOutUser();
_auth.StateChanged -= AuthStateChanged;
_auth = null;
}
2024-11-15 04:15:45 +08:00
/// <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
2024-11-15 04:15:45 +08:00
if (_auth.CurrentUser == _user) return;
// if the user has changed, check if they've signed in or out
2024-11-17 23:17:24 +08:00
IsSignedIn = _user != _auth.CurrentUser && _auth.CurrentUser != null;
// if we're not signed in, but we still hold _user locally, we've signed out
2024-11-17 23:17:24 +08:00
if (!IsSignedIn && _user != null)
2024-11-17 07:29:22 +08:00
{
2024-11-17 23:17:24 +08:00
Debug.Log("moi-moi");
FireOnSignOutCallbacks();
2024-11-17 07:29:22 +08:00
}
// they have signed in, update _user
_user = _auth.CurrentUser;
2024-11-17 23:17:24 +08:00
if (!IsSignedIn) return;
2024-11-17 19:10:01 +08:00
2024-11-17 23:17:24 +08:00
Debug.Log($"signed in successfully as {_user?.UserId}");
2024-11-17 19:10:01 +08:00
RetrieveUsernameWithCallback((_, _) =>
2024-11-17 07:29:22 +08:00
{
2024-11-17 23:17:24 +08:00
FireOnSignInCallbacks();
2024-11-17 19:10:01 +08:00
});
2024-11-15 04:15:45 +08:00
}
/// <summary>
/// function to register a callback for when the user signs in
/// </summary>
2024-11-17 23:17:24 +08:00
/// <param name="callback">callback function that takes in a <c>FirebaseUser</c> object</param>
2024-11-17 07:29:22 +08:00
public void RegisterOnSignInCallback(Action<FirebaseUser> callback)
2024-11-15 04:15:45 +08:00
{
2024-11-17 23:17:24 +08:00
Debug.Log("registering on sign in callback");
_onSignInCallback.Add(callback);
2024-11-15 04:15:45 +08:00
}
2024-11-17 23:17:24 +08:00
// /// <summary>
// /// function to register a callback for when the user signs out
// /// </summary>
// /// <param name="callback">callback function</param>
// public void RegisterOnSignOutCallback(Action callback)
// {
// _onSignOutCallback.Add(callback);
// }
2024-11-15 04:15:45 +08:00
/// <summary>
/// abstraction function to authenticate the user
/// </summary>
2024-11-17 07:29:22 +08:00
/// <param name="email">email string</param>
2024-11-15 04:15:45 +08:00
/// <param name="password">user raw password string</param>
/// <param name="callback">callback function that takes in an AuthenticationResult argument</param>
/// <param name="registerUser">whether to treat authentication as registration</param>
2024-11-17 07:29:22 +08:00
/// <param name="registeringUsername">username string if registering</param>
public void AuthenticateUser(
string email,
2024-11-15 04:15:45 +08:00
string password,
Action<AuthenticationResult> callback,
2024-11-17 07:29:22 +08:00
bool registerUser = false,
string registeringUsername = "")
2024-11-15 04:15:45 +08:00
{
2024-11-17 07:29:22 +08:00
if (GameManager.Instance.Backend.GetUser() != null)
{
callback(AuthenticationResult.AlreadyAuthenticated);
return;
}
if (registerUser)
{
// 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;
}
_auth.SignInWithEmailAndPasswordAsync(email, password)
.ContinueWithOnMainThread(signInTask =>
{
if (signInTask.IsCompletedSuccessfully)
{
2024-11-17 19:10:01 +08:00
RetrieveUsername();
2024-11-17 07:29:22 +08:00
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>
2024-11-17 19:10:01 +08:00
/// helper function to run RetrieveUsername with no callback
2024-11-17 07:29:22 +08:00
/// </summary>
private void RetrieveUsername()
2024-11-17 19:10:01 +08:00
{
2024-11-17 23:17:24 +08:00
RetrieveUsernameWithCallback((_, _) => { });
2024-11-17 19:10:01 +08:00
}
/// <summary>
/// function to retrieve the user's username from the database
/// </summary>
private void RetrieveUsernameWithCallback(Action<DatabaseTransactionResult, string> callback)
2024-11-17 07:29:22 +08:00
{
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
2024-11-17 19:10:01 +08:00
if (_user == null)
{
Debug.LogError("receiving username post-authentication but user is null (should be unreachable)");
callback(DatabaseTransactionResult.Unauthenticated, "Unknown");
return;
}
_db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWithOnMainThread(task =>
2024-11-17 07:29:22 +08:00
{
DatabaseTransactionResult result;
2024-11-17 07:29:22 +08:00
if (task.IsCompletedSuccessfully)
{
result = DatabaseTransactionResult.Ok;
2024-11-17 07:29:22 +08:00
_username = task.Result.Value.ToString();
Debug.Log($"our username is {_username}");
}
else
{
result = DatabaseTransactionResult.Error;
2024-11-17 19:10:01 +08:00
_username = "Unknown";
2024-11-17 07:29:22 +08:00
Debug.LogError("failed to get username");
}
callback(result, _username);
2024-11-17 07:29:22 +08:00
});
2024-11-15 04:15:45 +08:00
}
/// <summary>
/// abstraction function to retrieve the user
/// </summary>
/// <returns>the firebase user object</returns>
2024-11-17 07:29:22 +08:00
public FirebaseUser GetUser()
2024-11-15 04:15:45 +08:00
{
return _user;
}
2024-11-17 07:29:22 +08:00
public string GetUsername()
{
return _username;
}
2024-11-15 04:15:45 +08:00
/// <summary>
/// abstraction function to sign out the user
/// </summary>
2024-11-17 07:29:22 +08:00
public void SignOutUser()
2024-11-15 04:15:45 +08:00
{
_auth.SignOut();
}
/// <summary>
2024-11-17 19:10:01 +08:00
/// 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 ForgotPassword(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 user's recent scores from the database
2024-11-15 04:15:45 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
/// <param name="callback">
/// callback function that takes in a DatabaseTransactionResult and List of LocalPlayerData.Score
/// argument
2024-11-17 19:10:01 +08:00
/// </param>
public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
{
// TODO
callback(DatabaseTransactionResult.Error, new List<LocalPlayerData.Score>(0));
}
/// <summary>
/// abstraction function to submit a score to the database
/// </summary>
/// <param name="score">score</param>
2024-11-15 04:15:45 +08:00
/// <param name="callback">callback function that takes in one DatabaseTransactionResult argument</param>
2024-11-17 19:10:01 +08:00
public void SubmitScore(
LocalPlayerData.Score score,
2024-11-15 04:15:45 +08:00
Action<DatabaseTransactionResult> callback)
{
throw new NotImplementedException();
}
/// <summary>
/// abstraction function to get and calculate the user's rating from the database
/// calculation is done locally, call UpdateUserRating to update the user's rating in the database
/// </summary>
/// <param name="callback">callback function that takes in a DatabaseTransactionResult and float (user rating) argument</param>
2024-11-17 07:29:22 +08:00
public void CalculateUserRating(
2024-11-15 04:15:45 +08:00
Action<DatabaseTransactionResult, float> callback)
{
throw new NotImplementedException();
}
/// <summary>
/// abstraction function to update the user's rating in the database
/// </summary>
/// <param name="newRating">new user rating value as a float</param>
/// <param name="callback">callback function that takes in one DatabaseTransactionResult argument</param>
2024-11-17 07:29:22 +08:00
public void UpdateUserRating(
2024-11-15 04:15:45 +08:00
float newRating,
Action<DatabaseTransactionResult> callback)
{
throw new NotImplementedException();
}
/// <summary>
/// abstraction function to get the leaderboard from the database
/// </summary>
2024-11-17 07:29:22 +08:00
/// <param name="callback">
/// callback function that takes in a DatabaseTransactionResult and LeaderboardEntry[] argument
/// </param>
public void GetLeaderboard(
2024-11-15 04:15:45 +08:00
Action<DatabaseTransactionResult, LeaderboardEntry[]> callback)
{
throw new NotImplementedException();
}
2024-11-17 19:10:01 +08:00
/// <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 one DatabaseTransactionResult argument</param>
/// <exception cref="ArgumentOutOfRangeException">thrown when the target is not a valid UserAccountDetailTargetEnum</exception>
public void UpdateUserAccountDetail(
UserAccountDetailTargetEnum target,
string newValue,
Action<DatabaseTransactionResult> callback)
{
if (!Status.Equals(FirebaseConnectionStatus.Connected)) callback(DatabaseTransactionResult.Unauthenticated);
if (_user == null)
{
callback(DatabaseTransactionResult.Unauthenticated);
return;
}
switch (target)
{
case UserAccountDetailTargetEnum.Email:
_user.SendEmailVerificationBeforeUpdatingEmailAsync(newValue).ContinueWithOnMainThread(task =>
{
if (task.IsCompletedSuccessfully)
{
callback(DatabaseTransactionResult.Ok);
}
else
{
Debug.LogError(task.Exception);
callback(DatabaseTransactionResult.Error);
}
});
break;
case UserAccountDetailTargetEnum.Username:
_db.Child("users").Child(_user.UserId).Child("username").SetValueAsync(newValue)
.ContinueWithOnMainThread(task =>
{
if (task.IsCompletedSuccessfully)
{
_username = newValue;
callback(DatabaseTransactionResult.Ok);
}
else
{
Debug.LogError(task.Exception);
callback(DatabaseTransactionResult.Error);
}
});
break;
case UserAccountDetailTargetEnum.Password:
_user.UpdatePasswordAsync(newValue).ContinueWithOnMainThread(task =>
{
if (task.IsCompletedSuccessfully)
{
callback(DatabaseTransactionResult.Ok);
}
else
{
Debug.LogError(task.Exception);
callback(DatabaseTransactionResult.Error);
}
});
break;
default:
throw new ArgumentOutOfRangeException(nameof(target), target, null);
}
}
2024-11-17 07:29:22 +08:00
2024-11-15 04:15:45 +08:00
/// <summary>
/// struct for a leaderboard entry
/// </summary>
public struct LeaderboardEntry
{
public string Username;
public float Rating;
public int PlayCount;
2024-11-17 07:29:22 +08:00
public LeaderboardEntry(string username, float rating, int playCount)
{
Username = username;
Rating = rating;
PlayCount = playCount;
}
2024-11-15 04:15:45 +08:00
}
}