From ab767846125ad1f0e93768a52104216f2a1d8e78 Mon Sep 17 00:00:00 2001 From: StackDoubleFlow Date: Mon, 16 Sep 2024 22:55:56 -0400 Subject: [PATCH] tests: add tests for lever, trapdoor, lamp, and torches --- Cargo.lock | 4 + Cargo.toml | 6 + crates/blocks/src/blocks/props.rs | 3 +- crates/core/src/interaction.rs | 100 +-------- crates/network/src/packets/clientbound.rs | 4 +- crates/redpiler/src/lib.rs | 6 +- crates/redstone/src/lib.rs | 100 ++++++++- crates/world/src/storage.rs | 2 + tests/common/mod.rs | 249 ++++++++++++++++++++++ tests/components.rs | 144 +++++++++++++ 10 files changed, 517 insertions(+), 101 deletions(-) create mode 100644 tests/common/mod.rs create mode 100644 tests/components.rs diff --git a/Cargo.lock b/Cargo.lock index bfcbf439..43b388ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,7 +1172,11 @@ dependencies = [ name = "mchprs" version = "0.4.1" dependencies = [ + "mchprs_blocks", "mchprs_core", + "mchprs_redpiler", + "mchprs_redstone", + "mchprs_world", "tracing", "tracing-appender", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 695a0955..862d4276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,5 +43,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" tracing = "0.1" +[dev-dependencies] +mchprs_world ={ path = "./crates/world" } +mchprs_blocks ={ path = "./crates/blocks" } +mchprs_redpiler ={ path = "./crates/redpiler" } +mchprs_redstone = { path = "./crates/redstone" } + [patch.crates-io] hematite-nbt = { git = "https://github.com/StackDoubleFlow/hematite_nbt" } diff --git a/crates/blocks/src/blocks/props.rs b/crates/blocks/src/blocks/props.rs index 25426400..f04bc5fb 100644 --- a/crates/blocks/src/blocks/props.rs +++ b/crates/blocks/src/blocks/props.rs @@ -349,8 +349,9 @@ impl BlockTransform for RedstoneWire { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum TrapdoorHalf { + #[default] Top, Bottom, } diff --git a/crates/core/src/interaction.rs b/crates/core/src/interaction.rs index a14c5223..c086d371 100644 --- a/crates/core/src/interaction.rs +++ b/crates/core/src/interaction.rs @@ -7,9 +7,9 @@ use mchprs_blocks::blocks::*; use mchprs_blocks::items::{Item, ItemStack}; use mchprs_blocks::{BlockFace, BlockPos, SignType}; use mchprs_network::packets::clientbound::{COpenSignEditor, ClientBoundPacket}; -use mchprs_redstone::{self as redstone, noteblock}; +use mchprs_redstone as redstone; use mchprs_utils::nbt_unwrap_val; -use mchprs_world::{TickPriority, World}; +use mchprs_world::World; pub fn on_use( block: Block, @@ -18,80 +18,11 @@ pub fn on_use( pos: BlockPos, item_in_hand: Option, ) -> ActionResult { + if redstone::on_use(block, world, pos) { + return ActionResult::Success; + } + match block { - Block::RedstoneRepeater { repeater } => { - let mut repeater = repeater; - repeater.delay += 1; - if repeater.delay > 4 { - repeater.delay -= 4; - } - world.set_block(pos, Block::RedstoneRepeater { repeater }); - ActionResult::Success - } - Block::RedstoneComparator { comparator } => { - let mut comparator = comparator; - comparator.mode = comparator.mode.toggle(); - redstone::comparator::tick(comparator, world, pos); - world.set_block(pos, Block::RedstoneComparator { comparator }); - ActionResult::Success - } - Block::Lever { mut lever } => { - lever.powered = !lever.powered; - world.set_block(pos, Block::Lever { lever }); - redstone::update_surrounding_blocks(world, pos); - match lever.face { - LeverFace::Ceiling => { - redstone::update_surrounding_blocks(world, pos.offset(BlockFace::Top)); - } - LeverFace::Floor => { - redstone::update_surrounding_blocks(world, pos.offset(BlockFace::Bottom)); - } - LeverFace::Wall => redstone::update_surrounding_blocks( - world, - pos.offset(lever.facing.opposite().block_face()), - ), - } - ActionResult::Success - } - Block::StoneButton { mut button } => { - if !button.powered { - button.powered = true; - world.set_block(pos, Block::StoneButton { button }); - world.schedule_tick(pos, 10, TickPriority::Normal); - redstone::update_surrounding_blocks(world, pos); - match button.face { - ButtonFace::Ceiling => { - redstone::update_surrounding_blocks(world, pos.offset(BlockFace::Top)); - } - ButtonFace::Floor => { - redstone::update_surrounding_blocks(world, pos.offset(BlockFace::Bottom)); - } - ButtonFace::Wall => redstone::update_surrounding_blocks( - world, - pos.offset(button.facing.opposite().block_face()), - ), - } - } - ActionResult::Success - } - Block::RedstoneWire { wire } => { - use redstone::wire; - if wire::is_dot(wire) || wire::is_cross(wire) { - let mut new_wire = if wire::is_cross(wire) { - RedstoneWire::default() - } else { - wire::make_cross(0) - }; - new_wire.power = wire.power; - new_wire = wire::get_regulated_sides(new_wire, world, pos); - if wire != new_wire { - world.set_block(pos, Block::RedstoneWire { wire: new_wire }); - redstone::update_wire_neighbors(world, pos); - return ActionResult::Success; - } - } - ActionResult::Pass - } Block::SeaPickle { pickles } => { if let Some(Item::SeaPickle {}) = item_in_hand { if pickles < 4 { @@ -105,25 +36,6 @@ pub fn on_use( } ActionResult::Success } - Block::NoteBlock { note, powered, .. } => { - let note = (note + 1) % 25; - let instrument = noteblock::get_noteblock_instrument(world, pos); - - world.set_block( - pos, - Block::NoteBlock { - instrument, - note, - powered, - }, - ); - - if noteblock::is_noteblock_unblocked(world, pos) { - noteblock::play_note(world, pos, instrument, note); - } - - ActionResult::Success - } b if b.has_block_entity() => { // Open container let block_entity = world.get_block_entity(pos); diff --git a/crates/network/src/packets/clientbound.rs b/crates/network/src/packets/clientbound.rs index 2e11570a..b8f91ffd 100644 --- a/crates/network/src/packets/clientbound.rs +++ b/crates/network/src/packets/clientbound.rs @@ -1051,7 +1051,7 @@ impl ClientBoundPacket for CSetHeadRotation { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CUpdateSectionBlocksRecord { pub x: u8, pub y: u8, @@ -1059,7 +1059,7 @@ pub struct CUpdateSectionBlocksRecord { pub block_id: u32, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CUpdateSectionBlocks { pub chunk_x: i32, pub chunk_z: i32, diff --git a/crates/redpiler/src/lib.rs b/crates/redpiler/src/lib.rs index 3f67c43a..d2e1e6eb 100644 --- a/crates/redpiler/src/lib.rs +++ b/crates/redpiler/src/lib.rs @@ -32,7 +32,7 @@ fn block_powered_mut(block: &mut Block) -> Option<&mut bool> { }) } -#[derive(Default, PartialEq, Eq, Debug)] +#[derive(Default, PartialEq, Eq, Debug, Clone)] pub struct CompilerOptions { /// Enable optimization passes which may significantly increase compile times. pub optimize: bool, @@ -50,7 +50,7 @@ pub struct CompilerOptions { pub backend_variant: BackendVariant, } -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] pub enum BackendVariant { #[default] Direct, @@ -121,7 +121,7 @@ impl Compiler { pub fn compile( &mut self, - world: &mut W, + world: &W, bounds: (BlockPos, BlockPos), options: CompilerOptions, ticks: Vec, diff --git a/crates/redstone/src/lib.rs b/crates/redstone/src/lib.rs index d7557fec..27991aec 100644 --- a/crates/redstone/src/lib.rs +++ b/crates/redstone/src/lib.rs @@ -8,7 +8,7 @@ pub mod repeater; pub mod wire; use mchprs_blocks::block_entities::BlockEntity; -use mchprs_blocks::blocks::{Block, ButtonFace, LeverFace}; +use mchprs_blocks::blocks::{Block, ButtonFace, LeverFace, RedstoneWire}; use mchprs_blocks::{BlockDirection, BlockFace, BlockPos}; use mchprs_world::TickPriority; use mchprs_world::World; @@ -351,3 +351,101 @@ pub fn is_diode(block: Block) -> bool { Block::RedstoneRepeater { .. } | Block::RedstoneComparator { .. } ) } + +/// Returns true if the action was handled +pub fn on_use(block: Block, world: &mut impl World, pos: BlockPos) -> bool { + match block { + Block::RedstoneRepeater { repeater } => { + let mut repeater = repeater; + repeater.delay += 1; + if repeater.delay > 4 { + repeater.delay -= 4; + } + world.set_block(pos, Block::RedstoneRepeater { repeater }); + true + } + Block::RedstoneComparator { comparator } => { + let mut comparator = comparator; + comparator.mode = comparator.mode.toggle(); + comparator::tick(comparator, world, pos); + world.set_block(pos, Block::RedstoneComparator { comparator }); + true + } + Block::Lever { mut lever } => { + lever.powered = !lever.powered; + world.set_block(pos, Block::Lever { lever }); + update_surrounding_blocks(world, pos); + match lever.face { + LeverFace::Ceiling => { + update_surrounding_blocks(world, pos.offset(BlockFace::Top)); + } + LeverFace::Floor => { + update_surrounding_blocks(world, pos.offset(BlockFace::Bottom)); + } + LeverFace::Wall => update_surrounding_blocks( + world, + pos.offset(lever.facing.opposite().block_face()), + ), + } + true + } + Block::StoneButton { mut button } => { + if !button.powered { + button.powered = true; + world.set_block(pos, Block::StoneButton { button }); + world.schedule_tick(pos, 10, TickPriority::Normal); + update_surrounding_blocks(world, pos); + match button.face { + ButtonFace::Ceiling => { + update_surrounding_blocks(world, pos.offset(BlockFace::Top)); + } + ButtonFace::Floor => { + update_surrounding_blocks(world, pos.offset(BlockFace::Bottom)); + } + ButtonFace::Wall => update_surrounding_blocks( + world, + pos.offset(button.facing.opposite().block_face()), + ), + } + } + true + } + Block::RedstoneWire { wire } => { + if wire::is_dot(wire) || wire::is_cross(wire) { + let mut new_wire = if wire::is_cross(wire) { + RedstoneWire::default() + } else { + wire::make_cross(0) + }; + new_wire.power = wire.power; + new_wire = wire::get_regulated_sides(new_wire, world, pos); + if wire != new_wire { + world.set_block(pos, Block::RedstoneWire { wire: new_wire }); + update_wire_neighbors(world, pos); + return true; + } + } + false + } + Block::NoteBlock { note, powered, .. } => { + let note = (note + 1) % 25; + let instrument = noteblock::get_noteblock_instrument(world, pos); + + world.set_block( + pos, + Block::NoteBlock { + instrument, + note, + powered, + }, + ); + + if noteblock::is_noteblock_unblocked(world, pos) { + noteblock::play_note(world, pos, instrument, note); + } + + true + } + _ => false, + } +} diff --git a/crates/world/src/storage.rs b/crates/world/src/storage.rs index 614c5c35..f191c4b0 100644 --- a/crates/world/src/storage.rs +++ b/crates/world/src/storage.rs @@ -239,6 +239,7 @@ impl PalettedBitBuffer { } } +#[derive(Clone)] pub struct ChunkSection { buffer: PalettedBitBuffer, block_count: u32, @@ -391,6 +392,7 @@ impl Default for ChunkSection { } } +#[derive(Clone)] pub struct Chunk { pub sections: Vec, pub x: i32, diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..929a81c9 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,249 @@ +use mchprs_blocks::block_entities::BlockEntity; +use mchprs_blocks::blocks::Block; +use mchprs_blocks::BlockPos; +use mchprs_redpiler::{BackendVariant, Compiler, CompilerOptions}; +use mchprs_world::storage::Chunk; +use mchprs_world::{TickEntry, TickPriority, World}; + +#[derive(Clone)] +pub struct TestWorld { + chunks: Vec, + to_be_ticked: Vec, + size: i32, +} + +impl TestWorld { + pub fn new(size: i32) -> TestWorld { + let mut chunks = Vec::new(); + for x in 0..size { + for z in 0..size { + chunks.push(Chunk::empty(x, z, size as usize)); + } + } + TestWorld { + chunks, + to_be_ticked: Vec::new(), + size, + } + } + + fn get_chunk_index_for_chunk(&self, chunk_x: i32, chunk_z: i32) -> usize { + (chunk_x * self.size + chunk_z).unsigned_abs() as usize + } + + fn get_chunk_index_for_block(&self, block_x: i32, block_z: i32) -> Option { + let chunk_x = block_x >> 4; + let chunk_z = block_z >> 4; + if chunk_x >= self.size || chunk_z >= self.size || chunk_x < 0 || chunk_z < 0 { + return None; + } + Some(((chunk_x * self.size) + chunk_z).unsigned_abs() as usize) + } +} + +impl World for TestWorld { + /// Returns the block state id of the block at `pos` + fn get_block_raw(&self, pos: BlockPos) -> u32 { + let chunk_index = match self.get_chunk_index_for_block(pos.x, pos.z) { + Some(idx) => idx, + None => return 0, + }; + let chunk = &self.chunks[chunk_index]; + chunk.get_block((pos.x & 0xF) as u32, pos.y as u32, (pos.z & 0xF) as u32) + } + + /// Sets a block in storage. Returns true if a block was changed. + fn set_block_raw(&mut self, pos: BlockPos, block: u32) -> bool { + let chunk_index = match self.get_chunk_index_for_block(pos.x, pos.z) { + Some(idx) => idx, + None => return false, + }; + + // Check to see if block is within height limit + if pos.y >= self.size * 16 || pos.y < 0 { + return false; + } + + let chunk = &mut self.chunks[chunk_index]; + chunk.set_block( + (pos.x & 0xF) as u32, + pos.y as u32, + (pos.z & 0xF) as u32, + block, + ) + } + + fn delete_block_entity(&mut self, pos: BlockPos) { + let chunk_index = match self.get_chunk_index_for_block(pos.x, pos.z) { + Some(idx) => idx, + None => return, + }; + let chunk = &mut self.chunks[chunk_index]; + chunk.delete_block_entity(BlockPos::new(pos.x & 0xF, pos.y, pos.z & 0xF)); + } + + fn get_block_entity(&self, pos: BlockPos) -> Option<&BlockEntity> { + let chunk_index = match self.get_chunk_index_for_block(pos.x, pos.z) { + Some(idx) => idx, + None => return None, + }; + let chunk = &self.chunks[chunk_index]; + chunk.get_block_entity(BlockPos::new(pos.x & 0xF, pos.y, pos.z & 0xF)) + } + + fn set_block_entity(&mut self, pos: BlockPos, block_entity: BlockEntity) { + let chunk_index = match self.get_chunk_index_for_block(pos.x, pos.z) { + Some(idx) => idx, + None => return, + }; + let chunk = &mut self.chunks[chunk_index]; + chunk.set_block_entity(BlockPos::new(pos.x & 0xF, pos.y, pos.z & 0xF), block_entity); + } + + fn get_chunk(&self, x: i32, z: i32) -> Option<&Chunk> { + self.chunks.get(self.get_chunk_index_for_chunk(x, z)) + } + + fn get_chunk_mut(&mut self, x: i32, z: i32) -> Option<&mut Chunk> { + let chunk_idx = self.get_chunk_index_for_chunk(x, z); + self.chunks.get_mut(chunk_idx) + } + + fn schedule_tick(&mut self, pos: BlockPos, delay: u32, priority: TickPriority) { + self.to_be_ticked.push(TickEntry { + pos, + ticks_left: delay, + tick_priority: priority, + }); + } + + fn pending_tick_at(&mut self, _pos: BlockPos) -> bool { + false + } +} + +struct RedpilerInstance { + variant: BackendVariant, + options: CompilerOptions, + world: TestWorld, + compiler: Compiler, +} + +impl RedpilerInstance { + fn tick(&mut self) { + self.compiler.tick(); + self.compiler.flush(&mut self.world); + } +} + +pub struct AllBackendRunner { + redstone_world: TestWorld, + redpilers: Vec, +} + +impl AllBackendRunner { + pub fn new(world: TestWorld) -> AllBackendRunner { + let variants = [BackendVariant::Direct]; + let redpilers = variants + .iter() + .map(|&variant| { + let options = CompilerOptions { + backend_variant: variant, + ..Default::default() + }; + let mut compiler = Compiler::default(); + let max = world.size * 16 - 1; + let bounds = (BlockPos::new(0, 0, 0), BlockPos::new(max, max, max)); + let monitor = Default::default(); + let ticks = world.to_be_ticked.clone(); + compiler.compile(&world, bounds, options.clone(), ticks, monitor); + RedpilerInstance { + variant, + options, + world: world.clone(), + compiler, + } + }) + .collect(); + AllBackendRunner { + redstone_world: world, + redpilers, + } + } + + pub fn tick(&mut self) { + self.redstone_world + .to_be_ticked + .sort_by_key(|e| (e.ticks_left, e.tick_priority)); + for pending in &mut self.redstone_world.to_be_ticked { + pending.ticks_left = pending.ticks_left.saturating_sub(1); + } + while self + .redstone_world + .to_be_ticked + .first() + .map_or(1, |e| e.ticks_left) + == 0 + { + let entry = self.redstone_world.to_be_ticked.remove(0); + mchprs_redstone::tick( + self.redstone_world.get_block(entry.pos), + &mut self.redstone_world, + entry.pos, + ); + } + + for redpiler in &mut self.redpilers { + redpiler.compiler.tick(); + redpiler.compiler.flush(&mut redpiler.world); + } + } + + pub fn use_block(&mut self, pos: BlockPos) { + mchprs_redstone::on_use( + self.redstone_world.get_block(pos), + &mut self.redstone_world, + pos, + ); + for redpiler in &mut self.redpilers { + redpiler.compiler.on_use_block(pos); + redpiler.compiler.flush(&mut redpiler.world); + } + } + + pub fn check_block_powered(&self, pos: BlockPos, powered: bool) { + assert_eq!( + is_block_powered(self.redstone_world.get_block(pos)), + Some(powered) + ); + for redpiler in &self.redpilers { + assert_eq!( + is_block_powered(redpiler.world.get_block(pos)), + Some(powered) + ); + } + } + + pub fn check_powered_for(&mut self, pos: BlockPos, powered: bool, ticks: usize) { + for _ in 0..ticks { + self.check_block_powered(pos, powered); + self.tick(); + } + } +} + +fn is_block_powered(block: Block) -> Option { + Some(match block { + Block::RedstoneComparator { comparator } => comparator.powered, + Block::RedstoneTorch { lit } => lit, + Block::RedstoneWallTorch { lit, .. } => lit, + Block::RedstoneRepeater { repeater } => repeater.powered, + Block::Lever { lever } => lever.powered, + Block::StoneButton { button } => button.powered, + Block::StonePressurePlate { powered } => powered, + Block::RedstoneLamp { lit } => lit, + Block::IronTrapdoor { powered, .. } => powered, + Block::NoteBlock { powered, .. } => powered, + _ => return None, + }) +} diff --git a/tests/components.rs b/tests/components.rs new file mode 100644 index 00000000..609ae44a --- /dev/null +++ b/tests/components.rs @@ -0,0 +1,144 @@ +mod common; + +use common::{AllBackendRunner, TestWorld}; +use mchprs_blocks::blocks::{Block, Lever, LeverFace}; +use mchprs_blocks::{BlockDirection, BlockPos}; +use mchprs_redstone::wire::make_cross; +use mchprs_world::World; + +fn pos(x: i32, y: i32, z: i32) -> BlockPos { + BlockPos::new(x, y, z) +} + +/// Creates a lever at `lever_pos` with a block of sandstone below it +fn make_lever(world: &mut TestWorld, lever_pos: BlockPos) { + world.set_block(lever_pos - pos(0, 1, 0), Block::Sandstone {}); + world.set_block( + lever_pos, + Block::Lever { + lever: Lever { + face: LeverFace::Floor, + ..Default::default() + }, + }, + ); +} + +#[test] +fn lever_on_off() { + let lever_pos = pos(0, 1, 0); + + let mut world = TestWorld::new(1); + make_lever(&mut world, lever_pos); + + let mut runner = AllBackendRunner::new(world); + runner.check_block_powered(lever_pos, false); + + runner.use_block(lever_pos); + runner.check_block_powered(lever_pos, true); + + runner.use_block(lever_pos); + runner.check_block_powered(lever_pos, false); +} + +#[test] +fn trapdoor_on_off() { + let lever_pos = pos(0, 1, 0); + let trapdoor_pos = pos(1, 0, 0); + + let mut world = TestWorld::new(1); + make_lever(&mut world, lever_pos); + world.set_block( + trapdoor_pos, + Block::IronTrapdoor { + facing: Default::default(), + half: Default::default(), + powered: false, + }, + ); + + let mut runner = AllBackendRunner::new(world); + runner.check_block_powered(trapdoor_pos, false); + + runner.use_block(lever_pos); + runner.check_block_powered(trapdoor_pos, true); + + runner.use_block(lever_pos); + runner.check_block_powered(trapdoor_pos, false); +} + +#[test] +fn lamp_on_off() { + let lever_pos = pos(0, 1, 0); + let lamp_pos = pos(1, 0, 0); + + let mut world = TestWorld::new(1); + make_lever(&mut world, lever_pos); + world.set_block(lamp_pos, Block::RedstoneLamp { lit: false }); + + let mut runner = AllBackendRunner::new(world); + runner.check_block_powered(lamp_pos, false); + + runner.use_block(lever_pos); + runner.check_block_powered(lamp_pos, true); + + runner.use_block(lever_pos); + runner.check_powered_for(lamp_pos, true, 2); + runner.check_block_powered(lamp_pos, false); +} + +#[test] +fn wall_torch_on_off() { + let lever_pos = pos(0, 1, 0); + let torch_pos = pos(1, 0, 0); + + let mut world = TestWorld::new(1); + make_lever(&mut world, lever_pos); + world.set_block( + torch_pos, + Block::RedstoneWallTorch { + lit: true, + facing: BlockDirection::East, + }, + ); + + let mut runner = AllBackendRunner::new(world); + runner.check_block_powered(torch_pos, true); + + runner.use_block(lever_pos); + runner.check_powered_for(torch_pos, true, 1); + runner.check_block_powered(torch_pos, false); + + runner.use_block(lever_pos); + runner.check_powered_for(torch_pos, false, 1); + runner.check_block_powered(torch_pos, true); +} + +#[test] +fn torch_on_off() { + let lever_pos = pos(0, 2, 0); + let torch_pos = pos(2, 2, 0); + + let mut world = TestWorld::new(1); + make_lever(&mut world, lever_pos); + world.set_block(pos(1, 0, 0), Block::Sandstone {}); + world.set_block( + pos(1, 1, 0), + Block::RedstoneWire { + wire: make_cross(0), + }, + ); + world.set_block(pos(2, 1, 0), Block::Sandstone {}); + world.set_block(torch_pos, Block::RedstoneTorch { lit: true }); + + let mut runner = AllBackendRunner::new(world); + runner.check_block_powered(torch_pos, true); + + runner.use_block(lever_pos); + runner.check_powered_for(torch_pos, true, 1); + runner.check_block_powered(torch_pos, false); + + runner.use_block(lever_pos); + runner.check_powered_for(torch_pos, false, 1); + runner.check_block_powered(torch_pos, true); +}