game: fiddle around with execution order

generally found

- Awake -> init
- OnEnable -> init + register callbacks
- Start -> do anything that might call a callback
This commit is contained in:
Mark Joshwel 2024-11-18 00:16:53 +08:00
parent e8091632c4
commit 872f3f263e
9 changed files with 260 additions and 156 deletions

View file

@ -78,22 +78,11 @@ public class AccountUI : MonoBehaviour
/// </summary>
private Button _usernameUpdateButton;
private void Awake()
{
GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
}
public void Start()
{
if (state == State.UnassociatedState) throw new Exception("unreachable state");
// GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
}
/// <summary>
/// function to subscribe button events to their respective functions
/// function called when the object is enabled,
/// subscribes button events to their respective functions
/// </summary>
public void OnEnable()
private void OnEnable()
{
var ui = GetComponent<UIDocument>().rootVisualElement;
@ -121,17 +110,18 @@ public void OnEnable()
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
TransitionStateTo(State.NotSignedIn);
}
private void OnSignInCallback(FirebaseUser user)
if (state == State.UnassociatedState) throw new Exception("unreachable state");
GameManager.Instance.Backend.RegisterOnSignInCallback(_ =>
{
Debug.Log("sign in account ui callback");
Debug.Log("post-authentication callback, updating AccountView fields");
var username = GameManager.Instance.Backend.GetUsername();
_header.text = $"Signed in as {username}";
_passwordField.value = "";
_usernameField.value = username;
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
_passwordField.value = "";
_header.text = $"Signed in as {username}";
});
GameManager.Instance.RegisterOnLocalPlayerDataChangeCallback(PopulateFields);
}
private void TransitionStateTo(State newState, bool keepAccompanyingText = false)
@ -619,6 +609,18 @@ private void OnSecondaryActionButtonClick()
}
}
/// <summary>
/// populate the fields with the given username and email,
/// used as a callback to when local player data is changed
/// </summary>
public void PopulateFields(LocalPlayerData data)
{
Debug.Log(
$"populating AccountView fields with lkUsername={data.LastKnownUsername} and lkEmail={data.LastKnownEmail}");
_usernameField.value = data.LastKnownUsername;
_emailField.value = data.LastKnownEmail;
}
/// <summary>
/// state of the account view
/// </summary>

View file

@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
executionOrder: -10
icon: {instanceID: 0}
userData:
assetBundleName:

View file

@ -60,12 +60,18 @@ public enum UserAccountDetailTargetEnum
/// <summary>
/// callback functions to be invoked when the user signs in
/// </summary>
private readonly List<Action<FirebaseUser>> _onSignInCallback = new();
private readonly List<Action<FirebaseUser>> _onSignInCallbacks = new();
/// <summary>
/// callback functions to be invoked when the user signs out
/// </summary>
private readonly List<Action> _onSignOutCallback = new();
private readonly List<Action> _onSignOutCallbacks = new();
/// <summary>
/// callback functions to be invoked when the user signs in
/// </summary>
/// <returns></returns>
private readonly List<Action<FirebaseConnectionStatus>> _onConnectionStatusChangedCallbacks = new ();
/// <summary>
/// the firebase authentication object
@ -100,9 +106,9 @@ public enum UserAccountDetailTargetEnum
/// <summary>
/// variable initialisation function
/// </summary>
public void Initialise()
public void Initialise(Action<FirebaseConnectionStatus> callback)
{
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task =>
{
// cher is this robust enough
switch (task.Result)
@ -112,6 +118,7 @@ public void Initialise()
_auth.StateChanged += AuthStateChanged;
_db = FirebaseDatabase.DefaultInstance.RootReference;
Status = FirebaseConnectionStatus.Connected;
callback(Status);
break;
case DependencyStatus.UnavailableDisabled:
@ -119,21 +126,25 @@ public void Initialise()
case DependencyStatus.UnavilableMissing:
case DependencyStatus.UnavailablePermission:
Status = FirebaseConnectionStatus.ExternalError;
callback(Status);
break;
case DependencyStatus.UnavailableUpdating:
Status = FirebaseConnectionStatus.Updating;
RetryInitialiseAfterDelay();
callback(Status);
RetryInitialiseAfterDelay(callback);
break;
case DependencyStatus.UnavailableUpdaterequired:
Status = FirebaseConnectionStatus.UpdateRequired;
callback(Status);
break;
case DependencyStatus.UnavailableOther:
default:
Status = FirebaseConnectionStatus.InternalError;
Debug.LogError("firebase ??? blew up or something," + task.Result);
callback(Status);
break;
}
@ -146,8 +157,8 @@ public void Initialise()
/// </summary>
private void FireOnSignInCallbacks()
{
Debug.Log($"firing on sign in callbacks ({_onSignInCallback.Count})");
foreach (var callback in _onSignInCallback)
Debug.Log($"firing OnSignInCallbacks ({_onSignInCallbacks.Count})");
foreach (var callback in _onSignInCallbacks)
{
try
{
@ -155,18 +166,18 @@ private void FireOnSignInCallbacks()
}
catch (Exception e)
{
Debug.LogError(e);
Debug.LogError($"error invoking OnSignInCallback: {e.Message}");
}
}
}
/// <summary>
/// function to fire all on sign out callbacks
/// 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)
Debug.Log($"firing OnSignOutCallbacks ({_onSignOutCallbacks.Count})");
foreach (var callback in _onSignOutCallbacks)
{
try
{
@ -174,7 +185,7 @@ private void FireOnSignOutCallbacks()
}
catch (Exception e)
{
Debug.LogError(e);
Debug.LogError($"error invoking OnSignOutCallback: {e.Message}");
}
}
}
@ -182,10 +193,10 @@ private void FireOnSignOutCallbacks()
/// <summary>
/// async function to retry initialisation after a delay
/// </summary>
private async void RetryInitialiseAfterDelay()
private async void RetryInitialiseAfterDelay(Action<FirebaseConnectionStatus> callback)
{
await Task.Delay(TimeSpan.FromSeconds(10));
Initialise();
Initialise(callback);
}
/// <summary>
@ -235,25 +246,24 @@ private void AuthStateChanged(object sender, EventArgs eventArgs)
/// <param name="callback">callback function that takes in a <c>FirebaseUser</c> object</param>
public void RegisterOnSignInCallback(Action<FirebaseUser> callback)
{
Debug.Log("registering on sign in callback");
_onSignInCallback.Add(callback);
_onSignInCallbacks.Add(callback);
}
// /// <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);
// }
/// <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);
}
/// <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 AuthenticationResult argument</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(
@ -472,8 +482,7 @@ public void ForgotPassword(string email, Action<bool> callback)
/// abstraction function to get the user's recent scores from the database
/// </summary>
/// <param name="callback">
/// callback function that takes in a DatabaseTransactionResult and List of LocalPlayerData.Score
/// argument
/// callback function that takes in a <c>DatabaseTransactionResult</c> enum and a <c>List&lt;LocalPlayerData.Score&gt;</c>
/// </param>
public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerData.Score>> callback)
{
@ -485,7 +494,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// abstraction function to submit a score to the database
/// </summary>
/// <param name="score">score</param>
/// <param name="callback">callback function that takes in one DatabaseTransactionResult argument</param>
/// <param name="callback">callback function that takes in a <c>DatabaseTransactionResult</c> enum </param>
public void SubmitScore(
LocalPlayerData.Score score,
Action<DatabaseTransactionResult> callback)
@ -497,7 +506,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// 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>
/// <param name="callback">callback function that takes in a <c>DatabaseTransactionResult</c> enum and a user rating <c>float</c></param>
public void CalculateUserRating(
Action<DatabaseTransactionResult, float> callback)
{
@ -508,7 +517,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// 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>
/// <param name="callback">callback function that takes in a <c>DatabaseTransactionResult</c> enum </param>
public void UpdateUserRating(
float newRating,
Action<DatabaseTransactionResult> callback)
@ -520,7 +529,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// abstraction function to get the leaderboard from the database
/// </summary>
/// <param name="callback">
/// callback function that takes in a DatabaseTransactionResult and LeaderboardEntry[] argument
/// callback function that takes in a <c>DatabaseTransactionResult</c> enum and a <c>List&lt;LeaderboardEntry&gt;</c>
/// </param>
public void GetLeaderboard(
Action<DatabaseTransactionResult, LeaderboardEntry[]> callback)
@ -533,7 +542,7 @@ public void GetRecentScores(Action<DatabaseTransactionResult, List<LocalPlayerDa
/// </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>
/// <param name="callback">callback function that takes in a <c>DatabaseTransactionResult</c> enum</param>
/// <exception cref="ArgumentOutOfRangeException">thrown when the target is not a valid UserAccountDetailTargetEnum</exception>
public void UpdateUserAccountDetail(
UserAccountDetailTargetEnum target,

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
@ -20,7 +21,17 @@ public class GameManager : MonoBehaviour
/// <summary>
/// the local player data object for storing player data
/// </summary>
private LocalPlayerData _localPlayerData;
private LocalPlayerData _data;
/// <summary>
/// read-only property for accessing the local player data outside of this class
/// </summary>
public LocalPlayerData Data => _data;
/// <summary>
/// list of callbacks to call when the local player data changes
/// </summary>
private readonly List<Action<LocalPlayerData>> _onLocalPlayerDataChangeCallbacks = new List<Action<LocalPlayerData>>();
/// <summary>
/// backend object for handling communication with the firebase backend
@ -47,40 +58,55 @@ private void Awake()
}
}
private void Start()
{
// load the local player data and refresh the ui
_localPlayerData = new LocalPlayerData();
_localPlayerData.LoadFromTheWorld();
PopulateFields();
try
{
RenderFromPlayerData();
}
catch (Exception)
{
// TODO: remove this once the bug is fixed
Debug.LogWarning("handling known exception, remove this once the bug is fixed");
}
// register a callback to refresh the ui when the player signs in
Backend.RegisterOnSignInCallback(_ =>
{
Debug.Log("sign in callback, refreshing GameManager-controlled SideView UI");
_localPlayerData.LoadFromTheWorld();
PopulateFields();
RenderFromPlayerData();
});
}
/// <summary>
/// called when the game object is enabled, initialises variables
/// </summary>
private void OnEnable()
{
Backend = new Backend();
Backend.Initialise();
ui = UIManager.Instance;
// load the local player data and refresh the ui
_data = new LocalPlayerData();
Backend = new Backend();
Backend.Initialise(status =>
{
Debug.Log("initialised backend, setting connection status text");
ui.UI.Q<Label>("ConnectionStatusText").text = status switch
{
Backend.FirebaseConnectionStatus.Connected => "Status: Connected",
Backend.FirebaseConnectionStatus.Updating => "Status: Updating... (Retrying in a bit!)",
Backend.FirebaseConnectionStatus.NotConnected => "Status: Disconnected",
Backend.FirebaseConnectionStatus.UpdateRequired =>
"Status: Disconnected (Device Component Update Required)",
Backend.FirebaseConnectionStatus.ExternalError => "Status: Disconnected (External/Device Error)",
Backend.FirebaseConnectionStatus.InternalError => "Status: Disconnected (Internal Error)",
_ => "Status: Disconnected (unknown fcs state, this is unreachable and a bug)"
};
if (status == Backend.FirebaseConnectionStatus.Connected) return;
// if we're not connected, hide any online 'features'
ui.UI.Q<Button>("LeaderboardButton").style.display = DisplayStyle.None;
ui.UI.Q<Button>("AccountButton").style.display = DisplayStyle.None;
});
// 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);
});
}
/// <summary>
/// load in stuff
/// </summary>
private void Start()
{
Debug.Log("GameManager starts here");
_data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
}
/// <summary>
@ -90,76 +116,35 @@ private void OnApplicationQuit()
{
Debug.Log("running deferred cleanup/save functions");
Backend.Cleanup();
_localPlayerData.SaveToTheWorld();
_data.SaveToTheWorld();
}
/// <summary>
/// populate the fields with the given username and email, used by GameManager after local player data is loaded
/// function to register a callback to be called when the local player data changes
/// </summary>
public void PopulateFields()
/// <param name="callback">callback function that takes a <c>LocalPlayerData</c> object</param>
public void RegisterOnLocalPlayerDataChangeCallback(Action<LocalPlayerData> callback)
{
Debug.Log(
$"populating AccountView fields with lkUsername={_localPlayerData.LastKnownUsername} and lkEmail={_localPlayerData.LastKnownEmail}");
ui.UI.Q<TextField>("UsernameField").value = _localPlayerData.LastKnownUsername;
ui.UI.Q<TextField>("EmailField").value = _localPlayerData.LastKnownEmail;
_onLocalPlayerDataChangeCallbacks.Add(callback);
Debug.Log($"registering LocalPlayerDataChangeCallback ({_onLocalPlayerDataChangeCallbacks.Count})");
}
/// <summary>
/// function to update the ui with the latest data
/// function to fire all local player data change callbacks
/// </summary>
private void RenderFromPlayerData()
private void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data)
{
// calculate averages from both recent local scores and online scores
var totalLightnessAcc = 0f;
var totalChromaAcc = 0f;
var totalHueAcc = 0f;
var totalRounds = 0;
// average out all the scores we have to get a stable-ish average
foreach (var localScore in _localPlayerData.RecentLocalScores)
Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})");
foreach (var callback in _onLocalPlayerDataChangeCallbacks)
{
totalLightnessAcc += localScore.AvgLightnessAccuracy;
totalChromaAcc += localScore.AvgChromaAccuracy;
totalHueAcc += localScore.AvgHueAccuracy;
totalRounds += localScore.NoOfRounds;
try
{
callback.Invoke(data);
}
foreach (var onlineScore in _localPlayerData.RecentOnlineScores)
catch (Exception e)
{
totalLightnessAcc += onlineScore.AvgLightnessAccuracy;
totalChromaAcc += onlineScore.AvgChromaAccuracy;
totalHueAcc += onlineScore.AvgHueAccuracy;
totalRounds += onlineScore.NoOfRounds;
}
foreach (var onlineScore in _localPlayerData.BestOnlineScores)
{
totalLightnessAcc += onlineScore.AvgLightnessAccuracy;
totalChromaAcc += onlineScore.AvgChromaAccuracy;
totalHueAcc += onlineScore.AvgHueAccuracy;
totalRounds += onlineScore.NoOfRounds;
}
Debug.Log($"tL={totalLightnessAcc} tC={totalChromaAcc} tH={totalHueAcc} tR={totalRounds}");
if (totalRounds == 0) totalRounds = 1;
var lightnessAcc = totalLightnessAcc / totalRounds;
var chromaAcc = totalChromaAcc / totalRounds;
var hueAcc = totalHueAcc / totalRounds;
var playerText = Backend.IsSignedIn ? _localPlayerData.LastKnownUsername : $"{_localPlayerData.LastKnownUsername} (Not Signed In)";
// finally, set the labels
ui.UI.Q<Label>("PlayerText").text = playerText;
ui.UI.Q<Label>("LightnessAccuracyText").text = $"{lightnessAcc:F}";
ui.UI.Q<Label>("ChromaAccuracyText").text = $"{chromaAcc:F}";
ui.UI.Q<Label>("HueAccuracyText").text = $"{hueAcc:F}";
// and set the player rating, but after we get it from the backend
// (god I LOVE async (I am LYING out of my teeth))
Backend.CalculateUserRating((dtr, rating) =>
{
if (dtr != Backend.DatabaseTransactionResult.Ok) return;
ui.UI.Q<Label>("RatingText").text = $"{rating:F}";
});
Debug.LogError($"error invoking LocalPlayerDataChangeCallback: {e.Message}");
}
}
}
}

View file

@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -10
executionOrder: -50
icon: {instanceID: 0}
userData:
assetBundleName:

View file

@ -35,7 +35,7 @@ public class LocalPlayerData
/// <summary>
/// loads player data from player prefs and database
/// </summary>
public void LoadFromTheWorld()
public void LoadFromTheWorld(Action<LocalPlayerData> callback)
{
// load user data, possibly from the backend
var possibleUser = GameManager.Instance.Backend.GetUser();
@ -91,7 +91,8 @@ public void LoadFromTheWorld()
}
Debug.Log(
$"loaded lpdata from the world ({LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}), ROS.Count={RecentOnlineScores.Count}");
$"loaded lpdata from the world ({LastKnownUsername} <{LastKnownEmail}> with RLS.Count={RecentLocalScores.Count}, ROS.Count={RecentOnlineScores.Count}");
callback(this);
});
}

View file

@ -1,3 +1,4 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
@ -21,6 +22,36 @@ public class SideViewUI : MonoBehaviour
/// </summary>
private Button _playButton;
/// <summary>
/// connection status label for showing the connection status
/// </summary>
private Label _connectionStatusLabel;
/// <summary>
/// text label for showing the player's known name
/// </summary>
private Label _playerText;
/// <summary>
/// text label for showing the player's rating
/// </summary>
private Label _ratingText;
/// <summary>
/// text label for showing the player's stable-ish lightness accuracy
/// </summary>
private Label _lightnessAccuracyText;
/// <summary>
/// text label for showing the player's stable-ish chroma accuracy
/// </summary>
private Label _chromaAccuracyText;
/// <summary>
/// text label for showing the player's stable-ish hue accuracy
/// </summary>
private Label _hueAccuracyText;
/// <summary>
/// function to subscribe button events to their respective functions
/// </summary>
@ -36,6 +67,21 @@ private void OnEnable()
_accountButton = ui.Q<Button>("AccountButton");
_accountButton.clicked += OnAccountButtonClicked;
_connectionStatusLabel = ui.Q<Label>("ConnectionStatusLabel");
_playerText = ui.Q<Label>("PlayerText");
_ratingText = ui.Q<Label>("RatingText");
_lightnessAccuracyText = ui.Q<Label>("LightnessAccuracyText");
_chromaAccuracyText = ui.Q<Label>("ChromaAccuracyText");
_hueAccuracyText = ui.Q<Label>("HueAccuracyText");
GameManager.Instance.Backend.RegisterOnSignOutCallback(() =>
{
RenderFromPlayerData(GameManager.Instance.Data);
});
GameManager.Instance.RegisterOnLocalPlayerDataChangeCallback(RenderFromPlayerData);
}
/// <summary>
@ -61,4 +107,65 @@ private static void OnAccountButtonClicked()
{
GameManager.Instance.ui.SetDisplayState(UIManager.DisplayState.AccountView);
}
/// <summary>
/// function to update the ui with the latest data
/// </summary>
private void RenderFromPlayerData(LocalPlayerData data)
{
// calculate averages from both recent local scores and online scores
var totalLightnessAcc = 0f;
var totalChromaAcc = 0f;
var totalHueAcc = 0f;
var totalRounds = 0;
// average out all the scores we have to get a stable-ish average
foreach (var localScore in data.RecentLocalScores)
{
totalLightnessAcc += localScore.AvgLightnessAccuracy;
totalChromaAcc += localScore.AvgChromaAccuracy;
totalHueAcc += localScore.AvgHueAccuracy;
totalRounds += localScore.NoOfRounds;
}
foreach (var onlineScore in data.RecentOnlineScores)
{
totalLightnessAcc += onlineScore.AvgLightnessAccuracy;
totalChromaAcc += onlineScore.AvgChromaAccuracy;
totalHueAcc += onlineScore.AvgHueAccuracy;
totalRounds += onlineScore.NoOfRounds;
}
foreach (var onlineScore in data.BestOnlineScores)
{
totalLightnessAcc += onlineScore.AvgLightnessAccuracy;
totalChromaAcc += onlineScore.AvgChromaAccuracy;
totalHueAcc += onlineScore.AvgHueAccuracy;
totalRounds += onlineScore.NoOfRounds;
}
Debug.Log($"tL={totalLightnessAcc} tC={totalChromaAcc} tH={totalHueAcc} tR={totalRounds}");
if (totalRounds == 0) totalRounds = 1;
var lightnessAcc = totalLightnessAcc / totalRounds;
var chromaAcc = totalChromaAcc / totalRounds;
var hueAcc = totalHueAcc / totalRounds;
var playerText = GameManager.Instance.Backend.IsSignedIn
? data.LastKnownUsername
: $"{data.LastKnownUsername} (Not Signed In)";
// finally, set the labels
_playerText.text = playerText;
_lightnessAccuracyText.text = $"{lightnessAcc:F}";
_chromaAccuracyText.text = $"{chromaAcc:F}";
_hueAccuracyText.text = $"{hueAcc:F}";
// and set the player rating, but after we get it from the backend
// (god I LOVE async (I am LYING out of my teeth))
GameManager.Instance.Backend.CalculateUserRating((dtr, rating) =>
{
if (dtr != Backend.DatabaseTransactionResult.Ok) return;
_ratingText.text = $"{rating:F}";
});
}
}

View file

@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
executionOrder: -20
icon: {instanceID: 0}
userData:
assetBundleName:

View file

@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -15
executionOrder: -60
icon: {instanceID: 0}
userData:
assetBundleName: