More Int & Float Providers (#837)

* cloned

* feat: Expand float and int provider implementations with comprehensive random distributions

- Add ConstantFloatProvider, ClampedNormalFloatProvider, and TrapezoidFloatProvider to NormalFloatProvider enum
- Add ConstantIntProvider, BiasedToBottomIntProvider, ClampedIntProvider, ClampedNormalIntProvider, and WeightedListIntProvider to NormalIntProvider enum
- Implement ToTokens trait for code generation support across all provider types
- Replace external rand dependency with internal RandomImpl trait for consistent randomization
- Fix UniformFloatProvider range semantics (max_inclusive → max_exclusive)
- Add comprehensive unit tests for all provider implementations
- Update block experience drop calculation to use new random provider system

* cargo fmt
This commit is contained in:
Omar Afet
2025-06-07 21:52:56 +03:00
committed by GitHub
parent bfa6d97ed4
commit 17f80011d2
4 changed files with 788 additions and 91 deletions

View File

@@ -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<String>,
// 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<u16>,
}
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<String>,
// 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<u16>,
}

View File

@@ -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
);
}
}

View File

@@ -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<Weighted<IntProvider>>,
distribution: Vec<WeightedEntry>,
}
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<WeightedEntry>) -> 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
);
}
}

View File

@@ -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;