//! Seed-based ore prediction for Java 1.18+. //! //! Predicts scattered-ore placement attempts from the world seed by //! replaying Minecraft's chunk population RNG (decoration seed, feature seed, //! placement modifiers, and the ore blob's own RNG consumption). Terrain is //! not simulated, so results are estimates: caves and surface exposure can //! shift or remove blobs. Attempts are cross-checked against cubiomes' //! approximate surface height to grade confidence. use std::io; use serde::{Deserialize, Serialize}; use super::rng::{Xoroshiro, decoration_seed, feature_rng, mc_sin}; use super::{Dimension, parse_seed, resolve_java_version}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum OreKind { Diamond, Iron, IronVein, Copper, CopperVein, Gold, Redstone, Lapis, Coal, Netherite, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OreScanRequest { pub seed: String, pub version: String, pub dimension: Dimension, pub ores: Vec, /// Interleaved chunk coordinates: `[cx0, cz0, cx1, cz1, ...]`. pub chunks: Vec, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OreHit { pub ore: OreKind, pub x: i32, pub y: i32, pub z: i32, pub verified: bool, pub y_min: i32, pub y_max: i32, pub precision: u8, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OreChunkResult { pub cx: i32, pub cz: i32, pub hits: Vec, } #[derive(Clone, Copy, Debug)] enum CountModifier { Fixed(u32), Uniform { min: u32, max: u32 }, Rarity(u32), } #[derive(Clone, Copy, Debug)] enum HeightProvider { Uniform { min: i32, max: i32 }, Trapezoid { min: i32, max: i32, plateau: i32 }, } impl HeightProvider { fn sample(self, rng: &mut Xoroshiro) -> i32 { match self { Self::Uniform { min, max } => { rng.next_int_between_inclusive(min, max) } Self::Trapezoid { min, max, plateau } => { let span = max - min; if plateau >= span { return rng.next_int_between_inclusive(min, max); } let lower = (span - plateau) / 2; let upper = span - lower; min + rng.next_int_between_inclusive(0, upper) + rng.next_int_between_inclusive(0, lower) } } } } #[derive(Clone, Copy, Debug)] struct PlacedOre { feature_index: u32, step: u32, count: CountModifier, height: HeightProvider, blob_size: u32, discard_on_air: f32, precision: u8, scattered: bool, } const UNDERGROUND_ORES_STEP: u32 = 6; const UNDERGROUND_DECORATION_STEP: u32 = 7; /* * Global feature indices for the UNDERGROUND_ORES step, computed with * Minecraft's FeatureSorter over the vanilla biome sets (validated against * official generated data for 1.18.2 through 1.21.3): the base sequence * starts at ore_coal_upper = 9 after the nine stone-patch features, and * ore_copper sorts AFTER dripstone's ore_copper_large. `ore_diamond_medium` * joined the base list in 1.20.5, shifting every later entry by one. * Placement counts, height providers, blob sizes, and discard chances come * from the vanilla OrePlacements/OreFeatures definitions. */ fn overworld_ore_features(kind: OreKind, mc_code: i32) -> Vec { let has_medium_diamond = mc_code >= 12000; let shift = u32::from(has_medium_diamond); let ore = |feature_index: u32, count: CountModifier, height: HeightProvider, blob_size: u32, discard_on_air: f32, precision: u8| PlacedOre { feature_index, step: UNDERGROUND_ORES_STEP, count, height, blob_size, discard_on_air, precision, scattered: false, }; match kind { OreKind::Coal => vec![ ore( 9, CountModifier::Fixed(30), HeightProvider::Uniform { min: 136, max: 319 }, 17, 0.0, 88, ), ore( 10, CountModifier::Fixed(20), HeightProvider::Trapezoid { min: 0, max: 192, plateau: 0, }, 17, 0.5, 84, ), ], OreKind::Iron => vec![ ore( 11, CountModifier::Fixed(90), HeightProvider::Trapezoid { min: 80, max: 384, plateau: 0, }, 9, 0.0, 88, ), ore( 12, CountModifier::Fixed(10), HeightProvider::Trapezoid { min: -24, max: 56, plateau: 0, }, 9, 0.0, 90, ), ore( 13, CountModifier::Fixed(10), HeightProvider::Uniform { min: -64, max: 72 }, 4, 0.0, 90, ), ], OreKind::Gold => vec![ ore( 14, CountModifier::Fixed(4), HeightProvider::Trapezoid { min: -64, max: 32, plateau: 0, }, 9, 0.5, 87, ), ore( 15, CountModifier::Uniform { min: 0, max: 1 }, HeightProvider::Uniform { min: -64, max: -48 }, 9, 0.5, 87, ), ], OreKind::Redstone => vec![ ore( 16, CountModifier::Fixed(4), HeightProvider::Uniform { min: -64, max: 15 }, 8, 0.0, 90, ), ore( 17, CountModifier::Fixed(8), HeightProvider::Trapezoid { min: -96, max: -32, plateau: 0, }, 8, 0.0, 90, ), ], OreKind::Diamond => { let mut features = vec![ ore( 18, CountModifier::Fixed(7), HeightProvider::Trapezoid { min: -144, max: 16, plateau: 0, }, 4, 0.5, 88, ), ore( 19 + shift, CountModifier::Rarity(9), HeightProvider::Trapezoid { min: -144, max: 16, plateau: 0, }, 12, 0.7, 82, ), ore( 20 + shift, CountModifier::Fixed(4), HeightProvider::Trapezoid { min: -144, max: 16, plateau: 0, }, 8, 1.0, 92, ), ]; if has_medium_diamond { features.push(ore( 19, CountModifier::Fixed(2), HeightProvider::Uniform { min: -64, max: -4 }, 8, 0.5, 88, )); } features } OreKind::Lapis => vec![ ore( 21 + shift, CountModifier::Fixed(2), HeightProvider::Trapezoid { min: -32, max: 32, plateau: 0, }, 7, 0.0, 88, ), ore( 22 + shift, CountModifier::Fixed(4), HeightProvider::Uniform { min: -64, max: 64 }, 7, 1.0, 92, ), ], OreKind::Copper => vec![ore( 24 + shift, CountModifier::Fixed(16), HeightProvider::Trapezoid { min: -16, max: 112, plateau: 0, }, 10, 0.0, 88, )], OreKind::IronVein | OreKind::CopperVein | OreKind::Netherite => vec![], } } /* * Large veins come from the Overworld vein noises rather than the population * RNG, so they are scanned natively per chunk. Copper veins live at Y 0..50 * and iron veins at Y -60..-8. */ fn scan_vein_hits( kind: OreKind, world_seed: u64, cx: i32, cz: i32, surface: Option<&SurfaceGrid>, ) -> Vec { let vein_code = i32::from(kind != OreKind::CopperVein); let mut buffer = [0_i32; 24]; let count = unsafe { super::axolotl_seed_map_scan_vein( world_seed, cx, cz, vein_code, buffer.as_mut_ptr(), buffer.len() / 3, ) }; buffer[..count * 3] .chunks_exact(3) .map(|triple| { let (x, y, z) = (triple[0], triple[1], triple[2]); let verified = surface .map(|grid| (y as f32) < grid.height_at(x, z) - 6.0) .unwrap_or(false); OreHit { ore: kind, x, y, z, verified, y_min: y, y_max: y, precision: if verified { 93 } else { 78 }, } }) .collect() } /* * Ancient debris indices in the UNDERGROUND_DECORATION step are 21 and 22 * across every supported version (stable under all nether biome orderings). * Both are single-attempt scattered-ore features that are always buried. */ fn nether_ore_features(kind: OreKind, _mc_code: i32) -> Vec { if kind != OreKind::Netherite { return vec![]; } let ore = |feature_index: u32, height: HeightProvider, blob_size: u32| { PlacedOre { feature_index, step: UNDERGROUND_DECORATION_STEP, count: CountModifier::Fixed(1), height, blob_size, discard_on_air: 1.0, precision: 95, scattered: true, } }; vec![ ore( 21, HeightProvider::Trapezoid { min: 8, max: 24, plateau: 0, }, 3, ), ore(22, HeightProvider::Uniform { min: 8, max: 119 }, 2), ] } pub fn supports_ores(version_code: i32, dimension: Dimension) -> bool { version_code >= 11800 && !matches!(dimension, Dimension::End) } pub fn scan_ores(request: OreScanRequest) -> io::Result> { let (mc_version, version_code) = resolve_java_version(&request.version)?; if !supports_ores(version_code, request.dimension) { return Err(io::Error::other( "Ore prediction supports Java 1.18+ Overworld and Nether maps.", )); } if !request.chunks.len().is_multiple_of(2) || request.chunks.len() > 4_096 { return Err(io::Error::other("The ore scan chunk list is invalid.")); } let world_seed = parse_seed(&request.seed) as i64; let chunk_pairs: Vec<(i32, i32)> = request .chunks .chunks_exact(2) .map(|pair| (pair[0], pair[1])) .collect(); let surface = match request.dimension { Dimension::Overworld => { SurfaceGrid::sample(world_seed as u64, mc_version, &chunk_pairs) } _ => None, }; let (world_bottom, world_top) = match request.dimension { Dimension::Overworld => (-64, 320), _ => (0, 128), }; let requested_kinds = dedupe_kinds(&request.ores); let vein_kinds: Vec = if matches!(request.dimension, Dimension::Overworld) { requested_kinds .iter() .copied() .filter(|kind| { matches!(kind, OreKind::IronVein | OreKind::CopperVein) }) .collect() } else { Vec::new() }; let mut features: Vec<(OreKind, PlacedOre)> = Vec::new(); for kind in requested_kinds { let kind_features = match request.dimension { Dimension::Overworld => overworld_ore_features(kind, version_code), Dimension::Nether => nether_ore_features(kind, version_code), Dimension::End => vec![], }; features .extend(kind_features.into_iter().map(|feature| (kind, feature))); } let mut results = Vec::with_capacity(chunk_pairs.len()); for (cx, cz) in chunk_pairs { let chunk_decoration_seed = decoration_seed(world_seed, cx * 16, cz * 16); let mut hits = Vec::new(); for &kind in &vein_kinds { hits.extend(scan_vein_hits( kind, world_seed as u64, cx, cz, surface.as_ref(), )); } for &(kind, feature) in &features { let mut rng = feature_rng( chunk_decoration_seed, feature.feature_index, feature.step, ); let attempts = match feature.count { CountModifier::Fixed(count) => count, CountModifier::Uniform { min, max } => { min + rng.next_int(max - min + 1) } CountModifier::Rarity(chance) => { u32::from(rng.next_f32() < 1.0 / chance as f32) } }; for _ in 0..attempts { let x = cx * 16 + rng.next_int(16) as i32; let z = cz * 16 + rng.next_int(16) as i32; let y = feature.height.sample(&mut rng); let surface_height = surface.as_ref().map(|grid| grid.height_at(x, z)); let blob = if feature.scattered { Some((y, y)) } else { simulate_blob( &mut rng, x, y, z, feature.blob_size, feature.discard_on_air, surface_height, world_bottom, world_top, ) }; let Some((blob_min_y, blob_max_y)) = blob else { continue; }; let verified = match surface_height { Some(surface_y) => (blob_max_y as f32) < surface_y - 6.0, None => feature.discard_on_air >= 1.0, }; let precision = if verified { feature.precision } else { feature.precision.saturating_sub(18).max(50) }; hits.push(OreHit { ore: kind, x, y, z, verified, y_min: blob_min_y, y_max: blob_max_y, precision, }); } } results.push(OreChunkResult { cx, cz, hits }); } Ok(results) } fn dedupe_kinds(kinds: &[OreKind]) -> Vec { let mut seen = Vec::new(); for &kind in kinds { if !seen.contains(&kind) { seen.push(kind); } } seen } /* * Replays the RNG consumption of `OreFeature.place`, assuming solid terrain: * the blob angle, the two Y jitters, one radius roll per segment, and — for * partially air-sensitive ores — one roll per candidate cell. Returns the * simulated blob's Y extent, or None when the surface pre-check would skip * placement entirely. */ #[allow(clippy::too_many_arguments)] fn simulate_blob( rng: &mut Xoroshiro, x: i32, y: i32, z: i32, size: u32, discard_on_air: f32, surface_height: Option, world_bottom: i32, world_top: i32, ) -> Option<(i32, i32)> { let angle = rng.next_f32() * std::f32::consts::PI; let spread = size as f32 / 8.0; let margin = (((size as f32 / 16.0) * 2.0 + 1.0) / 2.0).ceil() as i32; let x0 = f64::from(x) + f64::from(angle).sin() * f64::from(spread); let x1 = f64::from(x) - f64::from(angle).sin() * f64::from(spread); let z0 = f64::from(z) + f64::from(angle).cos() * f64::from(spread); let z1 = f64::from(z) - f64::from(angle).cos() * f64::from(spread); let y0 = f64::from(y + rng.next_int(3) as i32 - 2); let y1 = f64::from(y + rng.next_int(3) as i32 - 2); let box_min_x = x - (spread.ceil() as i32) - margin; let box_min_y = y - 2 - margin; let box_min_z = z - (spread.ceil() as i32) - margin; if let Some(surface_y) = surface_height && box_min_y as f32 > surface_y { return None; } let segment_count = size as usize; let mut segments = vec![[0.0_f64; 4]; segment_count]; for (index, segment) in segments.iter_mut().enumerate() { let progress = index as f32 / segment_count as f32; let center_x = lerp(f64::from(progress), x0, x1); let center_y = lerp(f64::from(progress), y0, y1); let center_z = lerp(f64::from(progress), z0, z1); let radius_roll = rng.next_f64() * f64::from(size) / 16.0; let radius = ((f64::from(mc_sin(std::f32::consts::PI * progress)) + 1.0) * radius_roll + 1.0) / 2.0; *segment = [center_x, center_y, center_z, radius]; } for first in 0..segment_count.saturating_sub(1) { if segments[first][3] <= 0.0 { continue; } for second in first + 1..segment_count { if segments[second][3] <= 0.0 { continue; } let dx = segments[first][0] - segments[second][0]; let dy = segments[first][1] - segments[second][1]; let dz = segments[first][2] - segments[second][2]; let dr = segments[first][3] - segments[second][3]; if dr * dr > dx * dx + dy * dy + dz * dz { if dr > 0.0 { segments[second][3] = -1.0; } else { segments[first][3] = -1.0; } } } } let needs_cell_rolls = discard_on_air > 0.0 && discard_on_air < 1.0; let mut placed = std::collections::HashSet::new(); let mut blob_min_y = i32::MAX; let mut blob_max_y = i32::MIN; for segment in &segments { let radius = segment[3]; if radius < 0.0 { continue; } let min_cell_x = ((segment[0] - radius).floor() as i32).max(box_min_x); let min_cell_y = ((segment[1] - radius).floor() as i32).max(box_min_y); let min_cell_z = ((segment[2] - radius).floor() as i32).max(box_min_z); let max_cell_x = ((segment[0] + radius).floor() as i32).max(min_cell_x); let max_cell_y = ((segment[1] + radius).floor() as i32).max(min_cell_y); let max_cell_z = ((segment[2] + radius).floor() as i32).max(min_cell_z); for cell_x in min_cell_x..=max_cell_x { let dx = (f64::from(cell_x) + 0.5 - segment[0]) / radius; if dx * dx >= 1.0 { continue; } for cell_y in min_cell_y..=max_cell_y { let dy = (f64::from(cell_y) + 0.5 - segment[1]) / radius; if dx * dx + dy * dy >= 1.0 { continue; } for cell_z in min_cell_z..=max_cell_z { let dz = (f64::from(cell_z) + 0.5 - segment[2]) / radius; if dx * dx + dy * dy + dz * dz >= 1.0 { continue; } if cell_y < world_bottom || cell_y >= world_top { continue; } if !placed.insert((cell_x, cell_y, cell_z)) { continue; } if needs_cell_rolls { rng.next_f32(); } blob_min_y = blob_min_y.min(cell_y); blob_max_y = blob_max_y.max(cell_y); } } } } if blob_min_y > blob_max_y { return Some((y, y)); } Some((blob_min_y, blob_max_y)) } fn lerp(progress: f64, from: f64, to: f64) -> f64 { from + progress * (to - from) } /* * A cached approximate surface-height grid at 1:4 scale covering the bounding * box of the scanned chunks. */ struct SurfaceGrid { origin_x: i32, origin_z: i32, width: i32, height: i32, values: Vec, } impl SurfaceGrid { fn sample( seed: u64, mc_version: i32, chunks: &[(i32, i32)], ) -> Option { if chunks.is_empty() { return None; } let min_cx = chunks.iter().map(|chunk| chunk.0).min()?; let max_cx = chunks.iter().map(|chunk| chunk.0).max()?; let min_cz = chunks.iter().map(|chunk| chunk.1).min()?; let max_cz = chunks.iter().map(|chunk| chunk.1).max()?; let origin_x = min_cx * 4; let origin_z = min_cz * 4; let width = (max_cx - min_cx + 1) * 4; let height = (max_cz - min_cz + 1) * 4; if width <= 0 || height <= 0 || width as i64 * height as i64 > 1 << 20 { return None; } let mut values = vec![0.0_f32; (width * height) as usize]; let result = unsafe { super::axolotl_seed_map_surface_heights( seed, mc_version, 0, origin_x, origin_z, width, height, values.as_mut_ptr(), ) }; if result != 0 { return None; } Some(Self { origin_x, origin_z, width, height, values, }) } fn height_at(&self, block_x: i32, block_z: i32) -> f32 { let column = (block_x.div_euclid(4) - self.origin_x).clamp(0, self.width - 1); let row = (block_z.div_euclid(4) - self.origin_z).clamp(0, self.height - 1); self.values[(row * self.width + column) as usize] } } #[cfg(test)] mod tests { use super::*; fn scan( seed: &str, version: &str, dimension: Dimension, ores: Vec, ) -> Vec { scan_ores(OreScanRequest { seed: seed.to_owned(), version: version.to_owned(), dimension, ores, chunks: vec![0, 0, 1, 0, -1, -1], }) .expect("scan should succeed") } #[test] fn ore_scans_are_deterministic() { let first = scan( "10292992", "1.21.3", Dimension::Overworld, vec![OreKind::Diamond], ); let second = scan( "10292992", "1.21.3", Dimension::Overworld, vec![OreKind::Diamond], ); assert_eq!(first.len(), 3); for (a, b) in first.iter().zip(second.iter()) { assert_eq!(a.hits.len(), b.hits.len()); for (left, right) in a.hits.iter().zip(b.hits.iter()) { assert_eq!( (left.x, left.y, left.z), (right.x, right.y, right.z) ); } } } #[test] fn diamond_attempts_stay_in_distribution_range() { for chunk in scan( "axolotl", "1.21.3", Dimension::Overworld, vec![OreKind::Diamond], ) { for hit in chunk.hits { assert!( hit.y >= -144 && hit.y <= 16, "y={} out of range", hit.y ); assert!(hit.x >= chunk.cx * 16 && hit.x < chunk.cx * 16 + 16); assert!(hit.z >= chunk.cz * 16 && hit.z < chunk.cz * 16 + 16); } } } #[test] fn netherite_scans_use_the_nether_layout() { let results = scan( "10292992", "1.21.3", Dimension::Nether, vec![OreKind::Netherite], ); let hits: Vec<_> = results.iter().flat_map(|chunk| chunk.hits.iter()).collect(); assert!(!hits.is_empty()); for hit in hits { assert!(hit.y >= 8 && hit.y <= 119); assert!(hit.verified); } } #[test] fn vein_hits_stay_inside_their_bands_and_are_deterministic() { let chunks: Vec = (0..8_i32) .flat_map(|cx| (0..8_i32).flat_map(move |cz| [cx, cz])) .collect(); let request = OreScanRequest { seed: "10292992".to_owned(), version: "26.2".to_owned(), dimension: Dimension::Overworld, ores: vec![OreKind::IronVein, OreKind::CopperVein], chunks, }; let first = scan_ores(request.clone()).expect("vein scan should succeed"); let second = scan_ores(request).expect("vein scan should succeed"); let hits: Vec<_> = first.iter().flat_map(|chunk| chunk.hits.iter()).collect(); for hit in &hits { match hit.ore { OreKind::IronVein => assert!(hit.y >= -60 && hit.y <= -8), OreKind::CopperVein => assert!(hit.y >= 0 && hit.y <= 50), other => panic!("unexpected ore kind {other:?}"), } } let second_hits: Vec<_> = second.iter().flat_map(|chunk| chunk.hits.iter()).collect(); assert_eq!(hits.len(), second_hits.len()); } #[test] fn old_versions_reject_ore_scans() { let error = scan_ores(OreScanRequest { seed: "1".to_owned(), version: "1.16".to_owned(), dimension: Dimension::Overworld, ores: vec![OreKind::Iron], chunks: vec![0, 0], }); assert!(error.is_err()); } }