diff --git a/libretrogd/src/states/mod.rs b/libretrogd/src/states/mod.rs index dc9e9c8..eee062a 100644 --- a/libretrogd/src/states/mod.rs +++ b/libretrogd/src/states/mod.rs @@ -29,6 +29,7 @@ pub enum State { pub enum StateChange { Push(Box>), + Swap(Box>), Pop, } @@ -70,6 +71,10 @@ impl StateContainer { self.current_state } + pub fn has_pending_state_change(&self) -> bool { + self.pending_state_change.is_some() + } + #[inline] pub fn pending_state_change(&mut self) -> Option { self.pending_state_change.take() @@ -192,10 +197,23 @@ impl States { } } + fn swap_boxed_state(&mut self, boxed_state: Box>) -> Result<(), StateError> { + if !self.can_push_or_pop() { + Err(StateError::HasPendingStateChange) + } else { + self.command = Some(StateChange::Swap(boxed_state)); + Ok(()) + } + } + pub fn push(&mut self, state: impl GameState + 'static) -> Result<(), StateError> { self.push_boxed_state(Box::new(state)) } + pub fn swap(&mut self, state: impl GameState + 'static) -> Result<(), StateError> { + self.swap_boxed_state(Box::new(state)) + } + pub fn pop(&mut self) -> Result<(), StateError> { if !self.can_push_or_pop() { Err(StateError::HasPendingStateChange) @@ -222,12 +240,19 @@ impl States { match command { StateChange::Push(new_state) => { self.pending_state = Some(new_state); - } + }, StateChange::Pop => { if let Some(state) = self.states.front_mut() { state.pending_transition_out(TransitionTo::Dead); } } + StateChange::Swap(new_state) => { + // swap is basically pop+push combined together in one step + if let Some(state) = self.states.front_mut() { + state.pending_transition_out(TransitionTo::Dead); + } + self.pending_state = Some(new_state); + } } } @@ -242,7 +267,11 @@ impl States { // if the current state is active and there is a pending state waiting to be added, // we need to start transitioning out the active state towards a 'paused' state let state = self.states.front_mut().unwrap(); - state.pending_transition_out(TransitionTo::Paused); + // if this state is being swapped out for another, it will already have a + // pending state change to TransitionOut(Dead) here ... + if !state.has_pending_state_change() { + state.pending_transition_out(TransitionTo::Paused); + } } } @@ -340,6 +369,7 @@ impl States { if let Some(state_change) = state.state().update(current_state, context) { match state_change { StateChange::Push(state) => self.push_boxed_state(state)?, + StateChange::Swap(state) => self.swap_boxed_state(state)?, StateChange::Pop => self.pop()?, } } @@ -967,6 +997,109 @@ mod tests { Ok(()) } + #[test] + fn swap_states() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // push first state + states.push(TestState::new(FIRST))?; + assert_eq!(context.take_log(), vec![]); + + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Pending, Dead), + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // swap in second state + states.swap(TestState::new(SECOND))?; + assert_eq!(context.take_log(), vec![]); + + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // first state finished transitioning out, now dies. + // second state starts up, will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead)), + StateChange(SECOND, Pending, Dead), + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + struct SelfPushPopState { id: u32, counter: u32,