Skip to main content

rustorio_engine/
machine.rs

1//! Basic machine that can process recipes. Mods are encouraged to not export this, and instead define
2//! their own wrappers like
3//! ```rust
4//! use rustorio_engine::{machine::Machine, recipe::Recipe, Sealed};
5
6//! trait AssemblerRecipe: Recipe + Sealed {}
7
8//! pub struct Assembler<R: AssemblerRecipe>(Machine<R>);
9//! ```
10
11use crate::{
12    recipe::{Recipe, RecipeEx},
13    tick::Tick,
14};
15
16/// Location of a resource buffer in a machine.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum BufferLocation {
19    /// Input buffer.
20    Input,
21    /// Output buffer.
22    Output,
23}
24
25/// Error returned when trying to change a machine's recipe while it has non-empty input or output buffers.
26#[derive(Debug)]
27pub struct MachineNotEmptyError<M> {
28    /// Returning the machine with the original recipe.
29    pub machine: M,
30    /// Name of the type of the resource in the machine's buffers.
31    pub resource_type: &'static str,
32    /// The amount of the resource in the machine's buffers.
33    pub amount: u32,
34    /// Whether the resource is in the input or the output.
35    pub location: BufferLocation,
36}
37
38impl<M> MachineNotEmptyError<M> {
39    /// Converts the error to another machine type, keeping the same resource information.
40    pub fn map_machine<F, M2>(self, f: F) -> MachineNotEmptyError<M2>
41    where
42        F: FnOnce(M) -> M2,
43    {
44        MachineNotEmptyError {
45            machine: f(self.machine),
46            resource_type: self.resource_type,
47            amount: self.amount,
48            location: self.location,
49        }
50    }
51}
52
53impl<R: Recipe> std::fmt::Display for MachineNotEmptyError<R> {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(
56            f,
57            "Machine is not empty: machine has {} of resource {} in its {:?} buffer",
58            self.amount, self.resource_type, self.location
59        )
60    }
61}
62
63/// Basic machine that can process recipes.
64#[derive(Debug)]
65pub struct Machine<R: Recipe> {
66    inputs: R::Inputs,
67    outputs: R::Outputs,
68    tick: u64,
69    crafting_time: u64,
70}
71
72impl<R: RecipeEx> Machine<R> {
73    fn new_inner(tick: u64) -> Self {
74        Self {
75            inputs: R::new_inputs(),
76            outputs: R::new_outputs(),
77            tick,
78            crafting_time: 0,
79        }
80    }
81
82    /// Build a new machine.
83    pub fn new(tick: &Tick) -> Self {
84        Self::new_inner(tick.cur())
85    }
86
87    /// Update internal state and access input buffers.
88    pub fn inputs(&mut self, tick: &Tick) -> &mut R::Inputs {
89        self.tick(tick);
90        &mut self.inputs
91    }
92
93    /// Update internal state and access output buffers.
94    pub fn outputs(&mut self, tick: &Tick) -> &mut R::Outputs {
95        self.tick(tick);
96        &mut self.outputs
97    }
98
99    fn iter_inputs(&mut self) -> impl Iterator<Item = (&'static str, u32, &mut u32)> {
100        R::iter_inputs(&mut self.inputs)
101    }
102
103    fn iter_outputs(&mut self) -> impl Iterator<Item = (&'static str, u32, &mut u32)> {
104        R::iter_outputs(&mut self.outputs)
105    }
106
107    /// Changes the [`Recipe`](crate::recipe) of the machine.
108    /// Returns the original machine if the machine has any inputs or outputs.
109    pub fn change_recipe<R2: RecipeEx>(
110        mut self,
111        recipe: R2,
112    ) -> Result<Machine<R2>, MachineNotEmptyError<Self>> {
113        let _ = recipe;
114        fn find_nonempty<'a>(
115            mut iter: impl Iterator<Item = (&'static str, u32, &'a mut u32)>,
116            location: BufferLocation,
117        ) -> Option<(&'static str, u32, BufferLocation)> {
118            iter.find_map(|(resource_name, _needed, &mut current)| {
119                (current > 0).then_some((resource_name, current, location))
120            })
121        }
122
123        if let Some((resource_type, amount, location)) =
124            find_nonempty(self.iter_inputs(), BufferLocation::Input)
125                .or_else(|| find_nonempty(self.iter_outputs(), BufferLocation::Output))
126        {
127            Err(MachineNotEmptyError {
128                machine: self,
129                resource_type,
130                amount,
131                location,
132            })
133        } else {
134            Ok(Machine::new_inner(self.tick))
135        }
136    }
137
138    fn tick(&mut self, tick: &Tick) {
139        assert!(tick.cur() >= self.tick, "Tick must be non-decreasing");
140
141        self.crafting_time += tick.cur() - self.tick;
142        let crafting_time = self.crafting_time;
143        let count = self
144            .iter_inputs()
145            .map(|(_, needed, current)| *current / needed)
146            .chain((R::TIME > 0).then(|| (crafting_time / R::TIME).try_into().unwrap()))
147            .min()
148            .unwrap();
149
150        for (_, needed, current) in self.iter_inputs() {
151            *current -= count * needed;
152        }
153        for (_, needed, current) in self.iter_outputs() {
154            *current += count * needed;
155        }
156        self.crafting_time -= u64::from(count) * R::TIME;
157
158        if self
159            .iter_inputs()
160            .any(|(_, needed, current)| *current < needed)
161        {
162            self.crafting_time = 0;
163        }
164
165        self.tick = tick.cur();
166    }
167}