Files
Starlight_Launcher/apps/app/src/seed_map/mod.rs
T

1198 lines
35 KiB
Rust

use std::{
io::{self, Cursor},
path::Path,
};
use quartz_nbt::{NbtCompound, NbtTag};
use serde::{Deserialize, Serialize};
pub mod ores;
pub mod rng;
#[repr(C)]
#[derive(Clone, Copy)]
struct BridgeFeature {
x: i32,
z: i32,
kind: u32,
approximate: u8,
end_ship: i8,
}
unsafe extern "C" {
fn axolotl_seed_map_java_version(version: i32) -> i32;
fn axolotl_seed_map_render(
seed: u64,
minecraft_version: i32,
generator_flags: i32,
dimension: i32,
x: i32,
z: i32,
scale: i32,
width: i32,
height: i32,
elevation: i32,
terrain: i32,
contours: i32,
highlight_mask: *const u8,
rgb: *mut u8,
rgb_len: usize,
) -> i32;
fn axolotl_seed_map_find_features(
seed: u64,
minecraft_version: i32,
generator_flags: i32,
dimension: i32,
min_x: i32,
min_z: i32,
max_x: i32,
max_z: i32,
feature_mask: u32,
out: *mut BridgeFeature,
out_len: usize,
) -> usize;
fn axolotl_seed_map_get_spawn(
seed: u64,
minecraft_version: i32,
generator_flags: i32,
x: *mut i32,
z: *mut i32,
) -> i32;
fn axolotl_seed_map_biome_at(
seed: u64,
minecraft_version: i32,
generator_flags: i32,
dimension: i32,
x: i32,
y: i32,
z: i32,
) -> i32;
fn axolotl_seed_map_surface_heights(
seed: u64,
minecraft_version: i32,
generator_flags: i32,
x: i32,
z: i32,
width: i32,
height: i32,
out: *mut f32,
) -> i32;
fn axolotl_seed_map_scan_vein(
seed: u64,
chunk_x: i32,
chunk_z: i32,
vein_kind: i32,
out_xyz: *mut i32,
out_cap: usize,
) -> usize;
}
pub const FEATURE_VILLAGE: u32 = 1 << 0;
pub const FEATURE_OUTPOST: u32 = 1 << 1;
pub const FEATURE_SHIPWRECK: u32 = 1 << 2;
pub const FEATURE_MONUMENT: u32 = 1 << 3;
pub const FEATURE_MANSION: u32 = 1 << 4;
pub const FEATURE_ANCIENT_CITY: u32 = 1 << 5;
pub const FEATURE_TRAIL_RUINS: u32 = 1 << 6;
pub const FEATURE_TRIAL_CHAMBERS: u32 = 1 << 7;
pub const FEATURE_RUINED_PORTAL: u32 = 1 << 8;
pub const FEATURE_STRONGHOLD: u32 = 1 << 9;
pub const FEATURE_SLIME_CHUNK: u32 = 1 << 10;
pub const FEATURE_DESERT_PYRAMID: u32 = 1 << 11;
pub const FEATURE_JUNGLE_TEMPLE: u32 = 1 << 12;
pub const FEATURE_SWAMP_HUT: u32 = 1 << 13;
pub const FEATURE_IGLOO: u32 = 1 << 14;
pub const FEATURE_OCEAN_RUIN: u32 = 1 << 15;
pub const FEATURE_BURIED_TREASURE: u32 = 1 << 16;
pub const FEATURE_MINESHAFT: u32 = 1 << 17;
pub const FEATURE_DESERT_WELL: u32 = 1 << 18;
pub const FEATURE_GEODE: u32 = 1 << 19;
pub const FEATURE_FORTRESS: u32 = 1 << 20;
pub const FEATURE_BASTION: u32 = 1 << 21;
pub const FEATURE_END_CITY: u32 = 1 << 22;
pub const FEATURE_END_GATEWAY: u32 = 1 << 23;
pub const ALL_FEATURES: u32 = FEATURE_VILLAGE
| FEATURE_OUTPOST
| FEATURE_SHIPWRECK
| FEATURE_MONUMENT
| FEATURE_MANSION
| FEATURE_ANCIENT_CITY
| FEATURE_TRAIL_RUINS
| FEATURE_TRIAL_CHAMBERS
| FEATURE_RUINED_PORTAL
| FEATURE_STRONGHOLD
| FEATURE_SLIME_CHUNK
| FEATURE_DESERT_PYRAMID
| FEATURE_JUNGLE_TEMPLE
| FEATURE_SWAMP_HUT
| FEATURE_IGLOO
| FEATURE_OCEAN_RUIN
| FEATURE_BURIED_TREASURE
| FEATURE_MINESHAFT
| FEATURE_DESERT_WELL
| FEATURE_GEODE
| FEATURE_FORTRESS
| FEATURE_BASTION
| FEATURE_END_CITY
| FEATURE_END_GATEWAY;
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Edition {
Java,
#[serde(rename = "java-large-biomes")]
JavaLargeBiomes,
}
impl Edition {
fn generator_flags(self) -> i32 {
match self {
Self::JavaLargeBiomes => 1,
Self::Java => 0,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Dimension {
Overworld,
Nether,
End,
}
impl Dimension {
fn as_cubiomes(self) -> i32 {
match self {
Self::Overworld => 0,
Self::Nether => -1,
Self::End => 1,
}
}
}
/*
* Supported Java versions, newest first, using Axolotl version codes
* (major * 10000 + minor * 100 + patch) that the cubiomes bridge maps to its
* own engine versions.
*/
const JAVA_VERSIONS: &[(&str, i32)] = &[
("26.2", 260200),
("26.1.2", 260102),
("26.1", 260100),
("1.21.9", 12109),
("1.21.6", 12106),
("1.21.5", 12105),
("1.21.4", 12104),
("1.21.3", 12103),
("1.21.1", 12101),
("1.20", 12000),
("1.19.4", 11904),
("1.19.2", 11902),
("1.18", 11800),
("1.17", 11700),
("1.16", 11600),
("1.15", 11500),
("1.14", 11400),
("1.13", 11300),
("1.12", 11200),
("1.11", 11100),
("1.10", 11000),
("1.9", 10900),
("1.8", 10800),
("1.7", 10700),
("1.6", 10600),
("1.5", 10500),
("1.4", 10400),
("1.3", 10300),
("1.2", 10200),
("1.1", 10100),
("1.0", 10000),
];
const LARGE_BIOMES_MIN_CODE: i32 = 10300;
const NETHER_BIOMES_MIN_CODE: i32 = 11600;
const END_BIOMES_MIN_CODE: i32 = 10900;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TileRequest {
pub seed: String,
pub edition: Edition,
pub version: String,
pub dimension: Dimension,
pub x: i32,
pub z: i32,
pub scale: i32,
pub width: i32,
pub height: i32,
pub elevation: Option<i32>,
pub terrain: bool,
pub contours: bool,
pub highlight_biomes: Option<Vec<u16>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeatureQuery {
pub seed: String,
pub edition: Edition,
pub version: String,
pub dimension: Dimension,
pub min_x: i32,
pub min_z: i32,
pub max_x: i32,
pub max_z: i32,
pub feature_mask: Option<u32>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MapFeature {
pub kind: String,
pub x: i32,
pub z: i32,
pub approximate: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_ship: Option<bool>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpawnPoint {
pub x: i32,
pub z: i32,
pub approximate: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionProfile {
pub edition: Edition,
pub version: String,
pub available: bool,
pub dimensions: Vec<Dimension>,
pub ores: bool,
pub note: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LevelDatInfo {
pub seed: String,
pub version: Option<String>,
}
pub fn version_profiles() -> Vec<VersionProfile> {
let mut profiles = Vec::new();
for edition in [Edition::Java, Edition::JavaLargeBiomes] {
for &(version, code) in JAVA_VERSIONS {
if matches!(edition, Edition::JavaLargeBiomes)
&& code < LARGE_BIOMES_MIN_CODE
{
continue;
}
let mut dimensions = vec![Dimension::Overworld];
if code >= NETHER_BIOMES_MIN_CODE {
dimensions.push(Dimension::Nether);
}
if code >= END_BIOMES_MIN_CODE {
dimensions.push(Dimension::End);
}
profiles.push(VersionProfile {
edition,
version: version.to_owned(),
available: true,
dimensions,
ores: ores::supports_ores(code, Dimension::Overworld),
note: None,
});
}
}
profiles
}
/*
* Renders a tile and returns tightly packed RGBA pixels; the caller knows the
* requested dimensions and rebuilds the image without any encode/decode step.
*/
pub fn render_tile(request: TileRequest) -> io::Result<Vec<u8>> {
let (minecraft_version, _) = resolve_java_version(&request.version)?;
validate_tile_request(&request)?;
let highlight_mask = request.highlight_biomes.as_ref().and_then(|biomes| {
if biomes.is_empty() {
return None;
}
let mut mask = [0_u8; 256];
for &biome in biomes {
if usize::from(biome) < mask.len() {
mask[usize::from(biome)] = 1;
}
}
Some(mask)
});
let pixel_count = request.width as usize * request.height as usize;
let mut rgb = vec![0_u8; pixel_count * 3];
let result = unsafe {
axolotl_seed_map_render(
parse_seed(&request.seed),
minecraft_version,
request.edition.generator_flags(),
request.dimension.as_cubiomes(),
scaled_coordinate(request.x, request.scale),
scaled_coordinate(request.z, request.scale),
request.scale,
request.width,
request.height,
request.elevation.unwrap_or(128).clamp(-64, 320),
i32::from(request.terrain),
i32::from(request.contours),
highlight_mask
.as_ref()
.map_or(std::ptr::null(), |mask| mask.as_ptr()),
rgb.as_mut_ptr(),
rgb.len(),
)
};
if result != 0 {
return Err(io::Error::other(format!(
"cubiomes could not generate this tile ({result})"
)));
}
let mut rgba = vec![255_u8; pixel_count * 4];
for (source, target) in rgb.chunks_exact(3).zip(rgba.chunks_exact_mut(4)) {
target[..3].copy_from_slice(source);
}
Ok(rgba)
}
pub fn find_features(query: FeatureQuery) -> io::Result<Vec<MapFeature>> {
let (minecraft_version, _) = resolve_java_version(&query.version)?;
if query.min_x > query.max_x || query.min_z > query.max_z {
return Err(io::Error::other("The map bounds are invalid."));
}
let mut raw_features = vec![
BridgeFeature {
x: 0,
z: 0,
kind: 0,
approximate: 0,
end_ship: -1,
};
4_096
];
let count = unsafe {
axolotl_seed_map_find_features(
parse_seed(&query.seed),
minecraft_version,
query.edition.generator_flags(),
query.dimension.as_cubiomes(),
query.min_x,
query.min_z,
query.max_x,
query.max_z,
query.feature_mask.unwrap_or(ALL_FEATURES) & ALL_FEATURES,
raw_features.as_mut_ptr(),
raw_features.len(),
)
};
Ok(raw_features
.into_iter()
.take(count)
.filter_map(|feature| {
Some(MapFeature {
kind: feature_name(feature.kind)?.to_owned(),
x: feature.x,
z: feature.z,
approximate: feature.approximate != 0,
end_ship: match feature.end_ship {
0 => Some(false),
1 => Some(true),
_ => None,
},
})
})
.collect())
}
pub fn biome_at(
seed: String,
edition: Edition,
version: String,
dimension: Dimension,
x: i32,
y: i32,
z: i32,
) -> io::Result<i32> {
let (minecraft_version, _) = resolve_java_version(&version)?;
let biome = unsafe {
axolotl_seed_map_biome_at(
parse_seed(&seed),
minecraft_version,
edition.generator_flags(),
dimension.as_cubiomes(),
x,
y.clamp(-64, 319),
z,
)
};
Ok(biome)
}
pub fn spawn(
seed: String,
edition: Edition,
version: String,
) -> io::Result<SpawnPoint> {
let (minecraft_version, _) = resolve_java_version(&version)?;
let mut x = 0;
let mut z = 0;
let result = unsafe {
axolotl_seed_map_get_spawn(
parse_seed(&seed),
minecraft_version,
edition.generator_flags(),
&raw mut x,
&raw mut z,
)
};
if result != 0 {
return Err(io::Error::other(
"cubiomes could not estimate the spawn point.",
));
}
Ok(SpawnPoint {
x,
z,
approximate: true,
})
}
pub(crate) fn resolve_java_version(version: &str) -> io::Result<(i32, i32)> {
let code = JAVA_VERSIONS
.iter()
.find(|(name, _)| *name == version)
.map(|(_, code)| *code)
.ok_or_else(|| {
io::Error::other("This Minecraft version is not supported.")
})?;
let engine_version = unsafe { axolotl_seed_map_java_version(code) };
if engine_version == 0 {
return Err(io::Error::other(
"The bundled cubiomes version does not support this Minecraft version.",
));
}
Ok((engine_version, code))
}
pub fn read_level_dat(path: String) -> io::Result<LevelDatInfo> {
let path = Path::new(&path);
let root = read_nbt_file(path)?;
let data = root.get::<_, &NbtCompound>("Data").unwrap_or(&root);
let seed = match extract_world_seed(data) {
Some(seed) => seed,
None => read_split_world_gen_seed(path)?.ok_or_else(|| {
io::Error::other(
"The selected world does not contain a world seed.",
)
})?,
};
let version = data
.get::<_, &NbtCompound>("Version")
.ok()
.and_then(|version| version.get::<_, &str>("Name").ok())
.and_then(normalize_level_dat_version)
.or_else(|| {
data.get::<_, i32>("DataVersion")
.ok()
.and_then(version_from_data_version)
});
Ok(LevelDatInfo {
seed: seed.to_string(),
version,
})
}
fn read_nbt_file(path: &Path) -> io::Result<NbtCompound> {
let raw = std::fs::read(path)?;
read_nbt_any_compression(&raw)
}
fn read_split_world_gen_seed(level_dat_path: &Path) -> io::Result<Option<i64>> {
let Some(world_path) = level_dat_path.parent() else {
return Ok(None);
};
let settings_path = world_path
.join("data")
.join("minecraft")
.join("world_gen_settings.dat");
let settings = match read_nbt_file(&settings_path) {
Ok(settings) => settings,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Ok(None);
}
Err(error) => return Err(error),
};
Ok(extract_world_seed(&settings))
}
fn read_nbt_any_compression(raw: &[u8]) -> io::Result<NbtCompound> {
let flavors = [
quartz_nbt::io::Flavor::GzCompressed,
quartz_nbt::io::Flavor::Uncompressed,
quartz_nbt::io::Flavor::ZlibCompressed,
];
let mut last_error = None;
for flavor in flavors {
match quartz_nbt::io::read_nbt(&mut Cursor::new(raw), flavor) {
Ok((root, _)) => return Ok(root),
Err(error) => last_error = Some(error),
}
}
Err(io::Error::other(match last_error {
Some(error) => {
format!("The selected level.dat could not be parsed: {error}")
}
None => "The selected level.dat could not be parsed.".to_owned(),
}))
}
fn extract_world_seed(data: &NbtCompound) -> Option<i64> {
if let Ok(settings) = data.get::<_, &NbtCompound>("WorldGenSettings")
&& let Ok(seed) = settings.get::<_, &NbtTag>("seed")
&& let Some(seed) = numeric_seed(seed)
{
return Some(seed);
}
if let Ok(seed) = data.get::<_, &NbtTag>("RandomSeed")
&& let Some(seed) = numeric_seed(seed)
{
return Some(seed);
}
find_nested_world_seed(data)
}
fn find_nested_world_seed(compound: &NbtCompound) -> Option<i64> {
for (name, tag) in compound.inner() {
if name.eq_ignore_ascii_case("seed")
&& let Some(seed) = numeric_seed(tag)
{
return Some(seed);
}
let nested_seed = match tag {
NbtTag::Compound(child) => find_nested_world_seed(child),
NbtTag::List(list) => list.iter().find_map(find_world_seed_in_tag),
_ => None,
};
if let Some(seed) = nested_seed {
return Some(seed);
}
}
None
}
fn find_world_seed_in_tag(tag: &NbtTag) -> Option<i64> {
match tag {
NbtTag::Compound(compound) => find_nested_world_seed(compound),
NbtTag::List(list) => list.iter().find_map(find_world_seed_in_tag),
_ => None,
}
}
fn numeric_seed(tag: &NbtTag) -> Option<i64> {
match tag {
NbtTag::Byte(seed) => Some(i64::from(*seed)),
NbtTag::Short(seed) => Some(i64::from(*seed)),
NbtTag::Int(seed) => Some(i64::from(*seed)),
NbtTag::Long(seed) => Some(*seed),
_ => None,
}
}
fn validate_tile_request(request: &TileRequest) -> io::Result<()> {
if !matches!(request.scale, 1 | 4 | 16 | 64 | 256) {
return Err(io::Error::other(
"Map scale must be one of 1, 4, 16, 64, or 256.",
));
}
if !(1..=256).contains(&request.width)
|| !(1..=256).contains(&request.height)
{
return Err(io::Error::other(
"Map tiles must be between 1 and 256 pixels.",
));
}
if request
.highlight_biomes
.as_ref()
.is_some_and(|biomes| biomes.len() > 256)
{
return Err(io::Error::other(
"Too many highlighted biomes were requested.",
));
}
Ok(())
}
fn scaled_coordinate(block_coordinate: i32, scale: i32) -> i32 {
block_coordinate.div_euclid(scale)
}
pub(crate) fn parse_seed(seed: &str) -> u64 {
let trimmed = seed.trim();
if let Ok(seed) = trimmed.parse::<i64>() {
return seed as u64;
}
java_string_hash(trimmed) as i64 as u64
}
fn java_string_hash(value: &str) -> i32 {
value.encode_utf16().fold(0_i32, |hash, unit| {
hash.wrapping_mul(31).wrapping_add(unit as i32)
})
}
fn feature_name(feature: u32) -> Option<&'static str> {
match feature {
FEATURE_VILLAGE => Some("village"),
FEATURE_OUTPOST => Some("outpost"),
FEATURE_SHIPWRECK => Some("shipwreck"),
FEATURE_MONUMENT => Some("monument"),
FEATURE_MANSION => Some("mansion"),
FEATURE_ANCIENT_CITY => Some("ancient-city"),
FEATURE_TRAIL_RUINS => Some("trail-ruins"),
FEATURE_TRIAL_CHAMBERS => Some("trial-chambers"),
FEATURE_RUINED_PORTAL => Some("ruined-portal"),
FEATURE_STRONGHOLD => Some("stronghold"),
FEATURE_SLIME_CHUNK => Some("slime-chunk"),
FEATURE_DESERT_PYRAMID => Some("desert-pyramid"),
FEATURE_JUNGLE_TEMPLE => Some("jungle-temple"),
FEATURE_SWAMP_HUT => Some("swamp-hut"),
FEATURE_IGLOO => Some("igloo"),
FEATURE_OCEAN_RUIN => Some("ocean-ruin"),
FEATURE_BURIED_TREASURE => Some("buried-treasure"),
FEATURE_MINESHAFT => Some("mineshaft"),
FEATURE_DESERT_WELL => Some("desert-well"),
FEATURE_GEODE => Some("geode"),
FEATURE_FORTRESS => Some("fortress"),
FEATURE_BASTION => Some("bastion"),
FEATURE_END_CITY => Some("end-city"),
FEATURE_END_GATEWAY => Some("end-gateway"),
_ => None,
}
}
fn normalize_level_dat_version(version: &str) -> Option<String> {
if JAVA_VERSIONS.iter().any(|(name, _)| *name == version) {
return Some(version.to_owned());
}
let mut parts = version.split('.');
let major = parts.next()?;
let minor = parts.next()?;
let prefix = format!("{major}.{minor}");
JAVA_VERSIONS
.iter()
.map(|(name, _)| *name)
.find(|supported| {
*supported == prefix || supported.starts_with(&format!("{prefix}."))
})
.map(str::to_owned)
}
fn version_from_data_version(data_version: i32) -> Option<String> {
let version = match data_version {
4_325.. => "1.21.5",
4_189.. => "1.21.4",
4_082.. => "1.21.3",
3_953.. => "1.21.1",
3_463.. => "1.20",
3_337.. => "1.19.4",
3_105.. => "1.19.2",
2_860.. => "1.18",
2_724.. => "1.17",
2_566.. => "1.16",
2_225.. => "1.15",
1_952.. => "1.14",
1_519.. => "1.13",
1_139.. => "1.12",
921.. => "1.11",
512.. => "1.10",
169.. => "1.9",
_ => return None,
};
Some(version.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_seeds_use_minecrafts_java_hash() {
assert_eq!(parse_seed("hello"), 99_162_322);
assert_eq!(parse_seed("-42"), (-42_i64) as u64);
}
#[test]
fn profiles_cover_java_and_large_biomes() {
let profiles = version_profiles();
assert!(profiles.iter().any(|profile| {
matches!(profile.edition, Edition::Java)
&& profile.version == "1.21.3"
&& profile.available
&& profile.ores
}));
assert!(profiles.iter().any(|profile| {
matches!(profile.edition, Edition::JavaLargeBiomes)
&& profile.version == "1.21.3"
}));
assert!(!profiles.iter().any(|profile| {
matches!(profile.edition, Edition::JavaLargeBiomes)
&& profile.version == "1.0"
}));
let legacy = profiles
.iter()
.find(|profile| {
matches!(profile.edition, Edition::Java)
&& profile.version == "1.9"
})
.expect("1.9 profile should exist");
assert!(!legacy.ores);
assert_eq!(legacy.dimensions.len(), 2);
let oldest = profiles
.iter()
.find(|profile| {
matches!(profile.edition, Edition::Java)
&& profile.version == "1.8"
})
.expect("1.8 profile should exist");
assert_eq!(oldest.dimensions.len(), 1);
}
#[test]
fn java_1_21_feature_fixture_matches_cubiomes() {
let villages = find_features(FeatureQuery {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "1.21.3".to_owned(),
dimension: Dimension::Overworld,
min_x: -4_096,
min_z: -4_096,
max_x: 4_096,
max_z: 4_096,
feature_mask: Some(FEATURE_VILLAGE),
})
.expect("fixture query should succeed");
assert!(!villages.is_empty());
assert!(villages.iter().all(|feature| feature.kind == "village"));
let strongholds = find_features(FeatureQuery {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "1.21.3".to_owned(),
dimension: Dimension::Overworld,
min_x: -4_096,
min_z: -4_096,
max_x: 4_096,
max_z: 4_096,
feature_mask: Some(FEATURE_STRONGHOLD),
})
.expect("fixture query should succeed");
assert!(!strongholds.is_empty());
}
#[test]
fn nether_and_end_features_survive_the_native_bridge() {
let nether = find_features(FeatureQuery {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "1.21.3".to_owned(),
dimension: Dimension::Nether,
min_x: -4_096,
min_z: -4_096,
max_x: 4_096,
max_z: 4_096,
feature_mask: Some(FEATURE_FORTRESS | FEATURE_BASTION),
})
.expect("nether fixture query should succeed");
assert!(!nether.is_empty());
assert!(
nether.iter().all(|feature| feature.kind == "fortress"
|| feature.kind == "bastion")
);
assert!(nether.iter().any(|feature| feature.kind == "fortress"));
assert!(nether.iter().any(|feature| feature.kind == "bastion"));
let end = find_features(FeatureQuery {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "1.21.3".to_owned(),
dimension: Dimension::End,
min_x: -4_096,
min_z: -4_096,
max_x: 4_096,
max_z: 4_096,
feature_mask: Some(FEATURE_END_CITY | FEATURE_END_GATEWAY),
})
.expect("end fixture query should succeed");
assert!(!end.is_empty());
assert!(end.iter().all(|feature| feature.kind == "end-city"
|| feature.kind == "end-gateway"));
assert!(end.iter().any(|feature| feature.kind == "end-city"));
assert!(end.iter().any(|feature| feature.kind == "end-gateway"));
let end_cities = end
.iter()
.filter(|feature| feature.kind == "end-city")
.collect::<Vec<_>>();
assert!(end_cities.iter().all(|feature| feature.end_ship.is_some()));
assert!(
end_cities
.iter()
.any(|feature| feature.end_ship == Some(true))
);
assert!(
end_cities
.iter()
.any(|feature| feature.end_ship == Some(false))
);
}
#[test]
fn java_tiles_are_deterministic_for_a_fixed_request() {
let request = TileRequest {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "1.21.3".to_owned(),
dimension: Dimension::Overworld,
x: -128,
z: -128,
scale: 4,
width: 32,
height: 32,
elevation: Some(62),
terrain: true,
contours: true,
highlight_biomes: Some(vec![185]),
};
let first =
render_tile(request.clone()).expect("fixture tile should render");
let second = render_tile(request).expect("fixture tile should render");
assert_eq!(first, second);
assert_eq!(first.len(), 32 * 32 * 4);
assert!(first.chunks_exact(4).all(|pixel| pixel[3] == 255));
}
#[test]
fn modern_overworld_scale_sixteen_renders_negative_tile_origins() {
let request = TileRequest {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "26.2".to_owned(),
dimension: Dimension::Overworld,
x: -12_288,
z: 0,
scale: 16,
width: 8,
height: 8,
elevation: Some(128),
terrain: false,
contours: false,
highlight_biomes: None,
};
let rgba =
render_tile(request).expect("negative scale-16 tile should render");
assert!(rgba.chunks_exact(4).all(|pixel| pixel[3] == 255));
assert!(rgba.chunks_exact(4).any(|pixel| pixel[..3] != [0, 0, 0]));
}
#[test]
fn terrain_shading_changes_the_tile() {
let request = TileRequest {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "1.21.3".to_owned(),
dimension: Dimension::Overworld,
x: -128,
z: -128,
scale: 4,
width: 32,
height: 32,
elevation: Some(62),
terrain: false,
contours: false,
highlight_biomes: None,
};
let flat =
render_tile(request.clone()).expect("flat tile should render");
let shaded = render_tile(TileRequest {
terrain: true,
..request
})
.expect("shaded tile should render");
assert_ne!(flat, shaded);
}
#[test]
fn end_tiles_render_deterministic_islands_and_void() {
let request = TileRequest {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "26.2".to_owned(),
dimension: Dimension::End,
x: 100_096,
z: 0,
scale: 4,
width: 64,
height: 64,
elevation: Some(62),
terrain: true,
contours: false,
highlight_biomes: None,
};
let first =
render_tile(request.clone()).expect("end tile should render");
let second = render_tile(request).expect("end tile should render");
assert_eq!(first, second);
let void_pixels = first
.chunks_exact(4)
.filter(|pixel| {
pixel[0] == 0x0D && pixel[1] == 0x0D && pixel[2] == 0x16
})
.count();
assert!(void_pixels > 0, "outer end tiles should contain void");
}
#[test]
fn nether_tiles_use_the_canonical_biome_render_without_fake_relief() {
let request = TileRequest {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "26.2".to_owned(),
dimension: Dimension::Nether,
x: -128,
z: -128,
scale: 4,
width: 32,
height: 32,
elevation: Some(62),
terrain: false,
contours: false,
highlight_biomes: None,
};
let flat =
render_tile(request.clone()).expect("nether tile should render");
let terrain_requested = render_tile(TileRequest {
terrain: true,
..request
})
.expect("nether tile should render");
assert_eq!(flat, terrain_requested);
}
#[test]
fn biome_lookup_resolves_known_positions() {
let biome = biome_at(
"10292992".to_owned(),
Edition::Java,
"26.2".to_owned(),
Dimension::Overworld,
0,
62,
0,
)
.expect("biome lookup should succeed");
assert!(biome >= 0);
let nether = biome_at(
"10292992".to_owned(),
Edition::Java,
"26.2".to_owned(),
Dimension::Nether,
0,
64,
0,
)
.expect("nether biome lookup should succeed");
assert!(nether >= 0);
}
#[test]
#[ignore = "writes preview tiles for manual shading inspection"]
fn write_preview_tiles() {
let Some(dir) = std::env::var_os("AXOLOTL_SEED_MAP_PREVIEW_DIR") else {
return;
};
let dir = std::path::PathBuf::from(dir);
let cases: &[(&str, Dimension, i32, bool, bool, i32, i32)] = &[
(
"overworld_s1",
Dimension::Overworld,
1,
true,
false,
-128,
-128,
),
(
"overworld_s4",
Dimension::Overworld,
4,
true,
false,
-512,
-512,
),
(
"overworld_s4_contours",
Dimension::Overworld,
4,
true,
true,
-512,
-512,
),
(
"overworld_s16",
Dimension::Overworld,
16,
true,
false,
-2048,
-2048,
),
(
"overworld_s64",
Dimension::Overworld,
64,
true,
false,
-8192,
-8192,
),
("nether_s4", Dimension::Nether, 4, true, false, -512, -512),
(
"nether_s4_flat",
Dimension::Nether,
4,
false,
false,
-512,
-512,
),
("end_s4", Dimension::End, 4, false, false, 99_840, -512),
("end_s16", Dimension::End, 16, false, false, -2048, -2048),
];
for &(name, dimension, scale, terrain, contours, x, z) in cases {
let rgba = render_tile(TileRequest {
seed: "10292992".to_owned(),
edition: Edition::Java,
version: "26.2".to_owned(),
dimension,
x,
z,
scale,
width: 256,
height: 256,
elevation: Some(62),
terrain,
contours,
highlight_biomes: None,
})
.expect("preview tile should render");
let mut ppm = Vec::with_capacity(256 * 256 * 3 + 32);
ppm.extend_from_slice(b"P6\n256 256\n255\n");
for pixel in rgba.chunks_exact(4) {
ppm.extend_from_slice(&pixel[..3]);
}
std::fs::write(dir.join(format!("{name}.ppm")), ppm)
.expect("preview tile should be written");
}
}
#[test]
fn tile_origins_follow_the_scaled_coordinate_contract() {
assert_eq!(scaled_coordinate(1_024, 64), 16);
assert_eq!(scaled_coordinate(-1, 64), -1);
assert_eq!(scaled_coordinate(-1_025, 64), -17);
}
#[test]
fn level_dat_versions_are_reduced_to_supported_profiles() {
assert_eq!(
normalize_level_dat_version("1.21.3"),
Some("1.21.3".to_owned())
);
assert_eq!(
normalize_level_dat_version("1.20.4"),
Some("1.20".to_owned())
);
assert_eq!(
normalize_level_dat_version("1.17.1"),
Some("1.17".to_owned())
);
assert_eq!(version_from_data_version(3_700), Some("1.20".to_owned()));
assert_eq!(version_from_data_version(100), None);
}
#[test]
fn level_dat_reads_embedded_world_generation_settings() {
let mut settings = NbtCompound::new();
settings.insert("seed", 1_234_567_i64);
let mut data = NbtCompound::new();
data.insert("WorldGenSettings", settings);
assert_eq!(extract_world_seed(&data), Some(1_234_567));
}
#[test]
fn level_dat_reads_split_world_generation_settings() {
let temp_dir =
tempfile::tempdir().expect("temporary world should be created");
let level_dat_path = temp_dir.path().join("level.dat");
let mut version = NbtCompound::new();
version.insert("Name", "26.2");
let mut level_data = NbtCompound::new();
level_data.insert("Version", version);
let mut level_root = NbtCompound::new();
level_root.insert("Data", level_data);
let mut level_dat = std::fs::File::create(&level_dat_path)
.expect("level.dat should be created");
quartz_nbt::io::write_nbt(
&mut level_dat,
None,
&level_root,
quartz_nbt::io::Flavor::GzCompressed,
)
.expect("level.dat should be written");
let settings_path = temp_dir
.path()
.join("data")
.join("minecraft")
.join("world_gen_settings.dat");
std::fs::create_dir_all(settings_path.parent().unwrap())
.expect("world generation settings directory should be created");
let mut settings_data = NbtCompound::new();
settings_data.insert("seed", -7_654_321_i64);
let mut settings_root = NbtCompound::new();
settings_root.insert("data", settings_data);
let mut settings_dat = std::fs::File::create(settings_path)
.expect("world generation settings should be created");
quartz_nbt::io::write_nbt(
&mut settings_dat,
None,
&settings_root,
quartz_nbt::io::Flavor::GzCompressed,
)
.expect("world generation settings should be written");
let info =
read_level_dat(level_dat_path.to_string_lossy().into_owned())
.expect("split world generation settings should be read");
assert_eq!(info.seed, "-7654321");
assert_eq!(info.version, Some("26.2".to_owned()));
}
}