1 /**
2  * Retrograde Engine
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2014-2021 Mike Bierlee
7  * License:
8  *  This software is licensed under the terms of the MIT license.
9  *  The full terms of the license can be found in the LICENSE.txt file.
10  */
11 
12 module retrograde.state;
13 
14 import retrograde.entity;
15 import retrograde.option;
16 
17 import std.typecons;
18 
19 alias ExitDiscardedStates = Flag!"exitDiscardedStates";
20 alias EnableStateSuspension = Flag!"enableStateSuspension";
21 
22 class StateMachine {
23     private State[] stateStack;
24 
25     private bool stateSuspensionEnabled = false;
26 
27     this(EnableStateSuspension enableStateSuspension = EnableStateSuspension.no) {
28         stateSuspensionEnabled = enableStateSuspension;
29     }
30 
31     public void setState(State state, ExitDiscardedStates exitDiscardedStates = ExitDiscardedStates.no) {
32         clearStates(exitDiscardedStates);
33         pushState(state);
34     }
35 
36     public void switchState(State state) {
37         if (stateStack.length == 0) {
38             setState(state);
39         } else {
40             getCurrentState().ifNotEmpty(state => state.exitState(this));
41             stateStack[$ - 1] = state;
42             state.enterState(this);
43         }
44     }
45 
46     public void pushState(State state) {
47         getCurrentState().ifNotEmpty((state) {
48             stateSuspensionEnabled ? state.suspendState(this) : state.exitState(this);
49         });
50         stateStack ~= state;
51         state.enterState(this);
52     }
53 
54     public Option!State popState() {
55         auto poppedState = getCurrentState();
56         poppedState.ifNotEmpty(state => state.exitState(this));
57         stateStack = stateStack[0 .. $-1];
58         getCurrentState().ifNotEmpty((state) {
59             stateSuspensionEnabled ? state.resumeState(this) : state.enterState(this);
60         });
61         return poppedState;
62     }
63 
64     public void truncateStates(ExitDiscardedStates exitDiscardedStates = ExitDiscardedStates.no) {
65         if (stateStack.length > 0) {
66             auto topState = getCurrentState().get();
67             if (exitDiscardedStates) {
68                 foreach (state; stateStack[0 .. $-1]) {
69                     state.exitState(this);
70                 }
71             }
72             clearStates();
73             stateStack ~= topState;
74         }
75     }
76 
77     public void clearStates(ExitDiscardedStates exitDiscardedStates = ExitDiscardedStates.no) {
78         if (exitDiscardedStates) {
79             foreach (state; stateStack) {
80                 state.exitState(this);
81             }
82         }
83 
84         stateStack.destroy();
85     }
86 
87     public Option!State getCurrentState() {
88         return stateStack.length > 0 ? new Some!State(stateStack[$ - 1]) : new None!State();
89     }
90 }
91 
92 class StateMachineEntityComponent : EntityComponent, Snapshotable {
93     mixin EntityComponentIdentity!"StateMachineEntityComponent";
94 
95     private StateMachine _stateMachine;
96 
97     this(StateMachine stateMachine) {
98         this._stateMachine = stateMachine;
99     }
100 
101     public @property StateMachine stateMachine() {
102         return _stateMachine;
103     }
104 
105     public string[string] getSnapshotData() {
106         string currentStateName = "";
107         _stateMachine.getCurrentState().ifNotEmpty((s) { currentStateName = s.stateName; });
108 
109         return [
110             "currentState": currentStateName
111         ];
112     }
113 }
114 
115 class StateMachineProcessor : EntityProcessor {
116     public override bool acceptsEntity(Entity entity) {
117         return entity.hasComponent!StateMachineEntityComponent;
118     }
119 
120     public override void update() {
121         foreach (entity; _entities.getAll()) {
122             auto stateMachine = entity.getFromComponent!StateMachineEntityComponent(component => component.stateMachine);
123             stateMachine.getCurrentState().ifNotEmpty(state => state.update(stateMachine, entity));
124         }
125     }
126 }
127 
128 abstract class State {
129     private string _stateName;
130 
131     public @property string stateName() {
132         return _stateName;
133     }
134 
135     this(string stateName) {
136         this._stateName = stateName;
137     }
138 
139     public void enterState(StateMachine stateMachine){}
140     public void update(StateMachine stateMachine, Entity entity){}
141     public void exitState(StateMachine stateMachine){}
142     public void suspendState(StateMachine stateMachine){}
143     public void resumeState(StateMachine stateMachine){}
144 }