From 00b0e51ce45372e4e0b303bbf7f4ee1f9b38019e Mon Sep 17 00:00:00 2001 From: ReCore Date: Sat, 14 Mar 2026 01:53:20 +1030 Subject: [PATCH 1/5] logs --- src/world/block-placing/src/blocks/logs.rs | 66 ++++++++++++++++++++++ src/world/block-placing/src/blocks/mod.rs | 1 + src/world/block-placing/src/lib.rs | 12 ++++ 3 files changed, 79 insertions(+) create mode 100644 src/world/block-placing/src/blocks/logs.rs diff --git a/src/world/block-placing/src/blocks/logs.rs b/src/world/block-placing/src/blocks/logs.rs new file mode 100644 index 00000000..cc41956d --- /dev/null +++ b/src/world/block-placing/src/blocks/logs.rs @@ -0,0 +1,66 @@ +use std::collections::BTreeMap; +use temper_core::block_data::BlockData; +use crate::errors::BlockPlaceError; +use crate::{BlockFace, BlockPlaceContext, PlacableBlock, PlacedBlocks}; +use temper_macros::item; +use temper_state::GlobalState; + +pub(crate) struct PlacableLog; + +impl PlacableBlock for PlacableLog { + fn place( + context: BlockPlaceContext, + state: GlobalState, + ) -> Result { + let axis = match context.face_clicked { + BlockFace::Top | BlockFace::Bottom => "y", + BlockFace::North | BlockFace::South => "z", + BlockFace::West | BlockFace::East => "x", + }; + + let block_name = match context.item_used { + item!("oak_log") => "minecraft:oak_log", + item!("spruce_log") => "minecraft:spruce_log", + item!("birch_log") => "minecraft:birch_log", + item!("jungle_log") => "minecraft:jungle_log", + item!("acacia_log") => "minecraft:acacia_log", + item!("dark_oak_log") => "minecraft:dark_oak_log", + item!("mangrove_log") => "minecraft:mangrove_log", + item!("cherry_log") => "minecraft:cherry_log", + item!("pale_oak_log") => "minecraft:pale_oak_log", + item!("crimson_stem") => "minecraft:crimson_stem", + item!("warped_stem") => "minecraft:warped_stem", + + item!("stripped_oak_log") => "minecraft:stripped_oak_log", + item!("stripped_spruce_log") => "minecraft:stripped_spruce_log", + item!("stripped_birch_log") => "minecraft:stripped_birch_log", + item!("stripped_jungle_log") => "minecraft:stripped_jungle_log", + item!("stripped_acacia_log") => "minecraft:stripped_acacia_log", + item!("stripped_dark_oak_log") => "minecraft:stripped_dark_oak_log", + item!("stripped_mangrove_log") => "minecraft:stripped_mangrove_log", + item!("stripped_cherry_log") => "minecraft:stripped_cherry_log", + item!("stripped_pale_oak_log") => "minecraft:stripped_pale_oak_log", + item!("stripped_crimson_stem") => "minecraft:stripped_crimson_stem", + item!("stripped_warped_stem") => "minecraft:stripped_warped_stem", + _ => return Err(BlockPlaceError::ItemNotMappedToBlock(context.item_used)), + }; + + let block_data = BlockData { + name: block_name.to_string(), + properties: Some(BTreeMap::from([("axis".to_string(), axis.to_string())])), + }; + + let Some(block_id) = block_data.try_to_block_state_id() else { + return Err(BlockPlaceError::BlockNotMappedToBlockStateId(block_data)); + }; + + state.world.get_or_generate_mut(context.block_position.chunk(), temper_core::dimension::Dimension::Overworld) + .expect("Could not load chunk") + .set_block(context.block_position.chunk_block_pos(), block_id); + + Ok(PlacedBlocks { + blocks: std::collections::HashMap::from([(context.block_position, block_id)]), + take_item: true, + }) + } +} diff --git a/src/world/block-placing/src/blocks/mod.rs b/src/world/block-placing/src/blocks/mod.rs index 558c9640..b73dca64 100644 --- a/src/world/block-placing/src/blocks/mod.rs +++ b/src/world/block-placing/src/blocks/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod door; +pub(crate) mod logs; pub(super) mod torch; diff --git a/src/world/block-placing/src/lib.rs b/src/world/block-placing/src/lib.rs index 9e7fa3ae..98242954 100644 --- a/src/world/block-placing/src/lib.rs +++ b/src/world/block-placing/src/lib.rs @@ -58,6 +58,18 @@ pub fn place_item( | item!("acacia_door") | item!("dark_oak_door") => blocks::door::PlaceableDoor::place(context, state), + item!("oak_log") | item!("stripped_oak_log") + | item!("spruce_log") | item!("stripped_spruce_log") + | item!("birch_log") | item!("stripped_birch_log") + | item!("jungle_log") | item!("stripped_jungle_log") + | item!("acacia_log") | item!("stripped_acacia_log") + | item!("dark_oak_log") | item!("stripped_dark_oak_log") + | item!("mangrove_log") | item!("stripped_mangrove_log") + | item!("cherry_log") | item!("stripped_cherry_log") + | item!("pale_oak_log") | item!("stripped_pale_oak_log") + | item!("crimson_stem") | item!("stripped_crimson_stem") + | item!("warped_stem") | item!("stripped_warped_stem") => blocks::logs::PlacableLog::place(context, state), + unhandled => { let block_opt = ITEM_TO_BLOCK_MAPPING.get(&unhandled.0.0); if let Some(block) = block_opt { From c973d997e3e234411f1e49effea1def254c8d2a9 Mon Sep 17 00:00:00 2001 From: ReCore Date: Sat, 14 Mar 2026 03:08:42 +1030 Subject: [PATCH 2/5] slabs --- src/app/runtime/src/lib.rs | 3 + src/app/runtime/src/setup.rs | 6 + src/core/src/block_state_id.rs | 82 +++-- .../interactions/src/block_interactions.rs | 12 +- .../src/packets/src/place_block.rs | 12 +- src/world/block-placing/src/blocks/door.rs | 43 ++- src/world/block-placing/src/blocks/logs.rs | 63 ++-- src/world/block-placing/src/blocks/mod.rs | 1 + src/world/block-placing/src/blocks/slab.rs | 314 ++++++++++++++++++ src/world/block-placing/src/blocks/torch.rs | 11 + src/world/block-placing/src/errors.rs | 2 + src/world/block-placing/src/lib.rs | 70 ++-- 12 files changed, 487 insertions(+), 132 deletions(-) create mode 100644 src/world/block-placing/src/blocks/slab.rs diff --git a/src/app/runtime/src/lib.rs b/src/app/runtime/src/lib.rs index 33cf8454..615bd2f7 100644 --- a/src/app/runtime/src/lib.rs +++ b/src/app/runtime/src/lib.rs @@ -1,6 +1,7 @@ mod setup; use crate::errors::BinaryError; +use crate::setup::setup_block_and_item_mapping; use std::sync::Arc; use std::time::Instant; use temper_config::whitelist::create_whitelist; @@ -39,6 +40,8 @@ pub fn entry(start_time: Instant, no_tui: bool) -> Result<(), BinaryError> { #[cfg(feature = "dashboard")] temper_dashboard::start_dashboard(global_state.clone()); + setup_block_and_item_mapping(); + game_loop::start_game_loop(global_state.clone(), no_tui)?; if !no_tui { diff --git a/src/app/runtime/src/setup.rs b/src/app/runtime/src/setup.rs index d4bc0c7e..20d1fdc6 100644 --- a/src/app/runtime/src/setup.rs +++ b/src/app/runtime/src/setup.rs @@ -4,6 +4,7 @@ use crate::errors::BinaryError; use std::time::Instant; use temper_components::player::offline_player_data::OfflinePlayerData; use temper_config::server_config::get_global_config; +use temper_core::block_state_id::{init_block_mappings, init_item_to_block_mapping}; use temper_core::dimension::Dimension; use temper_core::pos::ChunkPos; use temper_state::GlobalState; @@ -72,3 +73,8 @@ pub fn setup_db(state: GlobalState) -> Result<(), BinaryError> { info!("Database setup complete."); Ok(()) } + +pub fn setup_block_and_item_mapping() { + init_item_to_block_mapping(); + init_block_mappings(); +} diff --git a/src/core/src/block_state_id.rs b/src/core/src/block_state_id.rs index 57e86388..72f4ff3f 100644 --- a/src/core/src/block_state_id.rs +++ b/src/core/src/block_state_id.rs @@ -2,8 +2,7 @@ use crate::block_data::BlockData; use ahash::RandomState; use bitcode_derive::{Decode, Encode}; use deepsize::DeepSizeOf; -use lazy_static::lazy_static; -use once_cell::sync::Lazy; +use once_cell::sync::OnceCell; use std::collections::HashMap; use std::fmt::Display; use std::process::exit; @@ -18,31 +17,36 @@ const BLOCK_ENTRIES: usize = 27914; const BLOCKSFILE: &str = include_str!("../../../assets/data/blockstates.json"); -lazy_static! { - pub static ref ID2BLOCK: Vec = { - let string_keys: HashMap = - serde_json::from_str(BLOCKSFILE).unwrap(); - if string_keys.len() != BLOCK_ENTRIES { - // Edit this number if the block mappings file changes - error!("Block mappings file is not the correct length"); - error!("Expected {} entries, found {}", BLOCK_ENTRIES, string_keys.len()); - exit(1); - } - let mut id2block = Vec::with_capacity(BLOCK_ENTRIES); - for _ in 0..BLOCK_ENTRIES { - id2block.push(BlockData::default()); - } - string_keys - .iter() - .map(|(k, v)| (k.parse::().unwrap(), v.clone())) - .for_each(|(k, v)| id2block[k as usize] = v); - id2block - }; - pub static ref BLOCK2ID: HashMap = ID2BLOCK +pub static ID2BLOCK: OnceCell> = OnceCell::new(); +pub static BLOCK2ID: OnceCell> = OnceCell::new(); + +pub fn init_block_mappings() { + let string_keys: HashMap = + serde_json::from_str(BLOCKSFILE).unwrap(); + if string_keys.len() != BLOCK_ENTRIES { + error!("Block mappings file is not the correct length"); + error!( + "Expected {} entries, found {}", + BLOCK_ENTRIES, + string_keys.len() + ); + exit(1); + } + let mut id2block = Vec::with_capacity(BLOCK_ENTRIES); + for _ in 0..BLOCK_ENTRIES { + id2block.push(BlockData::default()); + } + string_keys + .iter() + .map(|(k, v)| (k.parse::().unwrap(), v.clone())) + .for_each(|(k, v)| id2block[k as usize] = v); + let block2id: HashMap = id2block .iter() .enumerate() .map(|(k, v)| (v.clone(), k as i32)) .collect(); + ID2BLOCK.set(id2block).expect("Failed to set ID2BLOCK"); + BLOCK2ID.set(block2id).expect("Failed to set BLOCK2ID"); } /// An ID for a block, and it's state in the world. Use this over `BlockData` unless you need to @@ -62,17 +66,26 @@ impl BlockStateId { /// Given a BlockData, return a BlockStateId. Does not clone, should be quite fast. pub fn from_block_data(block_data: &BlockData) -> Self { - let id = BLOCK2ID.get(block_data).copied().unwrap_or_else(|| { - warn!("Block data '{block_data}' not found in block mappings file"); - 0 - }); + let id = BLOCK2ID + .get() + .expect("Mappings not initialized") + .get(block_data) + .copied() + .unwrap_or_else(|| { + warn!("Block data '{block_data}' not found in block mappings file"); + 0 + }); BlockStateId(id as u32) } /// Given a block state ID, return a BlockData. Will clone, so don't use in hot loops. /// If the ID is not found, returns None. pub fn to_block_data(&self) -> Option { - ID2BLOCK.get(self.0 as usize).cloned() + ID2BLOCK + .get() + .expect("Mappings not initialized") + .get(self.0 as usize) + .cloned() } pub fn from_varint(var_int: VarInt) -> Self { @@ -154,10 +167,12 @@ impl Default for BlockStateId { const ITEM_TO_BLOCK_MAPPING_FILE: &str = include_str!("../../../assets/data/item_to_block_mapping.json"); -pub static ITEM_TO_BLOCK_MAPPING: Lazy> = Lazy::new(|| { +pub static ITEM_TO_BLOCK_MAPPING: OnceCell> = OnceCell::new(); + +pub fn init_item_to_block_mapping() { let str_form: HashMap = serde_json::from_str(ITEM_TO_BLOCK_MAPPING_FILE) .expect("Failed to parse item_to_block_mapping.json"); - str_form + let res = str_form .into_iter() .map(|(k, v)| { ( @@ -165,5 +180,8 @@ pub static ITEM_TO_BLOCK_MAPPING: Lazy> = Lazy::new(| BlockStateId::new(u32::from_str(&v).unwrap()), ) }) - .collect() -}); + .collect(); + ITEM_TO_BLOCK_MAPPING + .set(res) + .expect("Failed to set ITEM_TO_BLOCK_MAPPING, it was already set"); +} diff --git a/src/game_systems/src/interactions/src/block_interactions.rs b/src/game_systems/src/interactions/src/block_interactions.rs index ddaa8a62..a85b9583 100644 --- a/src/game_systems/src/interactions/src/block_interactions.rs +++ b/src/game_systems/src/interactions/src/block_interactions.rs @@ -161,7 +161,9 @@ pub fn is_interactive(block_state_id: BlockStateId) -> bool { mod tests { use super::*; use std::collections::BTreeMap; - use temper_core::block_state_id::BlockStateId; + use temper_core::block_state_id::{ + init_block_mappings, init_item_to_block_mapping, BlockStateId, + }; use temper_macros::block; #[test] @@ -184,6 +186,8 @@ mod tests { #[test] fn test_try_interact_opens_door() { + init_item_to_block_mapping(); + init_block_mappings(); // A closed oak door (lower half, north-facing, left hinge, unpowered) let closed_door: BlockStateId = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); @@ -201,6 +205,8 @@ mod tests { #[test] fn test_try_interact_closes_door() { + init_item_to_block_mapping(); + init_block_mappings(); // An already-open oak door let open_door: BlockStateId = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: true, powered: false }); @@ -221,6 +227,8 @@ mod tests { #[test] fn test_try_interact_not_interactive() { + init_item_to_block_mapping(); + init_block_mappings(); let stone: BlockStateId = block!("stone"); assert!(matches!( try_interact(stone), @@ -230,6 +238,8 @@ mod tests { #[test] fn test_is_interactive() { + init_item_to_block_mapping(); + init_block_mappings(); let door: BlockStateId = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); let stone: BlockStateId = block!("stone"); diff --git a/src/game_systems/src/packets/src/place_block.rs b/src/game_systems/src/packets/src/place_block.rs index bf82d59a..b0de02d8 100644 --- a/src/game_systems/src/packets/src/place_block.rs +++ b/src/game_systems/src/packets/src/place_block.rs @@ -8,9 +8,9 @@ use temper_core::pos::BlockPos; use temper_messages::BlockInteractMessage; use temper_net_runtime::connection::StreamWriter; -use temper_protocol::PlaceBlockReceiver; use temper_protocol::outgoing::block_change_ack::BlockChangeAck; use temper_protocol::outgoing::block_update::BlockUpdate; +use temper_protocol::PlaceBlockReceiver; use temper_state::GlobalStateResource; use tracing::{debug, error, trace}; @@ -168,16 +168,6 @@ pub fn handle( chunk.get_block(offset_pos.chunk_block_pos()) }; - if !(match_block!("water", block_at_pos) - || match_block!("lava", block_at_pos) - || match_block!("air", block_at_pos)) - { - debug!( - "Block placement failed because the block at the target position is not replaceable" - ); - continue 'ev_loop; - } - let placed_blocks = block_placing::place_item( state.0.clone(), block_placing::BlockPlaceContext { diff --git a/src/world/block-placing/src/blocks/door.rs b/src/world/block-placing/src/blocks/door.rs index 3074d90e..ae185102 100644 --- a/src/world/block-placing/src/blocks/door.rs +++ b/src/world/block-placing/src/blocks/door.rs @@ -16,15 +16,22 @@ impl PlacableBlock for PlaceableDoor { context: BlockPlaceContext, state: GlobalState, ) -> Result { - let name = match context.item_used { - item!("oak_door") => "minecraft:oak_door", - item!("birch_door") => "minecraft:birch_door", - item!("spruce_door") => "minecraft:spruce_door", - item!("jungle_door") => "minecraft:jungle_door", - item!("acacia_door") => "minecraft:acacia_door", - item!("dark_oak_door") => "minecraft:dark_oak_door", - _ => return Err(BlockPlaceError::ItemNotMappedToBlock(context.item_used)), + let name = match context.item_used.to_name() { + Some(name) => name, + None => return Err(BlockPlaceError::ItemIdHasNoNameMapping(context.item_used)), }; + + let target_block = { + let chunk = state + .world + .get_or_generate_chunk(context.block_position.chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.get_block(context.block_position.chunk_block_pos()) + }; + if !match_block!("air", target_block) && !match_block!("cave_air", target_block) { + return Err(BlockPlaceError::TargetBlockNotEmpty(context.block_position)); + } + let block_above = { let chunk = state .world @@ -33,10 +40,9 @@ impl PlacableBlock for PlaceableDoor { chunk.get_block((context.block_position.pos + IVec3::new(0, 1, 0)).into()) }; if !(match_block!("air", block_above) || match_block!("cave_air", block_above)) { - return Ok(PlacedBlocks { - blocks: std::collections::HashMap::new(), - take_item: false, - }); + return Err(BlockPlaceError::TargetBlockNotEmpty( + context.block_position + IVec3::new(0, 1, 0).into(), + )); }; let facing = match context.face_clicked { BlockFace::North => "south", @@ -121,12 +127,15 @@ mod test { use super::*; use crate::BlockPlaceContext; use temper_components::player::rotation::Rotation; + use temper_core::block_state_id::{init_block_mappings, init_item_to_block_mapping}; use temper_core::dimension::Dimension; use temper_core::pos::BlockPos; use temper_macros::block; #[test] fn test_place_door() { + init_item_to_block_mapping(); + init_block_mappings(); let (state, _) = temper_state::create_test_state(); let context = BlockPlaceContext { block_clicked: Default::default(), @@ -174,6 +183,8 @@ mod test { #[test] fn test_place_door_with_block_above() { + init_item_to_block_mapping(); + init_block_mappings(); let (state, _) = temper_state::create_test_state(); // Place a block above the door position { @@ -197,16 +208,16 @@ mod test { player_position: Default::default(), }; let result = PlaceableDoor::place(context, state.0.clone()); - assert!(result.is_ok()); - let placed_blocks = result.unwrap(); assert!( - placed_blocks.blocks.is_empty(), - "Door should not be placed when there is a block above" + result.is_err(), + "Placing a door with a block above should return an error" ); } #[test] fn test_place_door_on_invalid_face() { + init_item_to_block_mapping(); + init_block_mappings(); let (state, _) = temper_state::create_test_state(); let context = BlockPlaceContext { block_clicked: Default::default(), diff --git a/src/world/block-placing/src/blocks/logs.rs b/src/world/block-placing/src/blocks/logs.rs index cc41956d..c7b8cbb1 100644 --- a/src/world/block-placing/src/blocks/logs.rs +++ b/src/world/block-placing/src/blocks/logs.rs @@ -1,8 +1,9 @@ -use std::collections::BTreeMap; -use temper_core::block_data::BlockData; use crate::errors::BlockPlaceError; +use crate::BlockStateId; use crate::{BlockFace, BlockPlaceContext, PlacableBlock, PlacedBlocks}; -use temper_macros::item; +use std::collections::BTreeMap; +use temper_core::block_data::BlockData; +use temper_macros::block; use temper_state::GlobalState; pub(crate) struct PlacableLog; @@ -12,52 +13,48 @@ impl PlacableBlock for PlacableLog { context: BlockPlaceContext, state: GlobalState, ) -> Result { + let target_block = { + let chunk = state + .world + .get_or_generate_mut( + context.block_position.chunk(), + temper_core::dimension::Dimension::Overworld, + ) + .expect("Could not load chunk"); + chunk.get_block(context.block_position.chunk_block_pos()) + }; + if target_block != block!("air") && target_block != block!("cave_air") { + return Err(BlockPlaceError::TargetBlockNotEmpty(context.block_position)); + } let axis = match context.face_clicked { BlockFace::Top | BlockFace::Bottom => "y", BlockFace::North | BlockFace::South => "z", BlockFace::West | BlockFace::East => "x", }; - let block_name = match context.item_used { - item!("oak_log") => "minecraft:oak_log", - item!("spruce_log") => "minecraft:spruce_log", - item!("birch_log") => "minecraft:birch_log", - item!("jungle_log") => "minecraft:jungle_log", - item!("acacia_log") => "minecraft:acacia_log", - item!("dark_oak_log") => "minecraft:dark_oak_log", - item!("mangrove_log") => "minecraft:mangrove_log", - item!("cherry_log") => "minecraft:cherry_log", - item!("pale_oak_log") => "minecraft:pale_oak_log", - item!("crimson_stem") => "minecraft:crimson_stem", - item!("warped_stem") => "minecraft:warped_stem", - - item!("stripped_oak_log") => "minecraft:stripped_oak_log", - item!("stripped_spruce_log") => "minecraft:stripped_spruce_log", - item!("stripped_birch_log") => "minecraft:stripped_birch_log", - item!("stripped_jungle_log") => "minecraft:stripped_jungle_log", - item!("stripped_acacia_log") => "minecraft:stripped_acacia_log", - item!("stripped_dark_oak_log") => "minecraft:stripped_dark_oak_log", - item!("stripped_mangrove_log") => "minecraft:stripped_mangrove_log", - item!("stripped_cherry_log") => "minecraft:stripped_cherry_log", - item!("stripped_pale_oak_log") => "minecraft:stripped_pale_oak_log", - item!("stripped_crimson_stem") => "minecraft:stripped_crimson_stem", - item!("stripped_warped_stem") => "minecraft:stripped_warped_stem", - _ => return Err(BlockPlaceError::ItemNotMappedToBlock(context.item_used)), + let block_name = match context.item_used.to_name() { + Some(name) => name, + None => return Err(BlockPlaceError::ItemIdHasNoNameMapping(context.item_used)), }; - + let block_data = BlockData { - name: block_name.to_string(), + name: block_name, properties: Some(BTreeMap::from([("axis".to_string(), axis.to_string())])), }; - + let Some(block_id) = block_data.try_to_block_state_id() else { return Err(BlockPlaceError::BlockNotMappedToBlockStateId(block_data)); }; - state.world.get_or_generate_mut(context.block_position.chunk(), temper_core::dimension::Dimension::Overworld) + state + .world + .get_or_generate_mut( + context.block_position.chunk(), + temper_core::dimension::Dimension::Overworld, + ) .expect("Could not load chunk") .set_block(context.block_position.chunk_block_pos(), block_id); - + Ok(PlacedBlocks { blocks: std::collections::HashMap::from([(context.block_position, block_id)]), take_item: true, diff --git a/src/world/block-placing/src/blocks/mod.rs b/src/world/block-placing/src/blocks/mod.rs index b73dca64..342cd99b 100644 --- a/src/world/block-placing/src/blocks/mod.rs +++ b/src/world/block-placing/src/blocks/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod door; pub(crate) mod logs; +pub mod slab; pub(super) mod torch; diff --git a/src/world/block-placing/src/blocks/slab.rs b/src/world/block-placing/src/blocks/slab.rs new file mode 100644 index 00000000..6c70ac99 --- /dev/null +++ b/src/world/block-placing/src/blocks/slab.rs @@ -0,0 +1,314 @@ +use crate::errors::BlockPlaceError; +use crate::{BlockPlaceContext, PlacableBlock, PlacedBlocks}; +use bevy_math::DVec3; +use temper_state::GlobalState; + +pub(crate) struct PlaceableSlab; + +impl PlacableBlock for PlaceableSlab { + fn place( + context: BlockPlaceContext, + state: GlobalState, + ) -> Result { + fn get_block_data_at( + pos: &temper_core::pos::BlockPos, + state: &GlobalState, + ) -> temper_core::block_data::BlockData { + let chunk = state + .world + .get_or_generate_chunk(pos.chunk(), temper_core::dimension::Dimension::Overworld) + .expect("Could not load chunk"); + let block = chunk.get_block(pos.chunk_block_pos()); + temper_core::block_data::BlockData::from_block_state_id(block) + } + + fn is_same_slab_block(data: &temper_core::block_data::BlockData, block_name: &str) -> bool { + data.name == block_name + && data + .properties + .as_ref() + .and_then(|p| p.get("type")) + .map(|t| t != "double") + .unwrap_or(false) + } + + fn get_clicked_pos( + block_position: &temper_core::pos::BlockPos, + face: &crate::BlockFace, + ) -> temper_core::pos::BlockPos { + match face { + crate::BlockFace::Top => temper_core::pos::BlockPos::of( + block_position.pos.x, + block_position.pos.y - 1, + block_position.pos.z, + ), + crate::BlockFace::Bottom => temper_core::pos::BlockPos::of( + block_position.pos.x, + block_position.pos.y + 1, + block_position.pos.z, + ), + crate::BlockFace::North => temper_core::pos::BlockPos::of( + block_position.pos.x, + block_position.pos.y, + block_position.pos.z + 1, + ), + crate::BlockFace::South => temper_core::pos::BlockPos::of( + block_position.pos.x, + block_position.pos.y, + block_position.pos.z - 1, + ), + crate::BlockFace::East => temper_core::pos::BlockPos::of( + block_position.pos.x - 1, + block_position.pos.y, + block_position.pos.z, + ), + crate::BlockFace::West => temper_core::pos::BlockPos::of( + block_position.pos.x + 1, + block_position.pos.y, + block_position.pos.z, + ), + } + } + + fn get_half( + face: &crate::BlockFace, + click_position: &DVec3, + block_position: &temper_core::pos::BlockPos, + ) -> &'static str { + match face { + crate::BlockFace::Top => "bottom", + crate::BlockFace::Bottom => "top", + _ => { + if click_position.y - block_position.pos.y as f64 > 0.5 { + "top" + } else { + "bottom" + } + } + } + } + + let block_name = match context.item_used.to_name() { + Some(name) => name, + None => return Err(BlockPlaceError::ItemIdHasNoNameMapping(context.item_used)), + }; + + let clicked_pos = get_clicked_pos(&context.block_position, &context.face_clicked); + + let clicked_block_data = get_block_data_at(&clicked_pos, &state); + + let should_combine = is_same_slab_block(&clicked_block_data, &block_name); + + let (place_position, existing_block_data) = if should_combine { + (clicked_pos, clicked_block_data) + } else { + let existing_block_data = get_block_data_at(&context.block_position, &state); + (context.block_position, existing_block_data) + }; + + let is_same_slab = is_same_slab_block(&existing_block_data, &block_name); + + let block_data = if is_same_slab { + temper_core::block_data::BlockData { + name: block_name.to_string(), + properties: Some(std::collections::BTreeMap::::from([ + ("type".to_string(), "double".to_string()), + ("waterlogged".to_string(), "false".to_string()), + ])), + } + } else if existing_block_data.name == "minecraft:air" { + let half = get_half( + &context.face_clicked, + &context.click_position, + &context.block_position, + ); + + temper_core::block_data::BlockData { + name: block_name.to_string(), + properties: Some(std::collections::BTreeMap::::from([ + ("type".to_string(), half.to_string()), + ("waterlogged".to_string(), "false".to_string()), + ])), + } + } else { + // Cancel placement if the location is occupied by a block other than air or a combinable slab, or if it's already a double slab + return Ok(PlacedBlocks { + blocks: std::collections::HashMap::new(), + take_item: false, + }); + }; + + let Some(block_id) = block_data.try_to_block_state_id() else { + return Err(BlockPlaceError::BlockNotMappedToBlockStateId(block_data)); + }; + + state + .world + .get_or_generate_mut( + place_position.chunk(), + temper_core::dimension::Dimension::Overworld, + ) + .expect("Could not load chunk") + .set_block(place_position.chunk_block_pos(), block_id); + + Ok(PlacedBlocks { + blocks: std::collections::HashMap::from([(place_position, block_id)]), + take_item: true, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BlockStateId; + use std::collections::BTreeMap; + use temper_components::player::rotation::Rotation; + use temper_core::block_data::BlockData; + use temper_core::block_state_id::{init_block_mappings, init_item_to_block_mapping}; + use temper_core::dimension::Dimension; + use temper_core::pos::BlockPos; + use temper_macros::{block, item}; + + fn slab_block_id(name: &str, slab_type: &str) -> BlockStateId { + let bd = BlockData { + name: name.to_string(), + properties: Some(BTreeMap::from([ + ("type".to_string(), slab_type.to_string()), + ("waterlogged".to_string(), "false".to_string()), + ])), + }; + bd.try_to_block_state_id() + .expect("slab block id should exist") + } + + #[test] + fn test_combine_slab_into_double() { + init_item_to_block_mapping(); + init_block_mappings(); + let (state, _tmp) = temper_state::create_test_state(); + + // Place an oak bottom slab at (0,64,0) + let bottom_id = slab_block_id("minecraft:oak_slab", "bottom"); + { + let mut chunk = state + .0 + .world + .get_or_generate_mut(BlockPos::of(0, 64, 0).chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.set_block(BlockPos::of(0, 64, 0).chunk_block_pos(), bottom_id); + } + + // Now simulate placing an oak slab by clicking the top face of the block above (so clicked_pos points to 0,64,0) + let context = BlockPlaceContext { + block_clicked: Default::default(), + block_position: BlockPos::of(0, 65, 0), + face_clicked: crate::BlockFace::Top, + click_position: Default::default(), + item_used: item!("oak_slab"), + player_rotation: Rotation { + yaw: 0.0, + pitch: 0.0, + }, + player_position: Default::default(), + }; + + let result = PlaceableSlab::place(context, state.0.clone()); + assert!(result.is_ok()); + let placed = result.unwrap(); + + // Expect the bottom slab to have been converted to a double at (0,64,0) + let double_id = slab_block_id("minecraft:oak_slab", "double"); + assert_eq!(placed.blocks.len(), 1); + assert_eq!( + placed.blocks.get(&BlockPos::of(0, 64, 0)).copied(), + Some(double_id) + ); + + // And world should reflect the double slab + let chunk = state + .0 + .world + .get_or_generate_chunk(BlockPos::of(0, 64, 0).chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + assert_eq!( + chunk.get_block(BlockPos::of(0, 64, 0).chunk_block_pos()), + double_id + ); + } + + #[test] + fn test_cancel_when_target_not_air() { + init_item_to_block_mapping(); + init_block_mappings(); + let (state, _tmp) = temper_state::create_test_state(); + + // Put a solid block (stone) at the target position + { + let mut chunk = state + .0 + .world + .get_or_generate_mut(BlockPos::of(0, 64, 0).chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.set_block(BlockPos::of(0, 64, 0).chunk_block_pos(), block!("stone")); + } + + let context = BlockPlaceContext { + block_clicked: Default::default(), + block_position: BlockPos::of(0, 64, 0), + face_clicked: crate::BlockFace::Top, + click_position: Default::default(), + item_used: item!("oak_slab"), + player_rotation: Rotation { + yaw: 0.0, + pitch: 0.0, + }, + player_position: Default::default(), + }; + + let result = PlaceableSlab::place(context, state.0.clone()); + assert!(result.is_ok()); + let placed = result.unwrap(); + // Should cancel placement + assert!(placed.blocks.is_empty()); + assert!(!placed.take_item); + } + + #[test] + fn test_cancel_when_target_already_double() { + init_item_to_block_mapping(); + init_block_mappings(); + let (state, _tmp) = temper_state::create_test_state(); + + // Put a double oak slab at (0,64,0) + let double_id = slab_block_id("minecraft:oak_slab", "double"); + { + let mut chunk = state + .0 + .world + .get_or_generate_mut(BlockPos::of(0, 64, 0).chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.set_block(BlockPos::of(0, 64, 0).chunk_block_pos(), double_id); + } + + let context = BlockPlaceContext { + block_clicked: Default::default(), + block_position: BlockPos::of(0, 64, 0), + face_clicked: crate::BlockFace::Top, + click_position: Default::default(), + item_used: item!("oak_slab"), + player_rotation: Rotation { + yaw: 0.0, + pitch: 0.0, + }, + player_position: Default::default(), + }; + + let result = PlaceableSlab::place(context, state.0.clone()); + assert!(result.is_ok()); + let placed = result.unwrap(); + // Should cancel placement because target is already double + assert!(placed.blocks.is_empty()); + assert!(!placed.take_item); + } +} diff --git a/src/world/block-placing/src/blocks/torch.rs b/src/world/block-placing/src/blocks/torch.rs index 5e36e0eb..2521d85a 100644 --- a/src/world/block-placing/src/blocks/torch.rs +++ b/src/world/block-placing/src/blocks/torch.rs @@ -13,6 +13,17 @@ impl PlacableBlock for PlaceableTorch { context: BlockPlaceContext, state: GlobalState, ) -> Result { + let target_block = { + let chunk = state + .world + .get_or_generate_mut(context.block_position.chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.get_block(context.block_position.chunk_block_pos()) + }; + if target_block != block!("air") && target_block != block!("cave_air") { + return Err(BlockPlaceError::TargetBlockNotEmpty(context.block_position)); + } + let block = match context.face_clicked { BlockFace::Top => block!("torch"), BlockFace::East => block!("wall_torch", {facing: "east"}), diff --git a/src/world/block-placing/src/errors.rs b/src/world/block-placing/src/errors.rs index 188b4762..406b24a6 100644 --- a/src/world/block-placing/src/errors.rs +++ b/src/world/block-placing/src/errors.rs @@ -19,4 +19,6 @@ pub enum BlockPlaceError { ItemNotMappedToBlock(ItemID), #[error("Block can't be mapped to block state id")] BlockNotMappedToBlockStateId(BlockData), + #[error("Item ID {0} does not have a name mapping")] + ItemIdHasNoNameMapping(ItemID), } diff --git a/src/world/block-placing/src/lib.rs b/src/world/block-placing/src/lib.rs index 98242954..3a361daa 100644 --- a/src/world/block-placing/src/lib.rs +++ b/src/world/block-placing/src/lib.rs @@ -10,7 +10,6 @@ use temper_core::block_state_id::{BlockStateId, ITEM_TO_BLOCK_MAPPING}; use temper_core::dimension::Dimension; use temper_core::pos::BlockPos; use temper_inventories::item::ItemID; -use temper_macros::item; use temper_state::GlobalState; pub struct PlacedBlocks { @@ -49,46 +48,39 @@ pub fn place_item( state: GlobalState, context: BlockPlaceContext, ) -> Result { - match context.item_used { - item!("torch") => blocks::torch::PlaceableTorch::place(context, state), - item!("oak_door") - | item!("birch_door") - | item!("spruce_door") - | item!("jungle_door") - | item!("acacia_door") - | item!("dark_oak_door") => blocks::door::PlaceableDoor::place(context, state), - - item!("oak_log") | item!("stripped_oak_log") - | item!("spruce_log") | item!("stripped_spruce_log") - | item!("birch_log") | item!("stripped_birch_log") - | item!("jungle_log") | item!("stripped_jungle_log") - | item!("acacia_log") | item!("stripped_acacia_log") - | item!("dark_oak_log") | item!("stripped_dark_oak_log") - | item!("mangrove_log") | item!("stripped_mangrove_log") - | item!("cherry_log") | item!("stripped_cherry_log") - | item!("pale_oak_log") | item!("stripped_pale_oak_log") - | item!("crimson_stem") | item!("stripped_crimson_stem") - | item!("warped_stem") | item!("stripped_warped_stem") => blocks::logs::PlacableLog::place(context, state), - - unhandled => { - let block_opt = ITEM_TO_BLOCK_MAPPING.get(&unhandled.0.0); - if let Some(block) = block_opt { - match state - .world - .get_or_generate_mut(context.block_position.chunk(), Dimension::Overworld) - { - Ok(mut chunk) => { - chunk.set_block(context.block_position.chunk_block_pos(), *block); - Ok(PlacedBlocks { - blocks: HashMap::from([(context.block_position, *block)]), - take_item: true, - }) - } - Err(e) => Err(e.into()), + let Some(item_name) = context.item_used.to_name() else { + return Err(BlockPlaceError::ItemIdHasNoNameMapping(context.item_used)); + }; + let item_name = item_name.strip_prefix("minecraft:").unwrap_or(&item_name); + if item_name == "torch" { + blocks::torch::PlaceableTorch::place(context, state) + } else if item_name.ends_with("_slab") { + blocks::slab::PlaceableSlab::place(context, state) + } else if item_name.ends_with("_door") { + blocks::door::PlaceableDoor::place(context, state) + } else if item_name.ends_with("_log") { + blocks::logs::PlacableLog::place(context, state) + } else { + let block_opt = ITEM_TO_BLOCK_MAPPING + .get() + .expect("Mappings file uninitialized") + .get(&context.item_used.0.0); + if let Some(block) = block_opt { + match state + .world + .get_or_generate_mut(context.block_position.chunk(), Dimension::Overworld) + { + Ok(mut chunk) => { + chunk.set_block(context.block_position.chunk_block_pos(), *block); + Ok(PlacedBlocks { + blocks: HashMap::from([(context.block_position, *block)]), + take_item: true, + }) } - } else { - Err(BlockPlaceError::ItemNotPlaceable(context.item_used)) + Err(e) => Err(e.into()), } + } else { + Err(BlockPlaceError::ItemNotPlaceable(context.item_used)) } } } From 2fe71577c9f457f62b03c6940a6a71ef9aafd47b Mon Sep 17 00:00:00 2001 From: ReCore Date: Fri, 20 Mar 2026 19:19:26 +1030 Subject: [PATCH 3/5] fences --- .../interactions/src/block_interactions.rs | 2 +- .../src/packets/src/place_block.rs | 6 +- src/world/block-placing/src/blocks/door.rs | 4 +- src/world/block-placing/src/blocks/fence.rs | 116 ++++++++++++++++++ src/world/block-placing/src/blocks/logs.rs | 2 +- src/world/block-placing/src/blocks/mod.rs | 1 + src/world/block-placing/src/lib.rs | 2 + 7 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/world/block-placing/src/blocks/fence.rs diff --git a/src/game_systems/src/interactions/src/block_interactions.rs b/src/game_systems/src/interactions/src/block_interactions.rs index a85b9583..9f47ae2b 100644 --- a/src/game_systems/src/interactions/src/block_interactions.rs +++ b/src/game_systems/src/interactions/src/block_interactions.rs @@ -162,7 +162,7 @@ mod tests { use super::*; use std::collections::BTreeMap; use temper_core::block_state_id::{ - init_block_mappings, init_item_to_block_mapping, BlockStateId, + BlockStateId, init_block_mappings, init_item_to_block_mapping, }; use temper_macros::block; diff --git a/src/game_systems/src/packets/src/place_block.rs b/src/game_systems/src/packets/src/place_block.rs index b0de02d8..317adf59 100644 --- a/src/game_systems/src/packets/src/place_block.rs +++ b/src/game_systems/src/packets/src/place_block.rs @@ -8,9 +8,9 @@ use temper_core::pos::BlockPos; use temper_messages::BlockInteractMessage; use temper_net_runtime::connection::StreamWriter; +use temper_protocol::PlaceBlockReceiver; use temper_protocol::outgoing::block_change_ack::BlockChangeAck; use temper_protocol::outgoing::block_update::BlockUpdate; -use temper_protocol::PlaceBlockReceiver; use temper_state::GlobalStateResource; use tracing::{debug, error, trace}; @@ -19,12 +19,10 @@ use block_placing::PlacedBlocks; use std::collections::HashMap; use temper_components::player::rotation::Rotation; use temper_config::server_config::get_global_config; -use temper_core::block_state_id::BlockStateId; use temper_core::dimension::Dimension; use temper_core::mq; use temper_inventories::hotbar::Hotbar; use temper_inventories::inventory::Inventory; -use temper_macros::match_block; use temper_messages::world_change::WorldChange; use temper_text::{Color, NamedColor, TextComponentBuilder}; @@ -159,7 +157,7 @@ pub fn handle( continue 'ev_loop; } - let block_at_pos = { + let _block_at_pos = { let chunk = state .0 .world diff --git a/src/world/block-placing/src/blocks/door.rs b/src/world/block-placing/src/blocks/door.rs index ae185102..c63169c1 100644 --- a/src/world/block-placing/src/blocks/door.rs +++ b/src/world/block-placing/src/blocks/door.rs @@ -5,7 +5,7 @@ use bevy_math::IVec3; use std::collections::BTreeMap; use temper_core::block_data::BlockData; use temper_core::dimension::Dimension; -use temper_macros::{item, match_block}; +use temper_macros::match_block; use temper_state::GlobalState; use tracing::error; @@ -130,7 +130,7 @@ mod test { use temper_core::block_state_id::{init_block_mappings, init_item_to_block_mapping}; use temper_core::dimension::Dimension; use temper_core::pos::BlockPos; - use temper_macros::block; + use temper_macros::{block, item}; #[test] fn test_place_door() { diff --git a/src/world/block-placing/src/blocks/fence.rs b/src/world/block-placing/src/blocks/fence.rs new file mode 100644 index 00000000..d1ebb13f --- /dev/null +++ b/src/world/block-placing/src/blocks/fence.rs @@ -0,0 +1,116 @@ +use crate::BlockStateId; +use crate::errors::BlockPlaceError; +use crate::{BlockPlaceContext, PlacableBlock, PlacedBlocks}; +use bevy_math::IVec3; +use std::collections::{BTreeMap, HashMap}; +use temper_core::block_data::BlockData; +use temper_core::dimension::Dimension; +use temper_macros::match_block; +use temper_state::GlobalState; + +pub(crate) struct PlaceableFence; + +impl PlacableBlock for PlaceableFence { + fn place( + context: BlockPlaceContext, + state: GlobalState, + ) -> Result { + let name = match context.item_used.to_name() { + Some(name) => name, + None => return Err(BlockPlaceError::ItemIdHasNoNameMapping(context.item_used)), + }; + let target_block = { + let chunk = state + .world + .get_or_generate_chunk(context.block_position.chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.get_block(context.block_position.chunk_block_pos()) + }; + if !match_block!("air", target_block) && !match_block!("cave_air", target_block) { + return Err(BlockPlaceError::TargetBlockNotEmpty(context.block_position)); + } + + let mut props = BTreeMap::from([ + ("east".to_string(), "false".to_string()), + ("west".to_string(), "false".to_string()), + ("north".to_string(), "false".to_string()), + ("south".to_string(), "false".to_string()), + ("waterlogged".to_string(), "false".to_string()), + ]); + + let adjacent_positions = [ + (context.block_position + IVec3::new(1, 0, 0).into(), "east"), + (context.block_position + IVec3::new(-1, 0, 0).into(), "west"), + (context.block_position + IVec3::new(0, 0, 1).into(), "south"), + ( + context.block_position + IVec3::new(0, 0, -1).into(), + "north", + ), + ]; + + let mut changed_blocks = HashMap::new(); + + for (pos, direction) in adjacent_positions { + let block_id = { + let chunk = state + .world + .get_or_generate_chunk(pos.chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + chunk.get_block(pos.chunk_block_pos()) + }; + let block_data = block_id.to_block_data().unwrap_or_else(|| { + panic!("Block ID {} not found in block mappings file", block_id) + }); + let block_name = block_data + .name + .strip_prefix("minecraft:") + .unwrap_or(&block_data.name); + if block_name.ends_with("fence") + || block_name.ends_with("wall") + || block_name.ends_with("fence_gate") + { + props.insert(direction.to_string(), "true".to_string()); + // Update the adjacent block to connect to the new fence + // TODO: This should be moved to a proper block update system that updates all blocks around a changed block, but for now this will do + let mut adjacent_props = block_data.properties.unwrap_or_default(); + let opposite_direction = match direction { + "east" => "west", + "west" => "east", + "north" => "south", + "south" => "north", + _ => unreachable!(), + }; + adjacent_props.insert(opposite_direction.to_string(), "true".to_string()); + let updated_block_data = BlockData { + name: block_data.name.clone(), + properties: Some(adjacent_props), + }; + let updated_block_id = updated_block_data.to_block_state_id(); + state + .world + .get_or_generate_mut(pos.chunk(), Dimension::Overworld) + .expect("Could not load chunk") + .set_block(pos.chunk_block_pos(), updated_block_id); + changed_blocks.insert(pos, updated_block_id); + } + } + + let block_data = BlockData { + name: name.to_string(), + properties: Some(props), + }; + + let block_state_id = block_data.to_block_state_id(); + state + .world + .get_or_generate_mut(context.block_position.chunk(), Dimension::Overworld) + .expect("Could not load chunk") + .set_block(context.block_position.chunk_block_pos(), block_state_id); + changed_blocks.insert(context.block_position, block_state_id); + + Ok(PlacedBlocks { + blocks: changed_blocks, + take_item: true, + }) + } +} diff --git a/src/world/block-placing/src/blocks/logs.rs b/src/world/block-placing/src/blocks/logs.rs index c7b8cbb1..b078b725 100644 --- a/src/world/block-placing/src/blocks/logs.rs +++ b/src/world/block-placing/src/blocks/logs.rs @@ -1,5 +1,5 @@ -use crate::errors::BlockPlaceError; use crate::BlockStateId; +use crate::errors::BlockPlaceError; use crate::{BlockFace, BlockPlaceContext, PlacableBlock, PlacedBlocks}; use std::collections::BTreeMap; use temper_core::block_data::BlockData; diff --git a/src/world/block-placing/src/blocks/mod.rs b/src/world/block-placing/src/blocks/mod.rs index 342cd99b..117addcc 100644 --- a/src/world/block-placing/src/blocks/mod.rs +++ b/src/world/block-placing/src/blocks/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod door; +pub mod fence; pub(crate) mod logs; pub mod slab; pub(super) mod torch; diff --git a/src/world/block-placing/src/lib.rs b/src/world/block-placing/src/lib.rs index 3a361daa..64b6d2c6 100644 --- a/src/world/block-placing/src/lib.rs +++ b/src/world/block-placing/src/lib.rs @@ -60,6 +60,8 @@ pub fn place_item( blocks::door::PlaceableDoor::place(context, state) } else if item_name.ends_with("_log") { blocks::logs::PlacableLog::place(context, state) + } else if item_name.ends_with("_fence") { + blocks::fence::PlaceableFence::place(context, state) } else { let block_opt = ITEM_TO_BLOCK_MAPPING .get() From af8a7e568943dd1af950e9cef5e91cf67b9fcf19 Mon Sep 17 00:00:00 2001 From: ReCore Date: Fri, 20 Mar 2026 19:27:43 +1030 Subject: [PATCH 4/5] unit tests for fences --- src/world/block-placing/src/blocks/fence.rs | 93 ++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/src/world/block-placing/src/blocks/fence.rs b/src/world/block-placing/src/blocks/fence.rs index d1ebb13f..94b03233 100644 --- a/src/world/block-placing/src/blocks/fence.rs +++ b/src/world/block-placing/src/blocks/fence.rs @@ -1,5 +1,5 @@ -use crate::BlockStateId; use crate::errors::BlockPlaceError; +use crate::BlockStateId; use crate::{BlockPlaceContext, PlacableBlock, PlacedBlocks}; use bevy_math::IVec3; use std::collections::{BTreeMap, HashMap}; @@ -114,3 +114,94 @@ impl PlacableBlock for PlaceableFence { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::BlockFace; + use temper_core::block_state_id::{init_block_mappings, init_item_to_block_mapping}; + use temper_core::pos::BlockPos; + use temper_macros::item; + use temper_state::create_test_state; + + #[test] + fn test_place_fence() { + init_block_mappings(); + init_item_to_block_mapping(); + let (state, _) = create_test_state(); + let context = BlockPlaceContext { + block_clicked: BlockStateId::new(0), + block_position: BlockPos::of(0, 64, 0), + face_clicked: BlockFace::Top, + click_position: (0.5, 1.0, 0.5).into(), + player_position: (0.0, 64.0, -1.0).into(), + player_rotation: (0.0, 0.0).into(), + item_used: item!("oak_fence"), // Assuming this maps to a fence item + }; + let result = PlaceableFence::place(context, state.0); + assert!(result.is_ok()); + let placed_blocks = result.unwrap(); + assert_eq!(placed_blocks.blocks.len(), 1); + } + + #[test] + fn test_connects_to_neighboring_fences() { + init_block_mappings(); + init_item_to_block_mapping(); + let (state, _) = create_test_state(); + let base_position = BlockPos::of(0, 64, 0); + // Place a fence at the base position + let context1 = BlockPlaceContext { + block_clicked: BlockStateId::new(0), + block_position: base_position, + face_clicked: BlockFace::Top, + click_position: (0.5, 1.0, 0.5).into(), + player_position: (0.0, 64.0, -1.0).into(), + player_rotation: (0.0, 0.0).into(), + item_used: item!("oak_fence"), + }; + PlaceableFence::place(context1, state.0.clone()).unwrap(); + + // Place another fence to the east of the first one + let context2 = BlockPlaceContext { + block_clicked: BlockStateId::new(0), + block_position: base_position + IVec3::new(1, 0, 0).into(), + face_clicked: BlockFace::Top, + click_position: (1.5, 1.0, 0.5).into(), + player_position: (1.0, 64.0, -1.0).into(), + player_rotation: (90.0, 0.0).into(), + item_used: item!("oak_fence"), + }; + PlaceableFence::place(context2, state.0.clone()).unwrap(); + + // Check that both fences have the correct properties to connect to each other + let chunk = state + .0 + .world + .get_or_generate_chunk(base_position.chunk(), Dimension::Overworld) + .expect("Could not load chunk"); + let block_id1 = chunk.get_block(base_position.chunk_block_pos()); + let block_id2 = + chunk.get_block((base_position + IVec3::new(1, 0, 0).into()).chunk_block_pos()); + let block_data1 = block_id1.to_block_data().unwrap(); + let block_data2 = block_id2.to_block_data().unwrap(); + assert_eq!( + block_data1 + .properties + .as_ref() + .unwrap() + .get("east") + .unwrap(), + "true" + ); + assert_eq!( + block_data2 + .properties + .as_ref() + .unwrap() + .get("west") + .unwrap(), + "true" + ); + } +} From 7f6e007b8adb9dc179ec32969202e578eec76adc Mon Sep 17 00:00:00 2001 From: ReCore Date: Fri, 20 Mar 2026 19:35:44 +1030 Subject: [PATCH 5/5] fmt --- src/world/block-placing/src/blocks/fence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/world/block-placing/src/blocks/fence.rs b/src/world/block-placing/src/blocks/fence.rs index 94b03233..c76881aa 100644 --- a/src/world/block-placing/src/blocks/fence.rs +++ b/src/world/block-placing/src/blocks/fence.rs @@ -1,5 +1,5 @@ -use crate::errors::BlockPlaceError; use crate::BlockStateId; +use crate::errors::BlockPlaceError; use crate::{BlockPlaceContext, PlacableBlock, PlacedBlocks}; use bevy_math::IVec3; use std::collections::{BTreeMap, HashMap};