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    iter::Sum,
10    marker::PhantomData,
11    ops::{Add, AddAssign},
12};
13
14use crate::Sealed;
15
16/// A type that represents a specific kind of resource in the game.
17/// Implementors of this trait represent different resource types, such as iron, copper, or science packs.
18/// Only useful as a type parameter; has no associated methods.
19///
20/// ## Modding
21///
22/// To define a new resource type, use the `resource_type!` macro.
23pub trait ResourceType: Sealed + Debug {
24    /// A human readable name for this resource type.
25    const NAME: &'static str;
26}
27
28/// Macro to define a new resource type.
29///
30/// # Example
31/// ```rust
32/// use rustorio_engine::resource_type;
33/// resource_type!(
34///     /// Gold ingots used for advanced crafting.
35///     Gold);
36/// ```
37#[macro_export]
38macro_rules! resource_type {
39
40    ($(#[$outer:meta])*
41    $name:ident) => {
42        $(#[$outer])*
43        #[derive(Debug)]
44        pub struct $name;
45        impl $crate::Sealed for $name {}
46        impl $crate::ResourceType for $name {
47            const NAME: &'static str = stringify!($name);
48        }
49    };
50}
51
52/// Error returned when there are insufficient resources in a [`Resource`] to fulfill a request.
53#[derive(Debug, Clone)]
54pub struct InsufficientResourceError<Resource: ResourceType> {
55    /// The amount of resource that was requested.
56    pub requested_amount: u32,
57    /// The amount of resource that was actually available.
58    pub available_amount: u32,
59    phantom: PhantomData<Resource>,
60}
61
62impl<Resource: ResourceType> InsufficientResourceError<Resource> {
63    /// Creates a new `InsufficientResourceError`.
64    pub const fn new(requested_amount: u32, available_amount: u32) -> Self {
65        Self {
66            requested_amount,
67            available_amount,
68            phantom: PhantomData,
69        }
70    }
71}
72
73impl<Resource: ResourceType> Display for InsufficientResourceError<Resource> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(
76            f,
77            "Insufficient {:?}: requested {}, but only {} available",
78            Resource::NAME,
79            self.requested_amount,
80            self.available_amount
81        )
82    }
83}
84
85/// Holds an arbitrary amount of a resource.
86/// A [`Resource`] object can be split into smaller parts, combined or [`Bundle`]s can be extracted from them.
87#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
88#[must_use = "This resource is being dropped without being used. If this is intentional, use the `let _ = resource;` pattern to silence this warning."]
89pub struct Resource<Content: ResourceType> {
90    /// The amount of the resource contained in this [`Resource`].
91    pub(crate) amount: u32,
92    phantom: PhantomData<Content>,
93}
94
95/// Creates a new [`Resource`] with the specified amount.
96/// Should not be reexported in mods.
97pub const fn resource<Content: ResourceType>(amount: u32) -> Resource<Content> {
98    Resource::new(amount)
99}
100
101/// Returns a mutable reference to the amount of resource contained in the given [`Resource`].
102/// Should not be reexported in mods.
103pub const fn resource_amount_mut<Content: ResourceType>(
104    resource: &mut Resource<Content>,
105) -> &mut u32 {
106    resource.amount_mut()
107}
108
109impl<Content: ResourceType> Resource<Content> {
110    /// Creates a new empty [`Resource`].
111    pub const fn new_empty() -> Self {
112        Self {
113            amount: 0,
114            phantom: PhantomData,
115        }
116    }
117
118    pub(crate) const fn new(amount: u32) -> Self {
119        Self {
120            amount,
121            phantom: PhantomData,
122        }
123    }
124
125    /// The current amount of the resource contained in this [`Resource`].
126    pub const fn amount(&self) -> u32 {
127        self.amount
128    }
129
130    const fn amount_mut(&mut self) -> &mut u32 {
131        &mut self.amount
132    }
133
134    /// Splits the [`Resource`] into two smaller parts.
135    /// If there are insufficient resources in the [`Resource`], it returns an error with the original resource.
136    pub const fn split(self, amount: u32) -> Result<(Self, Self), Self> {
137        if let Some(remaining) = self.amount.checked_sub(amount) {
138            Ok((Self::new(remaining), Self::new(amount)))
139        } else {
140            Err(self)
141        }
142    }
143
144    /// Removes a specified amount of resources from this [`Resource`] and returns them as a new [`Resource`].
145    /// If there are insufficient resources in the [`Resource`], it returns `None`.
146    pub const fn split_off(
147        &mut self,
148        amount: u32,
149    ) -> Result<Self, InsufficientResourceError<Content>> {
150        if let Some(remaining) = self.amount.checked_sub(amount) {
151            self.amount = remaining;
152            Ok(Resource::new(amount))
153        } else {
154            Err(InsufficientResourceError::new(amount, self.amount))
155        }
156    }
157
158    /// Removes up to the specified amount of resources from this [`Resource`] and returns them as a new [`Resource`].
159    /// If there are insufficient resources in the [`Resource`], it returns all available resources.
160    pub const fn split_off_max(&mut self, amount: u32) -> Self {
161        if let Some(remaining) = self.amount.checked_sub(amount) {
162            self.amount = remaining;
163            Resource::new(amount)
164        } else {
165            let all = self.amount;
166            self.amount = 0;
167            Resource::new(all)
168        }
169    }
170
171    /// Empties this [`Resource`], returning all contained resources as a new [`Resource`].
172    pub const fn empty(&mut self) -> Self {
173        let amount = self.amount;
174        self.amount = 0;
175        Resource::new(amount)
176    }
177
178    /// Empties this [`Resource`] except for the specified amount, returning the emptied resources as a new [`Resource`].
179    pub const fn empty_except(&mut self, amount: u32) -> Self {
180        let to_empty = self.amount.saturating_sub(amount);
181        self.amount -= to_empty;
182        Resource::new(to_empty)
183    }
184
185    /// Empties this [`Resource`] into another [`Resource`], transferring all contained resources.
186    pub const fn empty_into(&mut self, other: &mut Self) {
187        other.amount += self.amount;
188        self.amount = 0;
189    }
190
191    /// Adds the entire contents of another resource container to this one.
192    pub fn add(&mut self, other: impl Into<Self>) {
193        self.amount += other.into().amount();
194    }
195
196    /// Consumes a [`Bundle`] of the same resource type and adds the contained resources to this [`Resource`].
197    pub const fn add_bundle<const AMOUNT: u32>(&mut self, bundle: Bundle<Content, AMOUNT>) {
198        self.amount += bundle.amount();
199    }
200
201    /// Takes a specified amount of resources from this [`Resource`] and puts it into a [`Bundle`].
202    pub const fn bundle<const AMOUNT: u32>(
203        &mut self,
204    ) -> Result<Bundle<Content, AMOUNT>, InsufficientResourceError<Content>> {
205        if let Some(remaining) = self.amount.checked_sub(AMOUNT) {
206            self.amount = remaining;
207            Ok(Bundle::new())
208        } else {
209            Err(InsufficientResourceError::new(AMOUNT, self.amount))
210        }
211    }
212}
213
214impl<Content: ResourceType> Display for Resource<Content> {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        write!(
217            f,
218            "{amount} {content}",
219            amount = self.amount,
220            content = Content::NAME
221        )
222    }
223}
224
225impl<Content: ResourceType> PartialOrd<u32> for Resource<Content> {
226    fn partial_cmp(&self, other: &u32) -> Option<std::cmp::Ordering> {
227        Some(self.amount.cmp(other))
228    }
229}
230
231impl<Content: ResourceType> PartialEq<u32> for Resource<Content> {
232    fn eq(&self, other: &u32) -> bool {
233        self.amount == *other
234    }
235}
236
237impl<Content: ResourceType> PartialOrd<Resource<Content>> for u32 {
238    fn partial_cmp(&self, other: &Resource<Content>) -> Option<std::cmp::Ordering> {
239        Some(self.cmp(&other.amount))
240    }
241}
242
243impl<Content: ResourceType> PartialEq<Resource<Content>> for u32 {
244    fn eq(&self, other: &Resource<Content>) -> bool {
245        *self == other.amount
246    }
247}
248
249impl<Content: ResourceType> AddAssign for Resource<Content> {
250    fn add_assign(&mut self, rhs: Self) {
251        self.amount += rhs.amount
252    }
253}
254
255impl<Content: ResourceType> Add for Resource<Content> {
256    type Output = Self;
257
258    fn add(mut self, rhs: Self) -> Self::Output {
259        self += rhs;
260        self
261    }
262}
263
264impl<Content: ResourceType> Sum for Resource<Content> {
265    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
266        iter.fold(Resource::new_empty(), |cur, next| cur + next)
267    }
268}
269
270/// Contains a fixed (compile-time known) amount of a resource.
271/// A [`Bundle`] can be used to build structures or as input for recipes.
272#[derive(Debug)]
273#[must_use = "This bundle is being dropped without being used. If this is intentional, use the `let _ = bundle;` pattern to silence this warning."]
274pub struct Bundle<Content: ResourceType, const AMOUNT: u32> {
275    dummy: PhantomData<Content>,
276}
277
278/// Creates a new [`Bundle`] with the specified resource type and amount.
279/// Should not be reexported in mods.
280pub fn bundle<Content: ResourceType, const AMOUNT: u32>() -> Bundle<Content, AMOUNT> {
281    Bundle::new()
282}
283
284/// A compile-time assertion that a condition is true.
285pub struct Assert<const OK: bool>;
286/// A trait implemented only for `Assert<true>`.
287pub trait IsTrue {}
288impl IsTrue for Assert<true> {}
289
290impl<Content: ResourceType, const AMOUNT: u32> Bundle<Content, AMOUNT> {
291    /// The fixed amount of resource contained in this [`Bundle`].
292    pub const AMOUNT: u32 = AMOUNT;
293
294    pub(crate) const fn new() -> Self {
295        Self { dummy: PhantomData }
296    }
297
298    /// Returns the fixed amount of resource contained in this [`Bundle`].
299    pub const fn amount(&self) -> u32 {
300        AMOUNT
301    }
302
303    /// Splits this [`Bundle`] into two smaller [`Bundle`]s with the specified amounts.
304    /// The sum of `AMOUNT1` and `AMOUNT2` must equal the amount of this [`Bundle`].
305    pub const fn split<const AMOUNT1: u32, const AMOUNT2: u32>(
306        self,
307    ) -> (Bundle<Content, AMOUNT1>, Bundle<Content, AMOUNT2>)
308    where
309        Assert<{ AMOUNT1 + AMOUNT2 == AMOUNT }>: IsTrue,
310    {
311        (Bundle::new(), Bundle::new())
312    }
313
314    /// Converts this [`Bundle`] into a [`Resource`] with the same resource type and amount.
315    pub const fn to_resource(self) -> Resource<Content> {
316        Resource::new(AMOUNT)
317    }
318}
319
320impl<Content: ResourceType, const AMOUNT: u32> AddAssign<Bundle<Content, AMOUNT>>
321    for Resource<Content>
322{
323    fn add_assign(&mut self, bundle: Bundle<Content, AMOUNT>) {
324        let _ = bundle;
325        self.amount += AMOUNT;
326    }
327}
328
329impl<Content: ResourceType, const AMOUNT: u32> Add<Bundle<Content, AMOUNT>> for Resource<Content> {
330    type Output = Self;
331
332    fn add(mut self, rhs: Bundle<Content, AMOUNT>) -> Self::Output {
333        self += rhs;
334        self
335    }
336}
337
338impl<Content: ResourceType, const AMOUNT: u32> Add<Resource<Content>> for Bundle<Content, AMOUNT> {
339    type Output = Resource<Content>;
340
341    fn add(self, mut rhs: Resource<Content>) -> Self::Output {
342        rhs += self;
343        rhs
344    }
345}
346
347impl<Content: ResourceType, const AMOUNT_LHS: u32, const AMOUNT_RHS: u32>
348    Add<Bundle<Content, AMOUNT_RHS>> for Bundle<Content, AMOUNT_LHS>
349where
350    [(); { AMOUNT_LHS + AMOUNT_RHS } as usize]:,
351{
352    type Output = Bundle<Content, { AMOUNT_LHS + AMOUNT_RHS }>;
353
354    fn add(self, rhs: Bundle<Content, AMOUNT_RHS>) -> Self::Output {
355        let _ = rhs;
356        Bundle::new()
357    }
358}
359
360impl<Content: ResourceType, const AMOUNT: u32> From<Bundle<Content, AMOUNT>> for Resource<Content> {
361    fn from(bundle: Bundle<Content, AMOUNT>) -> Self {
362        let _ = bundle;
363        Resource::new(AMOUNT)
364    }
365}