diff --git a/Blarg.GameFramework/Blarg.GameFramework.csproj b/Blarg.GameFramework/Blarg.GameFramework.csproj index c555f52..a61edce 100644 --- a/Blarg.GameFramework/Blarg.GameFramework.csproj +++ b/Blarg.GameFramework/Blarg.GameFramework.csproj @@ -160,6 +160,12 @@ + + + + + + @@ -178,6 +184,8 @@ + + diff --git a/Blarg.GameFramework/Processes/GameProcess.cs b/Blarg.GameFramework/Processes/GameProcess.cs new file mode 100644 index 0000000..5c03c25 --- /dev/null +++ b/Blarg.GameFramework/Processes/GameProcess.cs @@ -0,0 +1,108 @@ +using System; +using Blarg.GameFramework.Events; +using Blarg.GameFramework.States; + +namespace Blarg.GameFramework.Processes +{ + public class GameProcess : EventListener, IDisposable + { + public readonly IGameApp GameApp; + public readonly GameState GameState; + public readonly ProcessManager ProcessManager; + + public bool IsFinished { get; private set; } + + public bool IsTransitioning + { + get { return ProcessManager.IsProcessTransitioning(this); } + } + + public GameProcess(ProcessManager processManager, EventManager eventManager) + : base(eventManager) + { + if (processManager == null) + throw new ArgumentNullException("processManager"); + if (eventManager == null) + throw new ArgumentNullException("eventManager"); + + GameState = processManager.GameState; + ProcessManager = processManager; + GameApp = GameState.GameApp; + } + + protected void SetFinished() + { + IsFinished = true; + } + + #region Events + + public virtual void OnAdd() + { + } + + public virtual void OnRemove() + { + } + + public virtual void OnPause(bool dueToOverlay) + { + } + + public virtual void OnResume(bool fromOverlay) + { + } + + public virtual void OnAppGainFocus() + { + } + + public virtual void OnAppLostFocus() + { + } + + public virtual void OnAppPause() + { + } + + public virtual void OnAppResume() + { + } + + public virtual void OnLostContext() + { + } + + public virtual void OnNewContext() + { + } + + public virtual void OnRender(float delta) + { + } + + public virtual void OnResize() + { + } + + public virtual void OnUpdate(float delta) + { + } + + public virtual bool OnTransition(float delta, bool isTransitioningOut, bool started) + { + return true; + } + + public override bool Handle(Event e) + { + return false; + } + + #endregion + + public virtual void Dispose() + { + } + } +} diff --git a/Blarg.GameFramework/Processes/ProcessInfo.cs b/Blarg.GameFramework/Processes/ProcessInfo.cs new file mode 100644 index 0000000..49625c0 --- /dev/null +++ b/Blarg.GameFramework/Processes/ProcessInfo.cs @@ -0,0 +1,32 @@ +using System; + +namespace Blarg.GameFramework.Processes +{ + internal class ProcessInfo + { + public readonly GameProcess Process; + public readonly string Name; + public readonly string Descriptor; + + public bool IsTransitioning; + public bool IsTransitioningOut; + public bool IsTransitionStarting; + public bool IsInactive; + public bool IsBeingRemoved; + + public ProcessInfo(GameProcess process, string name = null) + { + if (process == null) + throw new ArgumentNullException("process"); + + Process = process; + Name = name; + IsInactive = true; + + if (String.IsNullOrEmpty(Name)) + Descriptor = process.GetType().Name; + else + Descriptor = String.Format("{0}[{1}]", process.GetType().Name, Name); + } + } +} diff --git a/Blarg.GameFramework/Processes/ProcessManager.cs b/Blarg.GameFramework/Processes/ProcessManager.cs new file mode 100644 index 0000000..3881029 --- /dev/null +++ b/Blarg.GameFramework/Processes/ProcessManager.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections.Generic; +using Blarg.GameFramework.States; + +namespace Blarg.GameFramework.Processes +{ + public class ProcessManager : IDisposable + { + const string LOG_TAG = "PROCESSES"; + + LinkedList _processes; + Queue _queue; + + public readonly GameState GameState; + + public ProcessManager(GameState state) + { + if (state == null) + throw new ArgumentNullException("state"); + + GameState = state; + _processes = new LinkedList(); + _queue = new Queue(); + } + + #region Complex Properties + + public bool IsTransitioning + { + get + { + for (var node = _processes.First; node != null; node = node.Next) + { + if (node.Value.IsTransitioning) + return true; + } + return false; + } + } + + public bool IsEmpty + { + get + { + return (_processes.Count == 0 && _queue.Count == 0); + } + } + + #endregion + + #region Events + + public void OnPause(bool dueToOverlay) + { + if (_processes.Count == 0) + return; + + if (dueToOverlay) + { + Platform.Logger.Info(LOG_TAG, "Pausing all active processes due to state being overlayed on to the parent state."); + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + { + Platform.Logger.Info(LOG_TAG, "Pausing process {0} due to parent state overlay.", processInfo.Descriptor); + processInfo.Process.OnPause(true); + } + } + } + else + { + Platform.Logger.Info(LOG_TAG, "Transitioning out all active processes pending pause."); + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + StartTransitionOut(processInfo, false); + } + } + } + + public void OnResume(bool fromOverlay) + { + if (_processes.Count == 0) + return; + + if (fromOverlay) + { + Platform.Logger.Info(LOG_TAG, "Resuming all active processes due to overlay state being removed from overtop of parent state."); + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + { + Platform.Logger.Info(LOG_TAG, "Resuming process {0} due to overlay state removal.", processInfo.Descriptor); + processInfo.Process.OnResume(true); + } + } + } + else + { + Platform.Logger.Info(LOG_TAG, "Resuming processes."); + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (processInfo.IsInactive && !processInfo.IsBeingRemoved) + { + Platform.Logger.Info(LOG_TAG, "Resuming process {0}", processInfo.Descriptor); + processInfo.Process.OnResume(false); + + StartTransitionIn(processInfo); + } + } + } + } + + public void OnAppGainFocus() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnAppGainFocus(); + } + } + + public void OnAppLostFocus() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnAppLostFocus(); + } + } + + public void OnAppPause() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnAppPause(); + } + } + + public void OnAppResume() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnAppResume(); + } + } + + public void OnLostContext() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnLostContext(); + } + } + + public void OnNewContext() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnNewContext(); + } + } + + public void OnRender(float delta) + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnRender(delta); + } + } + + public void OnResize() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnResize(); + } + } + + public void OnUpdate(float delta) + { + CleanupInactiveProcesses(); + CheckForFinishedProcesses(); + ProcessQueue(); + UpdateTransitions(delta); + + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive) + processInfo.Process.OnUpdate(delta); + } + } + + #endregion + + #region Add / Remove + + public T Add(string name = null) where T : GameProcess + { + var newProcess = (T)Activator.CreateInstance(typeof(T), this); + var processInfo = new ProcessInfo(newProcess, name); + Queue(processInfo); + return newProcess; + } + + public void Remove(string name) + { + var node = GetNodeFor(name); + if (node == null) + throw new Exception("No process with given name."); + StartTransitionOut(node.Value, true); + } + + public void RemoveFirstOf() where T : GameProcess + { + var node = GetNodeForFirst(); + if (node == null) + throw new Exception("No process of given type."); + StartTransitionOut(node.Value, true); + } + + public void RemoveAll() + { + Platform.Logger.Info(LOG_TAG, "Transitioning out all processes pending removal."); + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsTransitioning && !processInfo.IsInactive) + StartTransitionOut(processInfo, true); + } + } + + private void Queue(ProcessInfo newProcessInfo) + { + if (newProcessInfo == null) + throw new ArgumentNullException("newProcessInfo"); + if (newProcessInfo.Process == null) + throw new ArgumentException("No GameProcess provided."); + + Platform.Logger.Info(LOG_TAG, "Queueing process {0}.", newProcessInfo.Descriptor); + _queue.Enqueue(newProcessInfo); + } + + #endregion + + #region Internal Process Management + + private void StartTransitionIn(ProcessInfo processInfo) + { + if (processInfo == null) + throw new ArgumentNullException("processInfo"); + if (!processInfo.IsInactive || processInfo.IsTransitioning) + throw new InvalidOperationException(); + + processInfo.IsInactive = false; + processInfo.IsTransitioning = true; + processInfo.IsTransitioningOut = false; + processInfo.IsTransitionStarting = true; + Platform.Logger.Info(LOG_TAG, "Transition into process {0} started.", processInfo.Descriptor); + } + + private void StartTransitionOut(ProcessInfo processInfo, bool forRemoval) + { + if (processInfo == null) + throw new ArgumentNullException("processInfo"); + if (processInfo.IsInactive || processInfo.IsTransitioning) + throw new InvalidOperationException(); + + processInfo.IsTransitioning = true; + processInfo.IsTransitioningOut = true; + processInfo.IsTransitionStarting = true; + processInfo.IsBeingRemoved = forRemoval; + Platform.Logger.Info(LOG_TAG, "Transition out of process {0} started pending {1}.", processInfo.Descriptor, (forRemoval ? "removal" : "pause")); + } + + private void CleanupInactiveProcesses() + { + var node = _processes.First; + while (node != null) + { + var processInfo = node.Value; + if (processInfo.IsInactive && processInfo.IsBeingRemoved) + { + var next = node.Next; + _processes.Remove(node); + node = next; + + Platform.Logger.Info(LOG_TAG, "Deleting inactive process {0}.", processInfo.Descriptor); + processInfo.Process.Dispose(); + processInfo = null; + } + else + node = node.Next; + } + } + + private void CheckForFinishedProcesses() + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (!processInfo.IsInactive && processInfo.Process.IsFinished && !processInfo.IsTransitioning) + { + Platform.Logger.Info(LOG_TAG, "Process {0} marked as finished.", processInfo.Descriptor); + StartTransitionOut(processInfo, true); + } + } + } + + private void ProcessQueue() + { + while (_queue.Count > 0) + { + var processInfo = _queue.Dequeue(); + + Platform.Logger.Info(LOG_TAG, "Adding process {0} from queue.", processInfo.Descriptor); + _processes.AddLast(processInfo); + processInfo.Process.OnAdd(); + + StartTransitionIn(processInfo); + } + } + + private void UpdateTransitions(float delta) + { + for (var node = _processes.First; node != null; node = node.Next) + { + var processInfo = node.Value; + if (processInfo.IsTransitioning) + { + bool isDone = processInfo.Process.OnTransition(delta, processInfo.IsTransitioningOut, processInfo.IsTransitionStarting); + if (isDone) + { + Platform.Logger.Info(LOG_TAG, "Transition {0} into process {1} finished.", + (processInfo.IsTransitioningOut ? "out of" : "into"), + processInfo.Descriptor); + + // if the process was being transitioned out, then we should mark + // it as inactive, and trigger it's OnRemove event now + if (processInfo.IsTransitioningOut) + { + if (processInfo.IsBeingRemoved) + { + Platform.Logger.Info(LOG_TAG, "Removing process {0}.", processInfo.Descriptor); + processInfo.Process.OnRemove(); + } + else + { + Platform.Logger.Info(LOG_TAG, "Pausing process {0}.", processInfo.Descriptor); + processInfo.Process.OnPause(false); + } + processInfo.IsInactive = true; + } + + // done transitioning + processInfo.IsTransitioning = false; + processInfo.IsTransitioningOut = false; + } + processInfo.IsTransitionStarting = false; + } + } + } + + #endregion + + #region Misc + + public bool IsProcessTransitioning(GameProcess process) + { + var processInfo = GetProcessInfoFor(process); + if (processInfo == null) + return false; + else + return processInfo.IsTransitioning; + } + + public bool HasProcess(string name) + { + for (var node = _processes.First; node != null; node = node.Next) + { + if (!String.IsNullOrEmpty(node.Value.Name) && node.Value.Name == name) + return true; + } + return false; + } + + private LinkedListNode GetNodeFor(string processName) + { + if (String.IsNullOrEmpty(processName)) + throw new ArgumentNullException("processName"); + + for (var node = _processes.First; node != null; node = node.Next) + { + if (node.Value.Name == processName) + return node; + } + return null; + } + + private LinkedListNode GetNodeForFirst() where T : GameProcess + { + var type = typeof(T); + for (var node = _processes.First; node != null; node = node.Next) + { + if (node.Value.Process.GetType() == type) + return node; + } + return null; + } + + private ProcessInfo GetProcessInfoFor(GameProcess process) + { + if (process == null) + throw new ArgumentNullException("process"); + + for (var node = _processes.First; node != null; node = node.Next) + { + if (node.Value.Process == process) + return node.Value; + } + return null; + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_processes == null) + return; + + Platform.Logger.Info(LOG_TAG, "ProcessManager disposing."); + + while (_processes.Count > 0) + { + var processInfo = _processes.Last.Value; + Platform.Logger.Info(LOG_TAG, "Removing process {0} as part of ProcessManager shutdown.", processInfo.Descriptor); + processInfo.Process.OnRemove(); + processInfo.Process.Dispose(); + _processes.RemoveLast(); + } + + // the queues will likely not have anything in it, but just in case ... + while (_queue.Count > 0) + { + var processInfo = _queue.Dequeue(); + Platform.Logger.Info(LOG_TAG, "Removing queued process {0} as part of ProcessManager shutdown.", processInfo.Descriptor); + processInfo.Process.Dispose(); + } + + _processes = null; + _queue = null; + } + + #endregion + } +} diff --git a/Blarg.GameFramework/States/GameState.cs b/Blarg.GameFramework/States/GameState.cs new file mode 100644 index 0000000..5884002 --- /dev/null +++ b/Blarg.GameFramework/States/GameState.cs @@ -0,0 +1,148 @@ +using System; +using Blarg.GameFramework.Events; +using Blarg.GameFramework.Graphics.ScreenEffects; +using Blarg.GameFramework.Processes; + +namespace Blarg.GameFramework.States +{ + public abstract class GameState : EventListener, IDisposable + { + public IGameApp GameApp { get; private set; } + public readonly ProcessManager ProcessManager; + public readonly ScreenEffectManager EffectManager; + public readonly StateManager StateManager; + + public bool IsFinished { get; private set; } + public int? ReturnValue { get; private set; } + + public bool IsTransitioning + { + get { return StateManager.IsStateTransitioning(this); } + } + + public bool IsTopState + { + get { return StateManager.IsTopState(this); } + } + + public GameState(StateManager stateManager, EventManager eventManager) + : base(eventManager) + { + if (stateManager == null) + throw new ArgumentNullException("stateManager"); + + GameApp = stateManager.GameApp; + StateManager = stateManager; + + EffectManager = new ScreenEffectManager(); + ProcessManager = new ProcessManager(this); + } + + protected void SetFinished() + { + IsFinished = true; + ReturnValue = null; + } + + protected void SetFinished(int returnValue) + { + IsFinished = true; + ReturnValue = returnValue; + } + + #region Events + + public virtual void OnPush() + { + } + + public virtual void OnPop() + { + } + + public virtual void OnPause(bool dueToOverlay) + { + ProcessManager.OnPause(dueToOverlay); + } + + public virtual void OnResume(bool fromOverlay) + { + ProcessManager.OnResume(fromOverlay); + } + + public virtual void OnAppGainFocus() + { + ProcessManager.OnAppGainFocus(); + EffectManager.OnAppGainFocus(); + } + + public virtual void OnAppLostFocus() + { + ProcessManager.OnAppLostFocus(); + EffectManager.OnAppLostFocus(); + } + + public virtual void OnAppPause() + { + ProcessManager.OnAppPause(); + EffectManager.OnAppPause(); + } + + public virtual void OnAppResume() + { + ProcessManager.OnAppResume(); + EffectManager.OnAppResume(); + } + + public virtual void OnLostContext() + { + ProcessManager.OnLostContext(); + EffectManager.OnLostContext(); + } + + public virtual void OnNewContext() + { + ProcessManager.OnNewContext(); + EffectManager.OnNewContext(); + } + + public virtual void OnRender(float delta) + { + // switch it up and do effects before processes here so that processes + // (which would commonly be used for UI overlay elements) don't get + // overwritten by local effects (e.g. flashes, etc.) + EffectManager.OnRenderLocal(delta); + ProcessManager.OnRender(delta); + } + + public virtual void OnResize() + { + ProcessManager.OnResize(); + EffectManager.OnResize(); + } + + public virtual void OnUpdate(float delta) + { + ProcessManager.OnUpdate(delta); + EffectManager.OnUpdate(delta); + } + + public virtual bool OnTransition(float delta, bool isTransitioningOut, bool started) + { + return true; + } + + public override bool Handle(Event e) + { + return false; + } + + #endregion + + public virtual void Dispose() + { + EffectManager.Dispose(); + ProcessManager.Dispose(); + } + } +} diff --git a/Blarg.GameFramework/States/StateInfo.cs b/Blarg.GameFramework/States/StateInfo.cs new file mode 100644 index 0000000..3d78c57 --- /dev/null +++ b/Blarg.GameFramework/States/StateInfo.cs @@ -0,0 +1,34 @@ +using System; + +namespace Blarg.GameFramework.States +{ + internal class StateInfo + { + public readonly GameState State; + public readonly string Name; + public readonly string Descriptor; + + public bool IsOverlay; + public bool IsOverlayed; + public bool IsTransitioning; + public bool IsTransitioningOut; + public bool IsTransitionStarting; + public bool IsInactive; + public bool IsBeingPopped; + + public StateInfo(GameState state, string name = null) + { + if (state == null) + throw new ArgumentNullException("state"); + + State = state; + Name = name; + IsInactive = true; + + if (String.IsNullOrEmpty(Name)) + Descriptor = state.GetType().Name; + else + Descriptor = String.Format("{0}[{1}]", state.GetType().Name, Name); + } + } +} diff --git a/Blarg.GameFramework/States/StateManager.cs b/Blarg.GameFramework/States/StateManager.cs new file mode 100644 index 0000000..5d6ac28 --- /dev/null +++ b/Blarg.GameFramework/States/StateManager.cs @@ -0,0 +1,712 @@ +using System; +using System.Collections.Generic; +using Blarg.GameFramework.Events; + +namespace Blarg.GameFramework.States +{ + public class StateManager : IDisposable + { + const string LOG_TAG = "STATES"; + + LinkedList _states; + Queue _pushQueue; + Queue _swapQueue; + + bool _pushQueueHasOverlay; + bool _swapQueueHasOverlay; + bool _lastCleanedStatesWereAllOverlays; + + public readonly IGameApp GameApp; + public readonly EventManager EventManager; + + public int? LastReturnValue { get; private set; } + + public StateManager(IGameApp gameApp, EventManager eventManager) + { + if (gameApp == null) + throw new ArgumentNullException("gameApp"); + if (eventManager == null) + throw new ArgumentNullException("eventManager"); + + GameApp = gameApp; + EventManager = eventManager; + + _states = new LinkedList(); + _pushQueue = new Queue(); + _swapQueue = new Queue(); + } + + #region Complex Properties + + public GameState TopState + { + get + { + var topInfo = Top; + if (topInfo == null) + return null; + else + return topInfo.State; + } + } + + public GameState TopNonOverlayState + { + get + { + var topInfo = TopNonOverlay; + if (topInfo == null) + return null; + else + return topInfo.State; + } + } + + public bool IsTransitioning + { + get + { + for (var node = _states.First; node != null; node = node.Next) + { + if (node.Value.IsTransitioning) + return true; + } + return false; + } + } + + public bool IsEmpty + { + get + { + return (_states.Count == 0 && _pushQueue.Count == 0 && _swapQueue.Count == 0); + } + } + + private StateInfo Top + { + get + { + if (_states.Count == 0) + return null; + else + return _states.Last.Value; + } + } + + private StateInfo TopNonOverlay + { + get + { + var node = TopNonOverlayNode; + if (node != null) + return node.Value; + else + return null; + } + } + + private LinkedListNode TopNonOverlayNode + { + get + { + var node = _states.Last; + while (node != null && node.Value.IsOverlay) + node = node.Previous; + return node; + } + } + + #endregion + + #region Events + + public void OnAppGainFocus() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnAppGainFocus(); + } + } + + public void OnAppLostFocus() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnAppLostFocus(); + } + } + + public void OnAppPause() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnAppPause(); + } + } + + public void OnAppResume() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnAppResume(); + } + } + + public void OnLostContext() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnLostContext(); + } + } + + public void OnNewContext() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnNewContext(); + } + } + + public void OnRender(float delta) + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + { + node.Value.State.OnRender(delta); + node.Value.State.EffectManager.OnRenderGlobal(delta); + } + } + } + + public void OnResize() + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnResize(); + } + } + + public void OnUpdate(float delta) + { + // clear return values (ensuring they're only accessible for 1 tick) + LastReturnValue = null; + _lastCleanedStatesWereAllOverlays = false; + + CleanupInactiveStates(); + CheckForFinishedStates(); + ProcessQueues(); + ResumeStatesIfNeeded(); + UpdateTransitions(delta); + + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + if (!node.Value.IsInactive) + node.Value.State.OnUpdate(delta); + } + } + + #endregion + + #region Push / Pop / Overlay / Swap / Queue + + public T Push(string name = null) where T : GameState + { + var newState = (T)Activator.CreateInstance(typeof(T), this); + var stateInfo = new StateInfo(newState, name); + QueueForPush(stateInfo); + return newState; + } + + public T Overlay(string name = null) where T : GameState + { + var newState = (T)Activator.CreateInstance(typeof(T), this); + var stateInfo = new StateInfo(newState, name); + stateInfo.IsOverlay = true; + QueueForPush(stateInfo); + return newState; + } + + public T SwapTopWith(string name = null) where T : GameState + { + // figure out if the current top state is an overlay or not. use that + // same setting for the new state that is to be swapped in + var currentTopStateInfo = Top; + if (currentTopStateInfo == null) + throw new InvalidOperationException("Cannot swap, no existing states."); + bool isOverlay = currentTopStateInfo.IsOverlay; + + var newState = (T)Activator.CreateInstance(typeof(T), this); + var stateInfo = new StateInfo(newState, name); + stateInfo.IsOverlay = isOverlay; + QueueForSwap(stateInfo, false); + return newState; + } + + public T SwapTopNonOverlayWith(string name = null) where T : GameState + { + var newState = (T)Activator.CreateInstance(typeof(T), this); + var stateInfo = new StateInfo(newState, name); + QueueForSwap(stateInfo, true); + return newState; + } + + public void Pop() + { + if (IsTransitioning) + throw new InvalidOperationException(); + + Platform.Logger.Info(LOG_TAG, "Pop initiated for top-most state only."); + StartOnlyTopStateTransitioningOut(false); + } + + public void PopTopNonOverlay() + { + if (IsTransitioning) + throw new InvalidOperationException(); + + Platform.Logger.Info(LOG_TAG, "Pop initiated for all top active states"); + StartTopStatesTransitioningOut(false); + } + + private void QueueForPush(StateInfo newStateInfo) + { + if (newStateInfo == null) + throw new ArgumentNullException("newStateInfo"); + if (newStateInfo.State == null) + throw new ArgumentException("No GameState provided."); + if (_pushQueueHasOverlay && !newStateInfo.IsOverlay) + throw new InvalidOperationException("Cannot queue new non-overlay state while queue is active with overlay states."); + + Platform.Logger.Info(LOG_TAG, "Queueing state {0} for pushing.", newStateInfo.Descriptor); + + if (!newStateInfo.IsOverlay) + StartTopStatesTransitioningOut(true); + + _pushQueue.Enqueue(newStateInfo); + + if (newStateInfo.IsOverlay) + _pushQueueHasOverlay = true; + } + + private void QueueForSwap(StateInfo newStateInfo, bool swapTopNonOverlay) + { + if (newStateInfo == null) + throw new ArgumentNullException("newStateInfo"); + if (newStateInfo.State == null) + throw new ArgumentException("No GameState provided."); + if (_swapQueueHasOverlay && !newStateInfo.IsOverlay) + throw new InvalidOperationException("Cannot queue new non-overlay state while queue is active with overlay states."); + + Platform.Logger.Info(LOG_TAG, "Queueing state {0} for swapping with {1}.", newStateInfo.Descriptor, (swapTopNonOverlay ? "all top active states" : "only top-most active state.")); + + if (swapTopNonOverlay) + StartTopStatesTransitioningOut(false); + else + StartOnlyTopStateTransitioningOut(false); + + _swapQueue.Enqueue(newStateInfo); + + if (newStateInfo.IsOverlay) + _swapQueueHasOverlay = true; + } + + #endregion + + #region Internal State Management + + private void StartTopStatesTransitioningOut(bool pausing) + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + // only look at active states, since inactive ones have already + // been transitioned out and will be removed on the next OnUpdate() + if (!node.Value.IsInactive) + TransitionOut(node.Value, !pausing); + } + } + + private void StartOnlyTopStateTransitioningOut(bool pausing) + { + var stateInfo = Top; + // if it's not active, then it's just been transitioned out and will be + // removed on the next OnUpdate() + if (!stateInfo.IsInactive) + TransitionOut(stateInfo, !pausing); + } + + private void CleanupInactiveStates() + { + // we don't want to remove any states until everything is done transitioning. + // this is to avoid the scenario where the top non-overlay state finishes + // transitioning before one of the overlays. if we removed it, the overlays + // would then be overlayed over an inactive non-overlay (which wouldn't get + // resumed until the current active overlays were done being transitioned) + if (IsTransitioning) + return; + + bool cleanedUpSomething = false; + bool cleanedUpNonOverlay = false; + + var node = _states.First; + while (node != null) + { + var stateInfo = node.Value; + if (stateInfo.IsInactive && stateInfo.IsBeingPopped) + { + cleanedUpSomething = true; + if (!stateInfo.IsOverlay) + cleanedUpNonOverlay = true; + + // remove this state and move to the next node + var next = node.Next; + _states.Remove(node); + node = next; + + Platform.Logger.Info(LOG_TAG, "Deleting inactive popped state {0}.", stateInfo.Descriptor); + stateInfo.State.Dispose(); + stateInfo = null; + } + else + node = node.Next; + } + + if (cleanedUpSomething && !cleanedUpNonOverlay) + _lastCleanedStatesWereAllOverlays = true; + } + + private void CheckForFinishedStates() + { + if (_states.Count == 0) + return; + + // don't do anything if something is currently transitioning + if (IsTransitioning) + return; + + bool needToAlsoTransitionOutOverlays = false; + + // check the top non-overlay state first to see if it's finished + // and should be transitioned out + var topNonOverlayStateInfo = TopNonOverlay; + if (!topNonOverlayStateInfo.IsInactive && topNonOverlayStateInfo.State.IsFinished) + { + Platform.Logger.Info(LOG_TAG, "State {0} marked as finished.", topNonOverlayStateInfo.Descriptor); + TransitionOut(topNonOverlayStateInfo, true); + + needToAlsoTransitionOutOverlays = true; + } + + // now also check the overlay states (if there were any). we force them to + // transition out if the non-overlay state started to transition out so that + // we don't end up with overlay states without a parent non-overlay state + + // start the loop off 1 beyond the top non-overlay (which is where the + // overlays are, if any) + var node = TopNonOverlayNode; + if (node != null) + { + for (node = node.Next; node != null; node = node.Next) + { + var stateInfo = node.Value; + if (!stateInfo.IsInactive && (stateInfo.State.IsFinished || needToAlsoTransitionOutOverlays)) + { + Platform.Logger.Info(LOG_TAG, "State {0} marked as finished.", stateInfo.Descriptor); + TransitionOut(stateInfo, true); + } + } + } + } + + private void ProcessQueues() + { + // don't do anything if stuff is currently transitioning + if (IsTransitioning) + return; + + if (_pushQueue.Count > 0 && _swapQueue.Count > 0) + throw new InvalidOperationException("Cannot process queues when both the swap and push queues have items in them."); + + // for each state in the queue, add it to the main list and start + // transitioning it in + // (note, only one of these queues will be processed each tick due to the above check!) + + while (_pushQueue.Count > 0) + { + var stateInfo = _pushQueue.Dequeue(); + + if (_states.Count > 0) + { + // if this new state is an overlay, and the current top state is both + // currently active and is not currently marked as being overlay-ed + // then we should pause it due to overlay + var currentTopStateInfo = Top; + if (stateInfo.IsOverlay && !currentTopStateInfo.IsInactive && !currentTopStateInfo.IsOverlayed) + { + Platform.Logger.Info(LOG_TAG, "Pausing {0}state {1} due to overlay.", (currentTopStateInfo.IsOverlay ? "overlay " : ""), currentTopStateInfo.Descriptor); + currentTopStateInfo.State.OnPause(true); + + // also mark the current top state as being overlay-ed + currentTopStateInfo.IsOverlayed = true; + } + } + + Platform.Logger.Info(LOG_TAG, "Pushing {0}state {1} from push-queue.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + stateInfo.State.OnPush(); + + TransitionIn(stateInfo, false); + + _states.AddLast(stateInfo); + } + + while (_swapQueue.Count > 0) + { + var stateInfo = _swapQueue.Dequeue(); + + // if this new state is an overlay, and the current top state is both + // currently active and is not currently marked as being overlay-ed + // then we should pause it due to overlay + var currentTopStateInfo = Top; + if (stateInfo.IsOverlay && !currentTopStateInfo.IsInactive && !currentTopStateInfo.IsOverlayed) + { + Platform.Logger.Info(LOG_TAG, "Pausing {0}state {1} due to overlay.", (currentTopStateInfo.IsOverlay ? "overlay " : ""), currentTopStateInfo.Descriptor); + currentTopStateInfo.State.OnPause(true); + + // also mark the current top state as being overlay-ed + currentTopStateInfo.IsOverlayed = true; + } + + Platform.Logger.Info(LOG_TAG, "Pushing {0}state {1} from swap-queue.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + stateInfo.State.OnPush(); + + TransitionIn(stateInfo, false); + + _states.AddLast(stateInfo); + } + + _pushQueueHasOverlay = false; + _swapQueueHasOverlay = false; + } + + private void ResumeStatesIfNeeded() + { + if (_states.Count == 0) + return; + + // don't do anything if stuff is currently transitioning + if (IsTransitioning) + return; + + // did we just clean up one or more overlay states? + if (_lastCleanedStatesWereAllOverlays) + { + // then we need to resume the current top state + // (those paused with the flag "from an overlay") + var stateInfo = Top; + if (stateInfo.IsInactive || !stateInfo.IsOverlayed) + throw new InvalidOperationException(); + + Platform.Logger.Info(LOG_TAG, "Resuming {0}state {1} due to overlay removal.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + stateInfo.State.OnResume(true); + + stateInfo.IsOverlayed = false; + + return; + } + + // if the top state is not inactive, then we don't need to resume anything + if (!Top.IsInactive) + return; + + Platform.Logger.Info(LOG_TAG, "Top-most state is inactive. Resuming all top states up to and including the next non-overlay."); + + // top state is inactive, time to resume one or more states... + // find the topmost non-overlay state and take it and all overlay + // states that are above it and transition them in + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + var stateInfo = node.Value; + Platform.Logger.Info(LOG_TAG, "Resuming {0}state {1}.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + stateInfo.State.OnResume(false); + + TransitionIn(stateInfo, true); + } + + } + + private void UpdateTransitions(float delta) + { + for (var node = TopNonOverlayNode; node != null; node = node.Next) + { + var stateInfo = node.Value; + if (stateInfo.IsTransitioning) + { + bool isDone = stateInfo.State.OnTransition(delta, stateInfo.IsTransitioningOut, stateInfo.IsTransitionStarting); + if (isDone) + { + Platform.Logger.Info(LOG_TAG, "Transition {0} {1}state {2} finished.", + (stateInfo.IsTransitioningOut ? "out of" : "into"), + (stateInfo.IsOverlay ? "overlay " : ""), + stateInfo.Descriptor); + + // if the state was being transitioned out, then we should mark + // it as inactive, and trigger it's OnPop or OnPause event now + if (stateInfo.IsTransitioningOut) + { + if (stateInfo.IsBeingPopped) + { + Platform.Logger.Info(LOG_TAG, "Popping {0}state {1}", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + stateInfo.State.OnPop(); + + if (stateInfo.State.ReturnValue != null) + { + LastReturnValue = stateInfo.State.ReturnValue; + Platform.Logger.Info(LOG_TAG, "Return value of {0} retrieved from {1}.", LastReturnValue.Value, stateInfo.Descriptor); + } + } + else + { + Platform.Logger.Info(LOG_TAG, "Pausing {0}state {1}.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + stateInfo.State.OnPause(false); + } + stateInfo.IsInactive = true; + } + + // done transitioning + stateInfo.IsTransitioning = false; + stateInfo.IsTransitioningOut = false; + } + + stateInfo.IsTransitionStarting = false; + } + } + } + + private void TransitionIn(StateInfo stateInfo, bool forResuming) + { + stateInfo.IsInactive = false; + stateInfo.IsTransitioning = true; + stateInfo.IsTransitioningOut = false; + stateInfo.IsTransitionStarting = true; + Platform.Logger.Info(LOG_TAG, "Transition into {0}state {1} started.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + + if (forResuming) + stateInfo.State.ProcessManager.OnResume(false); + } + + private void TransitionOut(StateInfo stateInfo, bool forPopping) + { + stateInfo.IsTransitioning = true; + stateInfo.IsTransitioningOut = true; + stateInfo.IsTransitionStarting = true; + stateInfo.IsBeingPopped = forPopping; + Platform.Logger.Info(LOG_TAG, "Transition out of {0}state {1} started.", (stateInfo.IsOverlay ? "overlay " : ""), stateInfo.Descriptor); + + if (forPopping) + stateInfo.State.ProcessManager.RemoveAll(); + else + stateInfo.State.ProcessManager.OnPause(false); + } + + #endregion + + #region Misc + + private StateInfo GetStateInfoFor(GameState state) + { + if (state == null) + throw new ArgumentNullException("state"); + + for (var node = _states.First; node != null; node = node.Next) + { + if (node.Value.State == state) + return node.Value; + } + return null; + } + + public bool IsStateTransitioning(GameState state) + { + if (state == null) + throw new ArgumentNullException("state"); + + var info = GetStateInfoFor(state); + if (info == null) + return false; + else + return info.IsTransitioning; + } + + public bool IsTopState(GameState state) + { + if (state == null) + throw new ArgumentNullException("state"); + + var top = Top; + if (top == null) + return false; + else + return (top.State == state); + } + + public bool HasState(string name) + { + for (var node = _states.First; node != null; node = node.Next) + { + if (!String.IsNullOrEmpty(node.Value.Name) && node.Value.Name == name) + return true; + } + return false; + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_states == null) + return; + + Platform.Logger.Info(LOG_TAG, "StateManager disposing."); + + while (_states.Count > 0) + { + var stateInfo = _states.Last.Value; + Platform.Logger.Info(LOG_TAG, "Popping state {0} as part of StateManager shutdown.", stateInfo.Descriptor); + stateInfo.State.OnPop(); + stateInfo.State.Dispose(); + _states.RemoveLast(); + } + + // these queues will likely not have anything in them, but just in case ... + while (_pushQueue.Count > 0) + { + var stateInfo = _pushQueue.Dequeue(); + Platform.Logger.Info(LOG_TAG, "Deleting push-queued state {0} as part of StateManager shutdown.", stateInfo.Descriptor); + stateInfo.State.Dispose(); + } + while (_swapQueue.Count > 0) + { + var stateInfo = _swapQueue.Dequeue(); + Platform.Logger.Info(LOG_TAG, "Deleting swap-queued state {0} as part of StateManager shutdown.", stateInfo.Descriptor); + stateInfo.State.Dispose(); + } + + _states = null; + _pushQueue = null; + _swapQueue = null; + } + + #endregion + } +}