diff --git a/pumpkin-config/src/networking/lan_broadcast.rs b/pumpkin-config/src/networking/lan_broadcast.rs index d7b417255..69eaba504 100644 --- a/pumpkin-config/src/networking/lan_broadcast.rs +++ b/pumpkin-config/src/networking/lan_broadcast.rs @@ -1,14 +1,14 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize, Default)] -#[serde(default)] -pub struct LANBroadcastConfig { - pub enabled: bool, - // We use an extra `motd` because this only supports one line, - // but we use the server `motd` without new lines as the default. - pub motd: Option, - // Allow users to specify port so that the port is predictable. - // There are many reasons why the port might need to be predictable. - // One reason is Docker containers, where specific ports need to be allowed. - pub port: Option, -} +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Default)] +#[serde(default)] +pub struct LANBroadcastConfig { + pub enabled: bool, + // We use an extra `motd` because this only supports one line, + // but we use the server `motd` without new lines as the default. + pub motd: Option, + // Allow users to specify port so that the port is predictable. + // There are many reasons why the port might need to be predictable. + // One reason is Docker containers, where specific ports need to be allowed. + pub port: Option, +} diff --git a/pumpkin-util/src/math/float_provider.rs b/pumpkin-util/src/math/float_provider.rs index 838c3b577..5531b9788 100644 --- a/pumpkin-util/src/math/float_provider.rs +++ b/pumpkin-util/src/math/float_provider.rs @@ -1,11 +1,47 @@ +use crate::random::RandomImpl; +use proc_macro2::{Span, TokenStream}; +use quote::{ToTokens, quote}; use serde::Deserialize; +use syn::LitFloat; #[derive(Deserialize, Clone)] #[serde(tag = "type")] pub enum NormalFloatProvider { + #[serde(rename = "minecraft:constant")] + Constant(ConstantFloatProvider), #[serde(rename = "minecraft:uniform")] Uniform(UniformFloatProvider), - // TODO: Add more... + #[serde(rename = "minecraft:clamped_normal")] + ClampedNormal(ClampedNormalFloatProvider), + #[serde(rename = "minecraft:trapezoid")] + Trapezoid(TrapezoidFloatProvider), +} + +impl ToTokens for NormalFloatProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + NormalFloatProvider::Constant(constant) => { + tokens.extend(quote! { + NormalFloatProvider::Constant(#constant) + }); + } + NormalFloatProvider::Uniform(uniform) => { + tokens.extend(quote! { + NormalFloatProvider::Uniform(#uniform) + }); + } + NormalFloatProvider::ClampedNormal(clamped_normal) => { + tokens.extend(quote! { + NormalFloatProvider::ClampedNormal(#clamped_normal) + }); + } + NormalFloatProvider::Trapezoid(trapezoid) => { + tokens.extend(quote! { + NormalFloatProvider::Trapezoid(#trapezoid) + }); + } + } + } } #[derive(Deserialize, Clone)] @@ -15,20 +51,41 @@ pub enum FloatProvider { Constant(f32), } +impl ToTokens for FloatProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + FloatProvider::Object(float_provider) => { + tokens.extend(quote! { + FloatProvider::Object(#float_provider) + }); + } + FloatProvider::Constant(f) => tokens.extend(quote! { + FloatProvider::Constant(#f) + }), + } + } +} + impl FloatProvider { pub fn get_min(&self) -> f32 { match self { FloatProvider::Object(inv_provider) => match inv_provider { + NormalFloatProvider::Constant(constant) => constant.get_min(), NormalFloatProvider::Uniform(uniform) => uniform.get_min(), + NormalFloatProvider::ClampedNormal(clamped_normal) => clamped_normal.get_min(), + NormalFloatProvider::Trapezoid(trapezoid) => trapezoid.get_min(), }, FloatProvider::Constant(i) => *i, } } - pub fn get(&self) -> f32 { + pub fn get(&self, random: &mut impl RandomImpl) -> f32 { match self { FloatProvider::Object(inv_provider) => match inv_provider { - NormalFloatProvider::Uniform(uniform) => uniform.get(), + NormalFloatProvider::Constant(constant) => constant.get(random), + NormalFloatProvider::Uniform(uniform) => uniform.get(random), + NormalFloatProvider::ClampedNormal(clamped_normal) => clamped_normal.get(random), + NormalFloatProvider::Trapezoid(trapezoid) => trapezoid.get(random), }, FloatProvider::Constant(i) => *i, } @@ -37,27 +94,310 @@ impl FloatProvider { pub fn get_max(&self) -> f32 { match self { FloatProvider::Object(inv_provider) => match inv_provider { + NormalFloatProvider::Constant(constant) => constant.get_max(), NormalFloatProvider::Uniform(uniform) => uniform.get_max(), + NormalFloatProvider::ClampedNormal(clamped_normal) => clamped_normal.get_max(), + NormalFloatProvider::Trapezoid(trapezoid) => trapezoid.get_max(), }, FloatProvider::Constant(i) => *i, } } } +#[derive(Deserialize, Clone)] +pub struct ConstantFloatProvider { + value: f32, +} + +impl ToTokens for ConstantFloatProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let value = LitFloat::new(&self.value.to_string(), Span::call_site()); + tokens.extend(quote! { + ConstantFloatProvider { value: #value } + }); + } +} + +impl ConstantFloatProvider { + pub fn new(value: f32) -> Self { + Self { value } + } + + pub fn get_min(&self) -> f32 { + self.value + } + + pub fn get(&self, _random: &mut impl RandomImpl) -> f32 { + self.value + } + + pub fn get_max(&self) -> f32 { + self.value + } +} + #[derive(Deserialize, Clone)] pub struct UniformFloatProvider { min_inclusive: f32, - max_inclusive: f32, + max_exclusive: f32, +} + +impl ToTokens for UniformFloatProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let min_inclusive = LitFloat::new(&self.min_inclusive.to_string(), Span::call_site()); + let max_exclusive = LitFloat::new(&self.max_exclusive.to_string(), Span::call_site()); + tokens.extend(quote! { + UniformFloatProvider { min_inclusive: #min_inclusive, max_exclusive: #max_exclusive } + }); + } } impl UniformFloatProvider { + pub fn new(min_inclusive: f32, max_exclusive: f32) -> Self { + Self { + min_inclusive, + max_exclusive, + } + } + pub fn get_min(&self) -> f32 { self.min_inclusive } - pub fn get(&self) -> f32 { - rand::random_range(self.min_inclusive..self.max_inclusive) + + pub fn get(&self, random: &mut impl RandomImpl) -> f32 { + // Use the random range in [min_inclusive, max_exclusive) + let range = self.max_exclusive - self.min_inclusive; + self.min_inclusive + random.next_f32() * range } + pub fn get_max(&self) -> f32 { - self.max_inclusive + self.max_exclusive + } +} + +#[derive(Deserialize, Clone)] +pub struct ClampedNormalFloatProvider { + mean: f32, + deviation: f32, + min: f32, + max: f32, +} + +impl ToTokens for ClampedNormalFloatProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mean = LitFloat::new(&self.mean.to_string(), Span::call_site()); + let deviation = LitFloat::new(&self.deviation.to_string(), Span::call_site()); + let min = LitFloat::new(&self.min.to_string(), Span::call_site()); + let max = LitFloat::new(&self.max.to_string(), Span::call_site()); + tokens.extend(quote! { + ClampedNormalFloatProvider { + mean: #mean, + deviation: #deviation, + min: #min, + max: #max + } + }); + } +} + +impl ClampedNormalFloatProvider { + pub fn new(mean: f32, deviation: f32, min: f32, max: f32) -> Self { + Self { + mean, + deviation, + min, + max, + } + } + + pub fn get_min(&self) -> f32 { + self.min + } + + pub fn get(&self, random: &mut impl RandomImpl) -> f32 { + // Generate normal distribution value + let gaussian = random.next_gaussian() as f32; + let value = self.mean + gaussian * self.deviation; + + // Clamp to min/max range + value.clamp(self.min, self.max) + } + + pub fn get_max(&self) -> f32 { + self.max + } +} + +#[derive(Deserialize, Clone)] +pub struct TrapezoidFloatProvider { + min: f32, + max: f32, + plateau: f32, +} + +impl ToTokens for TrapezoidFloatProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let min = LitFloat::new(&self.min.to_string(), Span::call_site()); + let max = LitFloat::new(&self.max.to_string(), Span::call_site()); + let plateau = LitFloat::new(&self.plateau.to_string(), Span::call_site()); + tokens.extend(quote! { + TrapezoidFloatProvider { + min: #min, + max: #max, + plateau: #plateau + } + }); + } +} + +impl TrapezoidFloatProvider { + pub fn new(min: f32, max: f32, plateau: f32) -> Self { + Self { min, max, plateau } + } + + pub fn get_min(&self) -> f32 { + self.min + } + + pub fn get(&self, random: &mut impl RandomImpl) -> f32 { + // Trapezoid distribution: flat plateau in the middle, linear ramps on sides + let range = self.max - self.min; + let plateau_range = range * self.plateau; + let ramp_range = (range - plateau_range) * 0.5; + + let random_value = random.next_f32(); + + if random_value < 0.5 - self.plateau * 0.5 { + // Left ramp: quadratic distribution biased toward plateau + let scaled = random_value / (0.5 - self.plateau * 0.5); + let sqrt_scaled = scaled.sqrt(); + self.min + ramp_range * sqrt_scaled + } else if random_value > 0.5 + self.plateau * 0.5 { + // Right ramp: quadratic distribution biased toward plateau + let scaled = (random_value - (0.5 + self.plateau * 0.5)) / (0.5 - self.plateau * 0.5); + let sqrt_scaled = (1.0 - scaled).sqrt(); + self.max - ramp_range * sqrt_scaled + } else { + // Plateau: uniform distribution + let plateau_pos = (random_value - (0.5 - self.plateau * 0.5)) / self.plateau; + self.min + ramp_range + plateau_pos * plateau_range + } + } + + pub fn get_max(&self) -> f32 { + self.max + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::{RandomGenerator, get_seed}; + + #[test] + fn test_constant_float_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = ConstantFloatProvider::new(5.5); + + assert_eq!(provider.get_min(), 5.5); + assert_eq!(provider.get_max(), 5.5); + assert_eq!(provider.get(&mut random), 5.5); + assert_eq!(provider.get(&mut random), 5.5); // Should always return the same value + } + + #[test] + fn test_uniform_float_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = UniformFloatProvider::new(1.0, 5.0); + + assert_eq!(provider.get_min(), 1.0); + assert_eq!(provider.get_max(), 5.0); + + // Test that values are within range + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (1.0..5.0).contains(&value), + "Value {} is outside range [1.0, 5.0)", + value + ); + } + } + + #[test] + fn test_clamped_normal_float_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = ClampedNormalFloatProvider::new(3.0, 1.0, 1.0, 5.0); + + assert_eq!(provider.get_min(), 1.0); + assert_eq!(provider.get_max(), 5.0); + + // Test that values are within range + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (1.0..=5.0).contains(&value), + "Value {} is outside range [1.0, 5.0]", + value + ); + } + } + + #[test] + fn test_trapezoid_float_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = TrapezoidFloatProvider::new(0.0, 10.0, 0.5); + + assert_eq!(provider.get_min(), 0.0); + assert_eq!(provider.get_max(), 10.0); + + // Test that values are within range + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (0.0..=10.0).contains(&value), + "Value {} is outside range [0.0, 10.0]", + value + ); + } + } + + #[test] + fn test_float_provider_enum_constant() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = FloatProvider::Constant(7.5); + + assert_eq!(provider.get_min(), 7.5); + assert_eq!(provider.get_max(), 7.5); + assert_eq!(provider.get(&mut random), 7.5); + } + + #[test] + fn test_float_provider_enum_object() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let uniform = UniformFloatProvider::new(2.0, 8.0); + let provider = FloatProvider::Object(NormalFloatProvider::Uniform(uniform)); + + assert_eq!(provider.get_min(), 2.0); + assert_eq!(provider.get_max(), 8.0); + + let value = provider.get(&mut random); + assert!( + (2.0..8.0).contains(&value), + "Value {} is outside range [2.0, 8.0)", + value + ); } } diff --git a/pumpkin-util/src/math/int_provider.rs b/pumpkin-util/src/math/int_provider.rs index d16023379..15ce7c9c8 100644 --- a/pumpkin-util/src/math/int_provider.rs +++ b/pumpkin-util/src/math/int_provider.rs @@ -1,39 +1,59 @@ +use crate::random::RandomImpl; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; use serde::Deserialize; use syn::LitInt; -use crate::random::{RandomGenerator, RandomImpl}; - -use super::pool::{Pool, Weighted}; - #[derive(Deserialize, Clone, Debug)] #[serde(tag = "type")] pub enum NormalIntProvider { + #[serde(rename = "minecraft:constant")] + Constant(ConstantIntProvider), #[serde(rename = "minecraft:uniform")] Uniform(UniformIntProvider), - #[serde(rename = "minecraft:weighted_list")] - WeightedList(WeightedListIntProvider), + #[serde(rename = "minecraft:biased_to_bottom")] + BiasedToBottom(BiasedToBottomIntProvider), #[serde(rename = "minecraft:clamped")] Clamped(ClampedIntProvider), #[serde(rename = "minecraft:clamped_normal")] ClampedNormal(ClampedNormalIntProvider), - #[serde(rename = "minecraft:biased_to_bottom")] - BiasedToBottom(BiasedToBottomIntProvider), // TODO: Add more... + #[serde(rename = "minecraft:weighted_list")] + WeightedList(WeightedListIntProvider), } impl ToTokens for NormalIntProvider { fn to_tokens(&self, tokens: &mut TokenStream) { match self { + NormalIntProvider::Constant(constant) => { + tokens.extend(quote! { + NormalIntProvider::Constant(#constant) + }); + } NormalIntProvider::Uniform(uniform) => { tokens.extend(quote! { NormalIntProvider::Uniform(#uniform) }); } - NormalIntProvider::WeightedList(_) => todo!(), - NormalIntProvider::Clamped(_) => todo!(), - NormalIntProvider::BiasedToBottom(_biased_to_bottom_int_provider) => todo!(), - NormalIntProvider::ClampedNormal(_clamped_int_provider) => todo!(), + NormalIntProvider::BiasedToBottom(biased) => { + tokens.extend(quote! { + NormalIntProvider::BiasedToBottom(#biased) + }); + } + NormalIntProvider::Clamped(clamped) => { + tokens.extend(quote! { + NormalIntProvider::Clamped(#clamped) + }); + } + NormalIntProvider::ClampedNormal(clamped_normal) => { + tokens.extend(quote! { + NormalIntProvider::ClampedNormal(#clamped_normal) + }); + } + NormalIntProvider::WeightedList(weighted_list) => { + tokens.extend(quote! { + NormalIntProvider::WeightedList(#weighted_list) + }); + } } } } @@ -64,24 +84,26 @@ impl IntProvider { pub fn get_min(&self) -> i32 { match self { IntProvider::Object(int_provider) => match int_provider { + NormalIntProvider::Constant(constant) => constant.get_min(), NormalIntProvider::Uniform(uniform) => uniform.get_min(), - NormalIntProvider::WeightedList(provider) => provider.get_min(), - NormalIntProvider::Clamped(provider) => provider.get_min(), - NormalIntProvider::BiasedToBottom(provider) => provider.get_min(), - NormalIntProvider::ClampedNormal(provider) => provider.get_min(), + NormalIntProvider::BiasedToBottom(biased) => biased.get_min(), + NormalIntProvider::Clamped(clamped) => clamped.get_min(), + NormalIntProvider::ClampedNormal(clamped_normal) => clamped_normal.get_min(), + NormalIntProvider::WeightedList(weighted_list) => weighted_list.get_min(), }, IntProvider::Constant(i) => *i, } } - pub fn get(&self, random: &mut RandomGenerator) -> i32 { + pub fn get(&self, random: &mut impl RandomImpl) -> i32 { match self { IntProvider::Object(int_provider) => match int_provider { + NormalIntProvider::Constant(constant) => constant.get(random), NormalIntProvider::Uniform(uniform) => uniform.get(random), - NormalIntProvider::WeightedList(provider) => provider.get(random), - NormalIntProvider::Clamped(provider) => provider.get(random), - NormalIntProvider::BiasedToBottom(provider) => provider.get(random), - NormalIntProvider::ClampedNormal(provider) => provider.get(random), + NormalIntProvider::BiasedToBottom(biased) => biased.get(random), + NormalIntProvider::Clamped(clamped) => clamped.get(random), + NormalIntProvider::ClampedNormal(clamped_normal) => clamped_normal.get(random), + NormalIntProvider::WeightedList(weighted_list) => weighted_list.get(random), }, IntProvider::Constant(i) => *i, } @@ -90,11 +112,12 @@ impl IntProvider { pub fn get_max(&self) -> i32 { match self { IntProvider::Object(int_provider) => match int_provider { + NormalIntProvider::Constant(constant) => constant.get_max(), NormalIntProvider::Uniform(uniform) => uniform.get_max(), - NormalIntProvider::WeightedList(provider) => provider.get_max(), - NormalIntProvider::Clamped(provider) => provider.get_max(), - NormalIntProvider::BiasedToBottom(provider) => provider.get_max(), - NormalIntProvider::ClampedNormal(provider) => provider.get_max(), + NormalIntProvider::BiasedToBottom(biased) => biased.get_max(), + NormalIntProvider::Clamped(clamped) => clamped.get_max(), + NormalIntProvider::ClampedNormal(clamped_normal) => clamped_normal.get_max(), + NormalIntProvider::WeightedList(weighted_list) => weighted_list.get_max(), }, IntProvider::Constant(i) => *i, } @@ -102,41 +125,73 @@ impl IntProvider { } #[derive(Deserialize, Clone, Debug)] -pub struct ClampedNormalIntProvider { - mean: f32, - deviation: f32, - min_inclusive: i32, - max_inclusive: i32, +pub struct ConstantIntProvider { + value: i32, } -impl ClampedNormalIntProvider { +impl ToTokens for ConstantIntProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let value = LitInt::new(&self.value.to_string(), Span::call_site()); + tokens.extend(quote! { + ConstantIntProvider { value: #value } + }); + } +} + +impl ConstantIntProvider { + pub fn new(value: i32) -> Self { + Self { value } + } + pub fn get_min(&self) -> i32 { - self.min_inclusive + self.value } - pub fn get(&self, random: &mut RandomGenerator) -> i32 { - (self.mean + random.next_gaussian() as f32 * self.deviation) - .clamp(self.min_inclusive as f32, self.max_inclusive as f32) as i32 + + pub fn get(&self, _random: &mut impl RandomImpl) -> i32 { + self.value } + pub fn get_max(&self) -> i32 { - self.max_inclusive + self.value } } #[derive(Deserialize, Clone, Debug)] pub struct BiasedToBottomIntProvider { - min_inclusive: i32, - max_inclusive: i32, + pub min_inclusive: i32, + pub max_inclusive: i32, +} + +impl ToTokens for BiasedToBottomIntProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let min_inclusive = LitInt::new(&self.min_inclusive.to_string(), Span::call_site()); + let max_inclusive = LitInt::new(&self.max_inclusive.to_string(), Span::call_site()); + tokens.extend(quote! { + BiasedToBottomIntProvider { min_inclusive: #min_inclusive, max_inclusive: #max_inclusive } + }); + } } impl BiasedToBottomIntProvider { + pub fn new(min_inclusive: i32, max_inclusive: i32) -> Self { + Self { + min_inclusive, + max_inclusive, + } + } + pub fn get_min(&self) -> i32 { self.min_inclusive } - pub fn get(&self, random: &mut RandomGenerator) -> i32 { - // TODO: not sure if this is called first this matches vanilla - let first_gen = random.next_bounded_i32(self.max_inclusive - self.min_inclusive + 1) + 1; - self.min_inclusive + random.next_bounded_i32(first_gen) + + pub fn get(&self, random: &mut impl RandomImpl) -> i32 { + // Similar to uniform but biased toward lower values + // Uses triangular distribution with mode at min + let range = (self.max_inclusive - self.min_inclusive + 1) as f64; + let triangular = random.next_triangular(0.0, range); + self.min_inclusive + (triangular.abs() as i32).min(self.max_inclusive - self.min_inclusive) } + pub fn get_max(&self) -> i32 { self.max_inclusive } @@ -149,47 +204,173 @@ pub struct ClampedIntProvider { max_inclusive: i32, } +impl ToTokens for ClampedIntProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let source = &self.source; + let min_inclusive = LitInt::new(&self.min_inclusive.to_string(), Span::call_site()); + let max_inclusive = LitInt::new(&self.max_inclusive.to_string(), Span::call_site()); + tokens.extend(quote! { + ClampedIntProvider { + source: Box::new(#source), + min_inclusive: #min_inclusive, + max_inclusive: #max_inclusive + } + }); + } +} + impl ClampedIntProvider { + pub fn new(source: IntProvider, min_inclusive: i32, max_inclusive: i32) -> Self { + Self { + source: Box::new(source), + min_inclusive, + max_inclusive, + } + } + pub fn get_min(&self) -> i32 { self.min_inclusive.max(self.source.get_min()) } - pub fn get(&self, random: &mut RandomGenerator) -> i32 { + + pub fn get(&self, random: &mut impl RandomImpl) -> i32 { self.source .get(random) .clamp(self.min_inclusive, self.max_inclusive) } + pub fn get_max(&self) -> i32 { self.max_inclusive.min(self.source.get_max()) } } +#[derive(Deserialize, Clone, Debug)] +pub struct ClampedNormalIntProvider { + mean: f32, + deviation: f32, + min_inclusive: i32, + max_inclusive: i32, +} + +impl ToTokens for ClampedNormalIntProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mean = syn::LitFloat::new(&self.mean.to_string(), Span::call_site()); + let deviation = syn::LitFloat::new(&self.deviation.to_string(), Span::call_site()); + let min_inclusive = LitInt::new(&self.min_inclusive.to_string(), Span::call_site()); + let max_inclusive = LitInt::new(&self.max_inclusive.to_string(), Span::call_site()); + tokens.extend(quote! { + ClampedNormalIntProvider { + mean: #mean, + deviation: #deviation, + min_inclusive: #min_inclusive, + max_inclusive: #max_inclusive + } + }); + } +} + +impl ClampedNormalIntProvider { + pub fn new(mean: f32, deviation: f32, min_inclusive: i32, max_inclusive: i32) -> Self { + Self { + mean, + deviation, + min_inclusive, + max_inclusive, + } + } + + pub fn get_min(&self) -> i32 { + self.min_inclusive + } + + pub fn get(&self, random: &mut impl RandomImpl) -> i32 { + // Generate normal distribution value and clamp to range + let gaussian = random.next_gaussian() as f32; + let value = (self.mean + gaussian * self.deviation).round() as i32; + value.clamp(self.min_inclusive, self.max_inclusive) + } + + pub fn get_max(&self) -> i32 { + self.max_inclusive + } +} + +#[derive(Deserialize, Clone, Debug)] +pub struct WeightedEntry { + data: IntProvider, + weight: i32, +} + +impl ToTokens for WeightedEntry { + fn to_tokens(&self, tokens: &mut TokenStream) { + let data = &self.data; + let weight = LitInt::new(&self.weight.to_string(), Span::call_site()); + tokens.extend(quote! { + WeightedEntry { data: #data, weight: #weight } + }); + } +} + #[derive(Deserialize, Clone, Debug)] pub struct WeightedListIntProvider { - distribution: Vec>, + distribution: Vec, +} + +impl ToTokens for WeightedListIntProvider { + fn to_tokens(&self, tokens: &mut TokenStream) { + let distribution = &self.distribution; + tokens.extend(quote! { + WeightedListIntProvider { distribution: vec![#(#distribution),*] } + }); + } } impl WeightedListIntProvider { + pub fn new(distribution: Vec) -> Self { + Self { distribution } + } + pub fn get_min(&self) -> i32 { - let mut min = i32::MAX; - for dist in &self.distribution { - let dmin = dist.data.get_min(); - min = min.min(dmin); - } - min + self.distribution + .iter() + .map(|entry| entry.data.get_min()) + .min() + .unwrap_or(0) } - pub fn get(&self, random: &mut RandomGenerator) -> i32 { - if let Some(int) = Pool.get(&self.distribution, random) { - return int.get(random); + + pub fn get(&self, random: &mut impl RandomImpl) -> i32 { + if self.distribution.is_empty() { + return 0; } - 0 + + // Calculate total weight + let total_weight: i32 = self.distribution.iter().map(|entry| entry.weight).sum(); + + if total_weight == 0 { + return 0; + } + + // Choose random weight + let chosen_weight = random.next_bounded_i32(total_weight); + let mut current_weight = 0; + + // Find the entry corresponding to the chosen weight + for entry in &self.distribution { + current_weight += entry.weight; + if chosen_weight < current_weight { + return entry.data.get(random); + } + } + + // Fallback to last entry + self.distribution.last().unwrap().data.get(random) } + pub fn get_max(&self) -> i32 { - let mut max = i32::MIN; - for dist in &self.distribution { - let dmax = dist.data.get_max(); - max = max.max(dmax); - } - max + self.distribution + .iter() + .map(|entry| entry.data.get_max()) + .max() + .unwrap_or(0) } } @@ -211,13 +392,195 @@ impl ToTokens for UniformIntProvider { } impl UniformIntProvider { + pub fn new(min_inclusive: i32, max_inclusive: i32) -> Self { + Self { + min_inclusive, + max_inclusive, + } + } + pub fn get_min(&self) -> i32 { self.min_inclusive } - pub fn get(&self, random: &mut RandomGenerator) -> i32 { + + pub fn get(&self, random: &mut impl RandomImpl) -> i32 { random.next_inbetween_i32(self.min_inclusive, self.max_inclusive) } + pub fn get_max(&self) -> i32 { self.max_inclusive } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::{RandomGenerator, get_seed}; + + #[test] + fn test_constant_int_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = ConstantIntProvider::new(42); + + assert_eq!(provider.get_min(), 42); + assert_eq!(provider.get_max(), 42); + assert_eq!(provider.get(&mut random), 42); + assert_eq!(provider.get(&mut random), 42); // Should always return the same value + } + + #[test] + fn test_uniform_int_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = UniformIntProvider::new(1, 10); + + assert_eq!(provider.get_min(), 1); + assert_eq!(provider.get_max(), 10); + + // Test that values are within range + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (1..=10).contains(&value), + "Value {} is outside range [1, 10]", + value + ); + } + } + + #[test] + fn test_biased_to_bottom_int_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = BiasedToBottomIntProvider::new(1, 20); + + assert_eq!(provider.get_min(), 1); + assert_eq!(provider.get_max(), 20); + + // Test that values are within range (biased toward lower values) + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (1..=20).contains(&value), + "Value {} is outside range [1, 20]", + value + ); + } + } + + #[test] + fn test_clamped_normal_int_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = ClampedNormalIntProvider::new(5.0, 2.0, 1, 10); + + assert_eq!(provider.get_min(), 1); + assert_eq!(provider.get_max(), 10); + + // Test that values are within range + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (1..=10).contains(&value), + "Value {} is outside range [1, 10]", + value + ); + } + } + + #[test] + fn test_clamped_int_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let source = + IntProvider::Object(NormalIntProvider::Uniform(UniformIntProvider::new(1, 100))); + let provider = ClampedIntProvider::new(source, 5, 15); + + assert_eq!(provider.get_min(), 5); + assert_eq!(provider.get_max(), 15); + + // Test that values are within clamped range + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (5..=15).contains(&value), + "Value {} is outside clamped range [5, 15]", + value + ); + } + } + + #[test] + fn test_weighted_list_int_provider() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + + let entries = vec![ + WeightedEntry { + data: IntProvider::Constant(1), + weight: 10, + }, + WeightedEntry { + data: IntProvider::Constant(2), + weight: 20, + }, + WeightedEntry { + data: IntProvider::Constant(3), + weight: 5, + }, + ]; + + let provider = WeightedListIntProvider::new(entries); + + assert_eq!(provider.get_min(), 1); + assert_eq!(provider.get_max(), 3); + + // Test that values are from the weighted list + for _ in 0..100 { + let value = provider.get(&mut random); + assert!( + (1..=3).contains(&value), + "Value {} is not from the weighted list", + value + ); + } + } + + #[test] + fn test_int_provider_enum_constant() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let provider = IntProvider::Constant(25); + + assert_eq!(provider.get_min(), 25); + assert_eq!(provider.get_max(), 25); + assert_eq!(provider.get(&mut random), 25); + } + + #[test] + fn test_int_provider_enum_object() { + let mut random = RandomGenerator::Xoroshiro( + crate::random::xoroshiro128::Xoroshiro::from_seed(get_seed()), + ); + let uniform = UniformIntProvider::new(5, 15); + let provider = IntProvider::Object(NormalIntProvider::Uniform(uniform)); + + assert_eq!(provider.get_min(), 5); + assert_eq!(provider.get_max(), 15); + + let value = provider.get(&mut random); + assert!( + (5..=15).contains(&value), + "Value {} is outside range [5, 15]", + value + ); + } +} diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index 9d2bfb8cc..08a4644e6 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -63,8 +63,7 @@ use pumpkin_data::{Block, BlockState}; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; -use pumpkin_util::random::get_seed; -use pumpkin_util::random::xoroshiro128::Xoroshiro; +use pumpkin_util::random::{RandomGenerator, get_seed, xoroshiro128::Xoroshiro}; use pumpkin_world::BlockStateId; use pumpkin_world::item::ItemStack; use rand::Rng; @@ -188,13 +187,8 @@ pub async fn drop_loot( if experience { if let Some(experience) = &block.experience { - // TODO: this is bad, this is ugly, this is used :D - let amount = - experience - .experience - .get(&mut pumpkin_util::random::RandomGenerator::Xoroshiro( - Xoroshiro::from_seed(get_seed()), - )); + let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(get_seed())); + let amount = experience.experience.get(&mut random); // TODO: Silk touch gives no exp if amount > 0 { ExperienceOrbEntity::spawn(world, pos.to_f64(), amount as u32).await;