game(scripts): add backend and adjacent scripts

This commit is contained in:
Mark Joshwel 2024-11-27 01:31:47 +08:00
parent 9149887e4e
commit 9cc96fdcc6
9 changed files with 994 additions and 0 deletions

8
Game/Assets/Scripts.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b2fa2ae79460cd147a2f52ae9879eb9f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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&lt;LeaderboardEntry&gt;</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);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4abe93509ef91f47a2e59dd474a7137
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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}");
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: deaef88acf07406db5e0c354472ae40c
timeCreated: 1732641033

View 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;
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 827c6e446cfa4da886052fe3934bdc1f
timeCreated: 1732641719

View 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;
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 51a8ca9c59474177bbfaab42d698cd3c
timeCreated: 1732641111