1use serde::{Deserialize, Deserializer, de::Error as _};
14
15use crate::random::Random;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum VerticalAnchor {
23 Absolute(i32),
25 AboveBottom(i32),
27 BelowTop(i32),
29}
30
31impl VerticalAnchor {
32 #[must_use]
36 pub const fn resolve_y(self, min_y: i32, height: i32) -> i32 {
37 match self {
38 Self::Absolute(y) => y,
39 Self::AboveBottom(offset) => min_y + offset,
40 Self::BelowTop(offset) => min_y + height - 1 - offset,
41 }
42 }
43}
44
45impl<'de> Deserialize<'de> for VerticalAnchor {
46 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
47 #[derive(Deserialize)]
48 #[serde(deny_unknown_fields)]
49 struct Raw {
50 #[serde(default)]
51 absolute: Option<i32>,
52 #[serde(default)]
53 above_bottom: Option<i32>,
54 #[serde(default)]
55 below_top: Option<i32>,
56 }
57 let raw = Raw::deserialize(d)?;
58 match (raw.absolute, raw.above_bottom, raw.below_top) {
59 (Some(y), None, None) => Ok(Self::Absolute(y)),
60 (None, Some(o), None) => Ok(Self::AboveBottom(o)),
61 (None, None, Some(o)) => Ok(Self::BelowTop(o)),
62 (None, None, None) => Err(D::Error::custom(
63 "VerticalAnchor requires exactly one of absolute/above_bottom/below_top",
64 )),
65 _ => Err(D::Error::custom(
66 "VerticalAnchor must have exactly one of absolute/above_bottom/below_top",
67 )),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy)]
79pub enum HeightProvider {
80 Constant(VerticalAnchor),
82 Uniform {
84 min_inclusive: VerticalAnchor,
86 max_inclusive: VerticalAnchor,
88 },
89 Trapezoid {
92 min_inclusive: VerticalAnchor,
94 max_inclusive: VerticalAnchor,
96 plateau: i32,
98 },
99 BiasedToBottom {
101 min_inclusive: VerticalAnchor,
103 max_inclusive: VerticalAnchor,
105 inner: i32,
107 },
108 VeryBiasedToBottom {
110 min_inclusive: VerticalAnchor,
112 max_inclusive: VerticalAnchor,
114 inner: i32,
116 },
117}
118
119impl HeightProvider {
120 pub fn sample<R: Random + ?Sized>(self, random: &mut R, min_y: i32, height: i32) -> i32 {
126 match self {
127 Self::Constant(anchor) => anchor.resolve_y(min_y, height),
128 Self::Uniform {
129 min_inclusive,
130 max_inclusive,
131 } => {
132 let min = min_inclusive.resolve_y(min_y, height);
133 let max = max_inclusive.resolve_y(min_y, height);
134 if min > max {
135 min
136 } else {
137 random.next_i32_between(min, max)
138 }
139 }
140 Self::Trapezoid {
141 min_inclusive,
142 max_inclusive,
143 plateau,
144 } => {
145 let min = min_inclusive.resolve_y(min_y, height);
146 let max = max_inclusive.resolve_y(min_y, height);
147 if min > max {
148 min
149 } else {
150 let range = max - min;
151 if plateau >= range {
152 random.next_i32_between(min, max)
153 } else {
154 let plateau_start = (range - plateau) / 2;
155 let plateau_end = range - plateau_start;
156 min + random.next_i32_between(0, plateau_end)
157 + random.next_i32_between(0, plateau_start)
158 }
159 }
160 }
161 Self::BiasedToBottom {
162 min_inclusive,
163 max_inclusive,
164 inner,
165 } => {
166 let min = min_inclusive.resolve_y(min_y, height);
167 let max = max_inclusive.resolve_y(min_y, height);
168 if max - min - inner < 0 {
169 min
170 } else {
171 let limit = random.next_i32_bounded(max - min - inner + 1);
172 random.next_i32_bounded(limit + inner) + min
173 }
174 }
175 Self::VeryBiasedToBottom {
176 min_inclusive,
177 max_inclusive,
178 inner,
179 } => {
180 let min = min_inclusive.resolve_y(min_y, height);
181 let max = max_inclusive.resolve_y(min_y, height);
182 if max - min - inner < 0 {
183 min
184 } else {
185 let upper_inclusive = random.next_i32_between(min + inner, max);
186 let biased_upper_inclusive = random.next_i32_between(min, upper_inclusive - 1);
187 random.next_i32_between(min, biased_upper_inclusive - 1 + inner)
188 }
189 }
190 }
191 }
192}
193
194impl<'de> Deserialize<'de> for HeightProvider {
195 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
196 #[derive(Deserialize)]
197 #[serde(tag = "type", deny_unknown_fields)]
198 enum Tagged {
199 #[serde(rename = "minecraft:constant")]
200 Constant { value: VerticalAnchor },
201 #[serde(rename = "minecraft:uniform")]
202 Uniform {
203 min_inclusive: VerticalAnchor,
204 max_inclusive: VerticalAnchor,
205 },
206 #[serde(rename = "minecraft:trapezoid")]
207 Trapezoid {
208 min_inclusive: VerticalAnchor,
209 max_inclusive: VerticalAnchor,
210 #[serde(default)]
211 plateau: i32,
212 },
213 #[serde(rename = "minecraft:biased_to_bottom")]
214 BiasedToBottom {
215 min_inclusive: VerticalAnchor,
216 max_inclusive: VerticalAnchor,
217 #[serde(default = "default_inner")]
218 inner: i32,
219 },
220 #[serde(rename = "minecraft:very_biased_to_bottom")]
221 VeryBiasedToBottom {
222 min_inclusive: VerticalAnchor,
223 max_inclusive: VerticalAnchor,
224 #[serde(default = "default_inner")]
225 inner: i32,
226 },
227 }
228
229 const fn default_inner() -> i32 {
230 1
231 }
232
233 let value = serde_json::Value::deserialize(d)?;
234 let has_type = value
235 .as_object()
236 .is_some_and(|object| object.contains_key("type"));
237
238 if !has_type {
239 let anchor = VerticalAnchor::deserialize(value).map_err(D::Error::custom)?;
240 return Ok(Self::Constant(anchor));
241 }
242
243 Ok(
244 match serde_json::from_value(value).map_err(D::Error::custom)? {
245 Tagged::Constant { value } => Self::Constant(value),
246 Tagged::Uniform {
247 min_inclusive,
248 max_inclusive,
249 } => Self::Uniform {
250 min_inclusive,
251 max_inclusive,
252 },
253 Tagged::Trapezoid {
254 min_inclusive,
255 max_inclusive,
256 plateau,
257 } => Self::Trapezoid {
258 min_inclusive,
259 max_inclusive,
260 plateau,
261 },
262 Tagged::BiasedToBottom {
263 min_inclusive,
264 max_inclusive,
265 inner,
266 } => Self::BiasedToBottom {
267 min_inclusive,
268 max_inclusive,
269 inner,
270 },
271 Tagged::VeryBiasedToBottom {
272 min_inclusive,
273 max_inclusive,
274 inner,
275 } => Self::VeryBiasedToBottom {
276 min_inclusive,
277 max_inclusive,
278 inner,
279 },
280 },
281 )
282 }
283}
284
285#[derive(Debug, Clone)]
292pub enum IntProvider {
293 Constant(i32),
295 Uniform {
297 min_inclusive: i32,
299 max_inclusive: i32,
301 },
302 BiasedToBottom {
304 min_inclusive: i32,
306 max_inclusive: i32,
308 },
309 VeryBiasedToBottom {
311 min_inclusive: i32,
313 max_inclusive: i32,
315 inner: i32,
317 },
318 Trapezoid {
320 min: i32,
322 max: i32,
324 plateau: i32,
326 },
327 ClampedNormal {
329 mean: f32,
331 deviation: f32,
333 min_inclusive: i32,
335 max_inclusive: i32,
337 },
338 Clamped {
340 source: Box<IntProvider>,
342 min_inclusive: i32,
344 max_inclusive: i32,
346 },
347 WeightedList {
349 distribution: Vec<WeightedIntProvider>,
351 },
352}
353
354#[derive(Debug, Clone)]
356pub struct WeightedIntProvider {
357 pub data: IntProvider,
359 pub weight: i32,
361}
362
363#[derive(Debug, Clone, Copy)]
368pub struct UniformIntProvider {
369 pub min_inclusive: i32,
371 pub max_inclusive: i32,
373}
374
375impl UniformIntProvider {
376 pub fn sample<R: Random + ?Sized>(self, random: &mut R) -> i32 {
378 random.next_i32_between(self.min_inclusive, self.max_inclusive)
379 }
380
381 #[must_use]
383 pub const fn with_max_inclusive(self, max_inclusive: i32) -> Self {
384 Self {
385 min_inclusive: self.min_inclusive,
386 max_inclusive,
387 }
388 }
389}
390
391impl<'de> Deserialize<'de> for UniformIntProvider {
392 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
393 #[derive(Deserialize)]
394 #[serde(deny_unknown_fields)]
395 struct Range {
396 min_inclusive: i32,
397 max_inclusive: i32,
398 }
399
400 #[derive(Deserialize)]
401 #[serde(tag = "type", deny_unknown_fields)]
402 enum Tagged {
403 #[serde(rename = "minecraft:uniform")]
404 Uniform {
405 min_inclusive: i32,
406 max_inclusive: i32,
407 },
408 }
409
410 let value = serde_json::Value::deserialize(d)?;
411 let has_type = value
412 .as_object()
413 .is_some_and(|object| object.contains_key("type"));
414
415 let (min_inclusive, max_inclusive) = if has_type {
416 match serde_json::from_value(value).map_err(D::Error::custom)? {
417 Tagged::Uniform {
418 min_inclusive,
419 max_inclusive,
420 } => (min_inclusive, max_inclusive),
421 }
422 } else {
423 let Range {
424 min_inclusive,
425 max_inclusive,
426 } = Range::deserialize(value).map_err(D::Error::custom)?;
427 (min_inclusive, max_inclusive)
428 };
429
430 if min_inclusive > max_inclusive {
431 return Err(D::Error::custom(
432 "UniformIntProvider min_inclusive exceeds max_inclusive",
433 ));
434 }
435
436 Ok(Self {
437 min_inclusive,
438 max_inclusive,
439 })
440 }
441}
442
443impl IntProvider {
444 #[must_use]
446 pub fn min(&self) -> i32 {
447 match self {
448 Self::Constant(value) => *value,
449 Self::Uniform { min_inclusive, .. }
450 | Self::BiasedToBottom { min_inclusive, .. }
451 | Self::VeryBiasedToBottom { min_inclusive, .. }
452 | Self::Clamped { min_inclusive, .. }
453 | Self::ClampedNormal { min_inclusive, .. } => *min_inclusive,
454 Self::Trapezoid { min, .. } => *min,
455 Self::WeightedList { distribution } => {
456 let mut min = 0;
457 let mut found = false;
458 for entry in distribution {
459 let value = entry.data.min();
460 if !found || value < min {
461 min = value;
462 found = true;
463 }
464 }
465 min
466 }
467 }
468 }
469
470 #[must_use]
472 pub fn max(&self) -> i32 {
473 match self {
474 Self::Constant(value) => *value,
475 Self::Uniform { max_inclusive, .. }
476 | Self::BiasedToBottom { max_inclusive, .. }
477 | Self::VeryBiasedToBottom { max_inclusive, .. }
478 | Self::Clamped { max_inclusive, .. }
479 | Self::ClampedNormal { max_inclusive, .. } => *max_inclusive,
480 Self::Trapezoid { max, .. } => *max,
481 Self::WeightedList { distribution } => {
482 let mut max = 0;
483 let mut found = false;
484 for entry in distribution {
485 let value = entry.data.max();
486 if !found || value > max {
487 max = value;
488 found = true;
489 }
490 }
491 max
492 }
493 }
494 }
495
496 pub fn sample<R: Random + ?Sized>(&self, random: &mut R) -> i32 {
501 match self {
502 Self::Constant(v) => *v,
503 Self::Uniform {
504 min_inclusive,
505 max_inclusive,
506 } => random.next_i32_between(*min_inclusive, *max_inclusive),
507 Self::BiasedToBottom {
508 min_inclusive,
509 max_inclusive,
510 } => {
511 let span = *max_inclusive - *min_inclusive + 1;
512 let bound = random.next_i32_bounded(span) + 1;
513 *min_inclusive + random.next_i32_bounded(bound)
514 }
515 Self::VeryBiasedToBottom {
516 min_inclusive,
517 max_inclusive,
518 inner,
519 } => {
520 let limit = *max_inclusive - *min_inclusive - *inner + 1;
521 if limit <= 0 {
522 *min_inclusive
523 } else {
524 let upper_inclusive = random.next_i32_bounded(limit) + *min_inclusive + *inner;
525 let biased_upper_inclusive =
526 random.next_i32_between(*min_inclusive, upper_inclusive - 1);
527 random.next_i32_between(*min_inclusive, biased_upper_inclusive - 1 + *inner)
528 }
529 }
530 Self::Trapezoid { min, max, plateau } => {
531 if *plateau == 0 && *max == -*min {
532 random.next_i32_bounded(*max + 1) - random.next_i32_bounded(*max + 1)
533 } else {
534 let range = *max - *min;
535 if *plateau >= range {
536 random.next_i32_between(*min, *max)
537 } else {
538 let plateau_start = (range - *plateau) / 2;
539 let plateau_end = range - plateau_start;
540 *min + random.next_i32_between(0, plateau_end)
541 + random.next_i32_between(0, plateau_start)
542 }
543 }
544 }
545 Self::ClampedNormal {
546 mean,
547 deviation,
548 min_inclusive,
549 max_inclusive,
550 } => {
551 let sample = *mean + *deviation * random.next_gaussian() as f32;
552 sample.clamp(*min_inclusive as f32, *max_inclusive as f32) as i32
553 }
554 Self::Clamped {
555 source,
556 min_inclusive,
557 max_inclusive,
558 } => source.sample(random).clamp(*min_inclusive, *max_inclusive),
559 Self::WeightedList { distribution } => {
560 let total_weight: i32 = distribution.iter().map(|entry| entry.weight).sum();
561 if total_weight <= 0 {
562 return 0;
563 }
564 let mut target = random.next_i32_bounded(total_weight);
565 for entry in distribution {
566 target -= entry.weight;
567 if target < 0 {
568 return entry.data.sample(random);
569 }
570 }
571 0
572 }
573 }
574 }
575}
576
577impl<'de> Deserialize<'de> for IntProvider {
578 #[expect(
579 clippy::too_many_lines,
580 reason = "keeps the vanilla int-provider schema variants in one deserialization table"
581 )]
582 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
583 #[derive(Deserialize)]
584 #[serde(tag = "type", deny_unknown_fields)]
585 enum Tagged {
586 #[serde(rename = "minecraft:constant")]
587 Constant { value: i32 },
588 #[serde(rename = "minecraft:uniform")]
589 Uniform {
590 min_inclusive: i32,
591 max_inclusive: i32,
592 },
593 #[serde(rename = "minecraft:biased_to_bottom")]
594 BiasedToBottom {
595 min_inclusive: i32,
596 max_inclusive: i32,
597 },
598 #[serde(rename = "minecraft:very_biased_to_bottom")]
599 VeryBiasedToBottom {
600 min_inclusive: i32,
601 max_inclusive: i32,
602 #[serde(default = "default_inner")]
603 inner: i32,
604 },
605 #[serde(rename = "minecraft:trapezoid")]
606 Trapezoid { min: i32, max: i32, plateau: i32 },
607 #[serde(rename = "minecraft:clamped_normal")]
608 ClampedNormal {
609 mean: f32,
610 deviation: f32,
611 min_inclusive: i32,
612 max_inclusive: i32,
613 },
614 #[serde(rename = "minecraft:clamped")]
615 Clamped {
616 source: Box<IntProvider>,
617 min_inclusive: i32,
618 max_inclusive: i32,
619 },
620 #[serde(rename = "minecraft:weighted_list")]
621 WeightedList {
622 distribution: Vec<WeightedIntProvider>,
623 },
624 }
625
626 const fn default_inner() -> i32 {
627 1
628 }
629
630 let value = serde_json::Value::deserialize(d)?;
631 if value.is_number() {
632 return Ok(Self::Constant(
633 i32::deserialize(value).map_err(D::Error::custom)?,
634 ));
635 }
636
637 Ok(
638 match serde_json::from_value(value).map_err(D::Error::custom)? {
639 Tagged::Constant { value } => Self::Constant(value),
640 Tagged::Uniform {
641 min_inclusive,
642 max_inclusive,
643 } => Self::Uniform {
644 min_inclusive,
645 max_inclusive,
646 },
647 Tagged::BiasedToBottom {
648 min_inclusive,
649 max_inclusive,
650 } => Self::BiasedToBottom {
651 min_inclusive,
652 max_inclusive,
653 },
654 Tagged::VeryBiasedToBottom {
655 min_inclusive,
656 max_inclusive,
657 inner,
658 } => Self::VeryBiasedToBottom {
659 min_inclusive,
660 max_inclusive,
661 inner,
662 },
663 Tagged::Trapezoid { min, max, plateau } => Self::Trapezoid { min, max, plateau },
664 Tagged::ClampedNormal {
665 mean,
666 deviation,
667 min_inclusive,
668 max_inclusive,
669 } => Self::ClampedNormal {
670 mean,
671 deviation,
672 min_inclusive,
673 max_inclusive,
674 },
675 Tagged::Clamped {
676 source,
677 min_inclusive,
678 max_inclusive,
679 } => Self::Clamped {
680 source,
681 min_inclusive,
682 max_inclusive,
683 },
684 Tagged::WeightedList { distribution } => Self::WeightedList { distribution },
685 },
686 )
687 }
688}
689
690impl<'de> Deserialize<'de> for WeightedIntProvider {
691 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
692 #[derive(Deserialize)]
693 #[serde(deny_unknown_fields)]
694 struct Raw {
695 data: IntProvider,
696 weight: i32,
697 }
698
699 let raw = Raw::deserialize(d)?;
700 Ok(Self {
701 data: raw.data,
702 weight: raw.weight,
703 })
704 }
705}
706
707#[derive(Debug, Clone, Copy)]
714pub enum FloatProvider {
715 Constant(f32),
717 Uniform {
719 min_inclusive: f32,
721 max_exclusive: f32,
723 },
724 Trapezoid {
727 min: f32,
729 max: f32,
731 plateau: f32,
733 },
734 ClampedNormal {
736 mean: f32,
738 deviation: f32,
740 min: f32,
742 max: f32,
744 },
745}
746
747impl FloatProvider {
748 pub fn sample<R: Random + ?Sized>(self, random: &mut R) -> f32 {
753 match self {
754 Self::Constant(v) => v,
755 Self::Uniform {
756 min_inclusive,
757 max_exclusive,
758 } => random.next_f32() * (max_exclusive - min_inclusive) + min_inclusive,
759 Self::Trapezoid { min, max, plateau } => {
760 let range = max - min;
761 let plateau_start = (range - plateau) / 2.0;
762 let plateau_end = range - plateau_start;
763 min + random.next_f32() * plateau_end + random.next_f32() * plateau_start
764 }
765 Self::ClampedNormal {
766 mean,
767 deviation,
768 min,
769 max,
770 } => {
771 let sample = mean + deviation * random.next_gaussian() as f32;
773 sample.clamp(min, max)
774 }
775 }
776 }
777
778 #[must_use]
780 pub const fn min(self) -> f32 {
781 match self {
782 Self::Constant(v) => v,
783 Self::Uniform { min_inclusive, .. } => min_inclusive,
784 Self::Trapezoid { min, .. } | Self::ClampedNormal { min, .. } => min,
785 }
786 }
787
788 #[must_use]
790 pub const fn max(self) -> f32 {
791 match self {
792 Self::Constant(v) => v,
793 Self::Uniform { max_exclusive, .. } => max_exclusive,
794 Self::Trapezoid { max, .. } | Self::ClampedNormal { max, .. } => max,
795 }
796 }
797}
798
799impl<'de> Deserialize<'de> for FloatProvider {
800 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
801 #[derive(Deserialize)]
802 #[serde(tag = "type", deny_unknown_fields)]
803 enum Tagged {
804 #[serde(rename = "minecraft:constant")]
805 Constant { value: f32 },
806 #[serde(rename = "minecraft:uniform")]
807 Uniform {
808 min_inclusive: f32,
809 max_exclusive: f32,
810 },
811 #[serde(rename = "minecraft:trapezoid")]
812 Trapezoid { min: f32, max: f32, plateau: f32 },
813 #[serde(rename = "minecraft:clamped_normal")]
814 ClampedNormal {
815 mean: f32,
816 deviation: f32,
817 min: f32,
818 max: f32,
819 },
820 }
821
822 let value = serde_json::Value::deserialize(d)?;
823 if value.is_number() {
824 return Ok(Self::Constant(
825 f32::deserialize(value).map_err(D::Error::custom)?,
826 ));
827 }
828
829 Ok(
830 match serde_json::from_value(value).map_err(D::Error::custom)? {
831 Tagged::Constant { value: v } => Self::Constant(v),
832 Tagged::Uniform {
833 min_inclusive,
834 max_exclusive,
835 } => Self::Uniform {
836 min_inclusive,
837 max_exclusive,
838 },
839 Tagged::Trapezoid { min, max, plateau } => Self::Trapezoid { min, max, plateau },
840 Tagged::ClampedNormal {
841 mean,
842 deviation,
843 min,
844 max,
845 } => Self::ClampedNormal {
846 mean,
847 deviation,
848 min,
849 max,
850 },
851 },
852 )
853 }
854}
855
856#[cfg(test)]
859#[expect(
860 clippy::unwrap_used,
861 clippy::float_cmp,
862 reason = "test assertions: unwrap panics on parse failures, float equality is the check"
863)]
864mod test {
865 use super::*;
866 use crate::random::legacy_random::LegacyRandom;
867
868 #[test]
869 fn vertical_anchor_resolve() {
870 assert_eq!(VerticalAnchor::Absolute(42).resolve_y(-64, 384), 42);
871 assert_eq!(VerticalAnchor::AboveBottom(8).resolve_y(-64, 384), -56);
872 assert_eq!(VerticalAnchor::BelowTop(1).resolve_y(0, 128), 126);
873 }
874
875 #[test]
876 fn vertical_anchor_deserialize() {
877 let a: VerticalAnchor = serde_json::from_str(r#"{"absolute": 180}"#).unwrap();
878 assert_eq!(a, VerticalAnchor::Absolute(180));
879 let b: VerticalAnchor = serde_json::from_str(r#"{"above_bottom": 8}"#).unwrap();
880 assert_eq!(b, VerticalAnchor::AboveBottom(8));
881 let c: VerticalAnchor = serde_json::from_str(r#"{"below_top": 1}"#).unwrap();
882 assert_eq!(c, VerticalAnchor::BelowTop(1));
883 assert!(serde_json::from_str::<VerticalAnchor>(r"{}").is_err());
884 assert!(
885 serde_json::from_str::<VerticalAnchor>(r#"{"absolute": 1, "above_bottom": 2}"#)
886 .is_err()
887 );
888 }
889
890 #[test]
891 fn height_provider_deserialize_shortcut() {
892 let hp: HeightProvider = serde_json::from_str(r#"{"absolute": 180}"#).unwrap();
894 match hp {
895 HeightProvider::Constant(VerticalAnchor::Absolute(180)) => (),
896 other => panic!("expected Constant(Absolute(180)), got {other:?}"),
897 }
898 }
899
900 #[test]
901 fn height_provider_uniform_from_carver_json() {
902 let hp: HeightProvider = serde_json::from_str(
903 r#"{
904 "type": "minecraft:uniform",
905 "max_inclusive": {"absolute": 180},
906 "min_inclusive": {"above_bottom": 8}
907 }"#,
908 )
909 .unwrap();
910 match hp {
911 HeightProvider::Uniform {
912 min_inclusive,
913 max_inclusive,
914 } => {
915 assert_eq!(min_inclusive, VerticalAnchor::AboveBottom(8));
916 assert_eq!(max_inclusive, VerticalAnchor::Absolute(180));
917 }
918 other => panic!("expected Uniform, got {other:?}"),
919 }
920 }
921
922 #[test]
923 fn float_provider_bare_float() {
924 let fp: FloatProvider = serde_json::from_str("3.0").unwrap();
925 match fp {
926 FloatProvider::Constant(v) => assert!((v - 3.0).abs() < 1e-6),
927 other => panic!("expected Constant, got {other:?}"),
928 }
929 }
930
931 #[test]
932 fn float_provider_uniform_from_carver_json() {
933 let fp: FloatProvider = serde_json::from_str(
934 r#"{
935 "type": "minecraft:uniform",
936 "max_exclusive": 1.4,
937 "min_inclusive": 0.7
938 }"#,
939 )
940 .unwrap();
941 match fp {
942 FloatProvider::Uniform {
943 min_inclusive,
944 max_exclusive,
945 } => {
946 assert!((min_inclusive - 0.7).abs() < 1e-6);
947 assert!((max_exclusive - 1.4).abs() < 1e-6);
948 }
949 other => panic!("expected Uniform, got {other:?}"),
950 }
951 }
952
953 #[test]
954 fn float_provider_trapezoid_from_carver_json() {
955 let fp: FloatProvider = serde_json::from_str(
956 r#"{
957 "type": "minecraft:trapezoid",
958 "max": 6.0,
959 "min": 0.0,
960 "plateau": 2.0
961 }"#,
962 )
963 .unwrap();
964 match fp {
965 FloatProvider::Trapezoid { min, max, plateau } => {
966 assert_eq!(min, 0.0);
967 assert_eq!(max, 6.0);
968 assert_eq!(plateau, 2.0);
969 }
970 other => panic!("expected Trapezoid, got {other:?}"),
971 }
972 }
973
974 #[test]
975 fn int_provider_clamped_normal_prefers_tagged_shape() {
976 let provider: IntProvider = serde_json::from_str(
977 r#"{
978 "type": "minecraft:clamped_normal",
979 "mean": 0.0,
980 "deviation": 3.0,
981 "min_inclusive": -10,
982 "max_inclusive": 10
983 }"#,
984 )
985 .unwrap();
986
987 match provider {
988 IntProvider::ClampedNormal {
989 mean,
990 deviation,
991 min_inclusive,
992 max_inclusive,
993 } => {
994 assert_eq!(mean, 0.0);
995 assert_eq!(deviation, 3.0);
996 assert_eq!(min_inclusive, -10);
997 assert_eq!(max_inclusive, 10);
998 }
999 other => panic!("expected ClampedNormal, got {other:?}"),
1000 }
1001 }
1002
1003 #[test]
1004 fn provider_type_tags_require_extracted_registry_ids() {
1005 assert!(
1006 serde_json::from_str::<HeightProvider>(
1007 r#"{
1008 "type": "uniform",
1009 "max_inclusive": {"absolute": 180},
1010 "min_inclusive": {"above_bottom": 8}
1011 }"#,
1012 )
1013 .is_err()
1014 );
1015 assert!(
1016 serde_json::from_str::<UniformIntProvider>(
1017 r#"{
1018 "type": "uniform",
1019 "min_inclusive": 0,
1020 "max_inclusive": 10
1021 }"#,
1022 )
1023 .is_err()
1024 );
1025 assert!(
1026 serde_json::from_str::<IntProvider>(
1027 r#"{
1028 "type": "uniform",
1029 "min_inclusive": 0,
1030 "max_inclusive": 10
1031 }"#,
1032 )
1033 .is_err()
1034 );
1035 assert!(
1036 serde_json::from_str::<FloatProvider>(
1037 r#"{
1038 "type": "uniform",
1039 "min_inclusive": 0.0,
1040 "max_exclusive": 1.0
1041 }"#,
1042 )
1043 .is_err()
1044 );
1045 }
1046
1047 #[test]
1048 fn provider_typed_payloads_deny_unknown_fields() {
1049 assert!(
1050 serde_json::from_str::<HeightProvider>(
1051 r#"{
1052 "type": "minecraft:uniform",
1053 "max_inclusive": {"absolute": 180},
1054 "min_inclusive": {"above_bottom": 8},
1055 "extra": 0
1056 }"#,
1057 )
1058 .is_err()
1059 );
1060 assert!(
1061 serde_json::from_str::<UniformIntProvider>(
1062 r#"{
1063 "type": "minecraft:uniform",
1064 "min_inclusive": 0,
1065 "max_inclusive": 10,
1066 "extra": 0
1067 }"#,
1068 )
1069 .is_err()
1070 );
1071 assert!(
1072 serde_json::from_str::<IntProvider>(
1073 r#"{
1074 "type": "minecraft:clamped",
1075 "source": 4,
1076 "min_inclusive": 0,
1077 "max_inclusive": 10,
1078 "extra": 0
1079 }"#,
1080 )
1081 .is_err()
1082 );
1083 assert!(
1084 serde_json::from_str::<FloatProvider>(
1085 r#"{
1086 "type": "minecraft:uniform",
1087 "min_inclusive": 0.0,
1088 "max_exclusive": 1.0,
1089 "extra": 0.0
1090 }"#,
1091 )
1092 .is_err()
1093 );
1094 }
1095
1096 #[test]
1097 fn int_provider_requires_typed_object_or_bare_constant() {
1098 assert!(
1099 serde_json::from_str::<IntProvider>(
1100 r#"{
1101 "min_inclusive": 0,
1102 "max_inclusive": 10
1103 }"#,
1104 )
1105 .is_err()
1106 );
1107 assert!(
1108 serde_json::from_str::<IntProvider>(
1109 r#"{
1110 "type": "minecraft:weighted_list",
1111 "distribution": [
1112 {
1113 "data": 1,
1114 "weight": 2,
1115 "extra": 3
1116 }
1117 ]
1118 }"#,
1119 )
1120 .is_err()
1121 );
1122 }
1123
1124 #[test]
1125 fn int_provider_symmetric_trapezoid_sample_matches_vanilla_shortcut() {
1126 let provider = IntProvider::Trapezoid {
1127 min: -7,
1128 max: 7,
1129 plateau: 0,
1130 };
1131 let mut rng = LegacyRandom::from_seed(123);
1132 let mut rng_ref = LegacyRandom::from_seed(123);
1133 let sample = provider.sample(&mut rng);
1134 let expected = rng_ref.next_i32_bounded(8) - rng_ref.next_i32_bounded(8);
1135 assert_eq!(sample, expected);
1136 }
1137
1138 #[test]
1140 fn float_provider_uniform_sample_matches_vanilla() {
1141 let fp = FloatProvider::Uniform {
1142 min_inclusive: 0.7,
1143 max_exclusive: 1.4,
1144 };
1145 let mut rng = LegacyRandom::from_seed(0);
1146 let mut rng_ref = LegacyRandom::from_seed(0);
1147 let sample = fp.sample(&mut rng);
1148 let expected = rng_ref.next_f32() * (1.4 - 0.7) + 0.7;
1149 assert_eq!(sample, expected);
1150 }
1151
1152 #[test]
1154 fn height_provider_uniform_sample_matches_vanilla() {
1155 let hp = HeightProvider::Uniform {
1156 min_inclusive: VerticalAnchor::AboveBottom(8),
1157 max_inclusive: VerticalAnchor::Absolute(180),
1158 };
1159 let min_y = -64;
1160 let height = 384;
1161 let mut rng = LegacyRandom::from_seed(42);
1162 let mut rng_ref = LegacyRandom::from_seed(42);
1163 let sample = hp.sample(&mut rng, min_y, height);
1164 let expected = rng_ref.next_i32_between(-56, 180);
1166 assert_eq!(sample, expected);
1167 }
1168}