1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
//! # Pure HFSM
//!
//! A finite state machine library with a clear separation between the machine
//! definition and its changing state.
//!
//! I developed this library for my bevy project. This is why I give generic
//! lifetime parameters to `Behavior::World`, `Behavior::Update` and
//! `Transition::World` types. This requires GATs, but I found it was the only way
//! to get it to work with the horror that is `SystemParam` in bevy.
//!
//! The goal of the library is to have the state machine description be completely
//! independent from the state. From this, we get a lot of cool stuff that other
//! state machine libraries do not let you do easily or trivially, such as:
//! * Serialization and deserialization of state machines
//! * Compact representation of both the state and the descriptions
//! * Minimal mutable data, separate from the state machine. In bevy ECS, I can
//! store it independently from the state machine, as a `Component` (while
//! the state machines are loaded as `Asset`)
//! * Shared state machines with as many instances as you want
//!
//! There is a few downsides to know before using this library:
//! * There is more boilerplate to write to get things to work
//! * It's not type safe! However, you will get an error _when constructing_ the
//! state machine if you are referring to non-existent states or machines
//! in your `builder::StateMachines`! Which is far better than an error when
//! running transitions, but still not as optimal as a compilation error.
//! * There are _three_ different `StateMachines` type you have to interact with in
//! order to use this library (1) is the serialized representation `builder` (2)
//! is the compact immutable description (3) is the mutable state handle or `label`.
//!
//! For the serialized representation of the state machine to still be convenient
//! and maintainable while having a compact internal representation, you will need
//! to use `builder::StateMachines` for serde interface and convert it into a
//! runnable state machine as a second step.
//!
//! ## Features that may or may not be added in the future
//!
//! - [ ] `serde` cargo feature flag to be able to compile the library without serde
//! - [ ] Better documentation
//! - [ ] A version without the `StateData` `Box<dyn Any>`
//! - [ ] Tests
//! - [ ] A visual state machine editor
//!
//! # License
//!
//! Copyright © 2021 Nicola Papale
//!
//! This software is licensed under either MIT or Apache 2.0 at your leisure. See
//! LICENSE file for details.
//!
//! # How to use this library
//!
//! This library is divided in three types of state machines:
//! * [`builder::StateMachines`]: A serializable description of multiple interacting state machines
//! * [`StateMachines`]: is a compact description of multiple interacting state machines
//! * [`label::NestedMachine`]: is the running state of a state machine
//!
//! You will need to first describe the state machine with
//! [`builder::StateMachines`], use it's
//! [`build`](builder::StateMachines::build) method to get a [`StateMachines`].
//!
//! You will be able to control the execution of a Hierarchical Finite State
//! Machine (aka HFSM) with the [`label::NestedMachine`], passing it
//! a [`StateMachines`] when necessary.
#![feature(generic_associated_types)]
pub mod builder;
mod de;
pub mod label;
use smallvec::SmallVec;
use std::any::Any;
type SHandleInner = u8;
type SmHandleInner = u16;
pub type StateData = Box<dyn Any + Sync + Send>;
/// Behavior to adopt when in a state
pub trait Behavior {
/// The world we live in and influnces our behavior
type World<'w, 's>;
/// Things our behavior changes in the world
type Update<'w, 's>;
/// The behavior, what to do to `commands` given `world`
fn update<'w, 's, 'ww, 'ss>(
&self,
data: &mut StateData,
commands: &mut Self::Update<'w, 's>,
world: &Self::World<'ww, 'ss>,
);
}
/// Result of a transition
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Target {
/// Keep the current `State`
Continue,
/// Transition into a new `State`
Goto(SHandle),
/// Start a nested `StateMachine`, will come back to this `State` once
/// the nested state machine completes
Enter(SmHandle),
/// Terminate the state machine
Complete,
}
/// Decider for state transition
///
/// Each state has many `Transition`s. `Transition`s are ran every update in
/// order. The first to return a non-[`Target::Continue`] result will dictate
/// the next `State` or nested `StateMachine` the machine enters.
pub trait Transition {
/// The world to observe to make a transition decision
type World<'w, 's>;
/// To what [`Target`] transition given `world`?
fn decide<'w, 's>(&self, data: &mut StateData, world: &Self::World<'w, 's>) -> Target;
}
/// `State` handle
#[derive(Debug, Clone, PartialEq)]
pub struct SHandle(SHandleInner);
impl SHandle {
const INITIAL: Self = SHandle(0);
}
/// `StateMachine` handle
#[derive(Debug, Clone, PartialEq)]
pub struct SmHandle(SmHandleInner);
/// A classical state machine, you know the deal `:)`
#[derive(Debug, Clone)]
struct StateMachine<B, Trs> {
states: SmallVec<[State<B, Trs>; 2]>,
}
impl<B, T> StateMachine<B, T> {
fn state<'s>(&'s self, state: &SHandle) -> Option<&'s State<B, T>> {
self.states.get(state.0 as usize)
}
}
/// State and the transitions in a state machine
#[derive(Debug, Clone)]
struct State<B, Trs> {
/// Criterias for exiting the current State (See [`Transition`])
transitions: Vec<Trs>,
/// What to do when in this state (see [`Behavior`])
behavior: B,
}
/// Potental errors from running a state machine
#[derive(Debug)]
pub enum Error {
EmptyStack,
BadMachineName,
BadStateName,
}
// TODO: consider adding a version field to this and S[m]Name and check against
// this the version, then use indexing ops rather than slice::get, since we
// know we are running a state machine compatible with the provided S[m]Name
// there is no risk of out-of-bound access
/// A collection of state machines
///
/// Each state machines within this collection can refer to each other. You
/// should be using [`label::NestedMachine`] to manage the state of a state
/// machine.
///
/// State machines in this `struct` are represented in a space-efficient and
/// type-safe format, where it is impossible to refer to non-existing states
/// and machines.
#[derive(Debug)]
pub struct StateMachines<B, T> {
machines: SmallVec<[StateMachine<B, T>; 8]>,
machine_names: Vec<String>,
state_names: Vec<Vec<String>>,
}
impl<B, T> StateMachines<B, T> {
/// Get all machine names with their handles
pub fn machines<'s>(&'s self) -> impl Iterator<Item = (SmHandle, &'s str)> {
let to_name = |(i, n): (_, &'s String)| (SmHandle(i as u16), n.as_ref());
self.machine_names.iter().enumerate().map(to_name)
}
/// Get all state names with their state handles in provided machine
pub fn states<'s>(
&'s self,
machine: &SmHandle,
) -> Option<impl Iterator<Item = (SHandle, &'s str)>> {
let to_name = |(i, n): (_, &'s String)| (SHandle(i as u8), n.as_ref());
self.state_names
.get(machine.0 as usize)
.map(|names| names.iter().enumerate().map(to_name))
}
fn machine<'s>(&'s self, machine: &SmHandle) -> Option<&'s StateMachine<B, T>> {
self.machines.get(machine.0 as usize)
}
fn state_name(&self, machine: &SmHandle, state: &SHandle) -> Option<&str> {
self.state_names
.get(machine.0 as usize)
.and_then(|machine| machine.get(state.0 as usize))
.map(String::as_ref)
}
fn machine_name(&self, machine: &SmHandle) -> Option<&str> {
self.machine_names
.get(machine.0 as usize)
.map(String::as_ref)
}
/// Get machine handle for provided machine name
pub fn machine_handle(&self, name: &str) -> Option<SmHandle> {
self.machines()
.find(|handle| handle.1 == name)
.map(|hn| hn.0)
}
}