using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using BracerLib.StateManagement.Interfaces;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace BracerLib.StateManagement
{
public delegate IEnumerator GameStateEventHandler(GameState state);
[RequireComponent(typeof(StateMap))]
[DefaultExecutionOrder(-999)]
public class GameStateController : MonoBehaviour
{
///
/// Priority-specific holder of events that need to trigger.
///
private class GameStatePriorityEventHolder
{
///
/// Event that houses all registered delegates to be invoked.
///
public event GameStateEventHandler GameStateEventTrigger;
///
/// Priority reference of this particular event holder.
///
public GameStatePriority Priority { get; private set; }
///
/// The list of delegates that can be invoked and yielded piece-meal rather than all at once via the assigned event.
///
public IEnumerable InvocationList => GameStateEventTrigger?.GetInvocationList() ?? Array.Empty();
public GameStatePriorityEventHolder(GameStatePriority priority) => Priority = priority;
~GameStatePriorityEventHolder() => Clear();
///
/// Get rid of all the attached delegates before clearing this out so no (less?) memory leaks
///
public void Clear()
{
var invocations = InvocationList;
foreach (var i in invocations)
GameStateEventTrigger -= (GameStateEventHandler)i;
}
}
///
/// Tracks priority events for a given state.
///
private class GameStateEvent : IGameStateTrigger
{
///
/// Pre-allocation capacity for required states.
///
private const int MAXSTATECAPACITY = 2;
public IDictionary PrioritizedHandlers;
private bool isCompleted;
private IList requiredStates;
private IGameStateTrigger[] cachedRequiredStates;
///
/// this event represents.
///
public GameState State { get; }
///
/// The s required before this state can be triggered.
///
public IGameStateTrigger[] RequiredStates => cachedRequiredStates ??= requiredStates.ToArray();
///
/// is being processed. Can only be set true if all required
/// triggers have been completed. If triggered prematurely, set into a queue state to be polled
/// by until it can be triggered.
///
public bool IsTriggered { get; private set; }
///
/// Check to see if has been completed. Will rely on Coroutines or async behavior.
///
public bool IsComplete => IsTriggered && isCompleted;
public GameStateEvent(GameState state)
{
State = state;
requiredStates = new List(MAXSTATECAPACITY);
PrioritizedHandlers = new Dictionary();
var priorities = Enum.GetValues(typeof(GameStatePriority));
foreach (GameStatePriority p in priorities)
PrioritizedHandlers.Add(p, new GameStatePriorityEventHolder(p));
}
~GameStateEvent() => Clear();
///
/// Add a state requirement for this event to follow.
///
/// A state that must be complete before these events can be invoked.
public void AddRequiredState(IGameStateTrigger gameGameState)
{
if (!requiredStates.Contains(gameGameState))
requiredStates.Add(gameGameState);
}
public void Reset() => IsTriggered = false;
public void Clear()
{
Reset();
requiredStates.Clear();
foreach (var pair in PrioritizedHandlers)
pair.Value.Clear();
}
///
/// Upon being triggered, go through and wait for each registered coroutine to run and complete.
///
public IEnumerator WaitForGameStateEvents()
{
if (IsTriggered || isCompleted)
yield break;
IsTriggered = true;
yield return null;
foreach (var p in PriorityStates)
{
var invocations = PrioritizedHandlers[p].InvocationList;
foreach (var @delegate in invocations)
{
var handler = (GameStateEventHandler)@delegate;
// TODO: Figure out a way to get status or result from this
yield return handler.Invoke(State);
}
}
isCompleted = true;
}
private bool AllRequiredStatesTriggered()
{
var result = true;
foreach (var s in requiredStates)
result &= s.IsTriggered;
return result;
}
private bool AllRequiredStatesCompleted()
{
var result = true;
foreach (var s in requiredStates)
result &= s.IsComplete;
return result;
}
}
private static GameStateController INSTANCE;
private static IDictionary STATE_EVENTS = new Dictionary();
public static GameStateController Instance
{
get
{
if (INSTANCE == null)
#if UNITY_6000_0_OR_NEWER
INSTANCE = FindAnyObjectByType();
#else
INSTANCE = FindObjectOfType();
#endif
return INSTANCE;
}
}
public static GameStatePriority[] PriorityStates { get; } = (GameStatePriority[])Enum.GetValues(typeof(GameStatePriority));
///
/// Attempts to run the available action if a has been triggered, meaning all its
/// dependencies have been triggered as well.
///
/// The target state.
/// The handler or delegate to run.
/// The priority that the invocation should occur with.
/// A status the user can act upon, if needed.
public static GameStateInvocationStatus RunOrDefer(GameState state, GameStateEventHandler handler, GameStatePriority priority = GameStatePriority.Medium)
{
var triggered = IsGameStateTriggered(state) || IsGameStateComplete(state);
if (triggered)
{
Instance.StartCoroutine(handler(state));
return GameStateInvocationStatus.Success;
}
if (Instance == null && STATE_EVENTS.Count == 0)
return GameStateInvocationStatus.Failure;
if (!STATE_EVENTS.TryGetValue(state, out var gameStateEvent))
{
Debug.LogError($"GameState invocation failure: Could not defer invocation of ${handler.Method.Name}.");
return GameStateInvocationStatus.Failure;
}
STATE_EVENTS[state].PrioritizedHandlers[priority].GameStateEventTrigger += handler;
return GameStateInvocationStatus.Deferred;
}
public static bool IsGameStateTriggered(GameState state) => STATE_EVENTS.TryGetValue(state, out var stateEvent) && stateEvent.IsTriggered;
public static bool IsGameStateComplete(GameState state) => STATE_EVENTS.TryGetValue(state, out var stateEvent) && stateEvent.IsComplete;
private static void ResetStateFlags()
{
foreach (var entry in STATE_EVENTS)
entry.Value.Reset();
}
private StateMap stateMap;
private bool isTriggered;
private bool isCompleted;
private Coroutine initCoroutine;
private YieldInstruction frameWait;
private Stopwatch stateStopwatch;
private Stopwatch overallStopwatch;
///
/// Bootstrap all state dependencies upon awake so they can be registered first
///
private void Awake()
{
INSTANCE = this;
stateStopwatch = new Stopwatch();
overallStopwatch = new Stopwatch();
if (STATE_EVENTS.Count > 0)
{
Debug.LogError("No GameStates recognized. Exiting state registration. You've more than likely done something very wrong.");
return;
}
frameWait = new WaitForEndOfFrame();
stateMap = GetComponent();
foreach (var v in stateMap.GetGameStateMap())
STATE_EVENTS.Add(v, new GameStateEvent(v));
// Add state dependency map. Left-hand GameState requires right-hand GameState to be finished before proceeding
// Supports multiple-required states. Read as "LeftHandState requires RightHandState."
// New states can be added in this chain, but they must be linked from one end to the other
// e.g. (GameState.B, GameState.A), -> GameState.B requires GameState.A
// (GameState.C, GameState.B) -> GameState.C requires GameState.B
foreach (var t in stateMap.GetGameStateRequirements())
STATE_EVENTS[t.Item1].AddRequiredState(STATE_EVENTS[t.Item2]);
}
private void Start() => TriggerInitialization();
private void OnDestroy()
{
INSTANCE = null;
STATE_EVENTS.Clear();
foreach (var entry in STATE_EVENTS)
entry.Value.Clear();
}
private void TriggerInitialization()
{
if (initCoroutine != null)
return;
initCoroutine = StartCoroutine(PerformInitialization());
}
///
/// Run events attached to all states running up the chain of requirements for .
///
private IEnumerator PerformInitialization()
{
ResetStateFlags();
// Allow all other objects in scene to wake up
yield return new WaitForSeconds(0.5f);
var readyState = stateMap.GetReadyState();
IGameStateTriggerInvoker targetEvent;
while ((targetEvent = NextAvailableGameStateEvent(readyState)) != null)
{
Debug.Log($"Running GameState: {targetEvent.State}");
stateStopwatch.Restart();
yield return StartCoroutine(targetEvent.WaitForGameStateEvents());
stateStopwatch.Stop();
yield return frameWait;
var span = TimeSpan.FromMilliseconds(stateStopwatch.ElapsedMilliseconds);
Debug.Log($"{targetEvent.State}: {span.Minutes:D2} minutes, {span.Seconds:D2} seconds, {span.Milliseconds:D3} ms");
}
var overallSpan = TimeSpan.FromMilliseconds(stateStopwatch.ElapsedMilliseconds);
Debug.Log($"Finish initializing all GameStates\n{overallSpan.Minutes:D2} minutes, {overallSpan.Seconds:D2} seconds, {overallSpan.Milliseconds:D3} ms");
}
///
/// Recursively traverse the required that each event may have until it reaches
/// the state that needs to be run next.
///
/// State to start traversal from.
/// The earliest event whose events have not yet triggered.
private IGameStateTrigger NextAvailableGameStateEvent(GameState state)
{
var currentEvent = STATE_EVENTS[state];
var states = currentEvent.RequiredStates;
var index = 0;
while (index < states.Length)
{
var target = NextAvailableGameStateEvent(states[index++].State);
if (target != null && !target.IsTriggered)
return target;
}
return !currentEvent.IsTriggered ? currentEvent : null;
}
}
}