fix: ci
This commit is contained in:
parent
1abbc7bfec
commit
edb848d608
@ -1 +1 @@
|
||||
Subproject commit b44607b19fa796edb28562382a1f4f7f788a4504
|
||||
Subproject commit 2337e6f389293a6371667d495feace8e35570318
|
||||
@ -546,6 +546,10 @@ impl<S: SingleChunkDataSerializer + 'static> ChunkSerializer for AnvilChunkFile<
|
||||
fn read(r: Bytes) -> Result<Self, ChunkReadingError> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -94,15 +94,26 @@ fn extract_u16_array(tag: &pumpkin_nbt::tag::NbtTag) -> Option<Box<[BlockStateId
|
||||
pumpkin_nbt::tag::NbtTag::List(list) => {
|
||||
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<Box<[u8]>> {
|
||||
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<NbtTag> = 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<NbtTag> = 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,6 +101,13 @@ impl<S: ChunkSerializer<WriteBackend = PathBuf> + '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)))??;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
if node.in_degree != 0 {
|
||||
node.in_queue = false;
|
||||
if node.in_degree != 0 {
|
||||
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());
|
||||
|
||||
@ -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<rayon::ThreadPool>>,
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
@ -148,17 +148,31 @@ impl Level {
|
||||
dimension: Dimension,
|
||||
gen_pool: Option<Arc<rayon::ThreadPool>>,
|
||||
) -> Arc<Self> {
|
||||
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
|
||||
{
|
||||
root_folder.clone()
|
||||
} 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)
|
||||
|
||||
// 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()
|
||||
} 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 {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,12 +243,27 @@ 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(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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<WorldGenSettings> {
|
||||
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<WorldGenSettings>
|
||||
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()))
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
/**
|
||||
* This implementation is heavily based on <https://github.com/MCHPR/MCHPRS>
|
||||
* 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<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(
|
||||
block: &Block,
|
||||
state: &BlockState,
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,82 +69,74 @@ 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;
|
||||
|
||||
let new_side: WireConnection = match args.direction {
|
||||
BlockDirection::Up => {
|
||||
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;
|
||||
}
|
||||
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 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 {
|
||||
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(
|
||||
&up_block_pos,
|
||||
&down_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 {
|
||||
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(
|
||||
&down_block_pos,
|
||||
&up_pos,
|
||||
direction.opposite().to_block_direction(),
|
||||
args.flags,
|
||||
)
|
||||
@ -134,23 +144,6 @@ impl BlockBehaviour for RedstoneWireBlock {
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
|
||||
if is_cross(wire) || is_dot(wire) {
|
||||
let mut new_wire = if is_cross(wire) {
|
||||
RedstoneWireProperties::default(&Block::REDSTONE_WIRE)
|
||||
} else {
|
||||
BlockActionResult::Pass
|
||||
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 {
|
||||
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<World>, 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))
|
||||
}
|
||||
|
||||
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<World>, 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);
|
||||
|
||||
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(),
|
||||
)
|
||||
.set_block_state(pos, new_state_id, BlockFlags::empty())
|
||||
.await;
|
||||
update_wire_neighbors(world, block_pos).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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<World>, 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<World>, 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<World>, 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<BlockDirection>,
|
||||
) -> 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))
|
||||
}
|
||||
|
||||
@ -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 ¤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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user