diff --git a/MIGRATIONS.md b/MIGRATIONS.md index d2705cc..4871293 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -26,4 +26,4 @@ As the `wyrand` dependency has been updated and contains a breaking output chang ## 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. +`GlobalRngSeed` has been removed, instead being rolled into `GlobalEntropy`. This will allow better reflection tracking of the global rng source, and will allow for automatic reseeding without any custom system needing to be provided. Use the `reseed` method to reinstantiate the internal RNG source with the new seed, and `get_seed` to return a reference to the initial starting seed for the source. The serialized format of `GlobalEntropy` has changed and previously serialized instances are no longer compatible. diff --git a/bevy_prng/src/lib.rs b/bevy_prng/src/lib.rs index 350294a..de7a9cb 100644 --- a/bevy_prng/src/lib.rs +++ b/bevy_prng/src/lib.rs @@ -62,6 +62,43 @@ pub trait SeedableEntropySource: { } +/// Marker trait for a suitable seed for [`SeedableEntropySource`]. This is an auto trait which will +/// apply to all suitable types that meet the trait criteria. +#[cfg(feature = "serialize")] +pub trait EntropySeed: + Debug + + Default + + PartialEq + + Clone + + Sync + + Send + + Reflect + + TypePath + + FromReflect + + GetTypeRegistration + + Serialize + + for<'a> Deserialize<'a> +{ +} + +#[cfg(feature = "serialize")] +impl< + T: Debug + + Default + + PartialEq + + Clone + + Sync + + Send + + Reflect + + TypePath + + FromReflect + + GetTypeRegistration + + Serialize + + for<'a> Deserialize<'a>, + > EntropySeed for T +{ +} + /// A marker trait to define the required trait bounds for a seedable PRNG to /// integrate into `EntropyComponent` or `GlobalEntropy`. This is a sealed trait. #[cfg(not(feature = "serialize"))] @@ -81,6 +118,40 @@ pub trait SeedableEntropySource: { } +#[cfg(not(feature = "serialize"))] +/// Marker trait for a suitable seed for [`SeedableEntropySource`]. This is an auto trait which will +/// apply to all suitable types that meet the trait criteria. +pub trait EntropySeed: + Debug + + Default + + AsMut + + PartialEq + + Clone + + Sync + + Send + + Reflect + + TypePath + + FromReflect + + GetTypeRegistration //+ private::SealedSeed +{ +} + +#[cfg(not(feature = "serialize"))] +impl< + T: Debug + + Default + + PartialEq + + Clone + + Sync + + Send + + Reflect + + TypePath + + FromReflect + + GetTypeRegistration, + > EntropySeed for T +{ +} + mod private { pub trait SealedSeedable {} diff --git a/src/component.rs b/src/component.rs index 62354a1..8d231f5 100644 --- a/src/component.rs +++ b/src/component.rs @@ -1,14 +1,13 @@ use std::fmt::Debug; use crate::{ - resource::GlobalEntropy, seed::RngSeed, traits::{ EcsEntropySource, ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, ForkableSeed, }, }; -use bevy::prelude::{Component, Mut, Reflect, ReflectComponent, ReflectFromReflect, ResMut}; +use bevy::prelude::{Component, Reflect, ReflectComponent, ReflectFromReflect}; use bevy_prng::SeedableEntropySource; use rand_core::{RngCore, SeedableRng}; @@ -129,6 +128,7 @@ impl EntropyComponent { } impl Default for EntropyComponent { + #[inline] fn default() -> Self { Self::from_entropy() } @@ -187,34 +187,6 @@ impl SeedableRng for EntropyComponent { impl EcsEntropySource for EntropyComponent {} -impl From for EntropyComponent { - fn from(value: R) -> Self { - Self::new(value) - } -} - -impl From<&mut EntropyComponent> for EntropyComponent { - fn from(rng: &mut EntropyComponent) -> Self { - Self::from_rng(rng).unwrap() - } -} - -impl From<&mut Mut<'_, EntropyComponent>> - for EntropyComponent -{ - fn from(rng: &mut Mut<'_, EntropyComponent>) -> Self { - Self::from(rng.as_mut()) - } -} - -impl From<&mut ResMut<'_, GlobalEntropy>> - for EntropyComponent -{ - fn from(rng: &mut ResMut<'_, GlobalEntropy>) -> Self { - Self::from_rng(rng.as_mut()).unwrap() - } -} - impl ForkableRng for EntropyComponent where R: SeedableEntropySource + 'static, diff --git a/src/plugin.rs b/src/plugin.rs index 0221c7c..9e11af1 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,14 +1,7 @@ -use crate::{ - component::EntropyComponent, - resource::GlobalEntropy, - seed::{GlobalRngSeed, RngSeed}, - traits::SeedSource, -}; -use bevy::{ - prelude::{App, Plugin}, - reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}, -}; -use bevy_prng::SeedableEntropySource; +use crate::{component::EntropyComponent, resource::GlobalEntropy, seed::RngSeed}; +use bevy::prelude::{App, Plugin}; +use bevy_prng::{EntropySeed, SeedableEntropySource}; +use rand_core::SeedableRng; /// Plugin for integrating a PRNG that implements `RngCore` into /// the bevy engine, registering types for a global resource and @@ -69,22 +62,19 @@ where impl Plugin for EntropyPlugin where - R::Seed: Send + Sync + Clone + Reflect + FromReflect + GetTypeRegistration + TypePath, + R::Seed: EntropySeed, { fn build(&self, app: &mut App) { app.register_type::>() .register_type::>() - .register_type::>() .register_type::(); if let Some(seed) = self.seed.as_ref() { - app.insert_resource(GlobalRngSeed::::from_seed(seed.clone())); + app.insert_resource(GlobalEntropy::::from_seed(seed.clone())); } else { - app.init_resource::>(); + app.init_resource::>(); } - app.init_resource::>() - .world_mut() - .register_component_hooks::>(); + app.world_mut().register_component_hooks::>(); } } diff --git a/src/prelude.rs b/src/prelude.rs index a09c18e..aa46967 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, RngSeed}; +pub use crate::seed::RngSeed; pub use crate::traits::{ - ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, ForkableSeed, SeedSource + 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 0b30c93..0ba5644 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,16 +2,13 @@ use std::fmt::Debug; use crate::{ component::EntropyComponent, - seed::{GlobalRngSeed, RngSeed}, + seed::RngSeed, traits::{ EcsEntropySource, ForkableAsRng, ForkableAsSeed, ForkableInnerRng, ForkableRng, - ForkableSeed, SeedSource, + ForkableSeed, }, }; -use bevy::{ - ecs::world::{FromWorld, World}, - prelude::{Reflect, ReflectFromReflect, ReflectFromWorld, ReflectResource, Resource}, -}; +use bevy::prelude::{Reflect, ReflectFromReflect, ReflectFromWorld, ReflectResource, Resource}; use bevy_prng::SeedableEntropySource; use rand_core::{RngCore, SeedableRng}; @@ -22,7 +19,7 @@ use crate::thread_local_entropy::ThreadLocalEntropy; use bevy::prelude::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "serialize")] -use serde::Deserialize; +use serde::{Deserialize, Serialize}; /// A Global [`RngCore`] instance, meant for use as a Resource. Gets /// created automatically with [`crate::plugin::EntropyPlugin`], or @@ -41,14 +38,6 @@ use serde::Deserialize; /// } /// ``` #[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect)] -#[cfg_attr( - feature = "serialize", - derive(serde_derive::Serialize, serde_derive::Deserialize) -)] -#[cfg_attr( - feature = "serialize", - serde(bound(deserialize = "R: for<'a> Deserialize<'a>")) -)] #[cfg_attr( feature = "serialize", reflect( @@ -65,72 +54,89 @@ use serde::Deserialize; not(feature = "serialize"), reflect(Debug, PartialEq, Resource, FromReflect, FromWorld) )] -#[reflect(where R::Seed: Sync + Send + Clone)] -pub struct GlobalEntropy(R); +#[cfg_attr( + feature = "serialize", + derive(serde_derive::Serialize, serde_derive::Deserialize) +)] +#[cfg_attr( + feature = "serialize", + serde(bound(deserialize = "R: for<'a> Deserialize<'a>, R::Seed: for<'a> Deserialize<'a>")) +)] +#[cfg_attr(feature = "serialize", reflect(where R::Seed: PartialEq + Debug + Sync + Send + Clone + Serialize + for<'a> Deserialize<'a>))] +#[cfg_attr(not(feature = "serialize"), reflect(where R::Seed: PartialEq + Debug + Sync + Send + Clone))] +pub struct GlobalEntropy { + seed: R::Seed, + rng: R, +} -impl GlobalEntropy { - /// Create a new resource from a `RngCore` instance. +impl GlobalEntropy +where + R::Seed: Clone, +{ #[inline] #[must_use] - pub fn new(rng: R) -> Self { - Self(rng) + fn new(seed: R::Seed) -> Self { + Self { + seed: seed.clone(), + rng: R::from_seed(seed), + } } -} -impl GlobalEntropy { /// Reseeds the internal `RngCore` instance with a new seed. #[inline] pub fn reseed(&mut self, seed: R::Seed) { - self.0 = R::from_seed(seed); + self.seed = seed.clone(); + self.rng = R::from_seed(seed); + } + + /// Get a reference to the initial seed + #[inline] + pub fn get_seed(&self) -> &R::Seed { + &self.seed } } -impl FromWorld for GlobalEntropy +impl Default for GlobalEntropy where - R::Seed: Send + Sync + Clone, + R::Seed: Clone, { - fn from_world(world: &mut World) -> Self { - if let Some(seed) = world.get_resource::>() { - Self::new(R::from_seed(seed.clone_seed())) - } else { - Self::from_entropy() - } + #[inline] + fn default() -> Self { + Self::from_entropy() } } impl RngCore for GlobalEntropy { #[inline] fn next_u32(&mut self) -> u32 { - self.0.next_u32() + self.rng.next_u32() } #[inline] fn next_u64(&mut self) -> u64 { - self.0.next_u64() + self.rng.next_u64() } #[inline] fn fill_bytes(&mut self, dest: &mut [u8]) { - self.0.fill_bytes(dest); + self.rng.fill_bytes(dest); } #[inline] fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { - self.0.try_fill_bytes(dest) + self.rng.try_fill_bytes(dest) } } -impl SeedableRng for GlobalEntropy { +impl SeedableRng for GlobalEntropy +where + R::Seed: Clone, +{ type Seed = R::Seed; #[inline] fn from_seed(seed: Self::Seed) -> Self { - Self::new(R::from_seed(seed)) - } - - #[inline] - fn from_rng(rng: S) -> Result { - R::from_rng(rng).map(Self::new) + Self::new(seed) } /// Creates a new instance of the RNG seeded via [`ThreadLocalEntropy`]. This method is the recommended way @@ -144,28 +150,20 @@ impl SeedableRng for GlobalEntropy { #[cfg(feature = "thread_local_entropy")] #[cfg_attr(docsrs, doc(cfg(feature = "thread_local_entropy")))] fn from_entropy() -> Self { - // This operation should never yield Err on any supported PRNGs - Self::from_rng(ThreadLocalEntropy::new()).unwrap() - } -} + let mut seed = R::Seed::default(); -impl EcsEntropySource for GlobalEntropy {} + ThreadLocalEntropy::new().fill_bytes(seed.as_mut()); -impl From for GlobalEntropy { - fn from(value: R) -> Self { - Self::new(value) + Self::new(seed) } } -impl From<&mut R> for GlobalEntropy { - fn from(value: &mut R) -> Self { - Self::from_rng(value).unwrap() - } -} +impl EcsEntropySource for GlobalEntropy where R::Seed: Clone {} impl ForkableRng for GlobalEntropy where R: SeedableEntropySource + 'static, + R::Seed: Clone, { type Output = EntropyComponent; } @@ -173,6 +171,7 @@ where impl ForkableAsRng for GlobalEntropy where R: SeedableEntropySource + 'static, + R::Seed: Clone, { type Output = EntropyComponent where T: SeedableEntropySource; } @@ -180,6 +179,7 @@ where impl ForkableInnerRng for GlobalEntropy where R: SeedableEntropySource + 'static, + R::Seed: Clone, { type Output = R; } @@ -195,6 +195,7 @@ where impl ForkableAsSeed for GlobalEntropy where R: SeedableEntropySource + 'static, + R::Seed: Clone, { type Output = RngSeed where T: SeedableEntropySource, T::Seed: Send + Sync + Clone; } @@ -254,7 +255,7 @@ mod tests { let rng2 = rng1.fork_inner(); assert_ne!( - rng1.0, rng2, + rng1.rng, rng2, "forked ChaCha8Rngs should not match each other" ); } @@ -283,7 +284,7 @@ mod tests { assert_eq!( &serialized, - "{\"bevy_rand::resource::GlobalEntropy\":(((seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),stream:0,word_pos:1)))}" + "{\"bevy_rand::resource::GlobalEntropy\":(seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),rng:((seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),stream:0,word_pos:1)))}" ); let mut deserializer = ron::Deserializer::from_str(&serialized).unwrap(); @@ -333,7 +334,7 @@ mod tests { assert_eq!( &serialized, - "(((seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),stream:0,word_pos:1)))" + "(seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),rng:((seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),stream:0,word_pos:1)))" ); let mut deserializer = ron::Deserializer::from_str(&serialized).unwrap(); diff --git a/src/seed.rs b/src/seed.rs index dc95079..fc5ba9a 100644 --- a/src/seed.rs +++ b/src/seed.rs @@ -1,104 +1,11 @@ use std::marker::PhantomData; -use bevy::{ - app::App, - ecs::{component::StorageType, system::Resource}, - prelude::Component, - reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}, -}; +use bevy::{ecs::component::StorageType, prelude::Component, reflect::Reflect}; use bevy_prng::SeedableEntropySource; 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", - derive(serde_derive::Serialize, serde_derive::Deserialize) -)] -#[cfg_attr( - feature = "serialize", - serde(bound(deserialize = "R::Seed: Serialize + for<'a> Deserialize<'a>")) -)] -/// Resource for storing the initial seed used to initialize a [`crate::resource::GlobalEntropy`]. -/// Useful for tracking the starting seed or for forcing [`crate::resource::GlobalEntropy`] to reseed. -pub struct GlobalRngSeed { - seed: R::Seed, - #[reflect(ignore)] - rng: PhantomData, -} - -impl SeedSource for GlobalRngSeed -where - R::Seed: Sync + Send + Clone, -{ - /// Create a new instance of [`GlobalRngSeed`] 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 GlobalRngSeed -where - R::Seed: Sync + Send + Clone + Reflect + GetTypeRegistration + FromReflect + TypePath, -{ - /// Helper method to register the necessary types for [`Reflect`] purposes. Ensures - /// that not only the main type is registered, but also the correct seed type for the - /// PRNG. - pub fn register_type(app: &mut App) { - app.register_type::(); - app.register_type::(); - } -} - -impl GlobalRngSeed -where - R::Seed: Sync + Send + Clone, -{ - /// Set the global seed to a new value - pub fn set_seed(&mut self, seed: R::Seed) { - self.seed = seed; - } -} - -impl Default for GlobalRngSeed -where - R::Seed: Sync + Send + Clone, -{ - #[inline] - fn default() -> Self { - Self::from_entropy() - } -} - -impl AsMut<[u8]> for GlobalRngSeed -where - R::Seed: Sync + Send + Clone, -{ - #[inline] - fn as_mut(&mut self) -> &mut [u8] { - self.seed.as_mut() - } -} - /// 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 @@ -168,19 +75,19 @@ mod tests { fn reflection_serialization_round_trip_works() { use bevy::reflect::{ serde::{TypedReflectDeserializer, TypedReflectSerializer}, - GetTypeRegistration, TypeRegistry, + FromReflect, GetTypeRegistration, TypeRegistry, }; use bevy_prng::WyRand; use ron::to_string; use serde::de::DeserializeSeed; let mut registry = TypeRegistry::default(); - registry.register::>(); + registry.register::>(); registry.register::<[u8; 8]>(); - let registered_type = GlobalRngSeed::::get_type_registration(); + let registered_type = RngSeed::::get_type_registration(); - let val = GlobalRngSeed::::from_seed(u64::MAX.to_ne_bytes()); + let val = RngSeed::::from_seed(u64::MAX.to_ne_bytes()); let ser = TypedReflectSerializer::new(&val, ®istry); @@ -195,10 +102,10 @@ mod tests { let value = de.deserialize(&mut deserializer).unwrap(); assert!(value.is_dynamic()); - assert!(value.represents::>()); - assert!(!value.is::>()); + assert!(value.represents::>()); + assert!(!value.is::>()); - let recreated = GlobalRngSeed::::from_reflect(value.as_reflect()).unwrap(); + let recreated = RngSeed::::from_reflect(value.as_reflect()).unwrap(); assert_eq!(val.clone_seed(), recreated.clone_seed()); } diff --git a/src/traits.rs b/src/traits.rs index 204df83..daee817 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -174,7 +174,7 @@ pub trait ForkableAsSeed: EcsEntropySource { } } -/// A trait for providing [`crate::seed::GlobalRngSeed`] and [`crate::seed::RngSeed`] with +/// A trait for providing [`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 diff --git a/tests/integration/determinism.rs b/tests/integration/determinism.rs index a1a5e7b..a2b4fc6 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, SeedSource + EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, }; use rand::prelude::Rng; @@ -87,8 +87,8 @@ fn setup_sources(mut commands: Commands, mut rng: ResMut())); } -fn read_global_seed(seed: Res>) { - assert_eq!(seed.get_seed(), &[2; 32]); +fn read_global_seed(rng: Res>) { + assert_eq!(rng.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 c7374b3..e141f07 100644 --- a/tests/integration/reseeding.rs +++ b/tests/integration/reseeding.rs @@ -1,14 +1,13 @@ use bevy::{ - app::{App, PreStartup, PreUpdate, Update}, - prelude::{Commands, DetectChanges, Query, Res, ResMut}, + app::{App, PreStartup, Update}, + prelude::{Commands, Query, ResMut}, }; -use bevy_prng::{ChaCha8Rng, SeedableEntropySource, WyRand}; +use bevy_prng::{ChaCha8Rng, WyRand}; use bevy_rand::{ plugin::EntropyPlugin, prelude::EntropyComponent, resource::GlobalEntropy, - seed::GlobalRngSeed, - traits::{ForkableAsSeed, ForkableSeed, SeedSource}, + traits::{ForkableAsSeed, ForkableSeed}, }; use rand_core::{RngCore, SeedableRng}; @@ -18,36 +17,19 @@ 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.clone_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::); + app.add_plugins(EntropyPlugin::::with_seed(seed)); { 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(); @@ -60,12 +42,9 @@ fn test_global_reseeding() { } { - let mut global_seed = app.world_mut().resource_mut::>(); - - global_seed.set_seed([3; 32]); + let mut global_seed = app.world_mut().resource_mut::>(); - // The condition here should mean our reseeding system WILL run - assert!(global_seed.is_changed() && !global_seed.is_added()); + global_seed.reseed([3; 32]); } app.update();