This commit is contained in:
Alexander Medvedev 2026-08-23 12:42:37 +02:00
parent 1abbc7bfec
commit edb848d608
14 changed files with 1236 additions and 899 deletions

@ -1 +1 @@
Subproject commit b44607b19fa796edb28562382a1f4f7f788a4504 Subproject commit 2337e6f389293a6371667d495feace8e35570318

View File

@ -546,6 +546,10 @@ impl<S: SingleChunkDataSerializer + 'static> ChunkSerializer for AnvilChunkFile<
fn read(r: Bytes) -> Result<Self, ChunkReadingError> { fn read(r: Bytes) -> Result<Self, ChunkReadingError> {
let mut raw_file_bytes = r; let mut raw_file_bytes = r;
if raw_file_bytes.is_empty() {
return Ok(Self::default());
}
if raw_file_bytes.len() < SECTOR_BYTES * 2 { if raw_file_bytes.len() < SECTOR_BYTES * 2 {
return Err(ChunkReadingError::InvalidHeader); return Err(ChunkReadingError::InvalidHeader);
} }

View File

@ -94,15 +94,26 @@ fn extract_u16_array(tag: &pumpkin_nbt::tag::NbtTag) -> Option<Box<[BlockStateId
pumpkin_nbt::tag::NbtTag::List(list) => { pumpkin_nbt::tag::NbtTag::List(list) => {
let ids: Box<[BlockStateId]> = list let ids: Box<[BlockStateId]> = list
.iter() .iter()
.map(|t| { .map(|t| match t {
let val = match t { pumpkin_nbt::tag::NbtTag::Int(x) => BlockStateId::new_or_air(*x as u16),
pumpkin_nbt::tag::NbtTag::Int(x) => *x as u16, pumpkin_nbt::tag::NbtTag::Short(x) => BlockStateId::new_or_air(*x as u16),
pumpkin_nbt::tag::NbtTag::Short(x) => *x as u16, pumpkin_nbt::tag::NbtTag::Byte(x) => BlockStateId::new_or_air(*x as u16),
pumpkin_nbt::tag::NbtTag::Byte(x) => *x as u16, pumpkin_nbt::tag::NbtTag::Long(x) => BlockStateId::new_or_air(*x as u16),
pumpkin_nbt::tag::NbtTag::Long(x) => *x as u16, pumpkin_nbt::tag::NbtTag::Compound(compound) => {
_ => 0, if let Ok(entry) =
}; crate::generation::structure::template::PaletteEntry::from_nbt_compound(
BlockStateId::new_or_air(val) compound,
)
&& let Some(state) =
crate::generation::structure::template::BlockStateResolver::resolve_simple(
&entry,
)
{
return state.id;
}
BlockStateId::AIR
}
_ => BlockStateId::AIR,
}) })
.collect(); .collect();
Some(ids) Some(ids)
@ -122,6 +133,10 @@ fn extract_u8_array(tag: &pumpkin_nbt::tag::NbtTag) -> Option<Box<[u8]>> {
pumpkin_nbt::tag::NbtTag::Byte(x) => *x as u8, pumpkin_nbt::tag::NbtTag::Byte(x) => *x as u8,
pumpkin_nbt::tag::NbtTag::Int(x) => *x as u8, pumpkin_nbt::tag::NbtTag::Int(x) => *x as u8,
pumpkin_nbt::tag::NbtTag::Short(x) => *x as u8, pumpkin_nbt::tag::NbtTag::Short(x) => *x as u8,
pumpkin_nbt::tag::NbtTag::String(s) => {
let name = s.strip_prefix("minecraft:").unwrap_or(s);
pumpkin_data::biome::Biome::from_name(name).map_or(0, |b| b.id)
}
_ => 0, _ => 0,
}) })
.collect(); .collect();
@ -490,7 +505,27 @@ impl ChunkData {
let palette_tags: Vec<NbtTag> = block_states_nbt let palette_tags: Vec<NbtTag> = block_states_nbt
.palette .palette
.iter() .iter()
.map(|id| NbtTag::Int(BlockStateId::as_u16(*id) as i32)) .map(|&id| {
let block = Block::from_state_id(id);
let mut comp = NbtCompound::new();
let name = if block.name.starts_with("minecraft:") {
block.name.to_string()
} else {
format!("minecraft:{}", block.name)
};
comp.put_string("Name", name);
if let Some(props) = block.properties(id) {
let prop_vec = props.to_props();
if !prop_vec.is_empty() {
let mut props_comp = NbtCompound::new();
for (k, v) in prop_vec {
props_comp.put_string(k, v.to_string());
}
comp.put_compound("Properties", props_comp);
}
}
NbtTag::Compound(comp)
})
.collect(); .collect();
bs_comp.put_list("palette", palette_tags); bs_comp.put_list("palette", palette_tags);
section_comp.put_compound("block_states", bs_comp); section_comp.put_compound("block_states", bs_comp);
@ -504,7 +539,16 @@ impl ChunkData {
let biome_palette_tags: Vec<NbtTag> = biomes_nbt let biome_palette_tags: Vec<NbtTag> = biomes_nbt
.palette .palette
.iter() .iter()
.map(|&val| NbtTag::Byte(val as i8)) .map(|&val| {
let name = pumpkin_data::biome::Biome::from_id(val)
.map_or("plains", |b| b.registry_id);
let full_name = if name.starts_with("minecraft:") {
name.to_string()
} else {
format!("minecraft:{name}")
};
NbtTag::String(full_name.into())
})
.collect(); .collect();
b_comp.put_list("palette", biome_palette_tags); b_comp.put_list("palette", biome_palette_tags);
section_comp.put_compound("biomes", b_comp); section_comp.put_compound("biomes", b_comp);
@ -849,3 +893,63 @@ impl Default for LightContainer {
Self::new_empty(15) Self::new_empty(15)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use pumpkin_data::Block;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
#[test]
fn extract_u16_array_from_vanilla_compound_palette() {
let mut entry1 = NbtCompound::new();
entry1.put_string("Name", "minecraft:stone".to_string());
let mut entry2 = NbtCompound::new();
entry2.put_string("Name", "minecraft:repeater".to_string());
let mut props = NbtCompound::new();
props.put_string("facing", "north".to_string());
props.put_string("delay", "2".to_string());
props.put_string("locked", "false".to_string());
props.put_string("powered", "false".to_string());
entry2.put_compound("Properties", props);
let list_tag = NbtTag::List(vec![NbtTag::Compound(entry1), NbtTag::Compound(entry2)]);
let result = extract_u16_array(&list_tag).expect("should extract palette");
assert_eq!(result.len(), 2);
assert_eq!(result[0], Block::STONE.default_state.id);
let repeater_state = Block::REPEATER
.from_properties(&[
("facing", "north"),
("delay", "2"),
("locked", "false"),
("powered", "false"),
])
.to_state_id(&Block::REPEATER);
assert_eq!(result[1], repeater_state);
}
#[test]
fn extract_u8_array_from_vanilla_string_palette() {
let list_tag = NbtTag::List(vec![
NbtTag::String("minecraft:plains".to_string().into()),
NbtTag::String("minecraft:the_void".to_string().into()),
]);
let result = extract_u8_array(&list_tag).expect("should extract biome palette");
assert_eq!(result.len(), 2);
assert_eq!(
result[0],
pumpkin_data::biome::Biome::from_name("plains").unwrap().id
);
assert_eq!(
result[1],
pumpkin_data::biome::Biome::from_name("the_void")
.unwrap()
.id
);
}
}

View File

@ -101,6 +101,13 @@ impl<S: ChunkSerializer<WriteBackend = PathBuf> + 'static> ChunkSerializerLazyLo
match tokio::fs::read(&self.path).await { match tokio::fs::read(&self.path).await {
Ok(bytes) => { Ok(bytes) => {
if bytes.is_empty() {
trace!(
"File is empty (0 bytes), using default for: {}",
self.path.display()
);
return Ok(S::default());
}
let value = tokio::task::spawn_blocking(move || S::read(bytes.into())) let value = tokio::task::spawn_blocking(move || S::read(bytes.into()))
.await .await
.map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e)))??; .map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e)))??;

View File

@ -444,9 +444,12 @@ impl Cache {
.get_proto_chunk_mut() .get_proto_chunk_mut()
.set_structure_starts(noise_gen); .set_structure_starts(noise_gen);
} }
generator::WorldGenerator::Flat(_) => {} generator::WorldGenerator::Flat(_) => {
self.chunks[mid].get_proto_chunk_mut().stage = StagedChunkEnum::StructureStart;
}
generator::WorldGenerator::Custom(custom_gen) => { generator::WorldGenerator::Custom(custom_gen) => {
custom_gen.set_structure_starts(self.chunks[mid].get_proto_chunk_mut()); custom_gen.set_structure_starts(self.chunks[mid].get_proto_chunk_mut());
self.chunks[mid].get_proto_chunk_mut().stage = StagedChunkEnum::StructureStart;
} }
}, },
StagedChunkEnum::StructureReferences => match generator { StagedChunkEnum::StructureReferences => match generator {
@ -455,9 +458,14 @@ impl Cache {
.get_proto_chunk_mut() .get_proto_chunk_mut()
.set_structure_references(noise_gen); .set_structure_references(noise_gen);
} }
generator::WorldGenerator::Flat(_) => {} generator::WorldGenerator::Flat(_) => {
self.chunks[mid].get_proto_chunk_mut().stage =
StagedChunkEnum::StructureReferences;
}
generator::WorldGenerator::Custom(custom_gen) => { generator::WorldGenerator::Custom(custom_gen) => {
custom_gen.set_structure_references(self.chunks[mid].get_proto_chunk_mut()); custom_gen.set_structure_references(self.chunks[mid].get_proto_chunk_mut());
self.chunks[mid].get_proto_chunk_mut().stage =
StagedChunkEnum::StructureReferences;
} }
}, },
StagedChunkEnum::Biomes => match generator { StagedChunkEnum::Biomes => match generator {

View File

@ -268,9 +268,10 @@ impl GenerationSchedule {
let mut ready = Vec::new(); let mut ready = Vec::new();
for (key, node) in &mut graph.nodes { for (key, node) in &mut graph.nodes {
node.in_queue = false;
if node.stage == StagedChunkEnum::None if node.stage == StagedChunkEnum::None
|| node.in_degree != 0 || node.in_degree != 0
|| node.in_queue
|| node.in_flight
|| waiting_for_chunks.contains(&key) || waiting_for_chunks.contains(&key)
{ {
continue; continue;
@ -1242,8 +1243,8 @@ impl GenerationSchedule {
} }
if let Some(node) = self.graph.nodes.get_mut(task.1) { if let Some(node) = self.graph.nodes.get_mut(task.1) {
node.in_queue = false;
if node.in_degree != 0 { if node.in_degree != 0 {
node.in_queue = false;
continue; continue;
} }
node.in_flight = true; node.in_flight = true;
@ -1492,7 +1493,9 @@ impl GenerationSchedule {
&self.waiting_for_chunks, &self.waiting_for_chunks,
); );
if restored > 0 { if restored > 0 {
warn!("Restored {restored} stranded ready chunk tasks to generation queue"); debug!(
"Restored {restored} stranded ready chunk tasks to generation queue"
);
continue; continue;
} }
debug_assert!(self.debug_check()); debug_assert!(self.debug_check());

View File

@ -9,15 +9,9 @@ use crate::level::Level;
pub fn into_level( pub fn into_level(
dimension: Dimension, dimension: Dimension,
level_config: &LevelConfig, level_config: &LevelConfig,
mut base_directory: PathBuf, base_directory: PathBuf,
seed: i64, seed: i64,
gen_pool: Option<Arc<rayon::ThreadPool>>, gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Level> { ) -> Arc<Level> {
if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name {
} else if dimension.minecraft_name == Dimension::THE_NETHER.minecraft_name {
base_directory.push("DIM-1");
} else if dimension.minecraft_name == Dimension::THE_END.minecraft_name {
base_directory.push("DIM1");
}
Level::from_root_folder(level_config, base_directory, seed, dimension, gen_pool) Level::from_root_folder(level_config, base_directory, seed, dimension, gen_pool)
} }

View File

@ -148,17 +148,31 @@ impl Level {
dimension: Dimension, dimension: Dimension,
gen_pool: Option<Arc<rayon::ThreadPool>>, gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Self> { ) -> Arc<Self> {
let dim_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name let (namespace, name) = match dimension.minecraft_name.split_once(':') {
|| dimension.minecraft_name == Dimension::THE_NETHER.minecraft_name Some((ns, n)) => (ns, n),
|| dimension.minecraft_name == Dimension::THE_END.minecraft_name None => ("minecraft", dimension.minecraft_name),
};
// 26.2 canonical layout: root_folder/dimensions/<namespace>/<name>
let canonical_dim_folder = root_folder.join("dimensions").join(namespace).join(name);
// Check if canonical 26.2 folder exists, or fall back to pre-26.2 legacy folders
let dim_folder = if canonical_dim_folder.exists() {
canonical_dim_folder
} else if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name
&& root_folder.join("region").exists()
{ {
root_folder.clone() root_folder.clone()
} else if dimension.minecraft_name == Dimension::THE_NETHER.minecraft_name
&& root_folder.join("DIM-1").join("region").exists()
{
root_folder.join("DIM-1")
} else if dimension.minecraft_name == Dimension::THE_END.minecraft_name
&& root_folder.join("DIM1").join("region").exists()
{
root_folder.join("DIM1")
} else { } else {
let (namespace, name) = match dimension.minecraft_name.split_once(':') { canonical_dim_folder
Some((ns, n)) => (ns, n),
None => ("minecraft", dimension.minecraft_name),
};
root_folder.join("dimensions").join(namespace).join(name)
}; };
let region_folder = dim_folder.join("region"); let region_folder = dim_folder.join("region");
@ -177,21 +191,13 @@ impl Level {
poi_folder, poi_folder,
}); });
let main_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name { let main_folder = &level_folder.root_folder;
level_folder.root_folder.clone()
} else {
level_folder
.root_folder
.parent()
.unwrap_or(&level_folder.root_folder)
.to_path_buf()
};
let mut is_flat = false; let mut is_flat = false;
let mut flat_layers = Vec::new(); let mut flat_layers = Vec::new();
let mut flat_biome = "minecraft:plains".to_string(); let mut flat_biome = "minecraft:plains".to_string();
if let Some(wgs) = crate::world_info::data_files::read_world_gen_settings(&main_folder) if let Some(wgs) = crate::world_info::data_files::read_world_gen_settings(main_folder)
&& let Some(dim_settings) = wgs.dimensions.get(dimension.minecraft_name) && let Some(dim_settings) = wgs.dimensions.get(dimension.minecraft_name)
&& dim_settings.generator.generator_type == "minecraft:flat" && dim_settings.generator.generator_type == "minecraft:flat"
{ {
@ -1005,3 +1011,67 @@ impl Level {
.unwrap_or(false) .unwrap_or(false)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use pumpkin_config::world::LevelConfig;
use tempfile::TempDir;
#[tokio::test]
async fn dimension_paths_26_2() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path().to_path_buf();
let config = LevelConfig::default();
let overworld_level =
Level::from_root_folder(&config, root.clone(), 0, Dimension::OVERWORLD, None);
assert_eq!(
overworld_level.level_folder.dim_folder,
root.join("dimensions").join("minecraft").join("overworld")
);
assert_eq!(
overworld_level.level_folder.region_folder,
root.join("dimensions")
.join("minecraft")
.join("overworld")
.join("region")
);
let nether_level =
Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_NETHER, None);
assert_eq!(
nether_level.level_folder.dim_folder,
root.join("dimensions").join("minecraft").join("the_nether")
);
let end_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_END, None);
assert_eq!(
end_level.level_folder.dim_folder,
root.join("dimensions").join("minecraft").join("the_end")
);
}
#[tokio::test]
async fn legacy_dimension_fallback() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path().to_path_buf();
let config = LevelConfig::default();
// Create legacy directories
std::fs::create_dir_all(root.join("region")).unwrap();
std::fs::create_dir_all(root.join("DIM-1").join("region")).unwrap();
std::fs::create_dir_all(root.join("DIM1").join("region")).unwrap();
let overworld_level =
Level::from_root_folder(&config, root.clone(), 0, Dimension::OVERWORLD, None);
assert_eq!(overworld_level.level_folder.dim_folder, root);
let nether_level =
Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_NETHER, None);
assert_eq!(nether_level.level_folder.dim_folder, root.join("DIM-1"));
let end_level = Level::from_root_folder(&config, root.clone(), 0, Dimension::THE_END, None);
assert_eq!(end_level.level_folder.dim_folder, root.join("DIM1"));
}
}

View File

@ -20,7 +20,8 @@ use crate::world_info::{
data_files::{ data_files::{
minecraft_data_dir, read_game_rules, read_wandering_trader, read_weather, minecraft_data_dir, read_game_rules, read_wandering_trader, read_weather,
read_world_clocks, read_world_gen_settings, write_custom_boss_events_stub, read_world_clocks, read_world_gen_settings, write_custom_boss_events_stub,
write_game_rules, write_scheduled_events_stub, write_wandering_trader, write_weather, write_game_rules, write_random_sequences_stub, write_scheduled_events_stub,
write_scoreboard_stub, write_stopwatches_stub, write_wandering_trader, write_weather,
write_world_clocks, write_world_gen_settings, write_world_clocks, write_world_gen_settings,
}, },
default_data_packs, default_data_packs,
@ -189,6 +190,24 @@ fn update_world_border_from_nbt(level_data: &mut LevelData, data: &NbtCompound)
} }
fn update_spawn_from_nbt(level_data: &mut LevelData, data: &NbtCompound) { fn update_spawn_from_nbt(level_data: &mut LevelData, data: &NbtCompound) {
if let Some(spawn_comp) = data.get_compound("spawn") {
if let Some(pos) = spawn_comp.get_int_array("pos")
&& pos.len() >= 3
{
level_data.spawn_x = pos[0];
level_data.spawn_y = pos[1];
level_data.spawn_z = pos[2];
}
if let Some(spawn_yaw) = spawn_comp
.get_float("yaw")
.or_else(|| spawn_comp.get_float("SpawnAngle"))
{
level_data.spawn_yaw = spawn_yaw;
}
if let Some(spawn_pitch) = spawn_comp.get_float("pitch") {
level_data.spawn_pitch = spawn_pitch;
}
}
if let Some(spawn_x) = data.get_int("SpawnX") { if let Some(spawn_x) = data.get_int("SpawnX") {
level_data.spawn_x = spawn_x; level_data.spawn_x = spawn_x;
} }
@ -224,11 +243,26 @@ fn level_data_from_nbt(data: &NbtCompound, seed: i64) -> LevelData {
if let Some(data_version) = data.get_int("DataVersion") { if let Some(data_version) = data.get_int("DataVersion") {
level_data.data_version = data_version; level_data.data_version = data_version;
} }
if let Some(difficulty) = data.get_byte("Difficulty").and_then(difficulty_from_id) { if let Some(diff_comp) = data.get_compound("difficulty_settings") {
level_data.difficulty = difficulty; if let Some(diff_str) = diff_comp.get_string("difficulty") {
} match diff_str {
if let Some(difficulty_locked) = data.get_bool("DifficultyLocked") { "peaceful" => level_data.difficulty = Difficulty::Peaceful,
level_data.difficulty_locked = difficulty_locked; "easy" => level_data.difficulty = Difficulty::Easy,
"normal" => level_data.difficulty = Difficulty::Normal,
"hard" => level_data.difficulty = Difficulty::Hard,
_ => {}
}
}
if let Some(difficulty_locked) = diff_comp.get_bool("locked") {
level_data.difficulty_locked = difficulty_locked;
}
} else {
if let Some(difficulty) = data.get_byte("Difficulty").and_then(difficulty_from_id) {
level_data.difficulty = difficulty;
}
if let Some(difficulty_locked) = data.get_bool("DifficultyLocked") {
level_data.difficulty_locked = difficulty_locked;
}
} }
if let Some(last_played) = data.get_long("LastPlayed") { if let Some(last_played) = data.get_long("LastPlayed") {
level_data.last_played = last_played; level_data.last_played = last_played;
@ -268,10 +302,36 @@ fn level_data_to_nbt(info: &LevelData, data: &mut NbtCompound) {
data.put_double("BorderWarningTime", info.border_warning_time); data.put_double("BorderWarningTime", info.border_warning_time);
data.put_compound("DataPacks", data_packs_to_nbt(&info.data_packs)); data.put_compound("DataPacks", data_packs_to_nbt(&info.data_packs));
data.put_int("DataVersion", info.data_version); data.put_int("DataVersion", info.data_version);
// 26.2 difficulty_settings
let mut diff_comp = NbtCompound::new();
let diff_name = match info.difficulty {
Difficulty::Peaceful => "peaceful",
Difficulty::Easy => "easy",
Difficulty::Normal => "normal",
Difficulty::Hard => "hard",
};
diff_comp.put_string("difficulty", diff_name.to_string());
diff_comp.put_bool("hardcore", false);
diff_comp.put_bool("locked", info.difficulty_locked);
data.put_compound("difficulty_settings", diff_comp);
data.put_byte("Difficulty", info.difficulty as i8); data.put_byte("Difficulty", info.difficulty as i8);
data.put_bool("DifficultyLocked", info.difficulty_locked); data.put_bool("DifficultyLocked", info.difficulty_locked);
data.put_long("LastPlayed", info.last_played); data.put_long("LastPlayed", info.last_played);
data.put_string("LevelName", info.level_name.clone()); data.put_string("LevelName", info.level_name.clone());
// 26.2 spawn
let mut spawn_comp = NbtCompound::new();
spawn_comp.put_string("dimension", "minecraft:overworld".to_string());
spawn_comp.put(
"pos",
NbtTag::IntArray(vec![info.spawn_x, info.spawn_y, info.spawn_z]),
);
spawn_comp.put_float("pitch", info.spawn_pitch);
spawn_comp.put_float("yaw", info.spawn_yaw);
data.put_compound("spawn", spawn_comp);
data.put_int("SpawnX", info.spawn_x); data.put_int("SpawnX", info.spawn_x);
data.put_int("SpawnY", info.spawn_y); data.put_int("SpawnY", info.spawn_y);
data.put_int("SpawnZ", info.spawn_z); data.put_int("SpawnZ", info.spawn_z);
@ -318,6 +378,10 @@ impl WorldInfoReader for AnvilLevelInfo {
let mut level_data = level_data_from_nbt(data, seed); let mut level_data = level_data_from_nbt(data, seed);
if let Some(wgs) = read_world_gen_settings(level_folder) {
level_data.world_gen_settings = wgs;
}
// game_rules.dat – prefer the new file; fall back to level.dat values // game_rules.dat – prefer the new file; fall back to level.dat values
if minecraft_data_dir(level_folder) if minecraft_data_dir(level_folder)
.join("game_rules.dat") .join("game_rules.dat")
@ -363,6 +427,8 @@ impl WorldInfoWriter for AnvilLevelInfo {
// ── Write level.dat ─────────────────────────────────────────────────── // ── Write level.dat ───────────────────────────────────────────────────
let path = level_folder.join(LEVEL_DAT_FILE_NAME); let path = level_folder.join(LEVEL_DAT_FILE_NAME);
let path_new = level_folder.join("level.dat_new");
let path_old = level_folder.join(LEVEL_DAT_BACKUP_FILE_NAME);
let mut root = existing_level_dat_root(&path)?; let mut root = existing_level_dat_root(&path)?;
let mut data_comp = root let mut data_comp = root
@ -372,9 +438,14 @@ impl WorldInfoWriter for AnvilLevelInfo {
level_data_to_nbt(&level_data, &mut data_comp); level_data_to_nbt(&level_data, &mut data_comp);
root.put_compound(LEVEL_DATA_TAG, data_comp); root.put_compound(LEVEL_DATA_TAG, data_comp);
write_gzip_compound_tag(root, File::create(path)?) write_gzip_compound_tag(root, File::create(&path_new)?)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))?; .map_err(|e| WorldInfoError::SerializationError(e.to_string()))?;
if path.exists() {
let _ = std::fs::copy(&path, &path_old);
}
let _ = std::fs::rename(&path_new, &path);
let data_version = level_data.data_version; let data_version = level_data.data_version;
// ── Write data/minecraft/*.dat files ───────────────────────────────── // ── Write data/minecraft/*.dat files ─────────────────────────────────
@ -431,6 +502,21 @@ impl WorldInfoWriter for AnvilLevelInfo {
error!("Failed to write scheduled_events.dat: {e}"); error!("Failed to write scheduled_events.dat: {e}");
} }
// random_sequences.dat
if let Err(e) = write_random_sequences_stub(level_folder, data_version) {
error!("Failed to write random_sequences.dat: {e}");
}
// scoreboard.dat
if let Err(e) = write_scoreboard_stub(level_folder, data_version) {
error!("Failed to write scoreboard.dat: {e}");
}
// stopwatches.dat
if let Err(e) = write_stopwatches_stub(level_folder, data_version) {
error!("Failed to write stopwatches.dat: {e}");
}
Ok(()) Ok(())
} }
} }
@ -465,7 +551,7 @@ mod test {
DataPacks, LevelData, MAXIMUM_SUPPORTED_LEVEL_VERSION, DataPacks, LevelData, MAXIMUM_SUPPORTED_LEVEL_VERSION,
MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_LEVEL_VERSION,
MINIMUM_SUPPORTED_WORLD_DATA_VERSION, WorldGenSettings, WorldInfoError, WorldVersion, MINIMUM_SUPPORTED_WORLD_DATA_VERSION, WorldGenSettings, WorldInfoError, WorldVersion,
data_files::minecraft_data_dir, data_files::{minecraft_data_dir, read_world_gen_settings, write_world_gen_settings},
}, },
}; };
@ -813,4 +899,139 @@ mod test {
let bytes = pumpkin_nbt::Nbt::from(root).write(); let bytes = pumpkin_nbt::Nbt::from(root).write();
assert!(!bytes.is_empty()); assert!(!bytes.is_empty());
} }
#[test]
fn spawn_and_difficulty_format_26_2() {
let temp_dir = TempDir::new().unwrap();
let mut data_comp = NbtCompound::new();
data_comp.put_int("DataVersion", 4903);
data_comp.put_int("version", 19133);
data_comp.put_string("LevelName", "26.2 Test".to_string());
data_comp.put_long("LastPlayed", 123456789);
data_comp.put_long("DayTime", 1000);
// 26.2 difficulty_settings compound
let mut diff_comp = NbtCompound::new();
diff_comp.put_string("difficulty", "hard".to_string());
diff_comp.put_bool("locked", true);
diff_comp.put_bool("hardcore", false);
data_comp.put_compound("difficulty_settings", diff_comp);
// 26.2 spawn compound
let mut spawn_comp = NbtCompound::new();
spawn_comp.put_string("dimension", "minecraft:overworld".to_string());
spawn_comp.put("pos", NbtTag::IntArray(vec![42, -60, 99]));
spawn_comp.put_float("pitch", 15.0);
spawn_comp.put_float("yaw", 180.0);
data_comp.put_compound("spawn", spawn_comp);
let mut root = NbtCompound::new();
root.put_compound("Data", data_comp);
write_level_dat(temp_dir.path(), root);
// Write world_gen_settings.dat
let mut wgs = WorldGenSettings::new(Seed(777));
wgs.dimensions.insert(
"minecraft:overworld".to_string(),
crate::world_info::Dimension {
generator: crate::world_info::Generator {
settings: Some(crate::world_info::GeneratorSettings::Compound(
serde_json::json!({
"biome": "minecraft:the_void",
"layers": [
{ "block": "minecraft:air", "height": 1 }
]
}),
)),
biome_source: None,
generator_type: "minecraft:flat".to_string(),
},
dimension_type: "minecraft:overworld".to_string(),
},
);
write_world_gen_settings(temp_dir.path(), &wgs, 4903).unwrap();
let loaded = AnvilLevelInfo.read_world_info(temp_dir.path()).unwrap();
assert_eq!(loaded.spawn_x, 42);
assert_eq!(loaded.spawn_y, -60);
assert_eq!(loaded.spawn_z, 99);
assert_eq!(loaded.spawn_pitch, 15.0);
assert_eq!(loaded.spawn_yaw, 180.0);
assert_eq!(loaded.difficulty, Difficulty::Hard);
assert!(loaded.difficulty_locked);
let read_wgs = read_world_gen_settings(temp_dir.path()).unwrap();
assert_eq!(read_wgs.seed, 777);
let overworld_dim = read_wgs.dimensions.get("minecraft:overworld").unwrap();
assert_eq!(overworld_dim.generator.generator_type, "minecraft:flat");
if let Some(crate::world_info::GeneratorSettings::Compound(val)) =
&overworld_dim.generator.settings
{
assert_eq!(val["biome"], "minecraft:the_void");
} else {
panic!("Expected Compound generator settings");
}
}
#[test]
fn all_26_2_minecraft_data_files_written_and_read() {
let temp_dir = TempDir::new().unwrap();
let mut level_data = LEVEL_DAT.data.clone();
level_data.data_version = 4903;
level_data.day_time = 24000;
level_data.clear_weather_time = 100;
level_data.world_gen_settings = WorldGenSettings::new(Seed(9999));
AnvilLevelInfo
.write_world_info(&level_data, temp_dir.path())
.unwrap();
let data_dir = temp_dir.path().join("data").join("minecraft");
let expected_files = [
"game_rules.dat",
"random_sequences.dat",
"scoreboard.dat",
"stopwatches.dat",
"wandering_trader.dat",
"world_clocks.dat",
"world_gen_settings.dat",
"scheduled_events.dat",
"custom_boss_events.dat",
"weather.dat",
];
for file_name in &expected_files {
let file_path = data_dir.join(file_name);
assert!(
file_path.exists(),
"Expected file {file_name} to exist in data/minecraft/"
);
}
// Verify world_gen_settings.dat read
let loaded_wgs =
crate::world_info::data_files::read_world_gen_settings(temp_dir.path()).unwrap();
assert_eq!(loaded_wgs.seed, 9999);
// Verify weather.dat read
let loaded_weather = crate::world_info::data_files::read_weather(temp_dir.path());
assert_eq!(loaded_weather.clear_weather_time, 100);
// Verify world_clocks.dat read
let loaded_clocks = crate::world_info::data_files::read_world_clocks(temp_dir.path());
assert_eq!(
loaded_clocks
.clocks
.get("minecraft:overworld")
.unwrap()
.total_ticks,
24000
);
// Verify wandering_trader.dat read
let loaded_wt = crate::world_info::data_files::read_wandering_trader(temp_dir.path());
assert_eq!(loaded_wt.spawn_delay, 24000);
assert_eq!(loaded_wt.spawn_chance, 25);
}
} }

View File

@ -166,11 +166,72 @@ pub fn write_weather(level_folder: &Path, data: &WeatherData) -> Result<(), Worl
data_comp.put_bool("raining", data.raining); data_comp.put_bool("raining", data.raining);
data_comp.put_bool("thundering", data.thundering); data_comp.put_bool("thundering", data.thundering);
let mut root = NbtCompound::new(); let mut root = NbtCompound::new();
root.put_int("DataVersion", data.data_version);
root.put_compound("data", data_comp); root.put_compound("data", data_comp);
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file)) pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string())) .map_err(|e| WorldInfoError::SerializationError(e.to_string()))
} }
#[must_use]
pub fn json_to_nbt_tag(val: &serde_json::Value) -> NbtTag {
match val {
serde_json::Value::Null => NbtTag::End,
serde_json::Value::Bool(b) => NbtTag::Byte(i8::from(*b)),
serde_json::Value::Number(n) => n.as_i64().map_or_else(
|| n.as_f64().map_or(NbtTag::End, NbtTag::Double),
|i| i32::try_from(i).map_or(NbtTag::Long(i), NbtTag::Int),
),
serde_json::Value::String(s) => NbtTag::String(s.clone().into()),
serde_json::Value::Array(arr) => NbtTag::List(arr.iter().map(json_to_nbt_tag).collect()),
serde_json::Value::Object(map) => {
let mut compound = NbtCompound::new();
for (k, v) in map {
compound.put(k, json_to_nbt_tag(v));
}
NbtTag::Compound(compound)
}
}
}
#[must_use]
pub fn nbt_tag_to_json(tag: &NbtTag) -> serde_json::Value {
match tag {
NbtTag::Byte(b) => serde_json::Value::Number((*b).into()),
NbtTag::Short(s) => serde_json::Value::Number((*s).into()),
NbtTag::Int(i) => serde_json::Value::Number((*i).into()),
NbtTag::Long(l) => serde_json::Value::Number((*l).into()),
NbtTag::Float(f) => serde_json::Number::from_f64(*f as f64)
.map_or(serde_json::Value::Null, serde_json::Value::Number),
NbtTag::Double(d) => serde_json::Number::from_f64(*d)
.map_or(serde_json::Value::Null, serde_json::Value::Number),
NbtTag::String(s) => serde_json::Value::String(s.to_string()),
NbtTag::List(list) => serde_json::Value::Array(list.iter().map(nbt_tag_to_json).collect()),
NbtTag::Compound(comp) => {
let mut map = serde_json::Map::new();
for (k, v) in &comp.child_tags {
map.insert(k.to_string(), nbt_tag_to_json(v));
}
serde_json::Value::Object(map)
}
NbtTag::ByteArray(arr) => serde_json::Value::Array(
arr.iter()
.map(|b| serde_json::Value::Number((*b).into()))
.collect(),
),
NbtTag::IntArray(arr) => serde_json::Value::Array(
arr.iter()
.map(|i| serde_json::Value::Number((*i).into()))
.collect(),
),
NbtTag::LongArray(arr) => serde_json::Value::Array(
arr.iter()
.map(|l| serde_json::Value::Number((*l).into()))
.collect(),
),
NbtTag::End => serde_json::Value::Null,
}
}
pub fn read_world_gen_settings(level_folder: &Path) -> Option<WorldGenSettings> { pub fn read_world_gen_settings(level_folder: &Path) -> Option<WorldGenSettings> {
let path = minecraft_data_dir(level_folder).join("world_gen_settings.dat"); let path = minecraft_data_dir(level_folder).join("world_gen_settings.dat");
if !path.exists() { if !path.exists() {
@ -179,16 +240,71 @@ pub fn read_world_gen_settings(level_folder: &Path) -> Option<WorldGenSettings>
match File::open(&path) { match File::open(&path) {
Ok(f) => match read_gzip_compound_tag(f) { Ok(f) => match read_gzip_compound_tag(f) {
Ok(compound) => { Ok(compound) => {
let seed = compound let data_compound = compound.get_compound("data");
.get_compound("data") let c = data_compound.as_ref().map_or(&compound, |v| v);
.and_then(|c| c.get_long("seed")); let seed = c.get_long("seed");
if seed.is_none() { if seed.is_none() {
warn!("world_gen_settings.dat has no seed"); warn!("world_gen_settings.dat has no seed");
} }
seed.map(|seed| WorldGenSettings { let seed = seed?;
seed, let mut dimensions = std::collections::HashMap::new();
dimensions: std::collections::HashMap::new(), if let Some(dims_comp) = c.get_compound("dimensions") {
}) for (dim_name, dim_tag) in &dims_comp.child_tags {
if let NbtTag::Compound(dim_c) = dim_tag {
let dim_type = dim_c.get_string("type").unwrap_or(dim_name).to_string();
if let Some(gen_c) = dim_c.get_compound("generator") {
let generator_type = gen_c
.get_string("type")
.unwrap_or("minecraft:noise")
.to_string();
let settings = gen_c
.get_string("settings")
.map(|s| {
crate::world_info::GeneratorSettings::Reference(
s.to_string(),
)
})
.or_else(|| {
gen_c.get_compound("settings").map(|settings_c| {
let json_val = nbt_tag_to_json(&NbtTag::Compound(
settings_c.clone(),
));
crate::world_info::GeneratorSettings::Compound(json_val)
})
});
let biome_source = gen_c.get_compound("biome_source").map(|bs_c| {
let biome_type = bs_c
.get_string("type")
.unwrap_or("minecraft:multi_noise")
.to_string();
match bs_c.get_string("preset") {
Some(preset) => {
crate::world_info::BiomeSource::WithPreset {
preset: preset.to_string(),
biome_type,
}
}
None => {
crate::world_info::BiomeSource::Simple { biome_type }
}
}
});
dimensions.insert(
dim_name.to_string(),
crate::world_info::Dimension {
generator: crate::world_info::Generator {
settings,
biome_source,
generator_type,
},
dimension_type: dim_type,
},
);
}
}
}
}
Some(WorldGenSettings { seed, dimensions })
} }
Err(e) => { Err(e) => {
warn!("Failed to deserialize world_gen_settings.dat: {e}"); warn!("Failed to deserialize world_gen_settings.dat: {e}");
@ -213,6 +329,43 @@ pub fn write_world_gen_settings(
let mut inner = NbtCompound::new(); let mut inner = NbtCompound::new();
inner.put_int("DataVersion", data_version); inner.put_int("DataVersion", data_version);
inner.put_long("seed", settings.seed); inner.put_long("seed", settings.seed);
inner.put_bool("generate_structures", true);
inner.put_bool("bonus_chest", false);
let mut dims_comp = NbtCompound::new();
for (dim_name, dim) in &settings.dimensions {
let mut dim_comp = NbtCompound::new();
dim_comp.put_string("type", dim.dimension_type.clone());
let mut gen_comp = NbtCompound::new();
gen_comp.put_string("type", dim.generator.generator_type.clone());
if let Some(s) = &dim.generator.settings {
match s {
crate::world_info::GeneratorSettings::Reference(r) => {
gen_comp.put_string("settings", r.clone());
}
crate::world_info::GeneratorSettings::Compound(json_val) => {
gen_comp.put("settings", json_to_nbt_tag(json_val));
}
}
}
if let Some(bs) = &dim.generator.biome_source {
let mut bs_comp = NbtCompound::new();
match bs {
crate::world_info::BiomeSource::WithPreset { preset, biome_type } => {
bs_comp.put_string("preset", preset.clone());
bs_comp.put_string("type", biome_type.clone());
}
crate::world_info::BiomeSource::Simple { biome_type } => {
bs_comp.put_string("type", biome_type.clone());
}
}
gen_comp.put_compound("biome_source", bs_comp);
}
dim_comp.put_compound("generator", gen_comp);
dims_comp.put_compound(dim_name, dim_comp);
}
inner.put_compound("dimensions", dims_comp);
let mut root = NbtCompound::new(); let mut root = NbtCompound::new();
root.put_compound("data", inner); root.put_compound("data", inner);
@ -379,10 +532,20 @@ pub fn read_wandering_trader(level_folder: &Path) -> WanderingTraderData {
Ok(compound) => { Ok(compound) => {
let data_compound = compound.get_compound("data"); let data_compound = compound.get_compound("data");
let c = data_compound.as_ref().map_or(&compound, |v| v); let c = data_compound.as_ref().map_or(&compound, |v| v);
let data_version = compound
.get_int("DataVersion")
.or_else(|| c.get_int("DataVersion"))
.unwrap_or(0);
WanderingTraderData { WanderingTraderData {
spawn_delay: c.get_int("WanderingTraderSpawnDelay").unwrap_or(24_000), spawn_delay: c
spawn_chance: c.get_int("WanderingTraderSpawnChance").unwrap_or(25), .get_int("spawn_delay")
data_version: c.get_int("DataVersion").unwrap_or(0), .or_else(|| c.get_int("WanderingTraderSpawnDelay"))
.unwrap_or(24_000),
spawn_chance: c
.get_int("spawn_chance")
.or_else(|| c.get_int("WanderingTraderSpawnChance"))
.unwrap_or(25),
data_version,
} }
} }
Err(e) => { Err(e) => {
@ -391,7 +554,7 @@ pub fn read_wandering_trader(level_folder: &Path) -> WanderingTraderData {
} }
}, },
Err(e) => { Err(e) => {
warn!("Failed to open wandering_trader.dat: {e}"); warn!("Failed to open wandering_trader.dat, using defaults: {e}");
WanderingTraderData::default() WanderingTraderData::default()
} }
} }
@ -405,9 +568,10 @@ pub fn write_wandering_trader(
let path = dir.join("wandering_trader.dat"); let path = dir.join("wandering_trader.dat");
let file = File::create(&path)?; let file = File::create(&path)?;
let mut data_comp = NbtCompound::new(); let mut data_comp = NbtCompound::new();
data_comp.put_int("WanderingTraderSpawnDelay", data.spawn_delay); data_comp.put_int("spawn_delay", data.spawn_delay);
data_comp.put_int("WanderingTraderSpawnChance", data.spawn_chance); data_comp.put_int("spawn_chance", data.spawn_chance);
let mut root = NbtCompound::new(); let mut root = NbtCompound::new();
root.put_int("DataVersion", data.data_version);
root.put_compound("data", data_comp); root.put_compound("data", data_comp);
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file)) pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string())) .map_err(|e| WorldInfoError::SerializationError(e.to_string()))
@ -419,18 +583,15 @@ pub fn write_custom_boss_events_stub(
) -> Result<(), WorldInfoError> { ) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?; let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("custom_boss_events.dat"); let path = dir.join("custom_boss_events.dat");
// Only create if absent; actual boss-bar persistence lives elsewhere.
if path.exists() { if path.exists() {
return Ok(()); return Ok(());
} }
let mut inner = NbtCompound::new();
inner.put_int("DataVersion", data_version);
let mut root = NbtCompound::new(); let mut root = NbtCompound::new();
root.put_compound("data", inner); root.put_int("DataVersion", data_version);
root.put_compound("data", NbtCompound::new());
let file = File::create(&path)?; let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file) pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string())) .map_err(|e| WorldInfoError::SerializationError(e.to_string()))
} }
@ -447,12 +608,70 @@ pub fn write_scheduled_events_stub(
let mut inner = NbtCompound::new(); let mut inner = NbtCompound::new();
inner.put("events", NbtTag::List(vec![])); inner.put("events", NbtTag::List(vec![]));
inner.put_int("DataVersion", data_version);
let mut root = NbtCompound::new(); let mut root = NbtCompound::new();
root.put_int("DataVersion", data_version);
root.put_compound("data", inner);
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn write_random_sequences_stub(
level_folder: &Path,
data_version: i32,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("random_sequences.dat");
if path.exists() {
return Ok(());
}
let mut inner = NbtCompound::new();
inner.put_int("salt", 0);
inner.put_compound("sequences", NbtCompound::new());
let mut root = NbtCompound::new();
root.put_int("DataVersion", data_version);
root.put_compound("data", inner);
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn write_scoreboard_stub(level_folder: &Path, data_version: i32) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("scoreboard.dat");
if path.exists() {
return Ok(());
}
let mut root = NbtCompound::new();
root.put_int("DataVersion", data_version);
root.put_compound("data", NbtCompound::new());
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn write_stopwatches_stub(
level_folder: &Path,
data_version: i32,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("stopwatches.dat");
if path.exists() {
return Ok(());
}
let mut inner = NbtCompound::new();
inner.put_compound("stopwatches", NbtCompound::new());
let mut root = NbtCompound::new();
root.put_int("DataVersion", data_version);
root.put_compound("data", inner); root.put_compound("data", inner);
let file = File::create(&path)?; let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file) pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string())) .map_err(|e| WorldInfoError::SerializationError(e.to_string()))
} }

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
/** /**
* This implementation is heavily based on <https://github.com/MCHPR/MCHPRS> * This implementation is heavily based on <https://github.com/MCHPR/MCHPRS>
* Updated to fit pumpkin by 4lve * Updated to fit pumpkin by 4lve
@ -31,32 +29,11 @@ pub mod sculk_sensor;
pub mod target_block; pub mod target_block;
pub mod tripwire; pub mod tripwire;
pub mod tripwire_hook; pub mod tripwire_hook;
pub mod turbo;
// abstract // abstract
pub mod abstract_redstone_gate; pub mod abstract_redstone_gate;
pub mod dispenser; pub mod dispenser;
pub async fn update_wire_neighbors(world: &Arc<World>, pos: &BlockPos) {
for direction in BlockDirection::all() {
let neighbor_pos = pos.offset(direction.to_offset());
let block = world.get_block(&neighbor_pos);
world
.block_registry
.on_neighbor_update(world, block, &neighbor_pos, block, true)
.await;
for n_direction in BlockDirection::all() {
let n_neighbor_pos = neighbor_pos.offset(n_direction.to_offset());
let block = world.get_block(&n_neighbor_pos);
world
.block_registry
.on_neighbor_update(world, block, &n_neighbor_pos, block, true)
.await;
}
}
}
pub async fn is_emitting_redstone_power( pub async fn is_emitting_redstone_power(
block: &Block, block: &Block,
state: &BlockState, state: &BlockState,

File diff suppressed because it is too large Load Diff

View File

@ -1,435 +0,0 @@
//! The implementation of "Redstone Wire Turbo" was largely based on
//! the accelerator created by theosib. For more information, see:
//! <https://bugs.mojang.com/browse/MC-81098>.
use pumpkin_data::{
Block, BlockDirection, BlockState,
block_properties::{BlockProperties, RedstoneWireLikeProperties},
};
use pumpkin_util::math::{position::BlockPos, vector3::Vector3};
use pumpkin_world::world::BlockFlags;
use rustc_hash::FxHashMap;
use std::sync::Arc;
use crate::world::World;
use super::get_redstone_power_no_dust;
type RedstoneWireProps = RedstoneWireLikeProperties;
fn unwrap_wire(state: &BlockState) -> RedstoneWireProps {
RedstoneWireProps::from_state_id(state.id, &Block::REDSTONE_WIRE)
}
#[derive(Clone, Copy)]
struct NodeId {
index: usize,
}
struct UpdateNode {
pos: BlockPos,
/// The cached state of the block
state: &'static BlockState,
/// This will only be `Some` when all the neighbors are identified.
neighbors: Option<Vec<NodeId>>,
visited: bool,
xbias: i32,
zbias: i32,
layer: u32,
}
impl UpdateNode {
fn new(world: &World, pos: BlockPos) -> Self {
Self {
pos,
state: world.get_block_state(&pos),
visited: false,
neighbors: None,
xbias: 0,
zbias: 0,
layer: 0,
}
}
}
pub(super) struct RedstoneWireTurbo {
nodes: Vec<UpdateNode>,
node_cache: FxHashMap<BlockPos, NodeId>,
update_queue: Vec<Vec<NodeId>>,
current_walk_layer: u32,
}
impl RedstoneWireTurbo {
// Internal numbering for cardinal directions
const NORTH: usize = 0;
const EAST: usize = 1;
const SOUTH: usize = 2;
const WEST: usize = 3;
pub fn new() -> Self {
Self {
nodes: Vec::new(),
node_cache: FxHashMap::default(),
update_queue: vec![vec![], vec![], vec![]],
current_walk_layer: 0,
}
}
fn get_node(&self, node_id: NodeId) -> &UpdateNode {
&self.nodes[node_id.index]
}
const fn compute_all_neighbors(pos: BlockPos) -> [BlockPos; 24] {
let Vector3 { x, y, z } = pos.0;
[
BlockPos::new(x - 1, y, z),
BlockPos::new(x + 1, y, z),
BlockPos::new(x, y - 1, z),
BlockPos::new(x, y + 1, z),
BlockPos::new(x, y, z - 1),
BlockPos::new(x, y, z + 1),
// Neighbors of neighbors, in the same order,
// except that duplicates are not included
BlockPos::new(x - 2, y, z),
BlockPos::new(x - 1, y - 1, z),
BlockPos::new(x - 1, y + 1, z),
BlockPos::new(x - 1, y, z - 1),
BlockPos::new(x - 1, y, z + 1),
BlockPos::new(x + 2, y, z),
BlockPos::new(x + 1, y - 1, z),
BlockPos::new(x + 1, y + 1, z),
BlockPos::new(x + 1, y, z - 1),
BlockPos::new(x + 1, y, z + 1),
BlockPos::new(x, y - 2, z),
BlockPos::new(x, y - 1, z - 1),
BlockPos::new(x, y - 1, z + 1),
BlockPos::new(x, y + 2, z),
BlockPos::new(x, y + 1, z - 1),
BlockPos::new(x, y + 1, z + 1),
BlockPos::new(x, y, z - 2),
BlockPos::new(x, y, z + 2),
]
}
fn compute_heading(rx: i32, rz: i32) -> usize {
let code = (rx + 1) + 3 * (rz + 1);
match code {
0 | 1 => Self::NORTH,
2 | 5 => Self::EAST,
3 | 4 => Self::WEST,
6..=8 => Self::SOUTH,
_ => {
tracing::error!("Invalid turbo heading code: {}", code);
Self::NORTH
}
}
}
// const UPDATE_REDSTONE: [bool; 24] = [
// true, true, false, false, true, true, // 0 to 5
// false, true, true, false, false, false, // 6 to 11
// true, true, false, false, false, true, // 12 to 17
// true, false, true, true, false, false // 18 to 23
// ];
fn identify_neighbors(&mut self, world: &World, upd1: NodeId) {
let pos = self.nodes[upd1.index].pos;
let neighbors = Self::compute_all_neighbors(pos);
let mut neighbors_visited = Vec::with_capacity(24);
let mut neighbor_nodes = Vec::with_capacity(24);
for neighbor_pos in &neighbors[0..24] {
let neighbor = if self.node_cache.contains_key(neighbor_pos) {
self.node_cache[neighbor_pos]
} else {
let node_id = NodeId {
index: self.nodes.len(),
};
self.node_cache.insert(*neighbor_pos, node_id);
self.nodes.push(UpdateNode::new(world, *neighbor_pos));
node_id
};
let node = &self.nodes[neighbor.index];
// if let Block::RedstoneWire { .. } = node.state {
// if RedstoneWireTurbo::UPDATE_REDSTONE[i] {
neighbor_nodes.push(neighbor);
neighbors_visited.push(node.visited);
// continue;
// }
// }
// neighbor_nodes.push(None);
// neighbors_visited.push(false);
}
let from_west = neighbors_visited[0] || neighbors_visited[7] || neighbors_visited[8];
let from_east = neighbors_visited[1] || neighbors_visited[12] || neighbors_visited[13];
let from_north = neighbors_visited[4] || neighbors_visited[17] || neighbors_visited[20];
let from_south = neighbors_visited[5] || neighbors_visited[18] || neighbors_visited[21];
let mut cx = 0;
let mut cz = 0;
if from_west {
cx += 1;
}
if from_east {
cx -= 1;
}
if from_north {
cz += 1;
}
if from_south {
cz -= 1;
}
let UpdateNode { xbias, zbias, .. } = &self.nodes[upd1.index];
let xbias = *xbias;
let zbias = *zbias;
let heading;
if cx == 0 && cz == 0 {
heading = Self::compute_heading(xbias, zbias);
for node_id in &neighbor_nodes {
// if let Some(node_id) = node_id {
let nn = &mut self.nodes[node_id.index];
nn.xbias = xbias;
nn.zbias = zbias;
// }
}
} else {
if cx != 0 && cz != 0 {
if xbias != 0 {
cz = 0;
}
if zbias != 0 {
cx = 0;
}
}
heading = Self::compute_heading(cx, cz);
for node_id in &neighbor_nodes {
// if let Some(node_id) = node_id {
let nn = &mut self.nodes[node_id.index];
nn.xbias = cx;
nn.zbias = cz;
// }
}
}
self.orient_neighbors(&neighbor_nodes, upd1, heading);
}
const REORDERING: [[usize; 24]; 4] = [
[
2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15,
],
[
2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10,
],
[
2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9,
],
[
2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14,
],
];
fn orient_neighbors(&mut self, src: &[NodeId], dst_id: NodeId, heading: usize) {
let dst = &mut self.nodes[dst_id.index];
let mut neighbors = Vec::with_capacity(24);
let re = Self::REORDERING[heading];
for i in &re {
neighbors.push(src[*i]);
}
dst.neighbors = Some(neighbors);
}
/// This is the start of a great adventure
pub async fn update_surrounding_neighbors(world: &Arc<World>, pos: BlockPos) {
let mut turbo = Self::new();
let mut root_node = UpdateNode::new(world, pos);
root_node.visited = true;
let node_id = NodeId { index: 0 };
turbo.node_cache.insert(pos, node_id);
turbo.nodes.push(root_node);
turbo.propagate_changes(world, node_id, 0);
turbo.breadth_first_walk(world).await;
}
fn propagate_changes(&mut self, world: &World, upd1: NodeId, layer: u32) {
if self.nodes[upd1.index].neighbors.is_none() {
self.identify_neighbors(world, upd1);
}
let Some(neighbors_ref) = self.nodes[upd1.index].neighbors.as_ref() else {
return;
};
let Ok(neighbors): Result<[NodeId; 24], _> = neighbors_ref[0..24].try_into() else {
return;
};
let layer1 = layer + 1;
for neighbor_id in neighbors {
let neighbor = &mut self.nodes[neighbor_id.index];
if layer1 > neighbor.layer {
neighbor.layer = layer1;
self.update_queue[1].push(neighbor_id);
}
}
let layer2 = layer + 2;
for neighbor_id in &neighbors[0..4] {
let neighbor = &mut self.nodes[neighbor_id.index];
if layer2 > neighbor.layer {
neighbor.layer = layer2;
self.update_queue[2].push(*neighbor_id);
}
}
}
async fn breadth_first_walk(&mut self, world: &Arc<World>) {
self.shift_queue();
self.current_walk_layer = 1;
while !self.update_queue[0].is_empty() || !self.update_queue[1].is_empty() {
let mut current_queue = self.update_queue.remove(0);
self.update_queue.push(Vec::new());
for node_id in &current_queue {
let block = Block::from_state_id(self.nodes[node_id.index].state.id);
if block == &Block::REDSTONE_WIRE {
self.update_node(world, *node_id, self.current_walk_layer)
.await;
} else {
// This only works because updating any other block than a wire will
// never change the state of the block. If that changes in the future,
// the cached state will need to be updated
world
.update_neighbor(&self.nodes[node_id.index].pos, block)
.await;
}
}
current_queue.clear();
let last_idx = self.update_queue.len() - 1;
self.update_queue[last_idx] = current_queue;
self.current_walk_layer += 1;
}
self.current_walk_layer = 0;
}
fn shift_queue(&mut self) {
let mut t = self.update_queue.remove(0);
t.clear();
self.update_queue.push(t);
}
async fn update_node(&mut self, world: &Arc<World>, upd1: NodeId, layer: u32) {
let old_wire = {
let node = &mut self.nodes[upd1.index];
node.visited = true;
unwrap_wire(node.state)
};
let new_wire = self.calculate_current_changes(world, upd1).await;
if old_wire.power != new_wire.power {
let node = &mut self.nodes[upd1.index];
let mut wire = unwrap_wire(node.state);
wire.power = new_wire.power;
node.state = BlockState::from_id(wire.to_state_id(&Block::REDSTONE_WIRE));
self.propagate_changes(world, upd1, layer);
}
}
const RS_NEIGHBORS: [usize; 4] = [4, 5, 6, 7];
const RS_NEIGHBORS_UP: [usize; 4] = [9, 11, 13, 15];
const RS_NEIGHBORS_DN: [usize; 4] = [8, 10, 12, 14];
async fn calculate_current_changes(
&mut self,
world: &Arc<World>,
upd: NodeId,
) -> RedstoneWireProps {
let mut wire = unwrap_wire(self.nodes[upd.index].state);
let i = wire.power;
let mut block_power = 0;
if self.nodes[upd.index].neighbors.is_none() {
self.identify_neighbors(world, upd);
}
let pos = self.nodes[upd.index].pos;
let mut wire_power = 0;
for side in BlockDirection::all() {
let neighbor_pos = pos.offset(side.to_offset());
let neighbor = &self.nodes[self.node_cache[&neighbor_pos].index].state;
wire_power = wire_power.max(
get_redstone_power_no_dust(
Block::from_state_id(neighbor.id),
neighbor,
world,
neighbor_pos,
side,
)
.await,
);
}
if wire_power < 15 {
let Some(neighbors) = self.nodes[upd.index].neighbors.as_ref() else {
return wire;
};
let center_up = self.nodes[neighbors[1].index].state;
for m in 0..4 {
let n = Self::RS_NEIGHBORS[m];
let neighbor_id = neighbors[n];
let neighbor = self.get_node(neighbor_id).state;
block_power = self.get_max_current_strength(neighbor_id, block_power);
if !neighbor.is_solid_block() {
let neighbor_down = neighbors[Self::RS_NEIGHBORS_DN[m]];
block_power = self.get_max_current_strength(neighbor_down, block_power);
} else if !center_up.is_solid_block() && neighbor.is_solid_block() {
let neighbor_up = neighbors[Self::RS_NEIGHBORS_UP[m]];
block_power = self.get_max_current_strength(neighbor_up, block_power);
}
}
}
let mut j = block_power.saturating_sub(1);
if wire_power > j {
j = wire_power;
}
if i != j {
wire.power = j;
world
.set_block_state(
&pos,
wire.to_state_id(&Block::REDSTONE_WIRE),
BlockFlags::empty(),
)
.await;
}
wire
}
fn get_max_current_strength(&self, upd: NodeId, strength: u8) -> u8 {
let node = &self.nodes[upd.index];
let block = Block::from_state_id(node.state.id);
if block == &Block::REDSTONE_WIRE {
unwrap_wire(node.state).power.max(strength)
} else {
strength
}
}
}

View File

@ -2301,7 +2301,12 @@ impl World {
let spawn_position = Vector2::new(level_info.spawn_x, level_info.spawn_z); let spawn_position = Vector2::new(level_info.spawn_x, level_info.spawn_z);
let chunk_pos = Vector2::new(level_info.spawn_x >> 4, level_info.spawn_z >> 4); let chunk_pos = Vector2::new(level_info.spawn_x >> 4, level_info.spawn_z >> 4);
self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await; self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await;
let pos_y = self.get_top_block(spawn_position) + 1; // +1 to spawn on top of the block let top = self.get_top_block(spawn_position);
let pos_y = if top > self.dimension.min_y {
top + 1
} else {
level_info.spawn_y
};
let position = Vector3::new( let position = Vector3::new(
f64::from(level_info.spawn_x) + 0.5, f64::from(level_info.spawn_x) + 0.5,
@ -3146,7 +3151,12 @@ impl World {
let spawn_position = Vector2::new(info.spawn_x, info.spawn_z); let spawn_position = Vector2::new(info.spawn_x, info.spawn_z);
let chunk_pos = Vector2::new(info.spawn_x >> 4, info.spawn_z >> 4); let chunk_pos = Vector2::new(info.spawn_x >> 4, info.spawn_z >> 4);
self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await; self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await;
let pos_y = self.get_top_block(spawn_position) + 1; // +1 to spawn on top of the block let top = self.get_top_block(spawn_position);
let pos_y = if top > self.dimension.min_y {
top + 1
} else {
info.spawn_y
};
let position = Vector3::new( let position = Vector3::new(
f64::from(info.spawn_x) + 0.5, f64::from(info.spawn_x) + 0.5,