diff --git a/rustorio-engine/src/gamemodes.rs b/rustorio-engine/src/gamemodes.rs index 66f9cb2..58a0cc3 100644 --- a/rustorio-engine/src/gamemodes.rs +++ b/rustorio-engine/src/gamemodes.rs @@ -1,14 +1,15 @@ //! A game mode defines the starting resources and victory conditions for a game. -use crate::tick::Tick; +use crate::{resources::TokenOfCreation, tick::Tick}; /// The starting resources of a game mode. These are provided to the player at the beginning of the game. pub trait StartingResources { /// Called once at the start of the game before control is handed to the player to create the starting resources. /// /// # Parameters + /// - `token`: Token that allows creating resources out of thin air for the duration of this function. /// - `tick`: The current game tick. Since this method is called at the start of the game, this will always be tick 0. - fn init(tick: &Tick) -> Self; + fn init(token: &TokenOfCreation, tick: &Tick) -> Self; } /// A game mode defines the starting resources and victory conditions for a game. diff --git a/rustorio-engine/src/lib.rs b/rustorio-engine/src/lib.rs index c2dce5f..f802ea9 100644 --- a/rustorio-engine/src/lib.rs +++ b/rustorio-engine/src/lib.rs @@ -17,6 +17,7 @@ pub mod tick; use std::{io::Write, net::TcpStream, sync::Once}; +use resources::creation_token; use rustorio_common::cli::{PORT_ENV_NAME, PlayOutput}; pub use crate::resources::{ResourceType, bundle, resource}; @@ -37,7 +38,7 @@ pub fn play(main: fn(Tick, G::StartingResources) -> (Tick, G::Victo ); } let tick = Tick::start(G::DEFAULT_MAX_TICK); - let start_resources = G::StartingResources::init(&tick); + let start_resources = G::StartingResources::init(creation_token(), &tick); let (tick, _points) = main(tick, start_resources); if let Ok(port) = std::env::var(PORT_ENV_NAME) { let port = port.parse().unwrap_or_else(|error| panic!("Failed to pass env variable '{PORT_ENV_NAME}' as port. Env var value: '{port}' Error: {error:?}")); diff --git a/rustorio-engine/src/machine.rs b/rustorio-engine/src/machine.rs index 7cd6e5e..134abbc 100644 --- a/rustorio-engine/src/machine.rs +++ b/rustorio-engine/src/machine.rs @@ -9,7 +9,8 @@ //! ``` use crate::{ - recipe::{MultiBundleEx, Recipe, RecipeEx}, + recipe::{MultiBundle, Recipe}, + resources::{TokenOfCreation, creation_token}, tick::{Tick, TickSnapshot}, }; @@ -69,7 +70,7 @@ pub struct Machine { crafting_time: u64, } -impl Machine { +impl Machine { fn new_inner(tick: TickSnapshot) -> Self { Self { inputs: Default::default(), @@ -80,7 +81,8 @@ impl Machine { } /// Build a new machine. - pub fn new(tick: &Tick) -> Self { + // Needs a token because this can be used to create resources by making a custom recipe. + pub fn new(_token: &TokenOfCreation, tick: &Tick) -> Self { Self::new_inner(tick.snapshot()) } @@ -96,20 +98,27 @@ impl Machine { &mut self.outputs } - fn iter_inputs(&mut self) -> impl Iterator { - ::iter(&mut self.inputs) + fn iter_inputs<'a>( + &'a mut self, + token: &'a TokenOfCreation, + ) -> impl Iterator { + ::iter_mut(token, &mut self.inputs) } - fn iter_outputs(&mut self) -> impl Iterator { - ::iter(&mut self.outputs) + fn iter_outputs<'a>( + &'a mut self, + token: &'a TokenOfCreation, + ) -> impl Iterator { + ::iter_mut(token, &mut self.outputs) } /// Changes the [`Recipe`](crate::recipe) of the machine. /// Returns the original machine if the machine has any inputs or outputs. - pub fn change_recipe( + pub fn change_recipe( mut self, recipe: R2, ) -> Result, MachineNotEmptyError> { + let token = creation_token(); let _ = recipe; fn find_nonempty<'a>( mut iter: impl Iterator, @@ -121,8 +130,8 @@ impl Machine { } if let Some((resource_type, amount, location)) = - find_nonempty(self.iter_inputs(), BufferLocation::Input) - .or_else(|| find_nonempty(self.iter_outputs(), BufferLocation::Output)) + find_nonempty(self.iter_inputs(token), BufferLocation::Input) + .or_else(|| find_nonempty(self.iter_outputs(token), BufferLocation::Output)) { Err(MachineNotEmptyError { machine: self, @@ -137,26 +146,27 @@ impl Machine { fn tick(&mut self, tick: &Tick) { let time_elapsed = self.tick.advance_to(tick).unwrap(); + let token = creation_token(); self.crafting_time += time_elapsed; let crafting_time = self.crafting_time; let count = self - .iter_inputs() + .iter_inputs(token) .map(|(_, needed, current)| *current / needed) .chain((R::TIME > 0).then(|| (crafting_time / R::TIME).try_into().unwrap())) .min() .unwrap(); - for (_, needed, current) in self.iter_inputs() { + for (_, needed, current) in self.iter_inputs(token) { *current -= count * needed; } - for (_, needed, current) in self.iter_outputs() { + for (_, needed, current) in self.iter_outputs(token) { *current += count * needed; } self.crafting_time -= u64::from(count) * R::TIME; if self - .iter_inputs() + .iter_inputs(token) .any(|(_, needed, current)| *current < needed) { self.crafting_time = 0; diff --git a/rustorio-engine/src/recipe.rs b/rustorio-engine/src/recipe.rs index f955cef..9a8bbcc 100644 --- a/rustorio-engine/src/recipe.rs +++ b/rustorio-engine/src/recipe.rs @@ -4,7 +4,7 @@ pub use rustorio_derive::{Recipe, recipe_doc}; use crate::{ ResourceType, Sealed, - resources::{Bundle, Resource}, + resources::{Bundle, Resource, TokenOfCreation, creation_token}, tick::Tick, }; @@ -19,19 +19,33 @@ pub trait MultiBundle: Sized + std::fmt::Debug { const AMOUNTS: Self::AmountsType; /// Count the number of bundle tuples available in the given resource tuple. - fn bundle_count(res: &Self::AsResources) -> u32; + fn bundle_count(res: &Self::AsResources) -> u32 { + Self::iter(res) + .map(|(_, expected, current)| current / expected) + .min() + .unwrap_or(u32::MAX) + } /// Add the bundle tuple to the resource tuple. fn add(res: &mut Self::AsResources, bundle: Self); /// Pop a bundle tuple from a resource tuple, if there are enough resources. fn bundle(res: &mut Self::AsResources) -> Option; -} -#[doc(hidden)] -pub trait MultiBundleEx: MultiBundle { - /// Factory function to create a new bundle tuple. - fn new_bundle() -> Self; + /// Create a new bundle tuple out of thin air. + /// + /// For use in mods only, cannot be used from the game. + fn new_bundle(token: &TokenOfCreation) -> Self; + + /// Iterate over the resources, returning for each the resource name, per-bundle expected + /// amount, and current amount. + fn iter(items: &Self::AsResources) -> impl Iterator; + /// Iterate over the resources, giving direct mutable access to the amounts. - fn iter(items: &mut Self::AsResources) -> impl Iterator; + /// + /// For use in mods only, cannot be used from the game. + fn iter_mut<'a>( + token: &'a TokenOfCreation, + items: &'a mut Self::AsResources, + ) -> impl Iterator; } // Special untupled case, for e.g. tech recipes that don't return a tuple. @@ -41,22 +55,23 @@ impl MultiBundle for Bundle { type AmountsType = (u32,); const AMOUNTS: Self::AmountsType = (N1,); - fn bundle_count(res: &Self::AsResources) -> u32 { - <(Self,) as MultiBundle>::bundle_count(res) - } fn add(res: &mut Self::AsResources, bundle: Self) { <(Self,) as MultiBundle>::add(res, (bundle,)) } fn bundle(res: &mut Self::AsResources) -> Option { <(Self,) as MultiBundle>::bundle(res).map(|(r,)| r) } -} -impl MultiBundleEx for Bundle { - fn new_bundle() -> Self { - <(Self,) as MultiBundleEx>::new_bundle().0 + fn new_bundle(token: &TokenOfCreation) -> Self { + <(Self,) as MultiBundle>::new_bundle(token).0 } - fn iter(items: &mut Self::AsResources) -> impl Iterator { - <(Self,) as MultiBundleEx>::iter(items) + fn iter(items: &Self::AsResources) -> impl Iterator { + <(Self,) as MultiBundle>::iter(items) + } + fn iter_mut<'a>( + token: &'a TokenOfCreation, + items: &'a mut Self::AsResources, + ) -> impl Iterator { + <(Self,) as MultiBundle>::iter_mut(token, items) } } @@ -93,23 +108,13 @@ macro_rules! impl_multi_bundle { $($amount,)* ); - fn bundle_count(res: &Self::AsResources) -> u32 { - [ - $( - res.$n.amount() / $amount, - )* - ].into_iter().min().unwrap_or(u32::MAX) - } fn add(res: &mut Self::AsResources, bundle: Self) { $( res.$n += bundle.$n; )* } fn bundle(res: &mut Self::AsResources) -> Option { - let enough_resources = true $( - && res.$n.amount() >= $amount - )*; - if enough_resources { + if Self::bundle_count(res) >= 1 { Some(( $( res.$n.bundle().ok()?, @@ -119,28 +124,38 @@ macro_rules! impl_multi_bundle { None } } - } - - #[allow(unused)] - impl< - $($ty: ResourceType, const $amount: u32),* - > MultiBundleEx for ($(Bundle<$ty, $amount>,)*) - { #[allow(clippy::unused_unit)] - fn new_bundle() -> Self { + fn new_bundle(token: &TokenOfCreation) -> Self { ( $( - replace_expr!($ty, crate::resources::bundle()), + replace_expr!($ty, crate::resources::bundle(token)), )* ) } - fn iter(items: &mut Self::AsResources) -> impl Iterator { + fn iter( + items: &Self::AsResources, + ) -> impl Iterator { + [ + $( + ( + <$ty as ResourceType>::NAME, + Self::AMOUNTS.$n, + items.$n.amount(), + ), + )* + ] + .into_iter() + } + fn iter_mut<'a>( + token: &'a TokenOfCreation, + items: &'a mut Self::AsResources, + ) -> impl Iterator { [ $( ( <$ty as ResourceType>::NAME, Self::AMOUNTS.$n, - crate::resources::resource_amount_mut(&mut items.$n), + items.$n.amount_mut(token), ), )* ] @@ -201,17 +216,13 @@ pub trait Recipe { type OutputResources: std::fmt::Debug + Default; } -#[doc(hidden)] -pub trait RecipeEx: Recipe {} -impl> RecipeEx for R {} - /// A recipe that can be hand-crafted by the player. -pub trait HandRecipe: std::fmt::Debug + Sealed + RecipeEx { +pub trait HandRecipe: std::fmt::Debug + Sealed + Recipe { /// Crafts the recipe by consuming the input bundle and producing the output bundle. /// Advances the provided `Tick` by the recipe's time. fn craft(tick: &mut Tick, inputs: Self::InputBundle) -> Self::OutputBundle { let _ = inputs; tick.advance_by(Self::TIME); - Self::OutputBundle::new_bundle() + Self::OutputBundle::new_bundle(creation_token()) } } diff --git a/rustorio-engine/src/research.rs b/rustorio-engine/src/research.rs index f095bb2..58581ca 100644 --- a/rustorio-engine/src/research.rs +++ b/rustorio-engine/src/research.rs @@ -9,7 +9,7 @@ pub use rustorio_derive::{TechnologyEx, technology_doc}; use crate::{ ResourceType, Sealed, - recipe::{MultiBundle, MultiBundleEx, Recipe}, + recipe::{MultiBundle, Recipe}, resources::{Bundle, Resource}, }; @@ -36,7 +36,7 @@ pub trait Technology: Sealed + Debug + Sized + TechnologyEx { pub trait TechnologyEx { /// A type guaranteed to contain exactly the input resources for one research point. /// Used in hand crafting. - type InputBundle: MultiBundleEx; + type InputBundle: MultiBundle; /// The amount of ticks it takes to create one research point for this technology. const POINT_RECIPE_TIME: u64; /// How many of this technology's research points (`ResearchPoint`) are needed to complete the research. diff --git a/rustorio-engine/src/resources.rs b/rustorio-engine/src/resources.rs index daa5012..9f48281 100644 --- a/rustorio-engine/src/resources.rs +++ b/rustorio-engine/src/resources.rs @@ -104,9 +104,23 @@ where phantom: PhantomData, } +/// A token required for any out-of-thin-air creation of resources. This can only be created with +/// [`creation_token`]; this is meant for mods and engine internals, and must not be exposed to +/// players. +// APIs that need this token take a borrow in order to enable APIs like `StartingResources` where +// we give temporary access to the token. +#[non_exhaustive] +pub struct TokenOfCreation; + +/// Create a token that allows out-of-thin-air creation of resources. Using it basically allows +/// cheating; for this reason this function must not be re-exported to players. +pub const fn creation_token() -> &'static TokenOfCreation { + &TokenOfCreation +} + /// Creates a new [`Resource`] with the specified amount. /// Should not be reexported in mods. -pub const fn resource(amount: u32) -> Resource +pub const fn resource(_token: &TokenOfCreation, amount: u32) -> Resource where R: ResourceType, { @@ -115,11 +129,14 @@ where /// Returns a mutable reference to the amount of resource contained in the given [`Resource`]. /// Should not be reexported in mods. -pub const fn resource_amount_mut(resource: &mut Resource) -> &mut u32 +pub const fn resource_amount_mut<'a, R>( + token: &'a TokenOfCreation, + resource: &'a mut Resource, +) -> &'a mut u32 where R: ResourceType, { - resource.amount_mut() + resource.amount_mut(token) } impl Resource @@ -134,7 +151,7 @@ where } } - pub(crate) const fn new(amount: u32) -> Self { + const fn new(amount: u32) -> Self { Self { amount, phantom: PhantomData, @@ -146,7 +163,7 @@ where self.amount } - const fn amount_mut(&mut self) -> &mut u32 { + pub(crate) const fn amount_mut(&mut self, _token: &TokenOfCreation) -> &mut u32 { &mut self.amount } @@ -186,9 +203,8 @@ where /// Empties this [`Resource`], returning all contained resources as a new [`Resource`]. pub const fn empty(&mut self) -> Self { - let amount = self.amount; - self.amount = 0; - Resource::new(amount) + #[allow(clippy::mem_replace_with_default)] // doesn't work in `const` + std::mem::replace(self, Self::new_empty()) } /// Empties this [`Resource`] except for the specified amount, returning the emptied resources as a new [`Resource`]. @@ -320,7 +336,7 @@ where /// Creates a new [`Bundle`] with the specified resource type and amount. /// Should not be reexported in mods. -pub fn bundle() -> Bundle +pub fn bundle(_token: &TokenOfCreation) -> Bundle where R: ResourceType, { diff --git a/rustorio/src/buildings.rs b/rustorio/src/buildings.rs index 1a69ffc..8ed36c8 100644 --- a/rustorio/src/buildings.rs +++ b/rustorio/src/buildings.rs @@ -9,8 +9,9 @@ use rustorio_engine::{ machine::{Machine, MachineNotEmptyError}, - recipe::{MultiBundle, Recipe, RecipeEx}, + recipe::{MultiBundle, Recipe}, research::{TechRecipe, Technology, tech_recipe}, + resources::creation_token, }; use crate::{ @@ -39,8 +40,9 @@ impl Assembler { copper_wires: Bundle, iron: Bundle, ) -> Self { + let token = creation_token(); let _ = (recipe, copper_wires, iron); - Self(Machine::new(tick)) + Self(Machine::new(token, tick)) } /// Changes the [`Recipe`](crate::recipes) of the assembler. @@ -91,8 +93,9 @@ pub struct Furnace(Machine); impl Furnace { /// Builds a furnace. Costs 10 [iron](crate::resources::Iron). pub fn build(tick: &Tick, recipe: R, iron: Bundle) -> Self { + let token = creation_token(); let _ = (recipe, iron); - Self(Machine::new(tick)) + Self(Machine::new(token, tick)) } /// Changes the [`Recipe`](crate::recipes) of the furnace. @@ -134,11 +137,11 @@ impl Furnace { #[derive(Debug)] pub struct Lab(Machine>) where - TechRecipe: RecipeEx; + TechRecipe: Recipe; impl Lab where - TechRecipe: RecipeEx, + TechRecipe: Recipe, { /// Creates a new `Lab` producing research points for the specified technology. pub fn build( @@ -147,8 +150,9 @@ where iron: Bundle, copper: Bundle, ) -> Self { + let token = creation_token(); let _ = (technology, iron, copper); - Self(Machine::new(tick)) + Self(Machine::new(token, tick)) } /// Changes the technology this `Lab` is producing research points for. @@ -157,7 +161,7 @@ where technology: &T2, ) -> Result, MachineNotEmptyError> where - TechRecipe: RecipeEx, + TechRecipe: Recipe, { let _ = technology; match self.0.change_recipe(tech_recipe()) { diff --git a/rustorio/src/gamemodes.rs b/rustorio/src/gamemodes.rs index 8e97a55..94e43c4 100644 --- a/rustorio/src/gamemodes.rs +++ b/rustorio/src/gamemodes.rs @@ -6,6 +6,7 @@ use rustorio_engine::{ bundle, gamemodes::{GameMode, StartingResources}, mod_reexports::Tick, + resources::TokenOfCreation, }; use crate::{ @@ -29,11 +30,11 @@ pub struct TutorialStartingResources { } impl StartingResources for TutorialStartingResources { - fn init(tick: &Tick) -> Self { + fn init(token: &TokenOfCreation, tick: &Tick) -> Self { Self { - iron: bundle(), - iron_territory: Territory::new(tick, 5), - copper_territory: Territory::new(tick, 5), + iron: bundle(token), + iron_territory: Territory::new(token, tick, 5), + copper_territory: Territory::new(token, tick, 5), guide: Guide, } } @@ -63,11 +64,11 @@ pub struct StandardStartingResources { pub steel_technology: SteelTechnology, } impl StartingResources for StandardStartingResources { - fn init(tick: &Tick) -> Self { + fn init(token: &TokenOfCreation, tick: &Tick) -> Self { Self { - iron: bundle(), - iron_territory: Territory::new(tick, 20), - copper_territory: Territory::new(tick, 20), + iron: bundle(token), + iron_territory: Territory::new(token, tick, 20), + copper_territory: Territory::new(token, tick, 20), steel_technology: SteelTechnology, } } diff --git a/rustorio/src/recipes.rs b/rustorio/src/recipes.rs index a201334..bbf3e64 100644 --- a/rustorio/src/recipes.rs +++ b/rustorio/src/recipes.rs @@ -9,7 +9,7 @@ use std::fmt::Debug; use rustorio_engine::{ Sealed, - recipe::{HandRecipe, Recipe, RecipeEx, recipe_doc}, + recipe::{HandRecipe, Recipe, recipe_doc}, }; use crate::{ @@ -18,7 +18,7 @@ use crate::{ }; /// Any recipe that implements this trait can be used in an [`Assembler`](crate::buildings::Assembler). -pub trait AssemblerRecipe: Debug + Sealed + RecipeEx {} +pub trait AssemblerRecipe: Debug + Sealed + Recipe {} #[derive(Debug, Clone, Copy, Recipe)] #[recipe_doc] @@ -84,7 +84,7 @@ impl Sealed for PointRecipe {} impl AssemblerRecipe for PointRecipe {} /// Any recipe that implements this trait can be used in a [`Furnace`](crate::buildings::Furnace). -pub trait FurnaceRecipe: Debug + Sealed + RecipeEx {} +pub trait FurnaceRecipe: Debug + Sealed + Recipe {} /// A [`Furnace`](crate::buildings::Furnace) recipe that smelts iron ore into iron. #[derive(Debug, Clone, Copy, Recipe)] diff --git a/rustorio/src/territory.rs b/rustorio/src/territory.rs index 4ac0f40..0d3fd9a 100644 --- a/rustorio/src/territory.rs +++ b/rustorio/src/territory.rs @@ -8,6 +8,7 @@ use rustorio_engine::{ ResourceType, bundle, mod_reexports::{Bundle, Resource, Tick}, resource, + resources::{TokenOfCreation, creation_token}, tick::TickSnapshot, }; @@ -72,7 +73,7 @@ pub struct Territory { impl Territory { /// Creates a new territory that can hold up to `max_miners` miners. - pub(crate) const fn new(tick: &Tick, max_miners: u32) -> Self { + pub(crate) const fn new(_token: &TokenOfCreation, tick: &Tick, max_miners: u32) -> Self { Self { tick: tick.snapshot(), max_miners, @@ -92,10 +93,12 @@ impl Territory { } fn tick(&mut self, tick: &Tick) { + let token = creation_token(); let time_elapsed = self.tick.advance_to(tick).unwrap(); for miner_tick in &mut self.miners { *miner_tick += time_elapsed; self.resources += resource( + token, u32::try_from(*miner_tick / Ore::MINING_TIME) .expect("Number of resources exceeds u32::MAX."), ); @@ -105,9 +108,10 @@ impl Territory { /// Mines ore by hand, advancing the tick by [`OreType::MINING_TIME`] for each unit mined. pub fn hand_mine(&mut self, tick: &mut Tick) -> Bundle { + let token = creation_token(); self.tick(tick); tick.advance_by((u64::from(AMOUNT)) * Ore::MINING_TIME); - bundle() + bundle(token) } /// Adds a miner to the territory.