From edb848d6083cf1d25d3129a9d67fa416adebb8b3 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 23 Aug 2026 12:42:37 +0200 Subject: [PATCH] fix: ci --- crates/pumpkin-plugin-wit | 2 +- .../pumpkin-world/src/chunk/format/anvil.rs | 4 + crates/pumpkin-world/src/chunk/format/mod.rs | 126 ++- .../src/chunk/io/file_manager.rs | 7 + .../src/chunk_system/generation_cache.rs | 12 +- .../src/chunk_system/schedule.rs | 9 +- crates/pumpkin-world/src/dimension.rs | 8 +- crates/pumpkin-world/src/level.rs | 106 ++- crates/pumpkin-world/src/world_info/anvil.rs | 237 ++++- .../src/world_info/data_files.rs | 259 ++++- .../pumpkin/src/block/blocks/redstone/mod.rs | 23 - .../block/blocks/redstone/redstone_wire.rs | 893 ++++++++++-------- .../src/block/blocks/redstone/turbo.rs | 435 --------- crates/pumpkin/src/world/mod.rs | 14 +- 14 files changed, 1236 insertions(+), 899 deletions(-) delete mode 100644 crates/pumpkin/src/block/blocks/redstone/turbo.rs diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index b44607b19..2337e6f38 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit b44607b19fa796edb28562382a1f4f7f788a4504 +Subproject commit 2337e6f389293a6371667d495feace8e35570318 diff --git a/crates/pumpkin-world/src/chunk/format/anvil.rs b/crates/pumpkin-world/src/chunk/format/anvil.rs index fea9a6288..faafa339e 100644 --- a/crates/pumpkin-world/src/chunk/format/anvil.rs +++ b/crates/pumpkin-world/src/chunk/format/anvil.rs @@ -546,6 +546,10 @@ impl ChunkSerializer for AnvilChunkFile< fn read(r: Bytes) -> Result { let mut raw_file_bytes = r; + if raw_file_bytes.is_empty() { + return Ok(Self::default()); + } + if raw_file_bytes.len() < SECTOR_BYTES * 2 { return Err(ChunkReadingError::InvalidHeader); } diff --git a/crates/pumpkin-world/src/chunk/format/mod.rs b/crates/pumpkin-world/src/chunk/format/mod.rs index cc67a7762..c2f86ac09 100644 --- a/crates/pumpkin-world/src/chunk/format/mod.rs +++ b/crates/pumpkin-world/src/chunk/format/mod.rs @@ -94,15 +94,26 @@ fn extract_u16_array(tag: &pumpkin_nbt::tag::NbtTag) -> Option { let ids: Box<[BlockStateId]> = list .iter() - .map(|t| { - let val = match t { - pumpkin_nbt::tag::NbtTag::Int(x) => *x as u16, - pumpkin_nbt::tag::NbtTag::Short(x) => *x as u16, - pumpkin_nbt::tag::NbtTag::Byte(x) => *x as u16, - pumpkin_nbt::tag::NbtTag::Long(x) => *x as u16, - _ => 0, - }; - BlockStateId::new_or_air(val) + .map(|t| match t { + pumpkin_nbt::tag::NbtTag::Int(x) => BlockStateId::new_or_air(*x as u16), + pumpkin_nbt::tag::NbtTag::Short(x) => BlockStateId::new_or_air(*x as u16), + pumpkin_nbt::tag::NbtTag::Byte(x) => BlockStateId::new_or_air(*x as u16), + pumpkin_nbt::tag::NbtTag::Long(x) => BlockStateId::new_or_air(*x as u16), + pumpkin_nbt::tag::NbtTag::Compound(compound) => { + if let Ok(entry) = + crate::generation::structure::template::PaletteEntry::from_nbt_compound( + compound, + ) + && let Some(state) = + crate::generation::structure::template::BlockStateResolver::resolve_simple( + &entry, + ) + { + return state.id; + } + BlockStateId::AIR + } + _ => BlockStateId::AIR, }) .collect(); Some(ids) @@ -122,6 +133,10 @@ fn extract_u8_array(tag: &pumpkin_nbt::tag::NbtTag) -> Option> { pumpkin_nbt::tag::NbtTag::Byte(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::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, }) .collect(); @@ -490,7 +505,27 @@ impl ChunkData { let palette_tags: Vec = block_states_nbt .palette .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(); bs_comp.put_list("palette", palette_tags); section_comp.put_compound("block_states", bs_comp); @@ -504,7 +539,16 @@ impl ChunkData { let biome_palette_tags: Vec = biomes_nbt .palette .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(); b_comp.put_list("palette", biome_palette_tags); section_comp.put_compound("biomes", b_comp); @@ -849,3 +893,63 @@ impl Default for LightContainer { 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 + ); + } +} diff --git a/crates/pumpkin-world/src/chunk/io/file_manager.rs b/crates/pumpkin-world/src/chunk/io/file_manager.rs index e0a313d09..936e3002d 100644 --- a/crates/pumpkin-world/src/chunk/io/file_manager.rs +++ b/crates/pumpkin-world/src/chunk/io/file_manager.rs @@ -101,6 +101,13 @@ impl + 'static> ChunkSerializerLazyLo match tokio::fs::read(&self.path).await { 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())) .await .map_err(|e| ChunkReadingError::IoError(std::io::Error::other(e)))??; diff --git a/crates/pumpkin-world/src/chunk_system/generation_cache.rs b/crates/pumpkin-world/src/chunk_system/generation_cache.rs index a291f7687..61b337dbc 100644 --- a/crates/pumpkin-world/src/chunk_system/generation_cache.rs +++ b/crates/pumpkin-world/src/chunk_system/generation_cache.rs @@ -444,9 +444,12 @@ impl Cache { .get_proto_chunk_mut() .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) => { 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 { @@ -455,9 +458,14 @@ impl Cache { .get_proto_chunk_mut() .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) => { 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 { diff --git a/crates/pumpkin-world/src/chunk_system/schedule.rs b/crates/pumpkin-world/src/chunk_system/schedule.rs index 1927dc973..162ed19e1 100644 --- a/crates/pumpkin-world/src/chunk_system/schedule.rs +++ b/crates/pumpkin-world/src/chunk_system/schedule.rs @@ -268,9 +268,10 @@ impl GenerationSchedule { let mut ready = Vec::new(); for (key, node) in &mut graph.nodes { - node.in_queue = false; if node.stage == StagedChunkEnum::None || node.in_degree != 0 + || node.in_queue + || node.in_flight || waiting_for_chunks.contains(&key) { continue; @@ -1242,8 +1243,8 @@ impl GenerationSchedule { } if let Some(node) = self.graph.nodes.get_mut(task.1) { + node.in_queue = false; if node.in_degree != 0 { - node.in_queue = false; continue; } node.in_flight = true; @@ -1492,7 +1493,9 @@ impl GenerationSchedule { &self.waiting_for_chunks, ); if restored > 0 { - warn!("Restored {restored} stranded ready chunk tasks to generation queue"); + debug!( + "Restored {restored} stranded ready chunk tasks to generation queue" + ); continue; } debug_assert!(self.debug_check()); diff --git a/crates/pumpkin-world/src/dimension.rs b/crates/pumpkin-world/src/dimension.rs index f5107730b..8a122aff1 100644 --- a/crates/pumpkin-world/src/dimension.rs +++ b/crates/pumpkin-world/src/dimension.rs @@ -9,15 +9,9 @@ use crate::level::Level; pub fn into_level( dimension: Dimension, level_config: &LevelConfig, - mut base_directory: PathBuf, + base_directory: PathBuf, seed: i64, gen_pool: Option>, ) -> Arc { - 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) } diff --git a/crates/pumpkin-world/src/level.rs b/crates/pumpkin-world/src/level.rs index d0c6b8092..032c14d44 100644 --- a/crates/pumpkin-world/src/level.rs +++ b/crates/pumpkin-world/src/level.rs @@ -148,17 +148,31 @@ impl Level { dimension: Dimension, gen_pool: Option>, ) -> Arc { - let dim_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name - || dimension.minecraft_name == Dimension::THE_NETHER.minecraft_name - || dimension.minecraft_name == Dimension::THE_END.minecraft_name + let (namespace, name) = match dimension.minecraft_name.split_once(':') { + Some((ns, n)) => (ns, n), + None => ("minecraft", dimension.minecraft_name), + }; + + // 26.2 canonical layout: root_folder/dimensions// + 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() + } 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 { - let (namespace, name) = match dimension.minecraft_name.split_once(':') { - Some((ns, n)) => (ns, n), - None => ("minecraft", dimension.minecraft_name), - }; - root_folder.join("dimensions").join(namespace).join(name) + canonical_dim_folder }; let region_folder = dim_folder.join("region"); @@ -177,21 +191,13 @@ impl Level { poi_folder, }); - let main_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name { - level_folder.root_folder.clone() - } else { - level_folder - .root_folder - .parent() - .unwrap_or(&level_folder.root_folder) - .to_path_buf() - }; + let main_folder = &level_folder.root_folder; let mut is_flat = false; let mut flat_layers = Vec::new(); 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) && dim_settings.generator.generator_type == "minecraft:flat" { @@ -1005,3 +1011,67 @@ impl Level { .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")); + } +} diff --git a/crates/pumpkin-world/src/world_info/anvil.rs b/crates/pumpkin-world/src/world_info/anvil.rs index 59e74c207..ebbcb49b6 100644 --- a/crates/pumpkin-world/src/world_info/anvil.rs +++ b/crates/pumpkin-world/src/world_info/anvil.rs @@ -20,7 +20,8 @@ use crate::world_info::{ data_files::{ minecraft_data_dir, read_game_rules, read_wandering_trader, read_weather, 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, }, 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) { + 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") { 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") { level_data.data_version = data_version; } - 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(diff_comp) = data.get_compound("difficulty_settings") { + if let Some(diff_str) = diff_comp.get_string("difficulty") { + match diff_str { + "peaceful" => level_data.difficulty = Difficulty::Peaceful, + "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") { 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_compound("DataPacks", data_packs_to_nbt(&info.data_packs)); 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_bool("DifficultyLocked", info.difficulty_locked); data.put_long("LastPlayed", info.last_played); 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("SpawnY", info.spawn_y); 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); + 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 if minecraft_data_dir(level_folder) .join("game_rules.dat") @@ -363,6 +427,8 @@ impl WorldInfoWriter for AnvilLevelInfo { // ── Write level.dat ─────────────────────────────────────────────────── 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 data_comp = root @@ -372,9 +438,14 @@ impl WorldInfoWriter for AnvilLevelInfo { level_data_to_nbt(&level_data, &mut 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()))?; + if path.exists() { + let _ = std::fs::copy(&path, &path_old); + } + let _ = std::fs::rename(&path_new, &path); + let data_version = level_data.data_version; // ── Write data/minecraft/*.dat files ───────────────────────────────── @@ -431,6 +502,21 @@ impl WorldInfoWriter for AnvilLevelInfo { 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(()) } } @@ -465,7 +551,7 @@ mod test { DataPacks, LevelData, MAXIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_LEVEL_VERSION, 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(); 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); + } } diff --git a/crates/pumpkin-world/src/world_info/data_files.rs b/crates/pumpkin-world/src/world_info/data_files.rs index e2cc058ee..640e246e0 100644 --- a/crates/pumpkin-world/src/world_info/data_files.rs +++ b/crates/pumpkin-world/src/world_info/data_files.rs @@ -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("thundering", data.thundering); let mut root = NbtCompound::new(); + root.put_int("DataVersion", data.data_version); root.put_compound("data", data_comp); pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file)) .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 { let path = minecraft_data_dir(level_folder).join("world_gen_settings.dat"); if !path.exists() { @@ -179,16 +240,71 @@ pub fn read_world_gen_settings(level_folder: &Path) -> Option match File::open(&path) { Ok(f) => match read_gzip_compound_tag(f) { Ok(compound) => { - let seed = compound - .get_compound("data") - .and_then(|c| c.get_long("seed")); + let data_compound = compound.get_compound("data"); + let c = data_compound.as_ref().map_or(&compound, |v| v); + let seed = c.get_long("seed"); if seed.is_none() { warn!("world_gen_settings.dat has no seed"); } - seed.map(|seed| WorldGenSettings { - seed, - dimensions: std::collections::HashMap::new(), - }) + let seed = seed?; + let mut 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) => { warn!("Failed to deserialize world_gen_settings.dat: {e}"); @@ -213,6 +329,43 @@ pub fn write_world_gen_settings( let mut inner = NbtCompound::new(); inner.put_int("DataVersion", data_version); 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(); root.put_compound("data", inner); @@ -379,10 +532,20 @@ pub fn read_wandering_trader(level_folder: &Path) -> WanderingTraderData { Ok(compound) => { let data_compound = compound.get_compound("data"); 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 { - spawn_delay: c.get_int("WanderingTraderSpawnDelay").unwrap_or(24_000), - spawn_chance: c.get_int("WanderingTraderSpawnChance").unwrap_or(25), - data_version: c.get_int("DataVersion").unwrap_or(0), + spawn_delay: c + .get_int("spawn_delay") + .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) => { @@ -391,7 +554,7 @@ pub fn read_wandering_trader(level_folder: &Path) -> WanderingTraderData { } }, Err(e) => { - warn!("Failed to open wandering_trader.dat: {e}"); + warn!("Failed to open wandering_trader.dat, using defaults: {e}"); WanderingTraderData::default() } } @@ -405,9 +568,10 @@ pub fn write_wandering_trader( let path = dir.join("wandering_trader.dat"); let file = File::create(&path)?; let mut data_comp = NbtCompound::new(); - data_comp.put_int("WanderingTraderSpawnDelay", data.spawn_delay); - data_comp.put_int("WanderingTraderSpawnChance", data.spawn_chance); + data_comp.put_int("spawn_delay", data.spawn_delay); + data_comp.put_int("spawn_chance", data.spawn_chance); let mut root = NbtCompound::new(); + root.put_int("DataVersion", data.data_version); root.put_compound("data", data_comp); pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, BufWriter::new(file)) .map_err(|e| WorldInfoError::SerializationError(e.to_string())) @@ -419,18 +583,15 @@ pub fn write_custom_boss_events_stub( ) -> Result<(), WorldInfoError> { let dir = ensure_minecraft_data_dir(level_folder)?; let path = dir.join("custom_boss_events.dat"); - // Only create if absent; actual boss-bar persistence lives elsewhere. if path.exists() { return Ok(()); } - let mut inner = NbtCompound::new(); - inner.put_int("DataVersion", data_version); 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)?; - pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file) .map_err(|e| WorldInfoError::SerializationError(e.to_string())) } @@ -447,12 +608,70 @@ pub fn write_scheduled_events_stub( let mut inner = NbtCompound::new(); inner.put("events", NbtTag::List(vec![])); - inner.put_int("DataVersion", data_version); 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); let file = File::create(&path)?; - pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file) .map_err(|e| WorldInfoError::SerializationError(e.to_string())) } diff --git a/crates/pumpkin/src/block/blocks/redstone/mod.rs b/crates/pumpkin/src/block/blocks/redstone/mod.rs index 4627a8b05..a403176d8 100644 --- a/crates/pumpkin/src/block/blocks/redstone/mod.rs +++ b/crates/pumpkin/src/block/blocks/redstone/mod.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - /** * This implementation is heavily based on * Updated to fit pumpkin by 4lve @@ -31,32 +29,11 @@ pub mod sculk_sensor; pub mod target_block; pub mod tripwire; pub mod tripwire_hook; -pub mod turbo; // abstract pub mod abstract_redstone_gate; pub mod dispenser; -pub async fn update_wire_neighbors(world: &Arc, 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( block: &Block, state: &BlockState, diff --git a/crates/pumpkin/src/block/blocks/redstone/redstone_wire.rs b/crates/pumpkin/src/block/blocks/redstone/redstone_wire.rs index a51fbcbff..6a2fe4d33 100644 --- a/crates/pumpkin/src/block/blocks/redstone/redstone_wire.rs +++ b/crates/pumpkin/src/block/blocks/redstone/redstone_wire.rs @@ -5,6 +5,8 @@ use pumpkin_data::block_properties::{ BlockProperties, EastRedstone, HorizontalFacing, NorthRedstone, ObserverLikeProperties, RedstoneWireLikeProperties, RepeaterLikeProperties, SouthRedstone, WestRedstone, }; +use pumpkin_data::block_rotation::{Mirror, Rotation}; +use pumpkin_data::tag::Taggable; use pumpkin_data::{Block, BlockDirection, BlockState, HorizontalFacingExt}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; @@ -12,16 +14,13 @@ use pumpkin_world::world::{BlockAccessor, BlockFlags}; use crate::block::registry::BlockActionResult; use crate::block::{ - BlockFuture, BrokenArgs, CanPlaceAtArgs, GetRedstonePowerArgs, GetStateForNeighborUpdateArgs, - OnNeighborUpdateArgs, OnPlaceArgs, PlacedArgs, PrepareArgs, -}; -use crate::{ - block::{BlockBehaviour, NormalUseArgs}, - world::World, + BlockBehaviour, BlockFuture, BrokenArgs, CanPlaceAtArgs, GetRedstonePowerArgs, + GetStateForNeighborUpdateArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs, PlacedArgs, + PrepareArgs, }; +use crate::world::World; -use super::turbo::RedstoneWireTurbo; -use super::{get_redstone_power_no_dust, update_wire_neighbors}; +use super::get_redstone_power_no_dust; type RedstoneWireProperties = RedstoneWireLikeProperties; @@ -30,19 +29,38 @@ pub struct RedstoneWireBlock; impl BlockBehaviour for RedstoneWireBlock { fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { - can_place_at(args.block_accessor, args.position) + can_survive(args.block_accessor, args.position) } fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { - let mut wire = RedstoneWireProperties::default(args.block); - wire.power = calculate_power(args.world, args.position).await; - wire = get_regulated_sides(wire, args.world, args.position).await; - if is_dot(wire) { - wire = make_cross(wire.power); + let initial_state = make_cross(0); + let wire = get_connection_state(args.world, initial_state, args.position).await; + wire.to_state_id(args.block) + }) + } + + fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + update_power_strength(args.world, args.position).await; + + for direction in [BlockDirection::Up, BlockDirection::Down] { + let neighbor_pos = args.position.offset(direction.to_offset()); + update_neighbors_at(args.world, &neighbor_pos, &Block::REDSTONE_WIRE).await; } - wire.to_state_id(args.block) + update_neighbors_of_neighboring_wires(args.world, args.position).await; + }) + } + + fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + for direction in BlockDirection::all() { + let neighbor_pos = args.position.offset(direction.to_offset()); + update_neighbors_at(args.world, &neighbor_pos, &Block::REDSTONE_WIRE).await; + } + + update_neighbors_of_neighboring_wires(args.world, args.position).await; }) } @@ -51,104 +69,79 @@ impl BlockBehaviour for RedstoneWireBlock { args: GetStateForNeighborUpdateArgs<'a>, ) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { - let mut wire = RedstoneWireProperties::from_state_id(args.state_id, args.block); - let old_state = wire; + if args.direction == BlockDirection::Down { + let (below_block, below_state) = + args.world.get_block_and_state(args.neighbor_position); + if !can_survive_on(below_block, below_state) { + return Block::AIR.default_state.id; + } + return args.state_id; + } - let new_side: WireConnection = match args.direction { - BlockDirection::Up => { - return args.state_id; - } - BlockDirection::Down => { - return get_regulated_sides(wire, args.world, args.position) - .await - .to_state_id(args.block); - } - BlockDirection::North => { - let side = get_side(args.world, args.position, BlockDirection::North).await; - wire.north = side.to_north(); - side - } - BlockDirection::South => { - let side = get_side(args.world, args.position, BlockDirection::South).await; - wire.south = side.to_south(); - side - } - BlockDirection::East => { - let side = get_side(args.world, args.position, BlockDirection::East).await; - wire.east = side.to_east(); - side - } - BlockDirection::West => { - let side = get_side(args.world, args.position, BlockDirection::West).await; - wire.west = side.to_west(); - side - } + let wire = RedstoneWireProperties::from_state_id(args.state_id, args.block); + + if args.direction == BlockDirection::Up { + let new_wire = get_connection_state(args.world, wire, args.position).await; + return new_wire.to_state_id(args.block); + } + + let can_connect_up = !args + .world + .get_block_state(&args.position.up()) + .is_solid_block(); + let side_connection = + get_connecting_side(args.world, args.position, args.direction, can_connect_up) + .await; + + let Some(horizontal) = args.direction.to_horizontal_facing() else { + return args.state_id; }; - wire = get_regulated_sides(wire, args.world, args.position).await; - wire.power = calculate_power(args.world, args.position).await; + let current_side = get_side_connection(wire, horizontal); + let is_connected_same = side_connection.is_connected() == current_side.is_connected(); - if is_cross(old_state) && new_side.is_none() { - return wire.to_state_id(args.block); + if is_connected_same && !is_cross(wire) { + let mut new_wire = wire; + set_side_connection(&mut new_wire, horizontal, side_connection); + new_wire.to_state_id(args.block) + } else { + let mut cross = make_cross(wire.power); + set_side_connection(&mut cross, horizontal, side_connection); + let new_wire = get_connection_state(args.world, cross, args.position).await; + new_wire.to_state_id(args.block) } - if !is_dot(old_state) && is_dot(wire) { - let power = wire.power; - wire = make_cross(power); - } - wire.to_state_id(args.block) }) } fn prepare<'a>(&'a self, args: PrepareArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { - let wire_props = - RedstoneWireLikeProperties::from_state_id(args.state_id, &Block::REDSTONE_WIRE); + let wire = RedstoneWireProperties::from_state_id(args.state_id, args.block); for direction in BlockDirection::horizontal() { - let other_block_pos = args.position.offset(direction.to_offset()); - let other_block = args.world.get_block(&other_block_pos); + if is_side_connected_prop(wire, direction) { + let dir_block_pos = args.position.offset(direction.to_offset()); + if args.world.get_block(&dir_block_pos) != &Block::REDSTONE_WIRE { + let down_pos = dir_block_pos.down(); + if args.world.get_block(&down_pos) == &Block::REDSTONE_WIRE { + args.world + .replace_with_state_for_neighbor_update( + &down_pos, + direction.opposite().to_block_direction(), + args.flags, + ) + .await; + } - if wire_props.is_side_connected(direction) && other_block != &Block::REDSTONE_WIRE { - let up_block_pos = other_block_pos.up(); - let up_block = args.world.get_block(&up_block_pos); - if up_block == &Block::REDSTONE_WIRE { - args.world - .replace_with_state_for_neighbor_update( - &up_block_pos, - direction.opposite().to_block_direction(), - args.flags, - ) - .await; - } - - let down_block_pos = other_block_pos.down(); - let down_block = args.world.get_block(&down_block_pos); - if down_block == &Block::REDSTONE_WIRE { - args.world - .replace_with_state_for_neighbor_update( - &down_block_pos, - direction.opposite().to_block_direction(), - args.flags, - ) - .await; - } - } - } - for side_dir in BlockDirection::all() { - let side_block_pos = args.position.offset(side_dir.to_offset()); - for dir in BlockDirection::all() { - if dir.opposite() == side_dir { - continue; - } - let side_neighbor_pos = side_block_pos.offset(dir.to_offset()); - let side_neighbor_block = args.world.get_block(&side_neighbor_pos); - if side_neighbor_block == &Block::REDSTONE_WALL_TORCH - || side_neighbor_block == &Block::PISTON - || side_neighbor_block == &Block::STICKY_PISTON - { - args.world - .update_neighbor(&side_neighbor_pos, &Block::REDSTONE_WIRE) - .await; + let up_pos = dir_block_pos.up(); + if args.world.get_block(&up_pos) == &Block::REDSTONE_WIRE { + args.world + .replace_with_state_for_neighbor_update( + &up_pos, + direction.opposite().to_block_direction(), + args.flags, + ) + .await; + } } } } @@ -159,32 +152,54 @@ impl BlockBehaviour for RedstoneWireBlock { Box::pin(async move { let state = args.world.get_block_state(args.position); let wire = RedstoneWireProperties::from_state_id(state.id, args.block); - if on_use(wire, args.world, args.position).await { - BlockActionResult::Success - } else { - BlockActionResult::Pass + + if is_cross(wire) || is_dot(wire) { + let mut new_wire = if is_cross(wire) { + RedstoneWireProperties::default(&Block::REDSTONE_WIRE) + } else { + make_cross(wire.power) + }; + new_wire.power = wire.power; + new_wire = get_connection_state(args.world, new_wire, args.position).await; + + if wire != new_wire { + args.world + .set_block_state( + args.position, + new_wire.to_state_id(&Block::REDSTONE_WIRE), + BlockFlags::NOTIFY_ALL, + ) + .await; + + for direction in BlockDirection::horizontal() { + let relative_pos = args.position.offset(direction.to_offset()); + let old_connected = is_side_connected_prop(wire, direction); + let new_connected = is_side_connected_prop(new_wire, direction); + + if old_connected != new_connected + && args.world.get_block_state(&relative_pos).is_solid_block() + { + args.world + .update_neighbors( + &relative_pos, + Some(direction.opposite().to_block_direction()), + ) + .await; + } + } + + return BlockActionResult::Success; + } } + + BlockActionResult::Pass }) } fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { - if can_place_at(args.world.as_ref(), args.position) { - let state = args.world.get_block_state(args.position); - let mut wire = RedstoneWireProperties::from_state_id(state.id, args.block); - let new_power = calculate_power(args.world, args.position).await; - if wire.power != new_power { - wire.power = new_power; - args.world - .set_block_state( - args.position, - wire.to_state_id(&Block::REDSTONE_WIRE), - BlockFlags::empty(), - ) - .await; - RedstoneWireTurbo::update_surrounding_neighbors(args.world, *args.position) - .await; - } + if can_survive(args.world.as_ref(), args.position) { + update_power_strength(args.world, args.position).await; } else { args.world .break_block(args.position, None, BlockFlags::NOTIFY_ALL) @@ -199,16 +214,18 @@ impl BlockBehaviour for RedstoneWireBlock { ) -> BlockFuture<'a, u8> { Box::pin(async move { let wire = RedstoneWireProperties::from_state_id(args.state.id, args.block); - let is_connected = args - .direction - .opposite() - .to_horizontal_facing() - .is_some_and(|f| wire.is_side_connected(f)); - if args.direction == BlockDirection::Up || is_connected { - wire.power - } else { - 0 + if wire.power == 0 || args.direction == BlockDirection::Down { + return 0; } + if args.direction == BlockDirection::Up { + return wire.power; + } + if let Some(horizontal) = args.direction.opposite().to_horizontal_facing() + && is_side_connected_prop(wire, horizontal) + { + return wire.power; + } + 0 }) } @@ -218,60 +235,358 @@ impl BlockBehaviour for RedstoneWireBlock { ) -> BlockFuture<'a, u8> { Box::pin(async move { let wire = RedstoneWireProperties::from_state_id(args.state.id, args.block); - let is_connected = args - .direction - .opposite() - .to_horizontal_facing() - .is_some_and(|f| wire.is_side_connected(f)); - if args.direction == BlockDirection::Up || is_connected { - wire.power - } else { - 0 + if wire.power == 0 || args.direction == BlockDirection::Down { + return 0; } + if args.direction == BlockDirection::Up { + return wire.power; + } + if let Some(horizontal) = args.direction.opposite().to_horizontal_facing() + && is_side_connected_prop(wire, horizontal) + { + return wire.power; + } + 0 }) } - fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { - Box::pin(async move { - update_wire_neighbors(args.world, args.position).await; - }) - } - - fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { - Box::pin(async move { - update_wire_neighbors(args.world, args.position).await; - }) - } -} - -fn can_place_at(world: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { - let floor = world.get_block_state(&block_pos.down()); - floor.is_side_solid(BlockDirection::Up) -} - -async fn on_use(wire: RedstoneWireProperties, world: &Arc, block_pos: &BlockPos) -> bool { - if is_cross(wire) || is_dot(wire) { - let mut new_wire = if is_cross(wire) { - RedstoneWireProperties::default(&Block::REDSTONE_WIRE) - } else { - make_cross(wire.power) + fn rotate( + &self, + block: &Block, + state_id: BlockStateId, + rotation: Rotation, + ) -> &'static BlockState { + let wire = RedstoneWireProperties::from_state_id(state_id, block); + let new_wire = match rotation { + Rotation::Rotate180 => RedstoneWireProperties { + north: wire.south.to_wire_connection().to_north(), + east: wire.west.to_wire_connection().to_east(), + south: wire.north.to_wire_connection().to_south(), + west: wire.east.to_wire_connection().to_west(), + power: wire.power, + }, + Rotation::CounterClockwise90 => RedstoneWireProperties { + north: wire.east.to_wire_connection().to_north(), + east: wire.south.to_wire_connection().to_east(), + south: wire.west.to_wire_connection().to_south(), + west: wire.north.to_wire_connection().to_west(), + power: wire.power, + }, + Rotation::Clockwise90 => RedstoneWireProperties { + north: wire.west.to_wire_connection().to_north(), + east: wire.north.to_wire_connection().to_east(), + south: wire.east.to_wire_connection().to_south(), + west: wire.south.to_wire_connection().to_west(), + power: wire.power, + }, + Rotation::None => wire, }; - new_wire.power = wire.power; + BlockState::from_id(new_wire.to_state_id(block)) + } - new_wire = get_regulated_sides(new_wire, world, block_pos).await; - if wire != new_wire { - world - .set_block_state( - block_pos, - new_wire.to_state_id(&Block::REDSTONE_WIRE), - BlockFlags::empty(), - ) - .await; - update_wire_neighbors(world, block_pos).await; - return true; + fn mirror(&self, block: &Block, state_id: BlockStateId, mirror: Mirror) -> &'static BlockState { + let wire = RedstoneWireProperties::from_state_id(state_id, block); + let new_wire = match mirror { + Mirror::LeftRight => RedstoneWireProperties { + north: wire.south.to_wire_connection().to_north(), + south: wire.north.to_wire_connection().to_south(), + east: wire.east, + west: wire.west, + power: wire.power, + }, + Mirror::FrontBack => RedstoneWireProperties { + north: wire.north, + south: wire.south, + east: wire.west.to_wire_connection().to_east(), + west: wire.east.to_wire_connection().to_west(), + power: wire.power, + }, + Mirror::None => wire, + }; + BlockState::from_id(new_wire.to_state_id(block)) + } +} + +// --------------------------------------------------------------------------- +// Evaluator Logic (matching DefaultRedstoneWireEvaluator) +// --------------------------------------------------------------------------- + +pub async fn update_power_strength(world: &Arc, pos: &BlockPos) { + let (block, state) = world.get_block_and_state(pos); + if block != &Block::REDSTONE_WIRE { + return; + } + + let mut wire = RedstoneWireProperties::from_state_id(state.id, block); + let target_strength = calculate_target_strength(world, pos).await; + + if wire.power != target_strength { + wire.power = target_strength; + let new_state_id = wire.to_state_id(&Block::REDSTONE_WIRE); + + world + .set_block_state(pos, new_state_id, BlockFlags::empty()) + .await; + + let mut to_update = Vec::with_capacity(7); + to_update.push(*pos); + for direction in BlockDirection::all() { + to_update.push(pos.offset(direction.to_offset())); + } + + for block_pos in to_update { + update_neighbors_at(world, &block_pos, &Block::REDSTONE_WIRE).await; } } - false +} + +async fn calculate_target_strength(world: &World, pos: &BlockPos) -> u8 { + let block_signal = get_block_signal(world, pos).await; + if block_signal == 15 { + return 15; + } + let wire_signal = get_incoming_wire_signal(world, pos); + block_signal.max(wire_signal) +} + +async fn get_block_signal(world: &World, pos: &BlockPos) -> u8 { + let mut max_signal = 0; + for side in BlockDirection::all() { + let neighbor_pos = pos.offset(side.to_offset()); + let (neighbor_block, neighbor_state) = world.get_block_and_state(&neighbor_pos); + let signal = + get_redstone_power_no_dust(neighbor_block, neighbor_state, world, neighbor_pos, side) + .await; + if signal == 15 { + return 15; + } + max_signal = max_signal.max(signal); + } + max_signal +} + +fn get_incoming_wire_signal(world: &World, pos: &BlockPos) -> u8 { + let mut max_wire_signal = 0; + let up_pos = pos.up(); + let is_up_conductor = world.get_block_state(&up_pos).is_solid_block(); + + for direction in BlockDirection::horizontal() { + let neighbor_pos = pos.offset(direction.to_offset()); + let (neighbor_block, neighbor_state) = world.get_block_and_state(&neighbor_pos); + + // Same level + if neighbor_block == &Block::REDSTONE_WIRE { + let wire = RedstoneWireProperties::from_state_id(neighbor_state.id, neighbor_block); + max_wire_signal = max_wire_signal.max(wire.power); + } + + // Wire UP: if pos.up() is not a solid conductor, and neighbor is a solid conductor + if !is_up_conductor && neighbor_state.is_solid_block() { + let neighbor_up_pos = neighbor_pos.up(); + let (up_block, up_state) = world.get_block_and_state(&neighbor_up_pos); + if up_block == &Block::REDSTONE_WIRE { + let wire = RedstoneWireProperties::from_state_id(up_state.id, up_block); + max_wire_signal = max_wire_signal.max(wire.power); + } + } else if !neighbor_state.is_solid_block() { + // Wire DOWN: if neighbor is not a solid conductor + let neighbor_down_pos = neighbor_pos.down(); + let (down_block, down_state) = world.get_block_and_state(&neighbor_down_pos); + if down_block == &Block::REDSTONE_WIRE { + let wire = RedstoneWireProperties::from_state_id(down_state.id, down_block); + max_wire_signal = max_wire_signal.max(wire.power); + } + } + } + + max_wire_signal.saturating_sub(1) +} + +pub async fn update_neighbors_at(world: &Arc, pos: &BlockPos, source_block: &Block) { + for direction in BlockDirection::update_order() { + let neighbor_pos = pos.offset(direction.to_offset()); + world.update_neighbor(&neighbor_pos, source_block).await; + } +} + +async fn check_corner_change_at(world: &Arc, pos: &BlockPos) { + if world.get_block(pos) == &Block::REDSTONE_WIRE { + update_neighbors_at(world, pos, &Block::REDSTONE_WIRE).await; + for direction in BlockDirection::all() { + let neighbor_pos = pos.offset(direction.to_offset()); + update_neighbors_at(world, &neighbor_pos, &Block::REDSTONE_WIRE).await; + } + } +} + +async fn update_neighbors_of_neighboring_wires(world: &Arc, pos: &BlockPos) { + for direction in BlockDirection::horizontal() { + let neighbor_pos = pos.offset(direction.to_offset()); + check_corner_change_at(world, &neighbor_pos).await; + } + + for direction in BlockDirection::horizontal() { + let target = pos.offset(direction.to_offset()); + if world.get_block_state(&target).is_solid_block() { + check_corner_change_at(world, &target.up()).await; + } else { + check_corner_change_at(world, &target.down()).await; + } + } +} + +// --------------------------------------------------------------------------- +// Connection & Shape Helper Functions +// --------------------------------------------------------------------------- + +pub async fn get_connection_state( + world: &World, + state: RedstoneWireProperties, + pos: &BlockPos, +) -> RedstoneWireProperties { + let was_dot = is_dot(state); + let mut default_state = RedstoneWireProperties::default(&Block::REDSTONE_WIRE); + default_state.power = state.power; + + let mut new_state = get_missing_connections(world, default_state, pos).await; + if was_dot && is_dot(new_state) { + return new_state; + } + + let north = new_state.north.to_wire_connection().is_connected(); + let south = new_state.south.to_wire_connection().is_connected(); + let east = new_state.east.to_wire_connection().is_connected(); + let west = new_state.west.to_wire_connection().is_connected(); + + let north_south_empty = !north && !south; + let east_west_empty = !east && !west; + + if !west && north_south_empty { + new_state.west = WestRedstone::Side; + } + if !east && north_south_empty { + new_state.east = EastRedstone::Side; + } + if !north && east_west_empty { + new_state.north = NorthRedstone::Side; + } + if !south && east_west_empty { + new_state.south = SouthRedstone::Side; + } + + new_state +} + +async fn get_missing_connections( + world: &World, + mut state: RedstoneWireProperties, + pos: &BlockPos, +) -> RedstoneWireProperties { + let can_connect_up = !world.get_block_state(&pos.up()).is_solid_block(); + + for direction in BlockDirection::horizontal() { + if !is_side_connected_prop(state, direction) { + let side_connection = + get_connecting_side(world, pos, direction.to_block_direction(), can_connect_up) + .await; + set_side_connection(&mut state, direction, side_connection); + } + } + + state +} + +async fn get_connecting_side( + world: &World, + pos: &BlockPos, + direction: BlockDirection, + can_connect_up: bool, +) -> WireConnection { + let relative_pos = pos.offset(direction.to_offset()); + let (relative_block, relative_state) = world.get_block_and_state(&relative_pos); + + if can_connect_up { + let is_placeable_above = + is_trapdoor(relative_block) || can_survive_on(relative_block, relative_state); + let (above_block, above_state) = world.get_block_and_state(&relative_pos.up()); + if is_placeable_above && should_connect_to(world, above_block, above_state, None).await { + if relative_state.is_side_solid(direction.opposite()) { + return WireConnection::Up; + } + return WireConnection::Side; + } + } + + let connects_to_relative = + should_connect_to(world, relative_block, relative_state, Some(direction)).await; + let (below_block, below_state) = world.get_block_and_state(&relative_pos.down()); + let connects_to_below = should_connect_to(world, below_block, below_state, None).await; + + if !connects_to_relative && (relative_state.is_solid_block() || !connects_to_below) { + WireConnection::None + } else { + WireConnection::Side + } +} + +async fn should_connect_to( + world: &World, + block: &Block, + state: &BlockState, + direction: Option, +) -> bool { + if block == &Block::REDSTONE_WIRE { + return true; + } + if block == &Block::REPEATER { + let repeater_props = RepeaterLikeProperties::from_state_id(state.id, block); + let repeater_facing = repeater_props.facing.to_block_direction(); + return direction + .is_some_and(|dir| repeater_facing == dir || repeater_facing == dir.opposite()); + } + if block == &Block::OBSERVER { + let observer_props = ObserverLikeProperties::from_state_id(state.id, block); + let observer_facing = observer_props.facing; + return direction.is_some_and(|dir| dir.to_facing() == observer_facing); + } + if let Some(dir) = direction { + world + .block_registry + .emits_redstone_power(block, state, dir) + .await + } else { + false + } +} + +fn is_trapdoor(block: &Block) -> bool { + block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_TRAPDOORS) +} + +fn can_survive(world: &dyn BlockAccessor, pos: &BlockPos) -> bool { + let below = pos.down(); + let (below_block, below_state) = world.get_block_and_state(&below); + can_survive_on(below_block, below_state) +} + +fn can_survive_on(relative_block: &Block, relative_state: &BlockState) -> bool { + relative_state.is_side_solid(BlockDirection::Up) || relative_block == &Block::HOPPER +} + +#[must_use] +pub fn is_dot(wire: RedstoneWireProperties) -> bool { + !wire.north.to_wire_connection().is_connected() + && !wire.south.to_wire_connection().is_connected() + && !wire.east.to_wire_connection().is_connected() + && !wire.west.to_wire_connection().is_connected() +} + +#[must_use] +pub fn is_cross(wire: RedstoneWireProperties) -> bool { + wire.north.to_wire_connection().is_connected() + && wire.south.to_wire_connection().is_connected() + && wire.east.to_wire_connection().is_connected() + && wire.west.to_wire_connection().is_connected() } #[must_use] @@ -285,149 +600,33 @@ pub const fn make_cross(power: u8) -> RedstoneWireProperties { } } -async fn can_connect_to( - world: &World, - block: &Block, - side: BlockDirection, - state: &BlockState, -) -> bool { - if world - .block_registry - .emits_redstone_power(block, state, side) - .await - { - return true; - } - if block == &Block::REPEATER { - let repeater_props = RepeaterLikeProperties::from_state_id(state.id, block); - return repeater_props.facing.to_block_direction() == side - || repeater_props.facing.to_block_direction() == side.opposite(); - } else if block == &Block::OBSERVER { - let observer_props = ObserverLikeProperties::from_state_id(state.id, block); - return observer_props.facing == side.to_facing(); - } else if block == &Block::REDSTONE_WIRE { - return true; - } - false -} - -fn can_connect_diagonal_to(block: &Block) -> bool { - block == &Block::REDSTONE_WIRE -} - -pub async fn get_side(world: &World, pos: &BlockPos, side: BlockDirection) -> WireConnection { - let neighbor_pos: BlockPos = pos.offset(side.to_offset()); - let (neighbor, state) = world.get_block_and_state(&neighbor_pos); - - if can_connect_to(world, neighbor, side, state).await { - return WireConnection::Side; - } - - let up_pos = pos.offset(BlockDirection::Up.to_offset()); - let up_state = world.get_block_state(&up_pos); - - if !up_state.is_solid_block() - && state.is_side_solid(side.opposite()) - && can_connect_diagonal_to( - world.get_block(&neighbor_pos.offset(BlockDirection::Up.to_offset())), - ) - { - WireConnection::Up - } else if !state.is_solid_block() - && can_connect_diagonal_to( - world.get_block(&neighbor_pos.offset(BlockDirection::Down.to_offset())), - ) - { - WireConnection::Side - } else { - WireConnection::None - } -} - -async fn get_all_sides( - mut wire: RedstoneWireProperties, - world: &World, - pos: &BlockPos, -) -> RedstoneWireProperties { - wire.north = get_side(world, pos, BlockDirection::North).await.to_north(); - wire.south = get_side(world, pos, BlockDirection::South).await.to_south(); - wire.east = get_side(world, pos, BlockDirection::East).await.to_east(); - wire.west = get_side(world, pos, BlockDirection::West).await.to_west(); - wire -} - -#[must_use] -pub fn is_dot(wire: RedstoneWireProperties) -> bool { - wire.north == NorthRedstone::None - && wire.south == SouthRedstone::None - && wire.east == EastRedstone::None - && wire.west == WestRedstone::None -} - -#[must_use] -pub fn is_cross(wire: RedstoneWireProperties) -> bool { - wire.north == NorthRedstone::Side - && wire.south == SouthRedstone::Side - && wire.east == EastRedstone::Side - && wire.west == WestRedstone::Side -} - -pub async fn get_regulated_sides( +fn get_side_connection( wire: RedstoneWireProperties, - world: &World, - pos: &BlockPos, -) -> RedstoneWireProperties { - let mut state = get_all_sides(wire, world, pos).await; - if is_dot(wire) && is_dot(state) { - return state; + direction: HorizontalFacing, +) -> WireConnection { + match direction { + HorizontalFacing::North => wire.north.to_wire_connection(), + HorizontalFacing::South => wire.south.to_wire_connection(), + HorizontalFacing::East => wire.east.to_wire_connection(), + HorizontalFacing::West => wire.west.to_wire_connection(), } - let north_none = state.north == NorthRedstone::None; - let south_none = state.south == SouthRedstone::None; - let east_none = state.east == EastRedstone::None; - let west_none = state.west == WestRedstone::None; - let north_south_none = north_none && south_none; - let east_west_none = east_none && west_none; - if north_none && east_west_none { - state.north = NorthRedstone::Side; - } - if south_none && east_west_none { - state.south = SouthRedstone::Side; - } - if east_none && north_south_none { - state.east = EastRedstone::Side; - } - if west_none && north_south_none { - state.west = WestRedstone::Side; - } - state } -trait RedstoneWireLikePropertiesExt { - fn is_side_connected(&self, direction: HorizontalFacing) -> bool; - //fn get_connection_type(&self, direction: BlockDirection) -> WireConnection; +const fn set_side_connection( + wire: &mut RedstoneWireProperties, + direction: HorizontalFacing, + connection: WireConnection, +) { + match direction { + HorizontalFacing::North => wire.north = connection.to_north(), + HorizontalFacing::South => wire.south = connection.to_south(), + HorizontalFacing::East => wire.east = connection.to_east(), + HorizontalFacing::West => wire.west = connection.to_west(), + } } -impl RedstoneWireLikePropertiesExt for RedstoneWireLikeProperties { - fn is_side_connected(&self, direction: HorizontalFacing) -> bool { - match direction { - HorizontalFacing::North => self.north.to_wire_connection().is_connected(), - HorizontalFacing::South => self.south.to_wire_connection().is_connected(), - HorizontalFacing::East => self.east.to_wire_connection().is_connected(), - HorizontalFacing::West => self.west.to_wire_connection().is_connected(), - } - } - - /* - fn get_connection_type(&self, direction: BlockDirection) -> WireConnection { - match direction { - BlockDirection::North => self.north.to_wire_connection(), - BlockDirection::South => self.south.to_wire_connection(), - BlockDirection::East => self.east.to_wire_connection(), - BlockDirection::West => self.west.to_wire_connection(), - _ => WireConnection::None, - } - } - */ +fn is_side_connected_prop(wire: RedstoneWireProperties, direction: HorizontalFacing) -> bool { + get_side_connection(wire, direction).is_connected() } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -438,15 +637,13 @@ pub enum WireConnection { } impl WireConnection { - fn is_connected(self) -> bool { - self != Self::None + #[must_use] + pub const fn is_connected(self) -> bool { + !matches!(self, Self::None) } - fn is_none(self) -> bool { - self == Self::None - } - - const fn to_north(self) -> NorthRedstone { + #[must_use] + pub const fn to_north(self) -> NorthRedstone { match self { Self::Up => NorthRedstone::Up, Self::Side => NorthRedstone::Side, @@ -454,7 +651,8 @@ impl WireConnection { } } - const fn to_south(self) -> SouthRedstone { + #[must_use] + pub const fn to_south(self) -> SouthRedstone { match self { Self::Up => SouthRedstone::Up, Self::Side => SouthRedstone::Side, @@ -462,7 +660,8 @@ impl WireConnection { } } - const fn to_east(self) -> EastRedstone { + #[must_use] + pub const fn to_east(self) -> EastRedstone { match self { Self::Up => EastRedstone::Up, Self::Side => EastRedstone::Side, @@ -470,7 +669,8 @@ impl WireConnection { } } - const fn to_west(self) -> WestRedstone { + #[must_use] + pub const fn to_west(self) -> WestRedstone { match self { Self::Up => WestRedstone::Up, Self::Side => WestRedstone::Side, @@ -478,7 +678,8 @@ impl WireConnection { } } } -trait CardinalWireConnectionExt { + +pub trait CardinalWireConnectionExt { fn to_wire_connection(&self) -> WireConnection; } @@ -521,49 +722,3 @@ impl CardinalWireConnectionExt for WestRedstone { } } } - -fn max_wire_power(wire_power: u8, world: &World, pos: BlockPos) -> u8 { - let (block, block_state) = world.get_block_and_state(&pos); - if block == &Block::REDSTONE_WIRE { - let wire = RedstoneWireProperties::from_state_id(block_state.id, block); - wire_power.max(wire.power) - } else { - wire_power - } -} - -async fn calculate_power(world: &World, pos: &BlockPos) -> u8 { - let mut block_power: u8 = 0; - let mut wire_power: u8 = 0; - - let up_pos = pos.offset(BlockDirection::Up.to_offset()); - let up_state = world.get_block_state(&up_pos); - - for side in BlockDirection::all() { - let neighbor_pos = pos.offset(side.to_offset()); - wire_power = max_wire_power(wire_power, world, neighbor_pos); - let (neighbor, neighbor_state) = world.get_block_and_state(&neighbor_pos); - block_power = block_power.max( - get_redstone_power_no_dust(neighbor, neighbor_state, world, neighbor_pos, side).await, - ); - if side.is_horizontal() { - if !up_state.is_solid_block() && neighbor_state.is_solid_block() { - wire_power = max_wire_power( - wire_power, - world, - neighbor_pos.offset(BlockDirection::Up.to_offset()), - ); - } - - if !neighbor_state.is_solid_block() { - wire_power = max_wire_power( - wire_power, - world, - neighbor_pos.offset(BlockDirection::Down.to_offset()), - ); - } - } - } - - block_power.max(wire_power.saturating_sub(1)) -} diff --git a/crates/pumpkin/src/block/blocks/redstone/turbo.rs b/crates/pumpkin/src/block/blocks/redstone/turbo.rs deleted file mode 100644 index a4f6b2077..000000000 --- a/crates/pumpkin/src/block/blocks/redstone/turbo.rs +++ /dev/null @@ -1,435 +0,0 @@ -//! The implementation of "Redstone Wire Turbo" was largely based on -//! the accelerator created by theosib. For more information, see: -//! . - -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>, - 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, - node_cache: FxHashMap, - update_queue: Vec>, - 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, 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) { - 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 ¤t_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, 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, - 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 - } - } -} diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index 5621e01f8..9e3aeacbf 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -2301,7 +2301,12 @@ impl World { 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); 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( 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 chunk_pos = Vector2::new(info.spawn_x >> 4, info.spawn_z >> 4); 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( f64::from(info.spawn_x) + 0.5,