rustorio_engine/
resources.rs

1//! Resources are the fundamental units of value in Rustorio.
2//! Resources are held in either [`Resource`] or [`Bundle`] objects.
3//! [`Bundle`] objects are used to hold a fixed amount of a resource, while [`Resource`] objects can hold any amount.
4//!
5//! This module the core definitions for resources, including the `ResourceType` trait, the `Resource` and `Bundle` structs, and the macro to define new resources.
6
7use std::{
8    fmt::{Debug, Display},
9    marker::PhantomData,
10    ops::{Add, AddAssign},
11};
12
13use crate::Sealed;
14
15/// A type that represents a specific kind of resource in the game.
16/// Implementors of this trait represent different resource types, such as iron, copper, or science packs.
17/// Only useful as a type parameter; has no associated methods.
18///
19/// To define a new resource type, use the `resource_type!` macro.
20pub trait ResourceType: Sealed + Debug {
21    /// A human readable name for this resource type.
22    const NAME: &'static str;
23}
24
25/// Macro to define a new resource type.
26///
27/// # Example
28/// ```rust
29/// use rustorio_engine::resource_type;
30/// resource_type!(
31///     /// Gold ingots used for advanced crafting.
32///     Gold);
33/// ```
34#[macro_export]
35macro_rules! resource_type {
36
37    ($(#[$outer:meta])*
38    $name:ident) => {
39        $(#[$outer])*
40        #[derive(Debug)]
41        pub struct $name;
42        impl $crate::Sealed for $name {}
43        impl $crate::ResourceType for $name {
44            const NAME: &'static str = stringify!($name);
45        }
46    };
47}
48
49/// Error returned when there are insufficient resources in a [`Resource`] to fulfill a request.
50#[derive(Debug, Clone)]
51pub struct InsufficientResourceError<Resource: ResourceType> {
52    /// The amount of resource that was requested.
53    pub requested_amount: u32,
54    /// The amount of resource that was actually available.
55    pub available_amount: u32,
56    phantom: PhantomData<Resource>,
57}
58
59impl<Resource: ResourceType> InsufficientResourceError<Resource> {
60    /// Creates a new `InsufficientResourceError`.
61    pub fn new(requested_amount: u32, available_amount: u32) -> Self {
62        Self {
63            requested_amount,
64            available_amount,
65            phantom: PhantomData,
66        }
67    }
68}
69
70impl<Resource: ResourceType> Display for InsufficientResourceError<Resource> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(
73            f,
74            "Insufficient {:?}: requested {}, but only {} available",
75            Resource::NAME,
76            self.requested_amount,
77            self.available_amount
78        )
79    }
80}
81
82/// Holds an arbitrary amount of a resource.
83/// A [`Resource`] object can be split into smaller parts, combined or [`Bundle`]s can be extracted from them.
84#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
85pub struct Resource<Content: ResourceType> {
86    /// The amount of the resource contained in this [`Resource`].
87    pub amount: u32,
88    phantom: PhantomData<Content>,
89}
90
91/// Creates a new [`Resource`] with the specified amount.
92/// Should not be reexported in mods.
93pub fn resource<Content: ResourceType>(amount: u32) -> Resource<Content> {
94    Resource::new(amount)
95}
96
97impl<Content: ResourceType> Resource<Content> {
98    /// Creates a new empty [`Resource`].
99    pub fn empty() -> Self {
100        Self {
101            amount: 0,
102            phantom: PhantomData,
103        }
104    }
105
106    pub(crate) fn new(amount: u32) -> Self {
107        Self {
108            amount,
109            phantom: PhantomData,
110        }
111    }
112
113    /// The current amount of the resource contained in this [`Resource`].
114    pub fn amount(&self) -> u32 {
115        self.amount
116    }
117
118    /// Splits the [`Resource`] into two smaller parts.
119    /// If there are insufficient resources in the [`Resource`], it returns an error with the original resource.
120    pub fn split(self, amount: u32) -> Result<(Self, Self), Self> {
121        if let Some(remaining) = self.amount.checked_sub(amount) {
122            Ok((Self::new(remaining), Self::new(amount)))
123        } else {
124            Err(self)
125        }
126    }
127
128    /// Removes a specified amount of resources from this [`Resource`] and returns them as a new [`Resource`].
129    /// If there are insufficient resources in the [`Resource`], it returns `None`.
130    pub fn split_off(&mut self, amount: u32) -> Result<Self, InsufficientResourceError<Content>> {
131        if let Some(remaining) = self.amount.checked_sub(amount) {
132            self.amount = remaining;
133            Ok(Resource::new(amount))
134        } else {
135            Err(InsufficientResourceError::new(amount, self.amount))
136        }
137    }
138
139    /// Consumes a [`Bundle`] of the same resource type and adds the contained resources to this [`Resource`].
140    pub fn add_bundle<const AMOUNT: u32>(&mut self, bundle: Bundle<Content, AMOUNT>) {
141        self.amount += bundle.amount();
142    }
143
144    /// Takes a specified amount of resources from this [`Resource`] and puts it into a [`Bundle`].
145    pub fn bundle<const AMOUNT: u32>(&mut self) -> Result<Bundle<Content, AMOUNT>, InsufficientResourceError<Content>> {
146        if let Some(remaining) = self.amount.checked_sub(AMOUNT) {
147            self.amount = remaining;
148            Ok(Bundle::new())
149        } else {
150            Err(InsufficientResourceError::new(AMOUNT, self.amount))
151        }
152    }
153}
154
155impl<Content: ResourceType> Display for Resource<Content> {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(f, "{:?} x {}", Content::NAME, self.amount)
158    }
159}
160
161impl<Content: ResourceType> PartialOrd<u32> for Resource<Content> {
162    fn partial_cmp(&self, other: &u32) -> Option<std::cmp::Ordering> {
163        Some(self.amount.cmp(other))
164    }
165}
166
167impl<Content: ResourceType> PartialEq<u32> for Resource<Content> {
168    fn eq(&self, other: &u32) -> bool {
169        self.amount == *other
170    }
171}
172
173impl<Content: ResourceType> PartialOrd<Resource<Content>> for u32 {
174    fn partial_cmp(&self, other: &Resource<Content>) -> Option<std::cmp::Ordering> {
175        Some(self.cmp(&other.amount))
176    }
177}
178
179impl<Content: ResourceType> PartialEq<Resource<Content>> for u32 {
180    fn eq(&self, other: &Resource<Content>) -> bool {
181        *self == other.amount
182    }
183}
184
185impl<Content: ResourceType> AddAssign for Resource<Content> {
186    fn add_assign(&mut self, rhs: Self) {
187        self.amount += rhs.amount
188    }
189}
190
191impl<Content: ResourceType> Add for Resource<Content> {
192    type Output = Self;
193
194    fn add(mut self, rhs: Self) -> Self::Output {
195        self += rhs;
196        self
197    }
198}
199
200/// Contains a fixed (compile-time known) amount of a resource.
201/// A [`Bundle`] can be used to build structures or as input for recipes.
202pub struct Bundle<Content: ResourceType, const AMOUNT: u32> {
203    dummy: PhantomData<Content>,
204}
205
206/// Creates a new [`Bundle`] with the specified resource type and amount.
207/// Should not be reexported in mods.
208pub fn bundle<Content: ResourceType, const AMOUNT: u32>() -> Bundle<Content, AMOUNT> {
209    Bundle::new()
210}
211
212impl<Content: ResourceType, const AMOUNT: u32> Bundle<Content, AMOUNT> {
213    /// The fixed amount of resource contained in this [`Bundle`].
214    pub const AMOUNT: u32 = AMOUNT;
215
216    pub(crate) fn new() -> Self {
217        Self { dummy: PhantomData }
218    }
219
220    /// Returns the fixed amount of resource contained in this [`Bundle`].
221    pub fn amount(&self) -> u32 {
222        AMOUNT
223    }
224
225    /// Splits this [`Bundle`] into two smaller [`Bundle`]s with the specified amounts.
226    /// The sum of `AMOUNT1` and `AMOUNT2` must equal the amount of this [`Bundle`].
227    pub fn split<const AMOUNT1: u32, const AMOUNT2: u32>(self) -> (Bundle<Content, AMOUNT1>, Bundle<Content, AMOUNT2>)
228    where
229        // Enforce that AMOUNT1 + AMOUNT2 == AMOUNT at compile time
230        [(); AMOUNT as usize - (AMOUNT1 as usize + AMOUNT2 as usize)]:,
231        [(); (AMOUNT1 as usize + AMOUNT2 as usize) - AMOUNT as usize]:,
232    {
233        (Bundle::new(), Bundle::new())
234    }
235
236    /// Converts this [`Bundle`] into a [`Resource`] with the same resource type and amount.
237    pub fn to_resource(self) -> Resource<Content> {
238        Resource::new(AMOUNT)
239    }
240}
241
242impl<Content: ResourceType, const AMOUNT: u32> AddAssign<Bundle<Content, AMOUNT>> for Resource<Content> {
243    fn add_assign(&mut self, bundle: Bundle<Content, AMOUNT>) {
244        let _ = bundle;
245        self.amount += AMOUNT;
246    }
247}
248
249impl<Content: ResourceType, const AMOUNT: u32> Add<Bundle<Content, AMOUNT>> for Resource<Content> {
250    type Output = Self;
251
252    fn add(mut self, rhs: Bundle<Content, AMOUNT>) -> Self::Output {
253        self += rhs;
254        self
255    }
256}
257
258impl<Content: ResourceType, const AMOUNT: u32> Add<Resource<Content>> for Bundle<Content, AMOUNT> {
259    type Output = Resource<Content>;
260
261    fn add(self, mut rhs: Resource<Content>) -> Self::Output {
262        rhs += self;
263        rhs
264    }
265}
266
267impl<Content: ResourceType, const AMOUNT_LHS: u32, const AMOUNT_RHS: u32> Add<Bundle<Content, AMOUNT_RHS>>
268    for Bundle<Content, AMOUNT_LHS>
269where
270    [(); { AMOUNT_LHS + AMOUNT_RHS } as usize]:,
271{
272    type Output = Bundle<Content, { AMOUNT_LHS + AMOUNT_RHS }>;
273
274    fn add(self, rhs: Bundle<Content, AMOUNT_RHS>) -> Self::Output {
275        let _ = rhs;
276        Bundle::new()
277    }
278}
279
280impl<Content: ResourceType, const AMOUNT: u32> From<Bundle<Content, AMOUNT>> for Resource<Content> {
281    fn from(bundle: Bundle<Content, AMOUNT>) -> Self {
282        let _ = bundle;
283        Resource::new(AMOUNT)
284    }
285}