From e5b35cab0c17260c096f031c971e9dc1dd32dc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 23 Jul 2024 14:38:41 +0200 Subject: [PATCH 1/4] feat: RngSeed & ForkableSeed traits --- Cargo.toml | 8 +- src/component.rs | 38 +++++-- src/lib.rs | 2 +- src/plugin.rs | 19 +++- src/prelude.rs | 4 +- src/resource.rs | 39 +++++-- src/seed.rs | 103 +++++++++++------ src/traits.rs | 128 ++++++++++++++++++++- tests/{ => integration}/determinism.rs | 73 +----------- tests/integration/mod.rs | 2 + tests/integration/reseeding.rs | 152 +++++++++++++++++++++++++ tests/suite.rs | 5 + 12 files changed, 434 insertions(+), 139 deletions(-) rename tests/{ => integration}/determinism.rs (62%) create mode 100644 tests/integration/mod.rs create mode 100644 tests/integration/reseeding.rs create mode 100644 tests/suite.rs diff --git a/Cargo.toml b/Cargo.toml index fa69698..c7d5eb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Gonçalo Rica Pais da Silva "] edition = "2021" repository = "https://github.com/Bluefinger/bevy_rand" license = "MIT OR Apache-2.0" -version = "0.7.1" +version = "0.8.0" rust-version = "1.76.0" [workspace.dependencies] @@ -45,7 +45,7 @@ wyrand = ["bevy_prng/wyrand"] [dependencies] # bevy bevy.workspace = true -bevy_prng = { path = "bevy_prng", version = "0.7" } +bevy_prng = { path = "bevy_prng", version = "0.8" } # others getrandom = "0.2" @@ -59,10 +59,10 @@ serde_derive = { workspace = true, optional = true } # cannot be out of step with bevy_rand due to dependencies on traits # and implementations between the two crates. [target.'cfg(any())'.dependencies] -bevy_prng = { path = "bevy_prng", version = "=0.7" } +bevy_prng = { path = "bevy_prng", version = "=0.8" } [dev-dependencies] -bevy_prng = { path = "bevy_prng", version = "0.7", features = ["rand_chacha", "wyrand"] } +bevy_prng = { path = "bevy_prng", version = "0.8", features = ["rand_chacha", "wyrand"] } rand = "0.8" ron = { version = "0.8.0", features = ["integer128"] } diff --git a/src/component.rs b/src/component.rs index 4cc5477..62354a1 100644 --- a/src/component.rs +++ b/src/component.rs @@ -2,7 +2,11 @@ use std::fmt::Debug; use crate::{ resource::GlobalEntropy, - traits::{EcsEntropySource, ForkableAsRng, ForkableInnerRng, ForkableRng}, + seed::RngSeed, + traits::{ + EcsEntropySource, ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, + ForkableSeed, + }, }; use bevy::prelude::{Component, Mut, Reflect, ReflectComponent, ReflectFromReflect, ResMut}; use bevy_prng::SeedableEntropySource; @@ -160,6 +164,11 @@ impl SeedableRng for EntropyComponent { Self::new(R::from_seed(seed)) } + #[inline] + fn from_rng(rng: S) -> Result { + R::from_rng(rng).map(Self::new) + } + /// Creates a new instance of the RNG seeded via [`ThreadLocalEntropy`]. This method is the recommended way /// to construct non-deterministic PRNGs since it is convenient and secure. It overrides the standard /// [`SeedableRng::from_entropy`] method while the `thread_local_entropy` feature is enabled. @@ -171,14 +180,8 @@ impl SeedableRng for EntropyComponent { #[cfg(feature = "thread_local_entropy")] #[cfg_attr(docsrs, doc(cfg(feature = "thread_local_entropy")))] fn from_entropy() -> Self { - let mut seed = Self::Seed::default(); - - // Source entropy from thread local user-space RNG instead of - // system entropy source to reduce overhead when creating many - // rng instances for many entities at once. - ThreadLocalEntropy::new().fill_bytes(seed.as_mut()); - - Self::from_seed(seed) + // This operation should never yield Err on any supported PRNGs + Self::from_rng(ThreadLocalEntropy::new()).unwrap() } } @@ -233,6 +236,21 @@ where type Output = R; } +impl ForkableSeed for EntropyComponent +where + R: SeedableEntropySource + 'static, + R::Seed: Send + Sync + Clone, +{ + type Output = RngSeed; +} + +impl ForkableAsSeed for EntropyComponent +where + R: SeedableEntropySource + 'static, +{ + type Output = RngSeed where T: SeedableEntropySource, T::Seed: Send + Sync + Clone; +} + #[cfg(test)] mod tests { use bevy::reflect::TypePath; @@ -296,7 +314,7 @@ mod tests { #[test] fn rng_untyped_serialization() { use bevy::reflect::{ - serde::{ReflectSerializer, ReflectDeserializer}, + serde::{ReflectDeserializer, ReflectSerializer}, TypeRegistry, }; use ron::to_string; diff --git a/src/lib.rs b/src/lib.rs index ade1557..c09334d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![warn(clippy::undocumented_unsafe_blocks)] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, allow(unused_attributes))] -#![deny(missing_docs)] +#![warn(missing_docs)] #![doc = include_str!("../README.md")] /// Components for integrating [`RngCore`] PRNGs into bevy. Must be newtyped to support [`Reflect`]. diff --git a/src/plugin.rs b/src/plugin.rs index 76e7c5a..0221c7c 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,4 +1,9 @@ -use crate::{component::EntropyComponent, resource::GlobalEntropy, seed::GlobalRngSeed}; +use crate::{ + component::EntropyComponent, + resource::GlobalEntropy, + seed::{GlobalRngSeed, RngSeed}, + traits::SeedSource, +}; use bevy::{ prelude::{App, Plugin}, reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}, @@ -68,16 +73,18 @@ where { fn build(&self, app: &mut App) { app.register_type::>() - .register_type::>(); - - GlobalRngSeed::::register_type(app); + .register_type::>() + .register_type::>() + .register_type::(); if let Some(seed) = self.seed.as_ref() { - app.insert_resource(GlobalRngSeed::::new(seed.clone())); + app.insert_resource(GlobalRngSeed::::from_seed(seed.clone())); } else { app.init_resource::>(); } - app.init_resource::>(); + app.init_resource::>() + .world_mut() + .register_component_hooks::>(); } } diff --git a/src/prelude.rs b/src/prelude.rs index 8deaccf..ff5f29b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,7 +2,9 @@ pub use crate::component::EntropyComponent; pub use crate::plugin::EntropyPlugin; pub use crate::resource::GlobalEntropy; pub use crate::seed::GlobalRngSeed; -pub use crate::traits::{ForkableAsRng, ForkableInnerRng, ForkableRng}; +pub use crate::traits::{ + ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, ForkableSeed, +}; #[cfg(feature = "wyrand")] #[cfg_attr(docsrs, doc(cfg(feature = "wyrand")))] pub use bevy_prng::WyRand; diff --git a/src/resource.rs b/src/resource.rs index 4adf2ec..3491d6f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,8 +2,11 @@ use std::fmt::Debug; use crate::{ component::EntropyComponent, - seed::GlobalRngSeed, - traits::{EcsEntropySource, ForkableAsRng, ForkableInnerRng, ForkableRng}, + seed::{GlobalRngSeed, RngSeed}, + traits::{ + EcsEntropySource, ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, + ForkableSeed, + }, }; use bevy::{ ecs::world::{FromWorld, World}, @@ -125,6 +128,11 @@ impl SeedableRng for GlobalEntropy { Self::new(R::from_seed(seed)) } + #[inline] + fn from_rng(rng: S) -> Result { + R::from_rng(rng).map(Self::new) + } + /// Creates a new instance of the RNG seeded via [`ThreadLocalEntropy`]. This method is the recommended way /// to construct non-deterministic PRNGs since it is convenient and secure. It overrides the standard /// [`SeedableRng::from_entropy`] method while the `thread_local_entropy` feature is enabled. @@ -136,14 +144,8 @@ impl SeedableRng for GlobalEntropy { #[cfg(feature = "thread_local_entropy")] #[cfg_attr(docsrs, doc(cfg(feature = "thread_local_entropy")))] fn from_entropy() -> Self { - let mut seed = Self::Seed::default(); - - // Source entropy from thread local user-space RNG instead of - // system entropy source to reduce overhead when creating many - // rng instances for many resources at once. - ThreadLocalEntropy::new().fill_bytes(seed.as_mut()); - - Self::from_seed(seed) + // This operation should never yield Err on any supported PRNGs + Self::from_rng(ThreadLocalEntropy::new()).unwrap() } } @@ -182,6 +184,21 @@ where type Output = R; } +impl ForkableSeed for GlobalEntropy +where + R: SeedableEntropySource + 'static, + R::Seed: Send + Sync + Clone, +{ + type Output = RngSeed; +} + +impl ForkableAsSeed for GlobalEntropy +where + R: SeedableEntropySource + 'static, +{ + type Output = RngSeed where T: SeedableEntropySource, T::Seed: Send + Sync + Clone; +} + #[cfg(test)] mod tests { use bevy::reflect::TypePath; @@ -246,7 +263,7 @@ mod tests { #[test] fn rng_untyped_serialization() { use bevy::reflect::{ - serde::{ReflectSerializer, ReflectDeserializer}, + serde::{ReflectDeserializer, ReflectSerializer}, TypeRegistry, }; use ron::ser::to_string; diff --git a/src/seed.rs b/src/seed.rs index f04b1a2..a267287 100644 --- a/src/seed.rs +++ b/src/seed.rs @@ -2,15 +2,18 @@ use std::marker::PhantomData; use bevy::{ app::App, - ecs::system::Resource, + ecs::{component::StorageType, system::Resource}, + prelude::Component, reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}, }; use bevy_prng::SeedableEntropySource; -use rand_core::RngCore; +use rand_core::SeedableRng; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; +use crate::{component::EntropyComponent, traits::SeedSource}; + #[derive(Debug, Resource, Reflect)] #[cfg_attr( feature = "serialize", @@ -28,6 +31,21 @@ pub struct GlobalRngSeed { rng: PhantomData, } +impl SeedSource for GlobalRngSeed +where + R::Seed: Sync + Send + Clone, +{ + /// Create a new instance of [`GlobalRngSeed`]. + #[inline] + #[must_use] + fn from_seed(seed: R::Seed) -> Self { + Self { + seed, + rng: PhantomData, + } + } +} + impl GlobalRngSeed where R::Seed: Sync + Send + Clone + Reflect + GetTypeRegistration + FromReflect + TypePath, @@ -45,16 +63,6 @@ impl GlobalRngSeed where R::Seed: Sync + Send + Clone, { - /// Create a new instance of [`GlobalRngSeed`]. - #[inline] - #[must_use] - pub fn new(seed: R::Seed) -> Self { - Self { - seed, - rng: PhantomData, - } - } - /// Returns a cloned instance of the seed value. #[inline] pub fn get_seed(&self) -> R::Seed { @@ -65,28 +73,6 @@ where pub fn set_seed(&mut self, seed: R::Seed) { self.seed = seed; } - - /// Initializes an instance of [`GlobalRngSeed`] with a randomised seed - /// value, drawn from thread-local or OS sources. - #[inline] - pub fn from_entropy() -> Self { - let mut seed = Self::new(R::Seed::default()); - - #[cfg(feature = "thread_local_entropy")] - { - use crate::thread_local_entropy::ThreadLocalEntropy; - - ThreadLocalEntropy::new().fill_bytes(seed.as_mut()); - } - #[cfg(not(feature = "thread_local_entropy"))] - { - use getrandom::getrandom; - - getrandom(seed.as_mut()).expect("Unable to source entropy for seeding"); - } - - seed - } } impl Default for GlobalRngSeed @@ -109,6 +95,53 @@ where } } +/// The initial seed/state for an [`EntropyComponent`]. Adding this component to an `Entity` will cause +/// an `EntropyComponent` to be initialised as well. To force a reseed, just insert this component to an +/// `Entity` to overwrite the old value, and the `EntropyComponent` will be overwritten with the new seed +/// in turn. +#[derive(Debug, Reflect)] +pub struct RngSeed { + seed: R::Seed, + #[reflect(ignore)] + rng: PhantomData, +} + +impl SeedSource for RngSeed +where + R::Seed: Sync + Send + Clone, +{ + fn from_seed(seed: R::Seed) -> Self { + Self { + seed, + rng: PhantomData, + } + } +} + +impl Component for RngSeed +where + R::Seed: Sync + Send + Clone, +{ + const STORAGE_TYPE: StorageType = StorageType::Table; + + fn register_component_hooks(hooks: &mut bevy::ecs::component::ComponentHooks) { + hooks + .on_insert(|mut world, entity, _| { + let seed = world.get::>(entity).unwrap().seed.clone(); + world + .commands() + .entity(entity) + .insert(EntropyComponent::::from_seed(seed)); + }) + .on_remove(|mut world, entity, _| { + world + .commands() + .entity(entity) + .remove::>(); + }); + } +} + #[cfg(test)] mod tests { use super::*; @@ -130,7 +163,7 @@ mod tests { let registered_type = GlobalRngSeed::::get_type_registration(); - let val = GlobalRngSeed::::new(u64::MAX.to_ne_bytes()); + let val = GlobalRngSeed::::from_seed(u64::MAX.to_ne_bytes()); let ser = TypedReflectSerializer::new(&val, ®istry); diff --git a/src/traits.rs b/src/traits.rs index 240ba19..31ef573 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -96,12 +96,138 @@ pub trait ForkableInnerRng: EcsEntropySource { } } +/// Trait for implementing forking behaviour for [`crate::component::EntropyComponent`] and [`crate::resource::GlobalEntropy`]. +/// Forking creates a new RNG instance using a generated seed from the original source. If the original is seeded with a known +/// seed, this process is deterministic. This trait enables forking from an entropy source to a seed component. +pub trait ForkableSeed: EcsEntropySource +where + S::Seed: Send + Sync + Clone, +{ + /// The type of seed component that is to be forked from the original source. + type Output: SeedSource; + + /// Fork a new seed from the original entropy source. + /// This method preserves the RNG algorithm between original instance and forked seed. + /// ``` + /// use bevy::prelude::*; + /// use bevy_prng::ChaCha8Rng; + /// use bevy_rand::prelude::{GlobalEntropy, ForkableSeed}; + /// + /// #[derive(Component)] + /// struct Source; + /// + /// fn setup_source(mut commands: Commands, mut global: ResMut>) { + /// commands + /// .spawn(( + /// Source, + /// global.fork_seed(), + /// )); + /// } + /// ``` + fn fork_seed(&mut self) -> Self::Output { + let mut seed = S::Seed::default(); + + self.fill_bytes(seed.as_mut()); + + Self::Output::from_seed(seed) + } +} + +/// Trait for implementing Forking behaviour for [`crate::component::EntropyComponent`] and [`crate::resource::GlobalEntropy`]. +/// Forking creates a new RNG instance using a generated seed from the original source. If the original is seeded with a known +/// seed, this process is deterministic. This trait enables forking from an entropy source to a seed component of a different +/// PRNG algorithm. +pub trait ForkableAsSeed: EcsEntropySource { + /// The type of seed component that is to be forked from the original source. + type Output: SeedSource + where + T: SeedableEntropySource, + T::Seed: Send + Sync + Clone; + + /// Fork a new seed from the original entropy source. + /// This method allows one to specify the RNG algorithm to be used for the forked seed. + /// ``` + /// use bevy::prelude::*; + /// use bevy_rand::prelude::{GlobalEntropy, ForkableAsSeed}; + /// use bevy_prng::{ChaCha8Rng, ChaCha12Rng}; + /// + /// #[derive(Component)] + /// struct Source; + /// + /// fn setup_source(mut commands: Commands, mut global: ResMut>) { + /// commands + /// .spawn(( + /// Source, + /// global.fork_as_seed::(), + /// )); + /// } + /// ``` + fn fork_as_seed(&mut self) -> Self::Output + where + T::Seed: Send + Sync + Clone, + { + let mut seed = T::Seed::default(); + + self.fill_bytes(seed.as_mut()); + + Self::Output::::from_seed(seed) + } +} + +/// A trait for providing [`crate::seed::GlobalRngSeed`] and [`crate::seed::RngSeed`] with +/// common initialization strategies. This trait is not object safe and is also a sealed trait. +pub trait SeedSource: private::SealedSeed +where + R::Seed: Send + Sync + Clone, +{ + /// Initialize a [`SeedSource`] from a given `seed` value. + fn from_seed(seed: R::Seed) -> Self; + + /// Initialize a [`SeedSource`] from a `seed` value obtained from a + /// OS-level or user-space RNG source. + fn from_entropy() -> Self + where + Self: Sized, + { + let mut dest = R::Seed::default(); + + #[cfg(feature = "thread_local_entropy")] + { + use crate::thread_local_entropy::ThreadLocalEntropy; + + ThreadLocalEntropy::new().fill_bytes(dest.as_mut()); + } + #[cfg(not(feature = "thread_local_entropy"))] + { + use getrandom::getrandom; + + getrandom(seed.as_mut()).expect("Unable to source entropy for seeding"); + } + + Self::from_seed(dest) + } +} + /// A marker trait for [`crate::component::EntropyComponent`] and [`crate::resource::GlobalEntropy`]. /// This is a sealed trait and cannot be consumed by downstream. pub trait EcsEntropySource: RngCore + SeedableRng + private::SealedSource {} mod private { + use super::{EcsEntropySource, SeedSource, SeedableEntropySource}; + pub trait SealedSource {} + pub trait SealedSeed + where + R: SeedableEntropySource, + { + } - impl SealedSource for T {} + impl SealedSource for T where T: EcsEntropySource {} + impl SealedSeed for T + where + T: SeedSource, + R: SeedableEntropySource, + R::Seed: Send + Sync + Clone, + { + } } diff --git a/tests/determinism.rs b/tests/integration/determinism.rs similarity index 62% rename from tests/determinism.rs rename to tests/integration/determinism.rs index 9742ab2..e757610 100644 --- a/tests/determinism.rs +++ b/tests/integration/determinism.rs @@ -1,18 +1,14 @@ -#![allow(clippy::type_complexity)] - use bevy::prelude::*; -use bevy_prng::{ChaCha12Rng, ChaCha8Rng, SeedableEntropySource, WyRand}; +use bevy_prng::{ChaCha12Rng, ChaCha8Rng, WyRand}; use bevy_rand::prelude::{ EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, GlobalRngSeed, }; -use rand::prelude::{Rng, SeedableRng}; +use rand::prelude::Rng; use rand_core::RngCore; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] -wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +use wasm_bindgen_test::*; #[derive(Component)] struct SourceA; @@ -132,66 +128,3 @@ fn test_parallel_determinism() { ) .run(); } - -#[test] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -fn test_global_reseeding() { - /// Basic Reseeding mechanism by change detection against GlobalRngSeed - fn reseed_global_rng( - seed: Res>, - mut rng: ResMut>, - ) where - R::Seed: Sync + Send + Clone, - { - if seed.is_changed() && !seed.is_added() { - rng.reseed(seed.get_seed()); - } - } - - let mut app = App::new(); - - let seed = [2; 32]; - - let rng_eq = GlobalEntropy::::from_seed(seed); - - app.add_plugins(EntropyPlugin::::with_seed(seed)) - .add_systems(PreUpdate, reseed_global_rng::); - - { - let global_rng = app.world().resource_ref::>(); - let global_seed = app.world().resource_ref::>(); - - // Our RNGs should be the same as each other as they were initialised with the same seed - assert_eq!(global_rng.as_ref(), &rng_eq); - - // The condition here should mean our reseeding system will NOT run - assert!(global_seed.is_changed() && global_seed.is_added()); - } - - app.update(); - - { - let global_rng = app.world().resource_ref::>(); - - // Our RNGs should remain the same as each other as we have not run the update - assert_eq!(global_rng.as_ref(), &rng_eq); - } - - { - let mut global_seed = app.world_mut().resource_mut::>(); - - global_seed.set_seed([3; 32]); - - // The condition here should mean our reseeding system WILL run - assert!(global_seed.is_changed() && !global_seed.is_added()); - } - - app.update(); - - { - let global_rng = app.world().resource_ref::>(); - - // Now our RNG will not be the same, even though we did not use it directly - assert_ne!(global_rng.as_ref(), &rng_eq); - } -} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs new file mode 100644 index 0000000..c20aff7 --- /dev/null +++ b/tests/integration/mod.rs @@ -0,0 +1,2 @@ +pub mod determinism; +pub mod reseeding; diff --git a/tests/integration/reseeding.rs b/tests/integration/reseeding.rs new file mode 100644 index 0000000..f926f86 --- /dev/null +++ b/tests/integration/reseeding.rs @@ -0,0 +1,152 @@ +use bevy::{ + app::{App, PreStartup, PreUpdate, Update}, + prelude::{Commands, DetectChanges, Query, Res, ResMut}, +}; +use bevy_prng::{ChaCha8Rng, SeedableEntropySource, WyRand}; +use bevy_rand::{ + plugin::EntropyPlugin, + prelude::EntropyComponent, + resource::GlobalEntropy, + seed::GlobalRngSeed, + traits::{ForkableAsSeed, ForkableSeed}, +}; +use rand_core::{RngCore, SeedableRng}; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::*; + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn test_global_reseeding() { + /// Basic Reseeding mechanism by change detection against GlobalRngSeed + fn reseed_global_rng( + seed: Res>, + mut rng: ResMut>, + ) where + R::Seed: Sync + Send + Clone, + { + if seed.is_changed() && !seed.is_added() { + rng.reseed(seed.get_seed()); + } + } + + let mut app = App::new(); + + let seed = [2; 32]; + + let rng_eq = GlobalEntropy::::from_seed(seed); + + app.add_plugins(EntropyPlugin::::with_seed(seed)) + .add_systems(PreUpdate, reseed_global_rng::); + + { + let global_rng = app.world().resource_ref::>(); + let global_seed = app.world().resource_ref::>(); + + // Our RNGs should be the same as each other as they were initialised with the same seed + assert_eq!(global_rng.as_ref(), &rng_eq); + + // The condition here should mean our reseeding system will NOT run + assert!(global_seed.is_changed() && global_seed.is_added()); + } + + app.update(); + + { + let global_rng = app.world().resource_ref::>(); + + // Our RNGs should remain the same as each other as we have not run the update + assert_eq!(global_rng.as_ref(), &rng_eq); + } + + { + let mut global_seed = app.world_mut().resource_mut::>(); + + global_seed.set_seed([3; 32]); + + // The condition here should mean our reseeding system WILL run + assert!(global_seed.is_changed() && !global_seed.is_added()); + } + + app.update(); + + { + let global_rng = app.world().resource_ref::>(); + + // Now our RNG will not be the same, even though we did not use it directly + assert_ne!(global_rng.as_ref(), &rng_eq); + } +} + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn component_fork_seed() { + let mut app = App::new(); + + let seed = [2; 32]; + + app.add_plugins(EntropyPlugin::::with_seed(seed)) + .add_systems( + PreStartup, + |mut commands: Commands, mut rng: ResMut>| { + for _ in 0..5 { + commands.spawn(rng.fork_seed()); + } + }, + ) + .add_systems( + Update, + |mut q_rng: Query<&mut EntropyComponent>| { + let rngs = q_rng.iter_mut(); + + assert_eq!(rngs.size_hint().0, 5); + + let values: Vec<_> = rngs.map(|mut rng| rng.next_u32()).collect(); + + assert_eq!( + &values, + &[3315785188, 1951699392, 911252207, 791343233, 1599472206] + ); + }, + ); + + app.update(); +} + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn component_fork_as_seed() { + let mut app = App::new(); + + let seed = [2; 32]; + + app.add_plugins(EntropyPlugin::::with_seed(seed)) + .add_systems( + PreStartup, + |mut commands: Commands, mut rng: ResMut>| { + for _ in 0..5 { + commands.spawn(rng.fork_as_seed::()); + } + }, + ) + .add_systems(Update, |mut q_rng: Query<&mut EntropyComponent>| { + let rngs = q_rng.iter_mut(); + + assert_eq!(rngs.size_hint().0, 5); + + let values: Vec<_> = rngs.map(|mut rng| rng.next_u64()).collect(); + + assert_eq!( + &values, + &[ + 10032395693880520184, + 15375025802368380325, + 10863580644061233257, + 7067543572507795213, + 7996461288508244033 + ] + ); + }); + + app.update(); +} diff --git a/tests/suite.rs b/tests/suite.rs new file mode 100644 index 0000000..76fcf65 --- /dev/null +++ b/tests/suite.rs @@ -0,0 +1,5 @@ +#![allow(clippy::type_complexity)] +pub mod integration; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); From f198fe2bb60e93cd2c0ddf0a30e0e0cb217052dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 23 Jul 2024 17:00:56 +0200 Subject: [PATCH 2/4] feat: Fix prelude, improve SeedSource methods --- src/prelude.rs | 4 ++-- src/resource.rs | 4 ++-- src/seed.rs | 33 ++++++++++++++++++++++++-------- src/traits.rs | 6 ++++++ tests/integration/determinism.rs | 4 ++-- tests/integration/reseeding.rs | 4 ++-- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/prelude.rs b/src/prelude.rs index ff5f29b..a09c18e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,9 +1,9 @@ pub use crate::component::EntropyComponent; pub use crate::plugin::EntropyPlugin; pub use crate::resource::GlobalEntropy; -pub use crate::seed::GlobalRngSeed; +pub use crate::seed::{GlobalRngSeed, RngSeed}; pub use crate::traits::{ - ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, ForkableSeed, + ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, ForkableSeed, SeedSource }; #[cfg(feature = "wyrand")] #[cfg_attr(docsrs, doc(cfg(feature = "wyrand")))] diff --git a/src/resource.rs b/src/resource.rs index 3491d6f..0b30c93 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -5,7 +5,7 @@ use crate::{ seed::{GlobalRngSeed, RngSeed}, traits::{ EcsEntropySource, ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, - ForkableSeed, + ForkableSeed, SeedSource, }, }; use bevy::{ @@ -91,7 +91,7 @@ where { fn from_world(world: &mut World) -> Self { if let Some(seed) = world.get_resource::>() { - Self::new(R::from_seed(seed.get_seed())) + Self::new(R::from_seed(seed.clone_seed())) } else { Self::from_entropy() } diff --git a/src/seed.rs b/src/seed.rs index a267287..dc95079 100644 --- a/src/seed.rs +++ b/src/seed.rs @@ -35,7 +35,7 @@ impl SeedSource for GlobalRngSeed where R::Seed: Sync + Send + Clone, { - /// Create a new instance of [`GlobalRngSeed`]. + /// Create a new instance of [`GlobalRngSeed`] from a given `seed` value. #[inline] #[must_use] fn from_seed(seed: R::Seed) -> Self { @@ -44,6 +44,16 @@ where rng: PhantomData, } } + + #[inline] + fn get_seed(&self) -> &R::Seed { + &self.seed + } + + #[inline] + fn clone_seed(&self) -> R::Seed { + self.seed.clone() + } } impl GlobalRngSeed @@ -63,12 +73,6 @@ impl GlobalRngSeed where R::Seed: Sync + Send + Clone, { - /// Returns a cloned instance of the seed value. - #[inline] - pub fn get_seed(&self) -> R::Seed { - self.seed.clone() - } - /// Set the global seed to a new value pub fn set_seed(&mut self, seed: R::Seed) { self.seed = seed; @@ -110,12 +114,25 @@ impl SeedSource for RngSeed where R::Seed: Sync + Send + Clone, { + /// Create a new instance of [`RngSeed`] from a given `seed` value. + #[inline] + #[must_use] fn from_seed(seed: R::Seed) -> Self { Self { seed, rng: PhantomData, } } + + #[inline] + fn get_seed(&self) -> &R::Seed { + &self.seed + } + + #[inline] + fn clone_seed(&self) -> R::Seed { + self.seed.clone() + } } impl Component for RngSeed @@ -183,6 +200,6 @@ mod tests { let recreated = GlobalRngSeed::::from_reflect(value.as_reflect()).unwrap(); - assert_eq!(val.get_seed(), recreated.get_seed()); + assert_eq!(val.clone_seed(), recreated.clone_seed()); } } diff --git a/src/traits.rs b/src/traits.rs index 31ef573..204df83 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -183,6 +183,12 @@ where /// Initialize a [`SeedSource`] from a given `seed` value. fn from_seed(seed: R::Seed) -> Self; + /// Returns a reference of the seed value. + fn get_seed(&self) -> &R::Seed; + + /// Returns a cloned instance of the seed value. + fn clone_seed(&self) -> R::Seed; + /// Initialize a [`SeedSource`] from a `seed` value obtained from a /// OS-level or user-space RNG source. fn from_entropy() -> Self diff --git a/tests/integration/determinism.rs b/tests/integration/determinism.rs index e757610..a1a5e7b 100644 --- a/tests/integration/determinism.rs +++ b/tests/integration/determinism.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use bevy_prng::{ChaCha12Rng, ChaCha8Rng, WyRand}; use bevy_rand::prelude::{ - EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, GlobalRngSeed, + EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, GlobalRngSeed, SeedSource }; use rand::prelude::Rng; @@ -88,7 +88,7 @@ fn setup_sources(mut commands: Commands, mut rng: ResMut>) { - assert_eq!(seed.get_seed(), [2; 32]); + assert_eq!(seed.get_seed(), &[2; 32]); } /// Entities having their own sources side-steps issues with parallel execution and scheduling diff --git a/tests/integration/reseeding.rs b/tests/integration/reseeding.rs index f926f86..c7374b3 100644 --- a/tests/integration/reseeding.rs +++ b/tests/integration/reseeding.rs @@ -8,7 +8,7 @@ use bevy_rand::{ prelude::EntropyComponent, resource::GlobalEntropy, seed::GlobalRngSeed, - traits::{ForkableAsSeed, ForkableSeed}, + traits::{ForkableAsSeed, ForkableSeed, SeedSource}, }; use rand_core::{RngCore, SeedableRng}; @@ -26,7 +26,7 @@ fn test_global_reseeding() { R::Seed: Sync + Send + Clone, { if seed.is_changed() && !seed.is_added() { - rng.reseed(seed.get_seed()); + rng.reseed(seed.clone_seed()); } } From a58423d50ed61224eecc4ee9aa247557836d4c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 23 Jul 2024 17:18:51 +0200 Subject: [PATCH 3/4] chore: Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 09c67ce..597ec8c 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ All supported PRNGs and compatible structs are provided by the `bevy_prng` crate #### `bevy_rand` feature activation ```toml rand_core = "0.6" -bevy_rand = { version = "0.7", features = ["rand_chacha", "wyrand"] } +bevy_rand = { version = "0.8", features = ["rand_chacha", "wyrand"] } ``` #### `bevy_prng` feature activation ```toml rand_core = "0.6" -bevy_rand = "0.7" -bevy_prng = { version = "0.7", features = ["rand_chacha", "wyrand"] } +bevy_rand = "0.8" +bevy_prng = { version = "0.8", features = ["rand_chacha", "wyrand"] } ``` The summary of what RNG algorithm to choose is: pick `wyrand` for almost all cases as it is faster and more portable than other algorithms. For cases where you need the extra assurance of entropy quality (for security, etc), then use `rand_chacha`. For more information, [go here](https://docs.rs/bevy_rand/latest/bevy_rand/tutorial/ch01_choosing_prng/index.html). @@ -128,7 +128,7 @@ fn setup_npc_from_source( | `bevy` | `bevy_rand` | | ------ | ------------ | -| v0.14 | v0.7 | +| v0.14 | v0.7 - v0.8 | | v0.13 | v0.5 - v0.6 | | v0.12 | v0.4 | | v0.11 | v0.2 - v0.3 | @@ -138,7 +138,7 @@ The versions of `rand_core`/`rand` that `bevy_rand` is compatible with is as fol | `bevy_rand` | `rand_core` | `rand` | | ------------ | ----------- | ------ | -| v0.1 -> v0.7 | v0.6 | v0.8 | +| v0.1 -> v0.8 | v0.6 | v0.8 | ## Migrations From 6836efe346ccd929c47fa6244bdeece6b138882a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 23 Jul 2024 17:29:02 +0200 Subject: [PATCH 4/4] chore: Update migration notes --- MIGRATIONS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 9b2973a..d2705cc 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -23,3 +23,7 @@ This **will** change the type path and the serialization format for the PRNGs, b ## Migrating from v0.5 to v0.6 As the `wyrand` dependency has been updated and contains a breaking output change, users of `bevy_rand` making use of the `wyrand` feature will need to update their code in cases where deterministic output from the old version is expected. The new `WyRand` output is considered to provide better entropy than the old version, so it is recommended to adopt the new version. In reality, this is likely to affect tests and serialised output rather than game code. + +## Migrating from v0.7 to v0.8 + +`GlobalRngSeed` has been changed to make use of `SeedSource` trait, for things like instantiation: `new` is now `from_seed`. `get_seed` is now `clone_seed`. Most of these changes can be done easily by importing the `SeedSource` trait.