diff --git a/Cargo.toml b/Cargo.toml index 6de66457..88792e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,11 @@ members = [ "src/base/macros", "src/base/profiling", "src/base/threadpool", + "src/blocks", + "src/blocks/crates/property", + "src/blocks/crates/generated", + "src/blocks/crates/build", + "src/blocks/crates/data", "src/bin", "src/commands", "src/components", @@ -55,7 +60,6 @@ members = [ "src/text", "src/utils", "src/world", - "src/world/block-placing", "src/world/db", "src/world/format", "src/world/gen", @@ -119,6 +123,9 @@ debug = true # Workspace members temper-app = { path = "src/app" } temper-anvil = { path = "src/adapters/anvil" } +temper-blocks = { path = "src/blocks" } +temper-block-properties = { path = "src/blocks/crates/property" } +temper-block-data = { path = "src/blocks/crates/data" } temper-config = { path = "src/config" } temper-core = { path = "src/core" } temper-default-commands = { path = "src/default_commands" } @@ -225,6 +232,7 @@ uuid = { version = "1.23.1", features = ["v4", "v3", "serde"] } indexmap = { version = "2.14.0", features = ["serde"] } bimap = "0.6.3" arrayvec = "0.7.6" +fastcache = "0.1.7" # Macros lazy_static = "1.5.0" diff --git a/src/base/macros/src/commands/mod.rs b/src/base/macros/src/commands/mod.rs index 2ef89d1c..5d899688 100644 --- a/src/base/macros/src/commands/mod.rs +++ b/src/base/macros/src/commands/mod.rs @@ -120,7 +120,7 @@ pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { if let Type::Path(path) = *refr.clone().elem { println!("path reference: {:?}", path.path.segments.clone()); let is_bevy = path.path.segments.iter().any(|seg| { - println!("{}", &seg.ident.to_string()); + println!("{}", seg.ident); &seg.ident.to_string() == "bevy_ecs" }); println!("is bevy? {is_bevy}"); diff --git a/src/blocks/Cargo.toml b/src/blocks/Cargo.toml new file mode 100644 index 00000000..dbd6cee2 --- /dev/null +++ b/src/blocks/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "temper-blocks" +edition.workspace = true +version = "0.0.1" + +[dependencies] +temper-world = { workspace = true } +temper-block-properties = { workspace = true } +temper-blocks-generated = { path = "crates/generated" } +temper-block-data = { workspace = true } +temper-core = { workspace = true } +temper-macros = { workspace = true } +bevy_math = { workspace = true } +tracing = { workspace = true } + +[build-dependencies] +temper-blocks-build = { path = "crates/build" } +quote = { workspace = true } + +[lints] +workspace = true diff --git a/src/blocks/README.md b/src/blocks/README.md new file mode 100644 index 00000000..0779d574 --- /dev/null +++ b/src/blocks/README.md @@ -0,0 +1,89 @@ +# Temper Blocks + +This is a quick tutorial on how to use this library to create and modify block behavior and structs. + +## Overview + +This is a brief overview of what this crate is actually doing. This is split up into 2 distinct pieces, +the struct generator and the behavior implementation. + +The struct generator (the `temper-blocks-build` crate) is given the `build_config.toml` and `blockstates.json` files +and generates structs based off of groups of blockstates. For each block in `blockstates.json`, it +figures out which properties that block has. It will then collect blocks that have those same properties +and group them under the same struct. When it's saving the structs, it will look at the `build_config.toml` +file and rename structs according to the names in that file. The `build_config.toml` also provides the struct +names of various block state properties. The list of block state properties Minecraft uses can be found at +`net.minecraft.world.level.block.state.properties.BlockStateProperties` (as of 1.21.11). + +The next step is the block behavior implementations (this is the main crate). To add behavior to blocks, implement +the `BlockBehavior` trait for that block struct. The trait has already been implemented for all block +structs so far (to avoid unsafe features, such as `min_specialization`, it's not the most convenient, but it works). +The build script for this crate uses the `blockstates.json` file to generate a list that maps protocol ids (index) +to block states (value). This list is then used to dispatch behavior functions based on the protocol id. + +This crate also implements a helper trait on `BlockStateId` that allows you to call block functions directly from it +(and it will also be updated if the function mutates the block state). + +As a side note, the `temper-blocks-generated` crate contains the glue code to use the generated structs from +the build crate. The build crate outputs to the build directory and this crate imports those files back in. +Additionally, the `temper-block-properties` crate is where block state property structs and enums can be found. + +Any data types used exclusively by this crate (for example, the `PlacedBlocks` struct) should be placed in the `temper-block-data` crate. + +## Tips and Notes + +- If the build script is emitting a warning about an unknown block, see the **Adding / Modifying Block Structs** section for more information. + +## Creating Block Behavior + +To create a new function for blocks to implement, navigate to the `behavior_trait.rs` file. Due to the complexity of the inner workings of the system, a macro has been built to make creating functions for blocks super easy. The macro syntax is pretty simple: + +```rust +block_behavior_trait!( + fn ([mut]; ) [-> ; {` rather than mutating self directly +- Performance improvements and code-size reduction +- Better struct combination capabilities (combine structs with not identical properties, property serialization depends on the block type set) + - Note to self: this would be super useful for things like torches where they have two different blocks representing the same item. Combining TorchBlock and WallTorchBlock into just TorchBlock would be very helpful +- And of course, more block behavior functions! \ No newline at end of file diff --git a/src/blocks/build.rs b/src/blocks/build.rs new file mode 100644 index 00000000..c8437ebc --- /dev/null +++ b/src/blocks/build.rs @@ -0,0 +1,40 @@ +use quote::__private::TokenStream; +use quote::quote; +use std::fs; +use std::path::Path; +use temper_blocks_build::complex::fill_complex_block_mappings; +use temper_blocks_build::config::{get_block_states, get_build_config}; +use temper_blocks_build::separate_blocks; +use temper_blocks_build::simple::fill_simple_block_mappings; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=build_config.toml"); + + let build_config = get_build_config(); + let block_states = get_block_states(); + + let mut mappings = Vec::with_capacity(block_states.len()); + mappings.resize(block_states.len(), TokenStream::new()); + let (simple_blocks, complex_blocks) = separate_blocks(block_states); + + let enum_const = fill_simple_block_mappings(&build_config, simple_blocks, &mut mappings); + let complex_consts = fill_complex_block_mappings(&build_config, complex_blocks, &mut mappings); + + let mapping_const = quote! { + { + use temper_blocks_generated::*; + + #enum_const + #(#complex_consts)* + + &[ + #(#mappings),* + ] + } + }; + + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let dir = Path::new(&out_dir).join("mappings.rs"); + fs::write(dir, mapping_const.to_string()).unwrap(); +} diff --git a/src/blocks/build_config.toml b/src/blocks/build_config.toml new file mode 100644 index 00000000..323ebb7a --- /dev/null +++ b/src/blocks/build_config.toml @@ -0,0 +1,228 @@ +# Overrides the default generated struct names (unreadable) for the property map given with the given struct name +[name_overrides] +"snowy" = "SnowyBlock" +"age" = "CropBlock" +"type+waterlogged" = "SlabBlock" +"attached+rotation+waterlogged" = "HangingSignBlock" +"waterlogged" = "WaterloggableBlock" +"thickness+vertical_direction+waterlogged" = "DripstoneBlock" +"stage" = "SaplingBlock" +"rotation+waterlogged" = "SignBlock" +"rotation" = "BannerBlock" +"powered" = "PressurePlateBlock" +"axis" = "PillarBlock" +"axis+creaking_heart_state+natural" = "CreakingHeartBlock" +"bites" = "CakeBlock" +"candles+lit+waterlogged" = "CandleBlock" +"conditional+facing" = "CommandBlock" +"distance+persistent+waterlogged" = "LeavesBlock" +"down+east+north+south+up+waterlogged+west" = "LichenBlock" +"down+east+north+south+up+west" = "LargeMushroomBlock" +"dusted" = "SuspiciousBlock" +"east+north+south+up+waterlogged+west" = "WallBlock" +"east+north+south+waterlogged+west" = "FenceAndPaneBlock" +"extended+facing" = "PistonBlock" +"face+facing+powered" = "ButtonBlock" +"facing" = "FacingBlock" +"facing+flower_amount" = "FlowerCoverBlock" +"facing+half+hinge+open+powered" = "DoorBlock" +"facing+half+open+powered+waterlogged" = "TrapdoorBlock" +"facing+half+shape+waterlogged" = "StairsBlock" +"facing+honey_level" = "HiveBlock" +"facing+in_wall+open+powered" = "FenceGateBlock" +"facing+lit" = "FurnaceBlock" +"facing+lit+signal_fire+waterlogged" = "CampfireBlock" +"facing+mode+powered" = "ComparatorBlock" +"facing+occupied+part" = "BedBlock" +"facing+ominous+vault_state" = "VaultBlock" +"facing+powered" = "WallSkullBlock" +"facing+triggered" = "DispenserBlock" +"facing+type+waterlogged" = "ChestBlock" +"facing+waterlogged" = "WaterloggableWallAttachedBlock" +"half" = "DoublePlantBlock" +"hanging+waterlogged" = "LanternBlock" +"lit" = "CandleCakeBlock" +"lit+powered" = "BulbBlock" +"ominous+trial_spawner_state" = "TrialSpawnerBlock" +"pickles+waterlogged" = "SeaPickleBlock" +"power" = "WeightedPressurePlateBlock" +"powered+rotation" = "SkullBlock" +"powered+shape+waterlogged" = "RedstoneRailBlock" +"shape+waterlogged" = "RailBlock" +"age+berries" = "GlowBerriesBlock" +"age+east+north+south+up+west" = "FireBlock" +"age+facing" = "CocoaBeansBlock" +"age+half" = "PitcherCropBlock" +"age+hanging+stage+waterlogged" = "MangrovePopaguleBlock" +"age+leaves+stage" = "BambooBlock" +"attached+disarmed+east+north+powered+south+west" = "TripwireBlock" +"attached+facing+powered" = "TripwireHookBlock" +"attachment+facing+powered" = "BellBlock" +"axis+waterlogged" = "ChainBlock" +"berries" = "GlowBerriesPlantBlock" +"bloom" = "SculkCatalystBlock" +"bottom+distance+waterlogged" = "ScaffoldingBlock" +"bottom+east+north+south+west" = "PaleMossCarpetBlock" +"can_summon+shrieking+waterlogged" = "SculkShriekerBlock" +"charges" = "RespawnAnchor" +"cracked+facing+waterlogged" = "DecoratedPotBlock" +"crafting+orientation+triggered" = "CrafterBlock" +"delay+facing+locked+powered" = "RepeaterBlock" +"drag" = "BubbleColumnBlock" +"east+north+power+south+west" = "RedstoneWireBlock" +"east+north+south+up+west" = "VineBlock" +"eggs+hatch" = "TurtleEggBlock" +"enabled+facing" = "HopperBlock" +"eye+facing" = "EndPortalBlock" +"face+facing" = "GrindstoneBlock" +"facing+half+waterlogged" = "SmallDripleafBlock" +"facing+has_book+powered" = "LecternBlock" +"facing+open" = "BarrelBlock" +"facing+power+sculk_sensor_phase+waterlogged" = "CalibratedSculkSensorBlock" +"facing+powered+waterlogged" = "LightningRodBlock" +"facing+segment_amount" = "LeafLitterBlock" +"facing+short+type" = "PistonHeadBlock" +"facing+slot_0_occupied+slot_1_occupied+slot_2_occupied+slot_3_occupied+slot_4_occupied+slot_5_occupied" = "ChiseledBookshelfBlock" +"facing+tilt+waterlogged" = "BigDripleafBlock" +"facing+type" = "MovingPistonBlock" +"has_bottle_0+has_bottle_1+has_bottle_2" = "BrewingStandBlock" +"has_record" = "JukeboxBlock" +"hatch" = "SnifferEggBlock" +"instrument+note+powered" = "NoteBlock" +"inverted+power" = "DaylightDetectorBlock" +"layers" = "SnowBlock" +"level+waterlogged" = "LightBlock" +"moisture" = "FarmlandBlock" +"orientation" = "JigsawBlock" +"power+sculk_sensor_phase+waterlogged" = "SculkSensorBlock" +"tip" = "PaleHangingMossBlock" +"unstable" = "TntBlock" + +# Tells the generator to use a different struct for the block specified. +# If the struct is defined above and the property map of the block and struct don't match, the generator will panic +[block_overrides] +"minecraft:frosted_ice" = "FrostedIceBlock" +"minecraft:redstone_ore" = "RedstoneOreBlock" +"minecraft:deepslate_redstone_ore" = "RedstoneOreBlock" +"minecraft:redstone_lamp" = "RedstoneLampBlock" +"minecraft:redstone_torch" = "RedstoneTorchBlock" +"minecraft:chorus_plant" = "ChorusPlantBlock" +"minecraft:lever" = "LeverBlock" +"minecraft:attached_melon_stem" = "StemBlock" +"minecraft:attached_pumpkin_stem" = "StemBlock" +"minecraft:redstone_wall_torch" = "WallRedstoneTorchBlock" +"minecraft:observer" = "ObserverBlock" +"minecraft:composter" = "ComposterBlock" +"minecraft:lava" = "LiquidBlock" +"minecraft:water" = "LiquidBlock" +"minecraft:powder_snow_cauldron" = "LevelCauldronBlock" +"minecraft:water_cauldron" = "LevelCauldronBlock" +"minecraft:target" = "TargetBlock" +"minecraft:structure_block" = "StructureBlock" +"minecraft:test_block" = "TestBlock" +"minecraft:torch" = "TorchBlock" +"minecraft:wall_torch" = "WallTorchBlock" +"minecraft:soul_torch" = "TorchBlock" +"minecraft:soul_wall_torch" = "WallTorchBlock" + +# From Minecraft's net.minecraft.world.level.block.state.properties.BlockStateProperties class +# +# Maps property names to the type the generator should use in the generated structs. +# If a property name isn't found in this list, the generator will panic. In this case, check to see what the type is and add it below. +# If a name could be multiple types, use a list. The generator will try to decipher the type from the context. +[property_types] +"attached" = "bool" +"berries" = "bool" +"bloom" = "bool" +"bottom" = "bool" +"can_summon" = "bool" +"conditional" = "bool" +"disarmed" = "bool" +"drag" = "bool" +"enabled" = "bool" +"extended" = "bool" +"eye" = "bool" +"falling" = "bool" +"hanging" = "bool" +"has_bottle_0" = "bool" +"has_bottle_1" = "bool" +"has_bottle_2" = "bool" +"has_record" = "bool" +"has_book" = "bool" +"inverted" = "bool" +"in_wall" = "bool" +"lit" = "bool" +"locked" = "bool" +"natural" = "bool" +"occupied" = "bool" +"open" = "bool" +"persistent" = "bool" +"powered" = "bool" +"short" = "bool" +"shrieking" = "bool" +"signal_fire" = "bool" +"snowy" = "bool" +"tip" = "bool" +"triggered" = "bool" +"unstable" = "bool" +"waterlogged" = "bool" +"horizontal_axis" = "Axis" +"axis" = "Axis" +"up" = "bool" +"down" = "bool" +"north" = ["bool", "WallSide", "RedstoneSide"] +"east" = ["bool", "WallSide", "RedstoneSide"] +"south" = ["bool", "WallSide", "RedstoneSide"] +"west" = ["bool", "WallSide", "RedstoneSide"] +"facing" = "Direction" +"flower_amount" = "i32" +"segment_amount" = "i32" +"orientation" = "FrontAndTop" +"face" = "AttachFace" +"attachment" = "BellAttachType" +"half" = ["DoubleBlockHalf", "Half"] +"side_chain" = "SideChainPart" +"shape" = ["RailShape", "StairsShape"] +"age" = "i32" +"bites" = "i32" +"candles" = "i32" +"delay" = "i32" +"distance" = "i32" +"eggs" = "i32" +"hatch" = "i32" +"layers" = "i32" +"level" = "i32" +"honey_level" = "i32" +"moisture" = "i32" +"note" = "i32" +"pickles" = "i32" +"power" = "i32" +"stage" = "i32" +"charges" = "i32" +"hydration" = "i32" +"rotation" = "i32" +"part" = "BedPart" +"type" = ["ChestType", "PistonType", "SlabType"] +"mode" = ["ComparatorMode", "StructureMode", "TestBlockMode"] +"hinge" = "DoorHingeSide" +"instrument" = "NoteBlockInstrument" +"leaves" = "BambooLeaves" +"tilt" = "Tilt" +"vertical_direction" = "Direction" +"thickness" = "DripstoneThickness" +"sculk_sensor_phase" = "SculkSensorPhase" +"slot_0_occupied" = "bool" +"slot_1_occupied" = "bool" +"slot_2_occupied" = "bool" +"slot_3_occupied" = "bool" +"slot_4_occupied" = "bool" +"slot_5_occupied" = "bool" +"dusted" = "i32" +"cracked" = "bool" +"crafting" = "bool" +"trial_spawner_state" = "TrialSpawnerState" +"vault_state" = "VaultState" +"creaking_heart_state" = "CreakingHeartState" +"ominous" = "bool" +"map" = "bool" +"copper_golem_pose" = "CopperGolemPose" \ No newline at end of file diff --git a/src/blocks/crates/build/Cargo.toml b/src/blocks/crates/build/Cargo.toml new file mode 100644 index 00000000..9b51d1fe --- /dev/null +++ b/src/blocks/crates/build/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "temper-blocks-build" +version = "0.1.0" +edition = "2024" + +[dependencies] +temper-block-properties = { path = "../property", features = ["block-struct-generation"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +heck = "0.5" +proc-macro2 = "1.0" +quote = "1.0" +toml = "0.9.8" +fxhash = "0.2.1" diff --git a/src/blocks/crates/build/src/complex.rs b/src/blocks/crates/build/src/complex.rs new file mode 100644 index 00000000..feb85604 --- /dev/null +++ b/src/blocks/crates/build/src/complex.rs @@ -0,0 +1,534 @@ +use crate::config::{BuildConfig, SingleOrMultiple}; +use fxhash::FxHashMap; +use heck::{ToPascalCase, ToShoutySnakeCase, ToSnakeCase}; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use std::collections::HashMap; +use temper_block_properties::{PropertyDescriptor, TYPES}; + +struct BlockStateConfiguration<'a> { + name: &'a str, + properties: Vec<(&'a str, &'a str)>, + values: FxHashMap>, +} + +pub struct ComplexBlock { + pub name: String, + pub properties: FxHashMap, +} + +#[allow(clippy::type_complexity)] +pub struct FinalizedConfiguration<'a> { + name: String, + properties: &'a [(&'a str, &'a str)], + associated_blocks: Vec<(&'a str, &'a FxHashMap>)>, +} + +pub fn fill_complex_block_mappings( + build_config: &BuildConfig, + block_states: Vec<(u32, ComplexBlock)>, + mappings: &mut [TokenStream], +) -> Vec { + let (blocks, configs) = dedup_blocks(build_config, &block_states); + + let mut final_configs = collect_configurations(build_config, &configs, &blocks); + final_configs.sort_by(|a, b| a.name.cmp(&b.name)); + + final_configs + .into_iter() + .map(|config| { + let struct_name = format_ident!("{}", config.name); + let vtable_name = format_ident!("VTABLE_{}", config.name.to_shouty_snake_case()); + + config.associated_blocks + .iter() + .for_each(|(_, values)| { + values + .iter() + .for_each(|(id, _)| { + mappings[*id as usize] = quote! { crate::StateBehaviorTable::spin_off(&#vtable_name, #id) } + }); + }); + + quote! { + const #vtable_name: crate::BlockBehaviorTable = crate::BlockBehaviorTable::from::<#struct_name>(); + } + }) + .collect() +} + +#[allow(clippy::type_complexity)] +pub fn generate_complex_blocks( + build_config: &BuildConfig, + block_states: Vec<(u32, ComplexBlock)>, +) -> (Vec<((TokenStream, TokenStream), Ident)>, TokenStream) { + let (blocks, configs) = dedup_blocks(build_config, &block_states); + + let mut finalized_configs = collect_configurations(build_config, &configs, &blocks); + finalized_configs.sort_by(|a, b| a.name.cmp(&b.name)); + + let ((structs, impls), names): ((Vec, Vec), Vec) = + finalized_configs + .into_iter() + .map( + |FinalizedConfiguration { + name, + properties: config, + associated_blocks, + }| { + let fields = config + .iter() + .map(|(name, _)| match *name { + "type" => format_ident!("ty"), + str => format_ident!("{str}"), + }) + .collect::>(); + + let types = config + .iter() + .map(|(_, value)| format_ident!("{value}")) + .collect::>(); + + if name.starts_with("GeneratedStruct") { + println!("cargo::warning=Unknown block type detected. See src/blocks/README.md for more information. (Saved as {}, associated blocks: {:?})", + name, + associated_blocks + .iter() + .map(|(name, _)| name) + ); + } + + let struct_name = format_ident!("{name}"); + + match associated_blocks.len() { + 0 => panic!("No blocks for {struct_name}"), + 1 => { + let trait_impl = generate_trait_impls( + &struct_name, + None, + config, + &associated_blocks, + ); + + ( + ( + quote! { + #[allow(unused_imports)] + use temper_block_properties::*; + + #[allow(dead_code)] + #[derive(Clone, Debug)] + pub struct #struct_name { + #(pub #fields: #types,)* + } + }, + quote! { + #[allow(unused_imports)] + use temper_block_properties::*; + use crate::#struct_name; + + #trait_impl + }, + ), + struct_name, + ) + } + _ => { + let mut variants = associated_blocks + .iter() + .map(|(name, _)| { + format_ident!( + "{}", + name.strip_prefix("minecraft:") + .unwrap_or(name) + .to_pascal_case() + ) + }) + .collect::>(); + + variants.sort(); + + let enum_name = format_ident!("{}Type", struct_name); + let trait_impl = generate_trait_impls( + &struct_name, + Some((&enum_name, &variants)), + config, + &associated_blocks, + ); + + ( + ( + quote! { + #[allow(unused_imports)] + use temper_block_properties::*; + + #[allow(dead_code)] + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum #enum_name { + #(#variants,)* + } + + #[allow(dead_code)] + #[derive(Clone, Debug)] + pub struct #struct_name { + pub block_type: #enum_name, + #(pub #fields: #types),* + } + }, + quote! { + #[allow(unused_imports)] + use temper_block_properties::*; + use crate::#struct_name; + use crate::#enum_name; + + #trait_impl + }, + ), + struct_name, + ) + } + } + }, + ) + .unzip(); + + let mod_names = names + .iter() + .map(|name| format_ident!("{}", name.to_string().to_snake_case())) + .collect::>(); + + let impl_names = mod_names + .iter() + .map(|name| format_ident!("{}_impl", name)) + .collect::>(); + + let modules = quote! { + #( + mod #mod_names; + mod #impl_names; + pub use #mod_names::*; + )* + }; + + let data = structs + .into_iter() + .zip(impls) + .zip(names) + .collect::>(); + + (data, modules) +} + +fn property_descriptor_of(key: &str) -> &PropertyDescriptor { + TYPES + .get(key) + .unwrap_or_else(|| panic!("Property type for {key} not found!")) +} + +fn get_field_values( + values: &[(&String, &String)], + properties: &[(&str, &str)], +) -> (Vec, Vec) { + values + .iter() + .map(|(field, value)| { + ( + match field.as_str() { + "type" => format_ident!("ty"), + field => format_ident!("{field}"), + }, + { + let ty = &properties + .iter() + .find(|(name1, _)| name1 == field) + .unwrap() + .1; + (property_descriptor_of(ty).ident_for)(value) + }, + ) + }) + .unzip() +} + +#[allow(clippy::type_complexity)] +fn generate_trait_impls( + struct_name: &Ident, + enum_name: Option<(&Ident, &[Ident])>, + properties: &[(&str, &str)], + values: &[(&str, &FxHashMap>)], +) -> TokenStream { + let (from_match_arms, into_match_arms): (Vec, Vec) = match enum_name { + Some((enum_name, enum_variants)) => { + let mut values = values.iter().collect::>(); + values.sort_by_key(|(name, _)| name); + + values.iter() + .flat_map(|(name, values)| { + let name_ident = format_ident!("{}", name.strip_prefix("minecraft:").unwrap_or(name).to_pascal_case()); + + match enum_variants.iter().find(|variant| *variant == &name_ident) { + Some(variant) => { + let mut values = values.iter().collect::>(); + values.sort_by_key(|(id, _)| *id); + + let mut out = Vec::with_capacity(values.len()); + + for (id, values) in values { + let mut values = values + .iter() + .collect::>(); + + values.sort_by_key(|(field, _)| field.as_str()); + + let (fields, values) = get_field_values(&values, properties); + + let data = quote! { + #struct_name { block_type: #enum_name::#variant, #(#fields: #values),* } + }; + + out.push(( + quote! { + #id => Ok(#data) + }, + quote! { + #data => Ok(#id) + } + )) + } + + out + }, + None => panic!("could not find {} enum variant for {}", enum_name, name), + } + }) + .unzip() + } + None => { + let (_, values) = &values[0]; + + let mut values = values.iter().collect::>(); + values.sort_by_key(|(id, _)| *id); + + values + .into_iter() + .map(|(id, values)| { + let mut values = values.iter().collect::>(); + + values.sort_by_key(|(field, _)| field.as_str()); + + let (fields, values) = get_field_values(&values, properties); + + let data = quote! { + #struct_name { #(#fields: #values),* } + }; + + ( + quote! { + #id => Ok(#data) + }, + quote! { + #data => Ok(#id) + }, + ) + }) + .unzip() + } + }; + + quote! { + impl TryFrom for #struct_name { + type Error = (); + + fn try_from(data: u32) -> Result { + match data { + #(#from_match_arms),*, + _ => Err(()) + } + } + } + + impl TryInto for #struct_name { + type Error = (); + + fn try_into(self) -> Result { + #[allow(unreachable_patterns)] + match self { + #(#into_match_arms),*, + _ => Err(()) + } + } + } + } +} + +fn struct_name(config: &BuildConfig, num_structs: usize, properties: &[(&str, &str)]) -> String { + let prop_str = properties.iter().map(|(name, _)| *name).collect::>(); + let prop_str = prop_str.join("+"); + + match config.name_overrides.iter().find(|(key, _)| { + let mut split = key + .split("+") + .map(|str| str.to_string()) + .collect::>(); + split.sort(); + let new_key = split.join("+"); + + new_key == prop_str + }) { + None => format!("GeneratedStruct{}", num_structs), + Some((_, struct_name)) => struct_name.clone(), + } +} + +#[allow(clippy::type_complexity)] +fn dedup_blocks<'a>( + build_config: &'a BuildConfig, + block_states: &'a [(u32, ComplexBlock)], +) -> ( + Vec>, + Vec>, +) { + let mut missing_types = build_config + .property_types + .values() + .flatten() + .filter(|str| !TYPES.contains_key(str.as_str())) + .collect::>(); + + missing_types.sort(); + missing_types.dedup(); + + if !missing_types.is_empty() { + panic!("Missing types for {:?}", missing_types); + } + + let configurations: FxHashMap<&str, FxHashMap>> = block_states + .iter() + .map(|(id, block)| (block.name.as_str(), (*id, &block.properties))) + .fold(FxHashMap::default(), |mut acc, (k, v)| { + acc.entry(k) + .or_insert_with(FxHashMap::default) + .insert(v.0, v.1); + acc + }); + + let blocks = configurations + .into_iter() + .map(|(name, properties)| { + let property_values = properties + .values() + .flat_map(|properties| { + properties.iter().map(|(name, value)| { + let name = name.as_str(); + let value = value.as_str(); + let possible_types: Vec<&str> = match build_config.property_types.get(name) + { + None => panic!("Property type for {name} not found!"), + Some(SingleOrMultiple::Single(ty)) => vec![ty.as_str()], + Some(SingleOrMultiple::Multiple(ty)) => { + ty.iter().map(String::as_str).collect() + } + }; + + (name, (possible_types, value)) + }) + }) + .fold(HashMap::new(), |mut acc, (k, v)| { + acc.entry(k) + .or_insert_with(|| (v.0, Vec::new())) + .1 + .push(v.1); + + acc + }); + + let mut property_map: Vec<(&str, &str)> = property_values + .into_iter() + .map(|(name, (possible_types, values))| { + let property_type = possible_types + .into_iter() + .find(|ty| { + values + .iter() + .all(|value| (property_descriptor_of(ty).matches_values)(value)) + }) + .unwrap_or_else(|| { + panic!("Failed to find property type for values {values:?}") + }); + + (name, property_type) + }) + .collect(); + + property_map.sort(); + + BlockStateConfiguration { + name, + properties: property_map, + values: properties, + } + }) + .collect::>(); + + let mut configs = blocks + .iter() + .map(|block| block.properties.to_vec()) + .collect::>(); + + configs.sort(); + configs.dedup(); + + (blocks, configs) +} + +fn collect_configurations<'a>( + build_config: &'a BuildConfig, + configs: &'a [Vec<(&'a str, &'a str)>], + blocks: &'a [BlockStateConfiguration<'a>], +) -> Vec> { + let mut num_structs = 0; + + configs + .iter() + .flat_map(|config| { + let associated_blocks = blocks + .iter() + .filter(|block| &block.properties == config) + .collect::>(); + + let mut configs = vec![FinalizedConfiguration { + name: struct_name(build_config, num_structs, config), + properties: config, + associated_blocks: Vec::with_capacity(associated_blocks.len()), + }]; + + for block in associated_blocks { + if let Some(name) = build_config.block_overrides.get(block.name) { + match configs + .iter_mut() + .find(|config| config.name == name.as_str()) + { + Some(config) => config.associated_blocks.push((block.name, &block.values)), + None => configs.push(FinalizedConfiguration { + name: name.clone(), + properties: config.as_slice(), + associated_blocks: vec![(block.name, &block.values)], + }), + } + } else { + configs[0] + .associated_blocks + .push((block.name, &block.values)); + } + } + + if configs[0].associated_blocks.is_empty() { + configs.remove(0); + } + + num_structs += 1; + + configs + }) + .collect() +} diff --git a/src/blocks/crates/build/src/config.rs b/src/blocks/crates/build/src/config.rs new file mode 100644 index 00000000..bfcb698e --- /dev/null +++ b/src/blocks/crates/build/src/config.rs @@ -0,0 +1,44 @@ +use crate::BlockState; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Deserialize)] +#[serde(untagged)] +pub enum SingleOrMultiple { + Single(String), + Multiple(Vec), +} + +#[derive(Deserialize)] +pub struct BuildConfig { + pub name_overrides: HashMap, + pub block_overrides: HashMap, + pub property_types: HashMap, +} + +pub const BUILD_CONFIG: &str = include_str!("../../../build_config.toml"); +pub const BLOCK_STATES: &str = include_str!("../../../../../assets/data/blockstates.json"); + +pub fn get_build_config() -> BuildConfig { + toml::from_str(BUILD_CONFIG).unwrap() +} + +pub fn get_block_states() -> HashMap { + let out: HashMap = serde_json::from_str(BLOCK_STATES).unwrap(); + + out.into_iter() + .map(|(k, v)| (k.parse::().unwrap(), v)) + .collect() +} + +impl<'a> IntoIterator for &'a SingleOrMultiple { + type Item = &'a String; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + SingleOrMultiple::Single(v) => vec![v].into_iter(), + SingleOrMultiple::Multiple(vals) => vals.iter().collect::>().into_iter(), + } + } +} diff --git a/src/blocks/crates/build/src/lib.rs b/src/blocks/crates/build/src/lib.rs new file mode 100644 index 00000000..0c2e5c16 --- /dev/null +++ b/src/blocks/crates/build/src/lib.rs @@ -0,0 +1,68 @@ +use crate::complex::ComplexBlock; +use fxhash::FxHashMap; +use serde::Deserialize; +use std::collections::HashMap; +use std::io::Write; + +pub mod complex; +pub mod config; +pub mod simple; + +#[derive(Deserialize, Debug)] +pub struct BlockState { + name: String, + properties: Option>, +} + +#[allow(clippy::type_complexity)] +pub fn separate_blocks( + input: HashMap, +) -> (Vec<(u32, String)>, Vec<(u32, ComplexBlock)>) { + let mut simple_blocks = Vec::new(); + let mut complex_blocks = Vec::new(); + + for (id, state) in input.into_iter() { + match state.properties { + Some(properties) => complex_blocks.push(( + id, + ComplexBlock { + name: state.name, + properties: FxHashMap::from_iter(properties), + }, + )), + None => simple_blocks.push((id, state.name)), + } + } + + (simple_blocks, complex_blocks) +} + +pub fn format_code(unformatted_code: &str) -> String { + let mut child = std::process::Command::new("rustfmt") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn rustfmt process."); + + child + .stdin + .take() + .expect("Failed to take rustfmt stdin") + .write_all(unformatted_code.as_bytes()) + .expect("Failed to write to rustfmt stdin."); + + let output = child + .wait_with_output() + .expect("Failed to wait for rustfmt process."); + + if output.status.success() { + String::from_utf8(output.stdout).expect("rustfmt output was not valid UTF-8.") + } else { + panic!( + "rustfmt failed with status: {}\n--- stderr ---\n{}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + } +} diff --git a/src/blocks/crates/build/src/simple.rs b/src/blocks/crates/build/src/simple.rs new file mode 100644 index 00000000..c79a7276 --- /dev/null +++ b/src/blocks/crates/build/src/simple.rs @@ -0,0 +1,131 @@ +use crate::config::BuildConfig; +use heck::{ToPascalCase, ToShoutySnakeCase}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use std::collections::HashMap; + +fn separate_enums( + build_config: &BuildConfig, + mut simple_blocks: Vec<(u32, String)>, +) -> HashMap> { + simple_blocks.sort_by_key(|(id, _)| *id); + + let mut enums = HashMap::new(); + + for (block_id, enum_name) in build_config.block_overrides.iter() { + let Some((idx, _)) = simple_blocks + .iter() + .enumerate() + .find(|(_, (_, id))| id == block_id) + else { + continue; + }; + + let entry = enums.entry(enum_name.clone()).or_insert_with(Vec::new); + + entry.push(simple_blocks.remove(idx)); + } + + enums.insert("SimpleBlock".to_string(), simple_blocks); + + enums +} + +pub fn fill_simple_block_mappings( + build_config: &BuildConfig, + simple_blocks: Vec<(u32, String)>, + mappings: &mut [TokenStream], +) -> TokenStream { + let enums = separate_enums(build_config, simple_blocks); + + let mut vtables = Vec::new(); + + for (enum_name, variants) in enums { + let vtable_name = format_ident!("VTABLE_{}", enum_name.to_shouty_snake_case()); + let enum_name = format_ident!("{}", enum_name); + + for (id, _) in variants { + mappings[id as usize] = + quote! { crate::StateBehaviorTable::spin_off(&#vtable_name, #id) }; + } + + vtables.push(quote! { + const #vtable_name: crate::BlockBehaviorTable = crate::BlockBehaviorTable::from::<#enum_name>(); + }); + } + + quote! { + #(#vtables)* + } +} + +pub fn generate_simple_block_enum( + build_config: &BuildConfig, + simple_blocks: Vec<(u32, String)>, +) -> (TokenStream, TokenStream) { + let enums = separate_enums(build_config, simple_blocks); + + let (enums, impls): (Vec, Vec) = enums + .into_iter() + .map(|(enum_name, enum_blocks)| { + let mut map_entries = Vec::new(); + let mut from_arms = Vec::new(); + let mut enum_variants = Vec::new(); + + let map_name = format_ident!("{}_BLOCK_MAP", enum_name.to_shouty_snake_case()); + let enum_name = format_ident!("{}", enum_name); + + for (id, name) in enum_blocks { + let variant = name.strip_prefix("minecraft:").unwrap_or(&name); + let variant = format_ident!("{}", variant.to_pascal_case()); + + enum_variants.push(quote! { #variant }); + from_arms.push(quote! { #id => Ok(#enum_name::#variant) }); + map_entries.push(quote! { #id }); + } + + ( + quote! { + #[repr(usize)] + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum #enum_name { + #(#enum_variants),* + } + }, + quote! { + const #map_name: &[u32] = &[ + #(#map_entries),* + ]; + + impl TryFrom for #enum_name { + type Error = (); + + fn try_from(data: u32) -> Result { + match data { + #(#from_arms),*, + _ => Err(()), + } + } + } + + impl TryInto for #enum_name { + type Error = (); + + fn try_into(self) -> Result { + Ok(#map_name[self as usize]) + } + } + }, + ) + }) + .unzip(); + + ( + quote! { + #(#enums)* + }, + quote! { + #(#impls)* + }, + ) +} diff --git a/src/blocks/crates/data/Cargo.toml b/src/blocks/crates/data/Cargo.toml new file mode 100644 index 00000000..31811c8a --- /dev/null +++ b/src/blocks/crates/data/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "temper-block-data" +version = "0.1.0" +edition = "2024" + +[dependencies] +temper-world = { workspace = true } +temper-core = { workspace = true } +bevy_math = { workspace = true } +temper-components = { workspace = true } \ No newline at end of file diff --git a/src/blocks/crates/data/src/lib.rs b/src/blocks/crates/data/src/lib.rs new file mode 100644 index 00000000..46fa501a --- /dev/null +++ b/src/blocks/crates/data/src/lib.rs @@ -0,0 +1,56 @@ +use bevy_math::DVec3; +use std::collections::HashMap; +use temper_components::player::rotation::Rotation; +use temper_core::block_face::BlockFace; +use temper_core::block_state_id::BlockStateId; +use temper_core::dimension::Dimension; +use temper_core::pos::BlockPos; +use temper_world::World; + +#[derive(Clone)] +pub struct PlacementContext<'a> { + pub face: BlockFace, + pub cursor: DVec3, + pub block_clicked: BlockPos, + pub block_pos: BlockPos, + pub level: &'a World, + pub dimension: Dimension, + pub player_rotation: &'a Rotation, +} + +/// Result of the get_placement_state function +pub struct PlacedBlocks { + /// Any extra blocks placed by the function + pub blocks: HashMap, + + /// Whether an item is taken from the player's inventory or not + pub take_item: bool, + + // TODO: when version 2 of the block system is implemented, this can be removed and will be replaced with an Option + /// Whether to place the original block or now + pub place_original: bool, +} + +/// Result of the try_break function +#[derive(Default)] +pub struct BrokenBlocks { + /// Any extra blocks broken by the function + pub blocks: Vec, +} + +/// Result of the interact function +#[derive(Default)] +pub struct BlockUpdates { + /// Any other blocks that are updated by the function + pub blocks: HashMap, +} + +impl Default for PlacedBlocks { + fn default() -> Self { + Self { + blocks: HashMap::default(), + take_item: true, + place_original: true, + } + } +} diff --git a/src/blocks/crates/generated/Cargo.toml b/src/blocks/crates/generated/Cargo.toml new file mode 100644 index 00000000..8fd32b3a --- /dev/null +++ b/src/blocks/crates/generated/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "temper-blocks-generated" +version = "0.1.0" +edition = "2024" + +[dependencies] +temper-block-properties = { path = "../property" } + +[build-dependencies] +temper-blocks-build = { path = "../build" } +heck = "0.5" +quote = { workspace = true } \ No newline at end of file diff --git a/src/blocks/crates/generated/build.rs b/src/blocks/crates/generated/build.rs new file mode 100644 index 00000000..23b5201d --- /dev/null +++ b/src/blocks/crates/generated/build.rs @@ -0,0 +1,57 @@ +use heck::ToSnakeCase; +use std::fs; +use temper_blocks_build::complex::generate_complex_blocks; +use temper_blocks_build::config::{get_block_states, get_build_config}; +use temper_blocks_build::simple::generate_simple_block_enum; +use temper_blocks_build::{format_code, separate_blocks}; + +fn main() { + let build_config = get_build_config(); + let block_states = get_block_states(); + + let (simple_blocks, complex_blocks) = separate_blocks(block_states); + + let (simple_enum, enum_impl) = generate_simple_block_enum(&build_config, simple_blocks); + let (block_structs, block_mod) = generate_complex_blocks(&build_config, complex_blocks); + + let out_dir = std::env::var("OUT_DIR").unwrap(); + + let enum_impl = quote::quote! { + use crate::*; + + #enum_impl + }; + + fs::write( + format!("{}/blocks.rs", out_dir), + format_code(&block_mod.to_string()), + ) + .unwrap(); + fs::write( + format!("{}/simple.rs", out_dir), + format_code(&simple_enum.to_string()), + ) + .unwrap(); + fs::write( + format!("{}/simple_impl.rs", out_dir), + format_code(&enum_impl.to_string()), + ) + .unwrap(); + + block_structs + .into_iter() + .for_each(|((structure, struct_impl), name)| { + let mod_name = name.to_string().to_snake_case(); + + fs::write( + format!("{}/{}.rs", out_dir, mod_name), + format_code(&structure.to_string()), + ) + .unwrap(); + fs::write( + format!("{}/{}_impl.rs", out_dir, mod_name), + format_code(&struct_impl.to_string()), + ) + .unwrap(); + }); +} diff --git a/src/blocks/crates/generated/src/blocks.rs b/src/blocks/crates/generated/src/blocks.rs new file mode 100644 index 00000000..e45e5b88 --- /dev/null +++ b/src/blocks/crates/generated/src/blocks.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/blocks.rs")); diff --git a/src/blocks/crates/generated/src/lib.rs b/src/blocks/crates/generated/src/lib.rs new file mode 100644 index 00000000..e5fc53e9 --- /dev/null +++ b/src/blocks/crates/generated/src/lib.rs @@ -0,0 +1,6 @@ +mod blocks; +mod simple; +mod simple_impl; + +pub use blocks::*; +pub use simple::*; diff --git a/src/blocks/crates/generated/src/simple.rs b/src/blocks/crates/generated/src/simple.rs new file mode 100644 index 00000000..bbf103d3 --- /dev/null +++ b/src/blocks/crates/generated/src/simple.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/simple.rs")); diff --git a/src/blocks/crates/generated/src/simple_impl.rs b/src/blocks/crates/generated/src/simple_impl.rs new file mode 100644 index 00000000..496435cc --- /dev/null +++ b/src/blocks/crates/generated/src/simple_impl.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/simple_impl.rs")); diff --git a/src/blocks/crates/property/Cargo.toml b/src/blocks/crates/property/Cargo.toml new file mode 100644 index 00000000..86235c0d --- /dev/null +++ b/src/blocks/crates/property/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "temper-block-properties" +version = "0.1.0" +edition = "2024" + +[dependencies] +bevy_math = { workspace = true } +proc-macro2 = { workspace = true, optional = true } +quote = { workspace = true, optional = true } +lazy_static = { workspace = true, optional = true } + +[features] +default = [] +block-struct-generation = ["dep:proc-macro2", "dep:quote", "dep:lazy_static"] \ No newline at end of file diff --git a/src/blocks/crates/property/src/double_block_half.rs b/src/blocks/crates/property/src/double_block_half.rs new file mode 100644 index 00000000..a886bed5 --- /dev/null +++ b/src/blocks/crates/property/src/double_block_half.rs @@ -0,0 +1,24 @@ +use crate::enum_property; +use bevy_math::IVec3; + +enum_property!( + DoubleBlockHalf, + Upper => "upper", + Lower => "lower", +); + +impl DoubleBlockHalf { + pub fn other_half(&self) -> Self { + match self { + Self::Upper => Self::Lower, + Self::Lower => Self::Upper, + } + } + + pub fn direction_to_other(&self) -> IVec3 { + match self { + Self::Upper => IVec3::new(0, -1, 0), + Self::Lower => IVec3::new(0, 1, 0), + } + } +} diff --git a/src/blocks/crates/property/src/lib.rs b/src/blocks/crates/property/src/lib.rs new file mode 100644 index 00000000..96c7c4dd --- /dev/null +++ b/src/blocks/crates/property/src/lib.rs @@ -0,0 +1,149 @@ +use std::str::FromStr; + +mod double_block_half; +mod note_block_instrument; +mod simple; + +pub use double_block_half::DoubleBlockHalf; +pub use note_block_instrument::NoteBlockInstrument; +pub use simple::*; + +#[cfg(feature = "block-struct-generation")] +lazy_static::lazy_static! { + pub static ref TYPES: std::collections::HashMap<&'static str, PropertyDescriptor> = { + let mut map = std::collections::HashMap::new(); + + map.insert("i32", PropertyDescriptor::I32); + map.insert("bool", PropertyDescriptor::BOOL); + map.insert("AttachFace", AttachFace::DESCRIPTOR); + map.insert("Axis", Axis::DESCRIPTOR); + map.insert("BambooLeaves", BambooLeaves::DESCRIPTOR); + map.insert("BedPart", BedPart::DESCRIPTOR); + map.insert("BellAttachType", BellAttachType::DESCRIPTOR); + map.insert("ChestType", ChestType::DESCRIPTOR); + map.insert("CreakingHeartState", CreakingHeartState::DESCRIPTOR); + map.insert("ComparatorMode", ComparatorMode::DESCRIPTOR); + map.insert("Direction", Direction::DESCRIPTOR); + map.insert("DoorHingeSide", DoorHingeSide::DESCRIPTOR); + map.insert("DoubleBlockHalf", DoubleBlockHalf::DESCRIPTOR); + map.insert("DripstoneThickness", DripstoneThickness::DESCRIPTOR); + map.insert("FrontAndTop", FrontAndTop::DESCRIPTOR); + map.insert("Half", Half::DESCRIPTOR); + map.insert("NoteBlockInstrument", NoteBlockInstrument::DESCRIPTOR); + map.insert("PistonType", PistonType::DESCRIPTOR); + map.insert("CopperGolemPose", CopperGolemPose::DESCRIPTOR); + map.insert("RailShape", RailShape::DESCRIPTOR); + map.insert("RedstoneSide", RedstoneSide::DESCRIPTOR); + map.insert("SculkSensorPhase", SculkSensorPhase::DESCRIPTOR); + map.insert("SideChainPart", SideChainPart::DESCRIPTOR); + map.insert("SlabType", SlabType::DESCRIPTOR); + map.insert("StairsShape", StairsShape::DESCRIPTOR); + map.insert("StructureMode", StructureMode::DESCRIPTOR); + map.insert("TestBlockMode", TestBlockMode::DESCRIPTOR); + map.insert("Tilt", Tilt::DESCRIPTOR); + map.insert("TrialSpawnerState", TrialSpawnerState::DESCRIPTOR); + map.insert("VaultState", VaultState::DESCRIPTOR); + map.insert("WallSide", WallSide::DESCRIPTOR); + + map + }; +} + +#[cfg(feature = "block-struct-generation")] +pub struct PropertyDescriptor { + pub matches_values: fn(&str) -> bool, + pub ident_for: fn(&str) -> proc_macro2::TokenStream, +} + +#[cfg(feature = "block-struct-generation")] +impl PropertyDescriptor { + const I32: PropertyDescriptor = PropertyDescriptor { + matches_values: |str| str.parse::().is_ok(), + ident_for: |str| { + let val = str.parse::().expect("failed to parse i32"); + quote::quote! { #val } + }, + }; + + const BOOL: PropertyDescriptor = PropertyDescriptor { + matches_values: |str| matches!(str, "true" | "false"), + ident_for: |str| { + let val = str.parse::().expect("failed to parse bool"); + quote::quote! { #val } + }, + }; +} + +/// Marker trait for types that can be used as block state property values +pub trait BlockStateProperty: FromStr + ToString { + fn values(&self) -> &[&str] { + &[] + } +} + +impl BlockStateProperty for i32 {} +impl BlockStateProperty for bool {} + +/// Helper macro to implement enum property types. The syntax is simple: +/// +/// ```rust +/// enum_property!( +/// MyBlockStateProperty, // Enum name +/// A => "a", // Enum variants and their block state property string value +/// West => "west", +/// // Etc... +/// ); +/// ``` +#[macro_export] +macro_rules! enum_property { + ($name:ident, $($variant:ident => $variant_str:expr),* $(,)?) => { + #[derive(Clone, Debug)] + pub enum $name { + $($variant),* + } + + #[cfg(feature = "block-struct-generation")] + impl $name { + pub const DESCRIPTOR: $crate::PropertyDescriptor = $crate::PropertyDescriptor { + matches_values: Self::matches_values, + ident_for: Self::ident_for, + }; + + fn matches_values(str: &str) -> bool { + matches!(str, $($variant_str)|*) + } + + fn ident_for(str: &str) -> proc_macro2::TokenStream { + match str { + $($variant_str => quote::quote!{ $name::$variant }),*, + str => panic!("{} is not a valid member of enum property {}", str, stringify!($name)), + } + } + } + + impl std::str::FromStr for $name { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + $($variant_str => Ok($name::$variant)),*, + s => Err(format!("Unknown property for {}: {}", stringify!($name), s)), + } + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + $($name::$variant => write!(f, $variant_str)),* + } + } + } + + impl $crate::BlockStateProperty for $name { + fn values(&self) -> &[&str] { + &[$($variant_str),*] + } + } + }; +} diff --git a/src/blocks/crates/property/src/note_block_instrument.rs b/src/blocks/crates/property/src/note_block_instrument.rs new file mode 100644 index 00000000..17acd8fa --- /dev/null +++ b/src/blocks/crates/property/src/note_block_instrument.rs @@ -0,0 +1,30 @@ +use crate::enum_property; + +enum_property!( + NoteBlockInstrument, + Harp => "harp", + BaseDrum => "basedrum", + Snare => "snare", + Hat => "hat", + Bass => "bass", + Flute => "flute", + Bell => "bell", + Guitar => "guitar", + Chime => "chime", + Xylophone => "xylophone", + IronXylophone => "iron_xylophone", + CowBell => "cow_bell", + Didgeridoo => "didgeridoo", + Bit => "bit", + Banjo => "banjo", + Pling => "pling", + Zombie => "zombie", + Skeleton => "skeleton", + Creeper => "creeper", + Dragon => "dragon", + WitherSkeleton => "wither_skeleton", + Piglin => "piglin", + CustomHead => "custom_head", +); + +// TODO: return sound events based on the variant diff --git a/src/blocks/crates/property/src/simple.rs b/src/blocks/crates/property/src/simple.rs new file mode 100644 index 00000000..0f2039fd --- /dev/null +++ b/src/blocks/crates/property/src/simple.rs @@ -0,0 +1,219 @@ +/// All of these enums come directly from the enums found in the `net.minecraft.world.level.block.state.properties` package. +use crate::enum_property; + +enum_property!( + AttachFace, + Floor => "floor", + Wall => "wall", + Ceiling => "ceiling", +); + +enum_property!( + Axis, + X => "x", + Y => "y", + Z => "z", +); + +enum_property!( + BambooLeaves, + None => "none", + Small => "small", + Large => "large", +); + +enum_property!( + BedPart, + Head => "head", + Foot => "foot", +); + +enum_property!( + BellAttachType, + Floor => "floor", + Ceiling => "ceiling", + SingleWall => "single_wall", + DoubleWall => "double_wall", +); + +enum_property!( + ChestType, + Single => "single", + Left => "left", + Right => "right", +); + +enum_property!( + ComparatorMode, + Compare => "compare", + Subtract => "subtract", +); + +enum_property!( + CreakingHeartState, + Uprooted => "uprooted", + Dormant => "dormant", + Awake => "awake", +); + +enum_property!( + Direction, + Down => "down", + Up => "up", + North => "north", + South => "south", + East => "east", + West => "west", +); + +enum_property!( + DoorHingeSide, + Left => "left", + Right => "right", +); + +enum_property!( + DripstoneThickness, + TipMerge => "tip_merge", + Tip => "tip", + Frustum => "frustum", + Middle => "middle", + Base => "base", +); + +enum_property!( + FrontAndTop, + DownEast => "down_east", + DownNorth => "down_north", + DownSouth => "down_south", + DownWest => "down_west", + UpEast => "up_east", + UpNorth => "up_north", + UpSouth => "up_south", + UpWest => "up_west", + WestUp => "west_up", + EastUp => "east_up", + NorthUp => "north_up", + SouthUp => "south_up", +); + +enum_property!( + Half, + Top => "top", + Bottom => "bottom", +); + +enum_property!( + CopperGolemPose, + Standing => "standing", + Sitting => "sitting", + Running => "running", + Star => "star", +); + +enum_property!( + PistonType, + Default => "normal", + Sticky => "sticky", +); + +enum_property!( + RailShape, + NorthSouth => "north_south", + EastWest => "east_west", + AscendingEast => "ascending_east", + AscendingWest => "ascending_west", + AscendingNorth => "ascending_north", + AscendingSouth => "ascending_south", + SouthEast => "south_east", + SouthWest => "south_west", + NorthWest => "north_west", + NorthEast => "north_east", +); + +enum_property!( + RedstoneSide, + Up => "up", + Side => "side", + None => "none", +); + +enum_property!( + SculkSensorPhase, + Inactive => "inactive", + Active => "active", + Cooldown => "cooldown", +); + +enum_property!( + SideChainPart, + Unconnected => "unconnected", + Right => "right", + Center => "center", + Left => "left", +); + +enum_property!( + SlabType, + Top => "top", + Bottom => "bottom", + Double => "double", +); + +enum_property!( + StairsShape, + Straight => "straight", + InnerLeft => "inner_left", + InnerRight => "inner_right", + OuterLeft => "outer_left", + OuterRight => "outer_right", +); + +enum_property!( + StructureMode, + Save => "save", + Load => "load", + Corner => "corner", + Data => "data", +); + +enum_property!( + TestBlockMode, + Start => "start", + Log => "log", + Fail => "fail", + Accept => "accept", +); + +enum_property!( + Tilt, + None => "none", + Unstable => "unstable", + Partial => "partial", + Full => "full", +); + +enum_property!( + TrialSpawnerState, + Inactive => "inactive", + WaitingForPlayers => "waiting_for_players", + Active => "active", + WaitingForRewardEjection => "waiting_for_reward_ejection", + EjectingReward => "ejecting_reward", + Cooldown => "cooldown", +); + +enum_property!( + VaultState, + Inactive => "inactive", + Active => "active", + Unlocking => "unlocking", + Ejecting => "ejecting", +); + +enum_property!( + WallSide, + None => "none", + Low => "low", + Tall => "tall", +); diff --git a/src/blocks/src/bed_block.rs b/src/blocks/src/bed_block.rs new file mode 100644 index 00000000..6616f7ea --- /dev/null +++ b/src/blocks/src/bed_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BedBlock; + +impl BlockBehavior for BedBlock {} diff --git a/src/blocks/src/behavior_trait.rs b/src/blocks/src/behavior_trait.rs new file mode 100644 index 00000000..c6c7b8f9 --- /dev/null +++ b/src/blocks/src/behavior_trait.rs @@ -0,0 +1,167 @@ +use crate::BLOCK_MAPPINGS; +use temper_block_data::{BlockUpdates, BrokenBlocks, PlacedBlocks, PlacementContext}; +use temper_core::block_state_id::BlockStateId; +use temper_core::pos::BlockPos; +use temper_world::World; + +/// Macro to autogenerate the `BlockBehavior` trait and associated VTable structs. +/// +/// This macro simply exists to make adding methods to blocks easier. It should NOT be used anywhere except in this file, and should also only be used once. +/// See below this macro for where to add functions. +/// +/// The syntax for this macro is as follows: `fn ([mut]; ) [-> ; ]` +/// - `name`: The name of the method +/// - `mut`: Optional, whether the function takes a mutable reference to the block or not +/// - `arguments`: Any additional arguments to the method +/// - `return type`: Optional, what the function returns +/// - `default return value`: Optional, required if return type is set, the default expression to use for blocks that do not implement this method. +/// +/// TODO: Later on, add support for methods like BlockStateId::(base_id) -> (BlockStateId, ). Would be useful for methods like +/// get_placement_state(). +macro_rules! block_behavior_trait { + ($(fn $name:ident($($mut_meta:ident)?; $($argument:ident: $ty:ty),*) $(-> $ret:ty; $default:expr)?),* $(,)?) => { + macro_rules! ptr_ret_ty { + (mut) => { + u32 + }; + () => { + () + }; + (mut $retb:ty) => { + (u32, $retb) + }; + ($retb:ty) => { + $retb + }; + } + + macro_rules! id_ret_decode { + (mut, $in_data:expr) => { + ($in_data, ()) + }; + (, $in_data:expr) => { + ((), ()) + }; + (mut $retb:ty, $in_data:expr) => { + $in_data + }; + ($retb:ty, $in_data:expr) => { + ((), $in_data) + }; + } + + macro_rules! lambda_ret_ty { + (mut; $data:expr;) => { + $data + .try_into() + .unwrap_or_else(|_| panic!("Failed to convert block data back into id")) + }; + () => { + () + }; + (mut; $data:expr; $retb:ty; $ret_val:expr) => { + ( + $data + .try_into() + .unwrap_or_else(|_| panic!("Failed to convert block data back into id")), + $ret_val, + ) + }; + ($retb:ty; $ret_val:expr) => { + $ret_val + }; + } + + pub trait BlockBehavior: + TryInto + TryFrom + Clone + std::fmt::Debug + { + $( + fn $name(&$($mut_meta)? self, $($argument: $ty),*) $(-> $ret)? { $($default)? } + )* + } + + #[allow(dead_code)] + pub trait BlockDispatch { + fn try_cast(&self) -> Option; + + $( + fn $name(&$($mut_meta)? self, $($argument: $ty),*) $(-> $ret)?; + )* + } + + pub struct BlockBehaviorTable { + $( + $name: fn(id: u32, $($argument: $ty),*) -> ptr_ret_ty!{$($mut_meta)? $($ret)?} + ),* + } + + impl BlockBehaviorTable { + pub const fn from() -> Self { + Self { + $( + $name: |id, $($argument),*| { + let $($mut_meta)? data = T::try_from(id).unwrap_or_else(|_| panic!("Failed to convert id to data")); + let _ret = data.$name($($argument),*); + lambda_ret_ty!($($mut_meta; data;)? $($ret; _ret)?) + } + ),* + } + } + } + + pub struct StateBehaviorTable { + block: &'static BlockBehaviorTable, + id: u32, + } + + impl StateBehaviorTable { + pub const fn spin_off(block: &'static BlockBehaviorTable, id: u32) -> Self { + Self { block, id } + } + + $( + pub fn $name(&self, $($argument: $ty),*) -> ptr_ret_ty!{$($mut_meta)? $($ret)?} { + (self.block.$name)(self.id, $($argument),*) + } + )* + } + + macro_rules! update_self { + (mut, $s:expr, $new_id:expr) => { + *$s = Self::new($new_id); + }; + (, $s:expr, $new_id:expr) => { + + }; + } + + impl BlockDispatch for BlockStateId { + fn try_cast(&self) -> Option { + T::try_from(self.raw()).ok() + } + + $( + fn $name(& $($mut_meta)? self, $($argument: $ty),*) $(-> $ret)? { + let (_new_id, _ret) = id_ret_decode!{$($mut_meta)? $($ret)?, BLOCK_MAPPINGS[self.raw() as usize].$name($($argument),*)}; + update_self!{$($mut_meta)?, self, _new_id} + _ret + } + )* + } + }; +} + +// This is where methods are defined for blocks. See the macro above for the syntax. +// +// This is the only place where the `block_behavior_trait!` macro should be used. +block_behavior_trait!( + fn get_placement_state(mut; _context: PlacementContext) -> PlacedBlocks; PlacedBlocks::default(), + fn interact(mut; _world: &World, _pos: BlockPos) -> BlockUpdates; BlockUpdates::default(), + fn try_break(; _world: &World, _pos: BlockPos) -> BrokenBlocks; BrokenBlocks::default(), + + fn is_interactable(;) -> bool; false, + fn is_solid(;) -> bool; true, // Used for fence and pane connections + + fn can_be_replaced(; _context: PlacementContext) -> bool; false, + fn update(mut; _world: &World, _pos: BlockPos) -> bool; false, // The boolean returned is whether to emit more adjacent updates from this block +); diff --git a/src/blocks/src/bubble_column.rs b/src/blocks/src/bubble_column.rs new file mode 100644 index 00000000..64fbff99 --- /dev/null +++ b/src/blocks/src/bubble_column.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BubbleColumnBlock; + +impl BlockBehavior for BubbleColumnBlock {} diff --git a/src/blocks/src/building/mod.rs b/src/blocks/src/building/mod.rs new file mode 100644 index 00000000..7ead741d --- /dev/null +++ b/src/blocks/src/building/mod.rs @@ -0,0 +1,4 @@ +mod pillar_block; +mod simple_block; +mod slab; +mod stairs; diff --git a/src/blocks/src/building/pillar_block.rs b/src/blocks/src/building/pillar_block.rs new file mode 100644 index 00000000..a046026d --- /dev/null +++ b/src/blocks/src/building/pillar_block.rs @@ -0,0 +1,22 @@ +use crate::BlockBehavior; +use std::collections::HashMap; +use temper_block_data::{PlacedBlocks, PlacementContext}; +use temper_block_properties::Axis; +use temper_blocks_generated::PillarBlock; +use temper_core::block_face::BlockFace; + +impl BlockBehavior for PillarBlock { + fn get_placement_state(&mut self, context: PlacementContext) -> PlacedBlocks { + self.axis = match context.face { + BlockFace::Top | BlockFace::Bottom => Axis::Y, + BlockFace::North | BlockFace::South => Axis::Z, + BlockFace::East | BlockFace::West => Axis::X, + }; + + PlacedBlocks { + take_item: true, + blocks: HashMap::with_capacity(0), + place_original: true, + } + } +} diff --git a/src/blocks/src/building/simple_block.rs b/src/blocks/src/building/simple_block.rs new file mode 100644 index 00000000..e9837b9a --- /dev/null +++ b/src/blocks/src/building/simple_block.rs @@ -0,0 +1,13 @@ +use crate::BlockBehavior; +use temper_block_data::PlacementContext; +use temper_blocks_generated::SimpleBlock; + +impl BlockBehavior for SimpleBlock { + fn is_solid(&self) -> bool { + !matches!(self, SimpleBlock::Air | SimpleBlock::CaveAir) + } + + fn can_be_replaced(&self, _context: PlacementContext) -> bool { + matches!(self, SimpleBlock::Air | SimpleBlock::CaveAir) + } +} diff --git a/src/blocks/src/building/slab.rs b/src/blocks/src/building/slab.rs new file mode 100644 index 00000000..81f7d369 --- /dev/null +++ b/src/blocks/src/building/slab.rs @@ -0,0 +1,47 @@ +use crate::{BlockBehavior, BlockDispatch}; +use temper_block_data::{PlacedBlocks, PlacementContext}; +use temper_block_properties::SlabType; +use temper_blocks_generated::SlabBlock; +use temper_core::block_face::BlockFace; +use temper_core::block_state_id::BlockStateId; +use temper_macros::match_block; + +impl BlockBehavior for SlabBlock { + fn get_placement_state(&mut self, context: PlacementContext) -> PlacedBlocks { + let block = context + .level + .get_chunk(context.block_pos.chunk(), context.dimension) + .map(|c| c.get_block(context.block_pos.chunk_block_pos())) + .unwrap_or(BlockStateId::new(0)); + + self.ty = if let Some(block) = block.try_cast::() { + if block.block_type == self.block_type { + SlabType::Double + } else { + // This will cause a bug where if you place a slab onto a slab of a different type it will replace the block with the one you're placing, + // but this can't really be fixed until BlockBehavior is updated + return PlacedBlocks::default(); // TODO: When BlockBehavior is updated with a return value for Option, return None here + } + } else { + match context.face { + BlockFace::Top => SlabType::Bottom, + BlockFace::Bottom => SlabType::Top, + _ => { + if context.cursor.y > 0.5 { + SlabType::Top + } else { + SlabType::Bottom + } + } + } + }; + + self.waterlogged = match_block!("water", block); + + PlacedBlocks::default() + } + + fn can_be_replaced(&self, _context: PlacementContext) -> bool { + true + } +} diff --git a/src/blocks/src/building/stairs.rs b/src/blocks/src/building/stairs.rs new file mode 100644 index 00000000..41d85074 --- /dev/null +++ b/src/blocks/src/building/stairs.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::StairsBlock; + +impl BlockBehavior for StairsBlock {} diff --git a/src/blocks/src/cake.rs b/src/blocks/src/cake.rs new file mode 100644 index 00000000..59f0b14f --- /dev/null +++ b/src/blocks/src/cake.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CakeBlock; + +impl BlockBehavior for CakeBlock {} diff --git a/src/blocks/src/candle_cake.rs b/src/blocks/src/candle_cake.rs new file mode 100644 index 00000000..01b02f9d --- /dev/null +++ b/src/blocks/src/candle_cake.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CandleCakeBlock; + +impl BlockBehavior for CandleCakeBlock {} diff --git a/src/blocks/src/decorative/banner.rs b/src/blocks/src/decorative/banner.rs new file mode 100644 index 00000000..9a54e979 --- /dev/null +++ b/src/blocks/src/decorative/banner.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BannerBlock; + +impl BlockBehavior for BannerBlock {} diff --git a/src/blocks/src/decorative/bell.rs b/src/blocks/src/decorative/bell.rs new file mode 100644 index 00000000..fd7bd9d2 --- /dev/null +++ b/src/blocks/src/decorative/bell.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BellBlock; + +impl BlockBehavior for BellBlock {} diff --git a/src/blocks/src/decorative/candle.rs b/src/blocks/src/decorative/candle.rs new file mode 100644 index 00000000..5196705e --- /dev/null +++ b/src/blocks/src/decorative/candle.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CandleBlock; + +impl BlockBehavior for CandleBlock {} diff --git a/src/blocks/src/decorative/chain.rs b/src/blocks/src/decorative/chain.rs new file mode 100644 index 00000000..a7b416ab --- /dev/null +++ b/src/blocks/src/decorative/chain.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ChainBlock; + +impl BlockBehavior for ChainBlock {} diff --git a/src/blocks/src/decorative/chiseled_bookshelf.rs b/src/blocks/src/decorative/chiseled_bookshelf.rs new file mode 100644 index 00000000..cf554ba7 --- /dev/null +++ b/src/blocks/src/decorative/chiseled_bookshelf.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ChiseledBookshelfBlock; + +impl BlockBehavior for ChiseledBookshelfBlock {} diff --git a/src/blocks/src/decorative/decorated_pot.rs b/src/blocks/src/decorative/decorated_pot.rs new file mode 100644 index 00000000..493276ca --- /dev/null +++ b/src/blocks/src/decorative/decorated_pot.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::DecoratedPotBlock; + +impl BlockBehavior for DecoratedPotBlock {} diff --git a/src/blocks/src/decorative/door.rs b/src/blocks/src/decorative/door.rs new file mode 100644 index 00000000..d51c3ccb --- /dev/null +++ b/src/blocks/src/decorative/door.rs @@ -0,0 +1,94 @@ +use crate::world_extensions::WorldBlockUpdates; +use crate::BlockBehavior; +use std::collections::HashMap; +use temper_block_data::{BlockUpdates, BrokenBlocks, PlacedBlocks, PlacementContext}; +use temper_block_properties::{Direction, DoorHingeSide, DoubleBlockHalf}; +use temper_blocks_generated::DoorBlock; +use temper_core::block_face::BlockFace; +use temper_core::block_state_id::BlockStateId; +use temper_core::pos::BlockPos; +use temper_world::World; +use tracing::error; + +impl BlockBehavior for DoorBlock { + fn get_placement_state(&mut self, context: PlacementContext) -> PlacedBlocks { + self.facing = match context.face { + BlockFace::North => Direction::South, + BlockFace::South => Direction::North, + BlockFace::East => Direction::West, + BlockFace::West => Direction::East, + BlockFace::Top => { + let yaw = (context.player_rotation.yaw + 180.0) % 360.0; + + match yaw { + 45.0..135.0 => Direction::East, + 135.0..225.0 => Direction::South, + 225.0..315.0 => Direction::West, + _ => Direction::North, + } + } + _ => { + error!("Invalid block face clicked"); + return PlacedBlocks::default(); // TODO: should return None or Err in the future + } + }; + + self.open = false; + self.powered = false; + self.hinge = DoorHingeSide::Left; + self.half = DoubleBlockHalf::Lower; + + let mut top_half = self.clone(); + top_half.half = DoubleBlockHalf::Upper; + + let mut placed_blocks = PlacedBlocks::default(); + placed_blocks.blocks.insert( + context.block_pos.above(), + BlockStateId::new(top_half.try_into().unwrap()), + ); + + placed_blocks + } + + fn interact(&mut self, world: &World, pos: BlockPos) -> BlockUpdates { + self.open = !self.open; + + let mut blocks = HashMap::new(); + let other_pos = match self.half { + DoubleBlockHalf::Upper => pos.below(), + DoubleBlockHalf::Lower => pos.above(), + }; + + if let Ok(other_state) = + world.update_block_cast::(other_pos, |block| block.open = self.open) + { + blocks.insert(other_pos, other_state); + } else { + error!( + "Expected door block at {}, but did not find one!", + other_pos + ); + } + + BlockUpdates { blocks } + } + + fn try_break(&self, world: &World, pos: BlockPos) -> BrokenBlocks { + let pos = match self.half { + DoubleBlockHalf::Upper => pos.below(), + DoubleBlockHalf::Lower => pos.above(), + }; + + BrokenBlocks { + blocks: if world.block_is::(pos) { + vec![pos] + } else { + vec![] + }, + } + } + + fn is_interactable(&self) -> bool { + true + } +} diff --git a/src/blocks/src/decorative/fence_gate.rs b/src/blocks/src/decorative/fence_gate.rs new file mode 100644 index 00000000..2b628cdf --- /dev/null +++ b/src/blocks/src/decorative/fence_gate.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FenceGateBlock; + +impl BlockBehavior for FenceGateBlock {} diff --git a/src/blocks/src/decorative/fence_pane.rs b/src/blocks/src/decorative/fence_pane.rs new file mode 100644 index 00000000..c145211b --- /dev/null +++ b/src/blocks/src/decorative/fence_pane.rs @@ -0,0 +1,102 @@ +use crate::world_extensions::WorldBlockUpdates; +use crate::{BlockBehavior, BlockDispatch}; +use bevy_math::IVec3; +use std::collections::HashMap; +use temper_block_data::{PlacedBlocks, PlacementContext}; +use temper_blocks_generated::{ + FenceAndPaneBlock, FenceAndPaneBlockType, LiquidBlock, LiquidBlockType, +}; +use temper_core::dimension::Dimension; +use temper_core::pos::BlockPos; +use temper_world::World; + +// North (-Z), East (+X), South (+Z), West (-X) +const SIDE_DIRECTIONS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)]; + +impl BlockBehavior for FenceAndPaneBlock { + fn get_placement_state(&mut self, context: PlacementContext) -> PlacedBlocks { + self.waterlogged = context + .level + .block_is_and::(context.block_pos, |liquid| { + matches!(liquid.block_type, LiquidBlockType::Water) + }); + + let block_is_pane = is_pane(&self.block_type); + + for ((dx, dz), flag) in SIDE_DIRECTIONS.iter().zip([ + &mut self.north, + &mut self.east, + &mut self.south, + &mut self.west, + ]) { + let pos = context.block_pos + IVec3::new(*dx, 0, *dz).into(); + let block = context + .level + .get_block(pos, context.dimension) + .unwrap_or_default(); + + *flag = !context + .level + .block_is_and::(pos, |block| { + block_is_pane ^ is_pane(&block.block_type) + }) + && block.is_solid(); + } + + PlacedBlocks { + take_item: true, + blocks: HashMap::with_capacity(0), + place_original: true, + } + } + + fn update(&mut self, world: &World, pos: BlockPos) -> bool { + let block_is_pane = is_pane(&self.block_type); + + let mut changed = false; + + for ((dx, dz), flag) in SIDE_DIRECTIONS.iter().zip([ + &mut self.north, + &mut self.east, + &mut self.south, + &mut self.west, + ]) { + let pos = pos + IVec3::new(*dx, 0, *dz).into(); + let block = world + .get_block(pos, Dimension::Overworld) + .unwrap_or_default(); + + let original_flag = *flag; + *flag = !world.block_is_and::(pos, |block| { + block_is_pane ^ is_pane(&block.block_type) + }) && block.is_solid(); + + changed = changed || (original_flag != *flag); + } + + changed + } +} + +pub fn is_pane(ty: &FenceAndPaneBlockType) -> bool { + matches!( + ty, + FenceAndPaneBlockType::GlassPane + | FenceAndPaneBlockType::RedStainedGlassPane + | FenceAndPaneBlockType::OrangeStainedGlassPane + | FenceAndPaneBlockType::YellowStainedGlassPane + | FenceAndPaneBlockType::GreenStainedGlassPane + | FenceAndPaneBlockType::BlueStainedGlassPane + | FenceAndPaneBlockType::PurpleStainedGlassPane + | FenceAndPaneBlockType::PinkStainedGlassPane + | FenceAndPaneBlockType::MagentaStainedGlassPane + | FenceAndPaneBlockType::LimeStainedGlassPane + | FenceAndPaneBlockType::LightBlueStainedGlassPane + | FenceAndPaneBlockType::WhiteStainedGlassPane + | FenceAndPaneBlockType::LightGrayStainedGlassPane + | FenceAndPaneBlockType::GrayStainedGlassPane + | FenceAndPaneBlockType::BlackStainedGlassPane + | FenceAndPaneBlockType::BrownStainedGlassPane + | FenceAndPaneBlockType::CyanStainedGlassPane + ) +} diff --git a/src/blocks/src/decorative/hanging_sign.rs b/src/blocks/src/decorative/hanging_sign.rs new file mode 100644 index 00000000..503e055b --- /dev/null +++ b/src/blocks/src/decorative/hanging_sign.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::HangingSignBlock; + +impl BlockBehavior for HangingSignBlock {} diff --git a/src/blocks/src/decorative/lantern.rs b/src/blocks/src/decorative/lantern.rs new file mode 100644 index 00000000..792dabd4 --- /dev/null +++ b/src/blocks/src/decorative/lantern.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LanternBlock; + +impl BlockBehavior for LanternBlock {} diff --git a/src/blocks/src/decorative/mod.rs b/src/blocks/src/decorative/mod.rs new file mode 100644 index 00000000..78cee89a --- /dev/null +++ b/src/blocks/src/decorative/mod.rs @@ -0,0 +1,16 @@ +mod banner; +mod bell; +mod candle; +mod chain; +mod chiseled_bookshelf; +mod decorated_pot; +mod door; +mod fence_gate; +mod fence_pane; +mod hanging_sign; +mod lantern; +mod note_block; +mod sign; +mod trapdoor; +mod wall; +mod waterloggable_wall; diff --git a/src/blocks/src/decorative/note_block.rs b/src/blocks/src/decorative/note_block.rs new file mode 100644 index 00000000..9c1c2117 --- /dev/null +++ b/src/blocks/src/decorative/note_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::NoteBlock; + +impl BlockBehavior for NoteBlock {} diff --git a/src/blocks/src/decorative/sign.rs b/src/blocks/src/decorative/sign.rs new file mode 100644 index 00000000..2e3c5519 --- /dev/null +++ b/src/blocks/src/decorative/sign.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SignBlock; + +impl BlockBehavior for SignBlock {} diff --git a/src/blocks/src/decorative/trapdoor.rs b/src/blocks/src/decorative/trapdoor.rs new file mode 100644 index 00000000..d9ac4967 --- /dev/null +++ b/src/blocks/src/decorative/trapdoor.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TrapdoorBlock; + +impl BlockBehavior for TrapdoorBlock {} diff --git a/src/blocks/src/decorative/wall.rs b/src/blocks/src/decorative/wall.rs new file mode 100644 index 00000000..18842b11 --- /dev/null +++ b/src/blocks/src/decorative/wall.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::WallBlock; + +impl BlockBehavior for WallBlock {} diff --git a/src/blocks/src/decorative/waterloggable_wall.rs b/src/blocks/src/decorative/waterloggable_wall.rs new file mode 100644 index 00000000..efc082b2 --- /dev/null +++ b/src/blocks/src/decorative/waterloggable_wall.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::WaterloggableWallAttachedBlock; + +impl BlockBehavior for WaterloggableWallAttachedBlock {} diff --git a/src/blocks/src/facing_block.rs b/src/blocks/src/facing_block.rs new file mode 100644 index 00000000..568a5cee --- /dev/null +++ b/src/blocks/src/facing_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FacingBlock; + +impl BlockBehavior for FacingBlock {} diff --git a/src/blocks/src/fire.rs b/src/blocks/src/fire.rs new file mode 100644 index 00000000..25a62b04 --- /dev/null +++ b/src/blocks/src/fire.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FireBlock; + +impl BlockBehavior for FireBlock {} diff --git a/src/blocks/src/functional/barrel.rs b/src/blocks/src/functional/barrel.rs new file mode 100644 index 00000000..29874041 --- /dev/null +++ b/src/blocks/src/functional/barrel.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BarrelBlock; + +impl BlockBehavior for BarrelBlock {} diff --git a/src/blocks/src/functional/brewing_stand.rs b/src/blocks/src/functional/brewing_stand.rs new file mode 100644 index 00000000..b9f299a6 --- /dev/null +++ b/src/blocks/src/functional/brewing_stand.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BrewingStandBlock; + +impl BlockBehavior for BrewingStandBlock {} diff --git a/src/blocks/src/functional/campfire.rs b/src/blocks/src/functional/campfire.rs new file mode 100644 index 00000000..a3f6e546 --- /dev/null +++ b/src/blocks/src/functional/campfire.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CampfireBlock; + +impl BlockBehavior for CampfireBlock {} diff --git a/src/blocks/src/functional/cauldron.rs b/src/blocks/src/functional/cauldron.rs new file mode 100644 index 00000000..c53c0b11 --- /dev/null +++ b/src/blocks/src/functional/cauldron.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LevelCauldronBlock; + +impl BlockBehavior for LevelCauldronBlock {} diff --git a/src/blocks/src/functional/chest.rs b/src/blocks/src/functional/chest.rs new file mode 100644 index 00000000..68c5e5b5 --- /dev/null +++ b/src/blocks/src/functional/chest.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ChestBlock; + +impl BlockBehavior for ChestBlock {} diff --git a/src/blocks/src/functional/command_block.rs b/src/blocks/src/functional/command_block.rs new file mode 100644 index 00000000..907b938a --- /dev/null +++ b/src/blocks/src/functional/command_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CommandBlock; + +impl BlockBehavior for CommandBlock {} diff --git a/src/blocks/src/functional/composter.rs b/src/blocks/src/functional/composter.rs new file mode 100644 index 00000000..30a916ba --- /dev/null +++ b/src/blocks/src/functional/composter.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ComposterBlock; + +impl BlockBehavior for ComposterBlock {} diff --git a/src/blocks/src/functional/end_portal.rs b/src/blocks/src/functional/end_portal.rs new file mode 100644 index 00000000..4aaa9d59 --- /dev/null +++ b/src/blocks/src/functional/end_portal.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::EndPortalBlock; + +impl BlockBehavior for EndPortalBlock {} diff --git a/src/blocks/src/functional/furnace.rs b/src/blocks/src/functional/furnace.rs new file mode 100644 index 00000000..843a8cb4 --- /dev/null +++ b/src/blocks/src/functional/furnace.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FurnaceBlock; + +impl BlockBehavior for FurnaceBlock {} diff --git a/src/blocks/src/functional/grindstone.rs b/src/blocks/src/functional/grindstone.rs new file mode 100644 index 00000000..e01b161e --- /dev/null +++ b/src/blocks/src/functional/grindstone.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::GrindstoneBlock; + +impl BlockBehavior for GrindstoneBlock {} diff --git a/src/blocks/src/functional/jigsaw.rs b/src/blocks/src/functional/jigsaw.rs new file mode 100644 index 00000000..b8edc923 --- /dev/null +++ b/src/blocks/src/functional/jigsaw.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::JigsawBlock; + +impl BlockBehavior for JigsawBlock {} diff --git a/src/blocks/src/functional/lectern.rs b/src/blocks/src/functional/lectern.rs new file mode 100644 index 00000000..98d8ebe6 --- /dev/null +++ b/src/blocks/src/functional/lectern.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LecternBlock; + +impl BlockBehavior for LecternBlock {} diff --git a/src/blocks/src/functional/light.rs b/src/blocks/src/functional/light.rs new file mode 100644 index 00000000..2052caeb --- /dev/null +++ b/src/blocks/src/functional/light.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LightBlock; + +impl BlockBehavior for LightBlock {} diff --git a/src/blocks/src/functional/mod.rs b/src/blocks/src/functional/mod.rs new file mode 100644 index 00000000..2bd61f0f --- /dev/null +++ b/src/blocks/src/functional/mod.rs @@ -0,0 +1,20 @@ +mod barrel; +mod brewing_stand; +mod campfire; +mod cauldron; +mod chest; +mod command_block; +mod composter; +mod end_portal; +mod furnace; +mod grindstone; +mod jigsaw; +mod lectern; +mod light; +mod respawn_anchor; +mod scaffolding; +mod structure; +mod test; +mod torch; +mod trial_spawner; +mod vault; diff --git a/src/blocks/src/functional/respawn_anchor.rs b/src/blocks/src/functional/respawn_anchor.rs new file mode 100644 index 00000000..f610a7e6 --- /dev/null +++ b/src/blocks/src/functional/respawn_anchor.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RespawnAnchor; + +impl BlockBehavior for RespawnAnchor {} diff --git a/src/blocks/src/functional/scaffolding.rs b/src/blocks/src/functional/scaffolding.rs new file mode 100644 index 00000000..856babb7 --- /dev/null +++ b/src/blocks/src/functional/scaffolding.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ScaffoldingBlock; + +impl BlockBehavior for ScaffoldingBlock {} diff --git a/src/blocks/src/functional/structure.rs b/src/blocks/src/functional/structure.rs new file mode 100644 index 00000000..f0241f0c --- /dev/null +++ b/src/blocks/src/functional/structure.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::StructureBlock; + +impl BlockBehavior for StructureBlock {} diff --git a/src/blocks/src/functional/test.rs b/src/blocks/src/functional/test.rs new file mode 100644 index 00000000..3acb04ad --- /dev/null +++ b/src/blocks/src/functional/test.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TestBlock; + +impl BlockBehavior for TestBlock {} diff --git a/src/blocks/src/functional/torch.rs b/src/blocks/src/functional/torch.rs new file mode 100644 index 00000000..c1aea65d --- /dev/null +++ b/src/blocks/src/functional/torch.rs @@ -0,0 +1,81 @@ +use crate::{BlockBehavior, BlockDispatch}; +use std::collections::HashMap; +use temper_block_data::{PlacedBlocks, PlacementContext}; +use temper_block_properties::Direction; +use temper_blocks_generated::{TorchBlock, WallTorchBlock, WallTorchBlockType}; +use temper_core::block_face::BlockFace; +use temper_core::block_state_id::BlockStateId; + +impl BlockBehavior for TorchBlock { + fn get_placement_state(&mut self, context: PlacementContext) -> PlacedBlocks { + place_torch(context, self.clone()) + } + + fn is_solid(&self) -> bool { + false + } +} + +impl BlockBehavior for WallTorchBlock { + fn get_placement_state(&mut self, context: PlacementContext) -> PlacedBlocks { + place_torch( + context, + match self.block_type { + WallTorchBlockType::WallTorch => TorchBlock::Torch, + WallTorchBlockType::SoulWallTorch => TorchBlock::SoulTorch, + }, + ) + } + + fn is_solid(&self) -> bool { + false + } +} + +fn place_torch(context: PlacementContext, ty: TorchBlock) -> PlacedBlocks { + let block = match context.face { + BlockFace::Top | BlockFace::Bottom => { + if context + .level + .get_block(context.block_pos.below(), context.dimension) + .unwrap_or_default() + .is_solid() + { + ty.try_into() + .expect("Should be able to convert TorchBlock to id") + } else { + return PlacedBlocks { + blocks: HashMap::with_capacity(0), + take_item: false, + place_original: false, + }; + } + } + face => { + let block_type = match ty { + TorchBlock::Torch => WallTorchBlockType::WallTorch, + TorchBlock::SoulTorch => WallTorchBlockType::SoulWallTorch, + }; + + let facing = match face { + BlockFace::East => Direction::East, + BlockFace::West => Direction::West, + BlockFace::North => Direction::North, + BlockFace::South => Direction::South, + _ => unreachable!(), + }; + + WallTorchBlock { block_type, facing } + .try_into() + .expect("Should be able to convert") + } + }; + + PlacedBlocks { + blocks: [(context.block_pos, BlockStateId::new(block))] + .into_iter() + .collect::>(), + take_item: true, + place_original: false, + } +} diff --git a/src/blocks/src/functional/trial_spawner.rs b/src/blocks/src/functional/trial_spawner.rs new file mode 100644 index 00000000..33d7187c --- /dev/null +++ b/src/blocks/src/functional/trial_spawner.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TrialSpawnerBlock; + +impl BlockBehavior for TrialSpawnerBlock {} diff --git a/src/blocks/src/functional/vault.rs b/src/blocks/src/functional/vault.rs new file mode 100644 index 00000000..1ebd1554 --- /dev/null +++ b/src/blocks/src/functional/vault.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::VaultBlock; + +impl BlockBehavior for VaultBlock {} diff --git a/src/blocks/src/lib.rs b/src/blocks/src/lib.rs new file mode 100644 index 00000000..defd59cb --- /dev/null +++ b/src/blocks/src/lib.rs @@ -0,0 +1,27 @@ +mod bed_block; +mod behavior_trait; +mod bubble_column; +mod building; +mod cake; +mod candle_cake; +mod decorative; +mod facing_block; +mod fire; +mod functional; +mod liquid; +mod nature; +mod redstone; +mod skull; +mod suspicious_block; +mod wall_skull; +mod waterloggable_block; +mod world_extensions; + +#[allow(unused_imports)] // Used in the include! +use crate::behavior_trait::BlockBehaviorTable; + +pub use crate::behavior_trait::{BlockBehavior, BlockDispatch, StateBehaviorTable}; +pub use temper_block_data::*; + +pub const BLOCK_MAPPINGS: &[StateBehaviorTable] = + include!(concat!(env!("OUT_DIR"), "/mappings.rs")); diff --git a/src/blocks/src/liquid.rs b/src/blocks/src/liquid.rs new file mode 100644 index 00000000..9e371111 --- /dev/null +++ b/src/blocks/src/liquid.rs @@ -0,0 +1,13 @@ +use crate::BlockBehavior; +use temper_block_data::PlacementContext; +use temper_blocks_generated::LiquidBlock; + +impl BlockBehavior for LiquidBlock { + fn can_be_replaced(&self, _context: PlacementContext) -> bool { + true + } + + fn is_solid(&self) -> bool { + false + } +} diff --git a/src/blocks/src/nature/bamboo.rs b/src/blocks/src/nature/bamboo.rs new file mode 100644 index 00000000..eca21792 --- /dev/null +++ b/src/blocks/src/nature/bamboo.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BambooBlock; + +impl BlockBehavior for BambooBlock {} diff --git a/src/blocks/src/nature/big_dripleaf.rs b/src/blocks/src/nature/big_dripleaf.rs new file mode 100644 index 00000000..2da3d6c5 --- /dev/null +++ b/src/blocks/src/nature/big_dripleaf.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BigDripleafBlock; + +impl BlockBehavior for BigDripleafBlock {} diff --git a/src/blocks/src/nature/chorus.rs b/src/blocks/src/nature/chorus.rs new file mode 100644 index 00000000..20b7016f --- /dev/null +++ b/src/blocks/src/nature/chorus.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ChorusPlantBlock; + +impl BlockBehavior for ChorusPlantBlock {} diff --git a/src/blocks/src/nature/cocoa_beans.rs b/src/blocks/src/nature/cocoa_beans.rs new file mode 100644 index 00000000..f7e601ca --- /dev/null +++ b/src/blocks/src/nature/cocoa_beans.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CocoaBeansBlock; + +impl BlockBehavior for CocoaBeansBlock {} diff --git a/src/blocks/src/nature/creaking_heart.rs b/src/blocks/src/nature/creaking_heart.rs new file mode 100644 index 00000000..ade177db --- /dev/null +++ b/src/blocks/src/nature/creaking_heart.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CreakingHeartBlock; + +impl BlockBehavior for CreakingHeartBlock {} diff --git a/src/blocks/src/nature/crop.rs b/src/blocks/src/nature/crop.rs new file mode 100644 index 00000000..144cc60b --- /dev/null +++ b/src/blocks/src/nature/crop.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CropBlock; + +impl BlockBehavior for CropBlock {} diff --git a/src/blocks/src/nature/double_plant_block.rs b/src/blocks/src/nature/double_plant_block.rs new file mode 100644 index 00000000..8e748545 --- /dev/null +++ b/src/blocks/src/nature/double_plant_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::DoublePlantBlock; + +impl BlockBehavior for DoublePlantBlock {} diff --git a/src/blocks/src/nature/dripstone.rs b/src/blocks/src/nature/dripstone.rs new file mode 100644 index 00000000..4a47076d --- /dev/null +++ b/src/blocks/src/nature/dripstone.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::DripstoneBlock; + +impl BlockBehavior for DripstoneBlock {} diff --git a/src/blocks/src/nature/farmland.rs b/src/blocks/src/nature/farmland.rs new file mode 100644 index 00000000..6ff7a2c2 --- /dev/null +++ b/src/blocks/src/nature/farmland.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FarmlandBlock; + +impl BlockBehavior for FarmlandBlock {} diff --git a/src/blocks/src/nature/flower_cover.rs b/src/blocks/src/nature/flower_cover.rs new file mode 100644 index 00000000..1df9d430 --- /dev/null +++ b/src/blocks/src/nature/flower_cover.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FlowerCoverBlock; + +impl BlockBehavior for FlowerCoverBlock {} diff --git a/src/blocks/src/nature/frosted_ice.rs b/src/blocks/src/nature/frosted_ice.rs new file mode 100644 index 00000000..dff0d7be --- /dev/null +++ b/src/blocks/src/nature/frosted_ice.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::FrostedIceBlock; + +impl BlockBehavior for FrostedIceBlock {} diff --git a/src/blocks/src/nature/glow_berries.rs b/src/blocks/src/nature/glow_berries.rs new file mode 100644 index 00000000..7b7f6163 --- /dev/null +++ b/src/blocks/src/nature/glow_berries.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::GlowBerriesBlock; + +impl BlockBehavior for GlowBerriesBlock {} diff --git a/src/blocks/src/nature/glow_berries_plant.rs b/src/blocks/src/nature/glow_berries_plant.rs new file mode 100644 index 00000000..e4c488d4 --- /dev/null +++ b/src/blocks/src/nature/glow_berries_plant.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::GlowBerriesPlantBlock; + +impl BlockBehavior for GlowBerriesPlantBlock {} diff --git a/src/blocks/src/nature/hive.rs b/src/blocks/src/nature/hive.rs new file mode 100644 index 00000000..e196b850 --- /dev/null +++ b/src/blocks/src/nature/hive.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::HiveBlock; + +impl BlockBehavior for HiveBlock {} diff --git a/src/blocks/src/nature/large_mushroom.rs b/src/blocks/src/nature/large_mushroom.rs new file mode 100644 index 00000000..2a92a2ee --- /dev/null +++ b/src/blocks/src/nature/large_mushroom.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LargeMushroomBlock; + +impl BlockBehavior for LargeMushroomBlock {} diff --git a/src/blocks/src/nature/leaf_litter.rs b/src/blocks/src/nature/leaf_litter.rs new file mode 100644 index 00000000..ad8b29b9 --- /dev/null +++ b/src/blocks/src/nature/leaf_litter.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LeafLitterBlock; + +impl BlockBehavior for LeafLitterBlock {} diff --git a/src/blocks/src/nature/leaves.rs b/src/blocks/src/nature/leaves.rs new file mode 100644 index 00000000..d532b9ca --- /dev/null +++ b/src/blocks/src/nature/leaves.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LeavesBlock; + +impl BlockBehavior for LeavesBlock {} diff --git a/src/blocks/src/nature/lichen.rs b/src/blocks/src/nature/lichen.rs new file mode 100644 index 00000000..44218421 --- /dev/null +++ b/src/blocks/src/nature/lichen.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LichenBlock; + +impl BlockBehavior for LichenBlock {} diff --git a/src/blocks/src/nature/mangrove_propagule.rs b/src/blocks/src/nature/mangrove_propagule.rs new file mode 100644 index 00000000..140a8715 --- /dev/null +++ b/src/blocks/src/nature/mangrove_propagule.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::MangrovePopaguleBlock; + +impl BlockBehavior for MangrovePopaguleBlock {} diff --git a/src/blocks/src/nature/mod.rs b/src/blocks/src/nature/mod.rs new file mode 100644 index 00000000..5dc98248 --- /dev/null +++ b/src/blocks/src/nature/mod.rs @@ -0,0 +1,31 @@ +mod bamboo; +mod big_dripleaf; +mod chorus; +mod cocoa_beans; +mod creaking_heart; +mod crop; +mod double_plant_block; +mod dripstone; +mod farmland; +mod flower_cover; +mod frosted_ice; +mod glow_berries; +mod glow_berries_plant; +mod hive; +mod large_mushroom; +mod leaf_litter; +mod leaves; +mod lichen; +mod mangrove_propagule; +mod pale_hanging_moss; +mod pale_moss_carpet; +mod pitcher_crop; +mod sapling_block; +mod sea_pickle; +mod small_dripleaf; +mod sniffer_egg; +mod snow; +mod snowy; +mod stem; +mod turtle_egg; +mod vine; diff --git a/src/blocks/src/nature/pale_hanging_moss.rs b/src/blocks/src/nature/pale_hanging_moss.rs new file mode 100644 index 00000000..2510c375 --- /dev/null +++ b/src/blocks/src/nature/pale_hanging_moss.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::PaleHangingMossBlock; + +impl BlockBehavior for PaleHangingMossBlock {} diff --git a/src/blocks/src/nature/pale_moss_carpet.rs b/src/blocks/src/nature/pale_moss_carpet.rs new file mode 100644 index 00000000..b7654db5 --- /dev/null +++ b/src/blocks/src/nature/pale_moss_carpet.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::PaleMossCarpetBlock; + +impl BlockBehavior for PaleMossCarpetBlock {} diff --git a/src/blocks/src/nature/pitcher_crop.rs b/src/blocks/src/nature/pitcher_crop.rs new file mode 100644 index 00000000..094174d6 --- /dev/null +++ b/src/blocks/src/nature/pitcher_crop.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::PitcherCropBlock; + +impl BlockBehavior for PitcherCropBlock {} diff --git a/src/blocks/src/nature/sapling_block.rs b/src/blocks/src/nature/sapling_block.rs new file mode 100644 index 00000000..567ff9ce --- /dev/null +++ b/src/blocks/src/nature/sapling_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SaplingBlock; + +impl BlockBehavior for SaplingBlock {} diff --git a/src/blocks/src/nature/sea_pickle.rs b/src/blocks/src/nature/sea_pickle.rs new file mode 100644 index 00000000..4c1509af --- /dev/null +++ b/src/blocks/src/nature/sea_pickle.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SeaPickleBlock; + +impl BlockBehavior for SeaPickleBlock {} diff --git a/src/blocks/src/nature/small_dripleaf.rs b/src/blocks/src/nature/small_dripleaf.rs new file mode 100644 index 00000000..1142e481 --- /dev/null +++ b/src/blocks/src/nature/small_dripleaf.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SmallDripleafBlock; + +impl BlockBehavior for SmallDripleafBlock {} diff --git a/src/blocks/src/nature/sniffer_egg.rs b/src/blocks/src/nature/sniffer_egg.rs new file mode 100644 index 00000000..056478ed --- /dev/null +++ b/src/blocks/src/nature/sniffer_egg.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SnifferEggBlock; + +impl BlockBehavior for SnifferEggBlock {} diff --git a/src/blocks/src/nature/snow.rs b/src/blocks/src/nature/snow.rs new file mode 100644 index 00000000..381109f7 --- /dev/null +++ b/src/blocks/src/nature/snow.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SnowBlock; + +impl BlockBehavior for SnowBlock {} diff --git a/src/blocks/src/nature/snowy.rs b/src/blocks/src/nature/snowy.rs new file mode 100644 index 00000000..8dfb6ca3 --- /dev/null +++ b/src/blocks/src/nature/snowy.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SnowyBlock; + +impl BlockBehavior for SnowyBlock {} diff --git a/src/blocks/src/nature/stem.rs b/src/blocks/src/nature/stem.rs new file mode 100644 index 00000000..d0fb1d3d --- /dev/null +++ b/src/blocks/src/nature/stem.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::StemBlock; + +impl BlockBehavior for StemBlock {} diff --git a/src/blocks/src/nature/turtle_egg.rs b/src/blocks/src/nature/turtle_egg.rs new file mode 100644 index 00000000..51c87126 --- /dev/null +++ b/src/blocks/src/nature/turtle_egg.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TurtleEggBlock; + +impl BlockBehavior for TurtleEggBlock {} diff --git a/src/blocks/src/nature/vine.rs b/src/blocks/src/nature/vine.rs new file mode 100644 index 00000000..94620341 --- /dev/null +++ b/src/blocks/src/nature/vine.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::VineBlock; + +impl BlockBehavior for VineBlock {} diff --git a/src/blocks/src/redstone/bulb.rs b/src/blocks/src/redstone/bulb.rs new file mode 100644 index 00000000..207901c3 --- /dev/null +++ b/src/blocks/src/redstone/bulb.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::BulbBlock; + +impl BlockBehavior for BulbBlock {} diff --git a/src/blocks/src/redstone/button.rs b/src/blocks/src/redstone/button.rs new file mode 100644 index 00000000..fbfdc0a9 --- /dev/null +++ b/src/blocks/src/redstone/button.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ButtonBlock; + +impl BlockBehavior for ButtonBlock {} diff --git a/src/blocks/src/redstone/calibrated_sculk_sensor.rs b/src/blocks/src/redstone/calibrated_sculk_sensor.rs new file mode 100644 index 00000000..0d1300f8 --- /dev/null +++ b/src/blocks/src/redstone/calibrated_sculk_sensor.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CalibratedSculkSensorBlock; + +impl BlockBehavior for CalibratedSculkSensorBlock {} diff --git a/src/blocks/src/redstone/comparator.rs b/src/blocks/src/redstone/comparator.rs new file mode 100644 index 00000000..4a0d0042 --- /dev/null +++ b/src/blocks/src/redstone/comparator.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ComparatorBlock; + +impl BlockBehavior for ComparatorBlock {} diff --git a/src/blocks/src/redstone/crafter.rs b/src/blocks/src/redstone/crafter.rs new file mode 100644 index 00000000..538be302 --- /dev/null +++ b/src/blocks/src/redstone/crafter.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::CrafterBlock; + +impl BlockBehavior for CrafterBlock {} diff --git a/src/blocks/src/redstone/daylight_detector.rs b/src/blocks/src/redstone/daylight_detector.rs new file mode 100644 index 00000000..9152b7f5 --- /dev/null +++ b/src/blocks/src/redstone/daylight_detector.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::DaylightDetectorBlock; + +impl BlockBehavior for DaylightDetectorBlock {} diff --git a/src/blocks/src/redstone/dispenser.rs b/src/blocks/src/redstone/dispenser.rs new file mode 100644 index 00000000..b2bbf5e4 --- /dev/null +++ b/src/blocks/src/redstone/dispenser.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::DispenserBlock; + +impl BlockBehavior for DispenserBlock {} diff --git a/src/blocks/src/redstone/hopper.rs b/src/blocks/src/redstone/hopper.rs new file mode 100644 index 00000000..a05f176a --- /dev/null +++ b/src/blocks/src/redstone/hopper.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::HopperBlock; + +impl BlockBehavior for HopperBlock {} diff --git a/src/blocks/src/redstone/jukebox.rs b/src/blocks/src/redstone/jukebox.rs new file mode 100644 index 00000000..7e975ce1 --- /dev/null +++ b/src/blocks/src/redstone/jukebox.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::JukeboxBlock; + +impl BlockBehavior for JukeboxBlock {} diff --git a/src/blocks/src/redstone/lamp.rs b/src/blocks/src/redstone/lamp.rs new file mode 100644 index 00000000..2e884248 --- /dev/null +++ b/src/blocks/src/redstone/lamp.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RedstoneLampBlock; + +impl BlockBehavior for RedstoneLampBlock {} diff --git a/src/blocks/src/redstone/lever.rs b/src/blocks/src/redstone/lever.rs new file mode 100644 index 00000000..17333344 --- /dev/null +++ b/src/blocks/src/redstone/lever.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LeverBlock; + +impl BlockBehavior for LeverBlock {} diff --git a/src/blocks/src/redstone/lightning_rod.rs b/src/blocks/src/redstone/lightning_rod.rs new file mode 100644 index 00000000..a1610a36 --- /dev/null +++ b/src/blocks/src/redstone/lightning_rod.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::LightningRodBlock; + +impl BlockBehavior for LightningRodBlock {} diff --git a/src/blocks/src/redstone/mod.rs b/src/blocks/src/redstone/mod.rs new file mode 100644 index 00000000..7438c999 --- /dev/null +++ b/src/blocks/src/redstone/mod.rs @@ -0,0 +1,30 @@ +mod bulb; +mod button; +mod calibrated_sculk_sensor; +mod comparator; +mod crafter; +mod daylight_detector; +mod dispenser; +mod hopper; +mod jukebox; +mod lamp; +mod lever; +mod lightning_rod; +mod observer; +mod ore; +mod piston; +mod pressure_plate; +mod rail; +mod redstone_rail; +mod repeater; +mod sculk_catalyst; +mod sculk_sensor; +mod sculk_shrieker; +mod target; +mod tnt; +mod torch; +mod tripwire; +mod tripwire_hook; +mod wall_torch; +mod weighted_pressure_plate; +mod wire; diff --git a/src/blocks/src/redstone/observer.rs b/src/blocks/src/redstone/observer.rs new file mode 100644 index 00000000..eff40d23 --- /dev/null +++ b/src/blocks/src/redstone/observer.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::ObserverBlock; + +impl BlockBehavior for ObserverBlock {} diff --git a/src/blocks/src/redstone/ore.rs b/src/blocks/src/redstone/ore.rs new file mode 100644 index 00000000..421f65a0 --- /dev/null +++ b/src/blocks/src/redstone/ore.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RedstoneOreBlock; + +impl BlockBehavior for RedstoneOreBlock {} diff --git a/src/blocks/src/redstone/piston/head.rs b/src/blocks/src/redstone/piston/head.rs new file mode 100644 index 00000000..6bdb4cef --- /dev/null +++ b/src/blocks/src/redstone/piston/head.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::PistonHeadBlock; + +impl BlockBehavior for PistonHeadBlock {} diff --git a/src/blocks/src/redstone/piston/mod.rs b/src/blocks/src/redstone/piston/mod.rs new file mode 100644 index 00000000..53fa6668 --- /dev/null +++ b/src/blocks/src/redstone/piston/mod.rs @@ -0,0 +1,7 @@ +use crate::BlockBehavior; +use temper_blocks_generated::PistonBlock; + +mod head; +mod moving_head; + +impl BlockBehavior for PistonBlock {} diff --git a/src/blocks/src/redstone/piston/moving_head.rs b/src/blocks/src/redstone/piston/moving_head.rs new file mode 100644 index 00000000..0c38afb5 --- /dev/null +++ b/src/blocks/src/redstone/piston/moving_head.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::MovingPistonBlock; + +impl BlockBehavior for MovingPistonBlock {} diff --git a/src/blocks/src/redstone/pressure_plate.rs b/src/blocks/src/redstone/pressure_plate.rs new file mode 100644 index 00000000..34e1a92c --- /dev/null +++ b/src/blocks/src/redstone/pressure_plate.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::PressurePlateBlock; + +impl BlockBehavior for PressurePlateBlock {} diff --git a/src/blocks/src/redstone/rail.rs b/src/blocks/src/redstone/rail.rs new file mode 100644 index 00000000..ac21e4f1 --- /dev/null +++ b/src/blocks/src/redstone/rail.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RailBlock; + +impl BlockBehavior for RailBlock {} diff --git a/src/blocks/src/redstone/redstone_rail.rs b/src/blocks/src/redstone/redstone_rail.rs new file mode 100644 index 00000000..b7c07e91 --- /dev/null +++ b/src/blocks/src/redstone/redstone_rail.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RedstoneRailBlock; + +impl BlockBehavior for RedstoneRailBlock {} diff --git a/src/blocks/src/redstone/repeater.rs b/src/blocks/src/redstone/repeater.rs new file mode 100644 index 00000000..c15cbd74 --- /dev/null +++ b/src/blocks/src/redstone/repeater.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RepeaterBlock; + +impl BlockBehavior for RepeaterBlock {} diff --git a/src/blocks/src/redstone/sculk_catalyst.rs b/src/blocks/src/redstone/sculk_catalyst.rs new file mode 100644 index 00000000..cc6039c2 --- /dev/null +++ b/src/blocks/src/redstone/sculk_catalyst.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SculkCatalystBlock; + +impl BlockBehavior for SculkCatalystBlock {} diff --git a/src/blocks/src/redstone/sculk_sensor.rs b/src/blocks/src/redstone/sculk_sensor.rs new file mode 100644 index 00000000..46cedbab --- /dev/null +++ b/src/blocks/src/redstone/sculk_sensor.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SculkSensorBlock; + +impl BlockBehavior for SculkSensorBlock {} diff --git a/src/blocks/src/redstone/sculk_shrieker.rs b/src/blocks/src/redstone/sculk_shrieker.rs new file mode 100644 index 00000000..7053e901 --- /dev/null +++ b/src/blocks/src/redstone/sculk_shrieker.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SculkShriekerBlock; + +impl BlockBehavior for SculkShriekerBlock {} diff --git a/src/blocks/src/redstone/target.rs b/src/blocks/src/redstone/target.rs new file mode 100644 index 00000000..7baed0a3 --- /dev/null +++ b/src/blocks/src/redstone/target.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TargetBlock; + +impl BlockBehavior for TargetBlock {} diff --git a/src/blocks/src/redstone/tnt.rs b/src/blocks/src/redstone/tnt.rs new file mode 100644 index 00000000..ae44feda --- /dev/null +++ b/src/blocks/src/redstone/tnt.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TntBlock; + +impl BlockBehavior for TntBlock {} diff --git a/src/blocks/src/redstone/torch.rs b/src/blocks/src/redstone/torch.rs new file mode 100644 index 00000000..07c1a93f --- /dev/null +++ b/src/blocks/src/redstone/torch.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RedstoneTorchBlock; + +impl BlockBehavior for RedstoneTorchBlock {} diff --git a/src/blocks/src/redstone/tripwire.rs b/src/blocks/src/redstone/tripwire.rs new file mode 100644 index 00000000..364b8267 --- /dev/null +++ b/src/blocks/src/redstone/tripwire.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TripwireBlock; + +impl BlockBehavior for TripwireBlock {} diff --git a/src/blocks/src/redstone/tripwire_hook.rs b/src/blocks/src/redstone/tripwire_hook.rs new file mode 100644 index 00000000..a14a0c0a --- /dev/null +++ b/src/blocks/src/redstone/tripwire_hook.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::TripwireHookBlock; + +impl BlockBehavior for TripwireHookBlock {} diff --git a/src/blocks/src/redstone/wall_torch.rs b/src/blocks/src/redstone/wall_torch.rs new file mode 100644 index 00000000..75016123 --- /dev/null +++ b/src/blocks/src/redstone/wall_torch.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::WallRedstoneTorchBlock; + +impl BlockBehavior for WallRedstoneTorchBlock {} diff --git a/src/blocks/src/redstone/weighted_pressure_plate.rs b/src/blocks/src/redstone/weighted_pressure_plate.rs new file mode 100644 index 00000000..ed3af340 --- /dev/null +++ b/src/blocks/src/redstone/weighted_pressure_plate.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::WeightedPressurePlateBlock; + +impl BlockBehavior for WeightedPressurePlateBlock {} diff --git a/src/blocks/src/redstone/wire.rs b/src/blocks/src/redstone/wire.rs new file mode 100644 index 00000000..eded0acd --- /dev/null +++ b/src/blocks/src/redstone/wire.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::RedstoneWireBlock; + +impl BlockBehavior for RedstoneWireBlock {} diff --git a/src/blocks/src/skull.rs b/src/blocks/src/skull.rs new file mode 100644 index 00000000..e6704b86 --- /dev/null +++ b/src/blocks/src/skull.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SkullBlock; + +impl BlockBehavior for SkullBlock {} diff --git a/src/blocks/src/suspicious_block.rs b/src/blocks/src/suspicious_block.rs new file mode 100644 index 00000000..00604b7f --- /dev/null +++ b/src/blocks/src/suspicious_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::SuspiciousBlock; + +impl BlockBehavior for SuspiciousBlock {} diff --git a/src/blocks/src/wall_skull.rs b/src/blocks/src/wall_skull.rs new file mode 100644 index 00000000..8c9b1cae --- /dev/null +++ b/src/blocks/src/wall_skull.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::WallSkullBlock; + +impl BlockBehavior for WallSkullBlock {} diff --git a/src/blocks/src/waterloggable_block.rs b/src/blocks/src/waterloggable_block.rs new file mode 100644 index 00000000..b877b4de --- /dev/null +++ b/src/blocks/src/waterloggable_block.rs @@ -0,0 +1,4 @@ +use crate::BlockBehavior; +use temper_blocks_generated::WaterloggableBlock; + +impl BlockBehavior for WaterloggableBlock {} diff --git a/src/blocks/src/world_extensions.rs b/src/blocks/src/world_extensions.rs new file mode 100644 index 00000000..839b7664 --- /dev/null +++ b/src/blocks/src/world_extensions.rs @@ -0,0 +1,64 @@ +use crate::BlockBehavior; +use temper_core::block_state_id::BlockStateId; +use temper_core::dimension::Dimension; +use temper_core::pos::BlockPos; +use temper_world::{World, WorldError}; + +// TODO: add dimensions to all these functions + +/// Trait to add useful functions to the World struct +pub trait WorldBlockUpdates { + fn update_block_cast( + &self, + block_pos: BlockPos, + callback: impl Fn(&mut T), + ) -> Result; + + fn block_is(&self, block_pos: BlockPos) -> bool; + fn block_is_and( + &self, + block_pos: BlockPos, + callback: impl Fn(T) -> bool, + ) -> bool; +} + +impl WorldBlockUpdates for World { + fn update_block_cast( + &self, + block_pos: BlockPos, + callback: impl Fn(&mut T), + ) -> Result { + self.get_chunk(block_pos.chunk(), Dimension::Overworld) + .and_then(|chunk| { + let id = chunk.get_block(block_pos.chunk_block_pos()); + let mut id = T::try_from(chunk.get_block(block_pos.chunk_block_pos()).raw()) + .map_err(|_| WorldError::InvalidBlock(id))?; + callback(&mut id); + + id.try_into() + .map(BlockStateId::new) + .map_err(|_| WorldError::InvalidBlock(BlockStateId::default())) + }) + } + + fn block_is(&self, block_pos: BlockPos) -> bool { + self.get_chunk(block_pos.chunk(), Dimension::Overworld) + .map(|chunk| T::try_from(chunk.get_block(block_pos.chunk_block_pos()).raw()).is_ok()) + .unwrap_or_default() + } + + fn block_is_and( + &self, + block_pos: BlockPos, + callback: impl Fn(T) -> bool, + ) -> bool { + self.get_chunk(block_pos.chunk(), Dimension::Overworld) + .and_then(|chunk| { + let block_id = chunk.get_block(block_pos.chunk_block_pos()); + let block = + T::try_from(block_id.raw()).map_err(|_| WorldError::InvalidBlock(block_id))?; + Ok(callback(block)) + }) + .unwrap_or_default() + } +} diff --git a/src/core/src/block_face.rs b/src/core/src/block_face.rs new file mode 100644 index 00000000..632b4073 --- /dev/null +++ b/src/core/src/block_face.rs @@ -0,0 +1,65 @@ +use bevy_math::IVec3; +use std::io::Read; +use temper_codec::decode::errors::NetDecodeError; +use temper_codec::decode::{NetDecode, NetDecodeOpts}; +use temper_codec::net_types::var_int::VarInt; + +#[derive(Debug, Clone)] +pub enum BlockFace { + Top, + Bottom, + North, + South, + East, + West, +} + +impl BlockFace { + pub fn is_x_axis(&self) -> bool { + matches!(self, BlockFace::East | BlockFace::West) + } + + pub fn is_y_axis(&self) -> bool { + matches!(self, BlockFace::Top | BlockFace::Bottom) + } + + pub fn is_z_axis(&self) -> bool { + matches!(self, BlockFace::North | BlockFace::South) + } + + /// Returns the translation vector that will get the block that touches this face. + pub fn get_normal(&self) -> IVec3 { + match self { + BlockFace::Top => IVec3::new(0, 1, 0), + BlockFace::Bottom => IVec3::new(0, -1, 0), + BlockFace::North => IVec3::new(0, 0, -1), + BlockFace::South => IVec3::new(0, 0, 1), + BlockFace::East => IVec3::new(1, 0, 0), + BlockFace::West => IVec3::new(-1, 0, 0), + } + } +} + +impl TryFrom for BlockFace { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(BlockFace::Bottom), + 1 => Ok(BlockFace::Top), + 2 => Ok(BlockFace::North), + 3 => Ok(BlockFace::South), + 4 => Ok(BlockFace::West), + 5 => Ok(BlockFace::East), + _ => Err(()), + } + } +} + +impl NetDecode for BlockFace { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> Result { + let VarInt(data) = VarInt::decode(reader, opts)?; + + BlockFace::try_from(data as u32).map_err(|_| NetDecodeError::InvalidEnumVariant) + } +} diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs index 073b9312..f4ca5256 100644 --- a/src/core/src/lib.rs +++ b/src/core/src/lib.rs @@ -2,6 +2,7 @@ pub mod errors; // Core structs/types. Usually used in ECS Components. pub mod block_data; +pub mod block_face; pub mod block_properties; pub mod block_state_id; pub mod color; diff --git a/src/core/src/pos.rs b/src/core/src/pos.rs index 76d4dce0..f22ca563 100644 --- a/src/core/src/pos.rs +++ b/src/core/src/pos.rs @@ -60,6 +60,18 @@ impl BlockPos { ), } } + + pub fn above(&self) -> BlockPos { + BlockPos { + pos: IVec3::new(self.pos.x, self.pos.y + 1, self.pos.z), + } + } + + pub fn below(&self) -> BlockPos { + BlockPos { + pos: IVec3::new(self.pos.x, self.pos.y - 1, self.pos.z), + } + } } impl From for BlockPos { diff --git a/src/game_systems/src/interactions/Cargo.toml b/src/game_systems/src/interactions/Cargo.toml index 3387d8d1..1340034a 100644 --- a/src/game_systems/src/interactions/Cargo.toml +++ b/src/game_systems/src/interactions/Cargo.toml @@ -15,6 +15,8 @@ temper-protocol = { workspace = true } temper-state = { workspace = true } temper-world = { workspace = true } tracing = { workspace = true } +temper-blocks = { workspace = true } +fastcache = { workspace = true } [dev-dependencies] temper-macros = { workspace = true } diff --git a/src/game_systems/src/interactions/src/block_interactions.rs b/src/game_systems/src/interactions/src/block_interactions.rs deleted file mode 100644 index 62b836eb..00000000 --- a/src/game_systems/src/interactions/src/block_interactions.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Direct world block interactions. -//! -//! This module handles interactions with blocks directly in the world (chunks), -//! without creating ECS entities. This is more performant for simple toggleable -//! blocks like doors, levers, trapdoors, and buttons. -//! -//! ## How it works -//! -//! 1. Player right-clicks on a block -//! 2. System checks if the block is "interactive" based on its name -//! 3. If yes, toggle the relevant property (e.g., "open" for doors) -//! 4. Update the block in the chunk -//! 5. Broadcast BlockUpdate to nearby players -//! -//! ## Supported blocks -//! -//! - Doors (oak_door, iron_door, etc.) - toggles "open" property -//! -//! ## Future supported block -//! -//! - Trapdoors - toggles "open" property -//! - Fence gates - toggles "open" property -//! - Levers - toggles "powered" property -//! - Buttons - activates temporarily (TODO: timer system) - -use temper_core::block_data::BlockData; -use temper_core::block_state_id::BlockStateId; -use tracing::{debug, warn}; - -/// Result of attempting to interact with a block. -#[derive(Debug, Clone)] -pub enum InteractionResult { - /// Block was toggled, returns the new BlockStateId - Toggled(BlockStateId), - /// Block is not interactive - NotInteractive, - /// Block state not found (shouldn't happen) - InvalidBlock, -} - -/// Checks if a block is interactive and returns its interaction type. -pub fn get_interaction_type(block_data: &BlockData) -> Option { - let name = &block_data.name; - - // Doors - if name.ends_with("_door") { - return Some(InteractionType::Toggleable("open")); - } - - None -} - -/// Type of interaction for a block. -#[derive(Debug, Clone, Copy)] -pub enum InteractionType { - /// Block toggles between two states (doors, levers) - /// The string is the property name to toggle - Toggleable(&'static str), - // In the future maybe add Momentary or something like that -} - -/// Attempts to interact with a block and returns the new state if successful. -/// -/// This function: -/// 1. Gets the BlockData from the BlockStateId -/// 2. Checks if it's an interactive block -/// 3. Toggles the appropriate property -/// 4. Returns the new BlockStateId -pub fn try_interact(block_state_id: BlockStateId) -> InteractionResult { - debug!( - "try_interact called with block_state_id: {:?} (raw: {})", - block_state_id, - block_state_id.raw() - ); - - // Get the block data - let Some(mut block_data) = block_state_id.to_block_data() else { - warn!( - "try_interact: InvalidBlock - could not convert {:?} to BlockData", - block_state_id - ); - return InteractionResult::InvalidBlock; - }; - - debug!( - "try_interact: block_data name='{}', properties={:?}", - block_data.name, block_data.properties - ); - - // Check if it's interactive - let Some(interaction_type) = get_interaction_type(&block_data) else { - debug!( - "try_interact: block '{}' is not interactive", - block_data.name - ); - return InteractionResult::NotInteractive; - }; - - debug!("try_interact: interaction_type={:?}", interaction_type); - - let Some(properties) = block_data.properties.as_mut() else { - warn!( - "try_interact: interactive block '{}' has no properties", - block_data.name - ); - return InteractionResult::InvalidBlock; - }; - - match interaction_type { - InteractionType::Toggleable(prop_name) => { - let current_value = properties - .get(prop_name) - .map(|s| s.as_str()) - .unwrap_or("false"); - let new_value = if current_value == "true" { - "false" - } else { - "true" - }; - debug!( - "try_interact: toggling '{}' from '{}' to '{}'", - prop_name, current_value, new_value - ); - properties.insert(prop_name.to_string(), new_value.to_string()); - } - } - - debug!( - "try_interact: modified block_data properties={:?}", - block_data.properties - ); - - // Convert back to BlockStateId - let new_state_id = BlockStateId::from_block_data(&block_data); - debug!( - "try_interact: new_state_id={:?} (raw: {})", - new_state_id, - new_state_id.raw() - ); - - if new_state_id.raw() == 0 { - warn!( - "try_interact: WARNING - new_state_id is 0 (air)! BlockData lookup failed for: name='{}', props={:?}", - block_data.name, block_data.properties - ); - } - - InteractionResult::Toggled(new_state_id) -} - -/// Checks if a block is interactive without modifying it. -pub fn is_interactive(block_state_id: BlockStateId) -> bool { - block_state_id - .to_block_data() - .as_ref() - .and_then(get_interaction_type) - .is_some() -} diff --git a/src/game_systems/src/interactions/src/door_interaction.rs b/src/game_systems/src/interactions/src/door_interaction.rs deleted file mode 100644 index 3c15b748..00000000 --- a/src/game_systems/src/interactions/src/door_interaction.rs +++ /dev/null @@ -1,139 +0,0 @@ -use bevy_ecs::prelude::*; -use temper_codec::net_types::network_position::NetworkPosition; -use temper_codec::net_types::var_int::VarInt; -use temper_components::player::position::Position; -use temper_core::block_state_id::BlockStateId; -use temper_core::pos::BlockPos; -use temper_messages::{BlockBrokenEvent, DoorToggledEvent}; -use temper_net_runtime::connection::StreamWriter; -use temper_protocol::outgoing::block_update::BlockUpdate; -use temper_state::GlobalStateResource; -use temper_world::Dimension; -use tracing::{debug, error}; - -use crate::block_interactions::{InteractionResult, try_interact}; - -/// Given a block state, if it's a door, returns the Y offset to the other half. -/// Lower half -> +1, upper half -> -1, not a door -> None. -pub fn door_other_half_y_offset(block_state_id: BlockStateId) -> Option { - let data = block_state_id.to_block_data()?; - if !data.name.ends_with("_door") { - return None; - } - let props = data.properties.as_ref()?; - let half = props.get("half")?; - match half.as_str() { - "lower" => Some(1), - "upper" => Some(-1), - _ => None, - } -} - -/// Gets the "open" state of a door/trapdoor/fence gate. -pub fn is_open(block_state_id: BlockStateId) -> Option { - let block_data = block_state_id.to_block_data()?; - let properties = block_data.properties.as_ref()?; - let open_value = properties.get("open")?; - Some(open_value == "true") -} - -/// Breaks a block and its door-pair (if applicable). -/// Sets both positions to air and emits `BlockBrokenEvent` for each. -/// Returns the list of all positions that were broken (always includes `pos`, -/// and may include the other door half). -pub fn break_block_with_door_half( - chunk: &mut temper_world::MutChunk, - pos: BlockPos, - block_break_writer: &mut MessageWriter, -) -> Vec { - let current_state = chunk.get_block(pos.chunk_block_pos()); - let other_half = door_other_half_y_offset(current_state).map(|y_off| pos + (0, y_off, 0)); - - chunk.set_block(pos.chunk_block_pos(), BlockStateId::default()); - block_break_writer.write(BlockBrokenEvent { position: pos }); - - let mut broken = vec![pos]; - - if let Some(other_pos) = other_half { - chunk.set_block(other_pos.chunk_block_pos(), BlockStateId::default()); - block_break_writer.write(BlockBrokenEvent { - position: other_pos, - }); - debug!( - "Also broke other door half at ({}, {}, {})", - other_pos.pos.x, other_pos.pos.y, other_pos.pos.z - ); - broken.push(other_pos); - } - - broken -} - -/// Reacts to `DoorToggledEvent` and toggles the other half of the door block. -pub fn handle_door_toggled( - mut events: MessageReader, - state: Res, - query: Query<(&StreamWriter, &Position)>, -) { - for event in events.read() { - let pos = event.position; - - let Some(y_offset) = door_other_half_y_offset(event.new_state) else { - continue; - }; - let other_pos = pos + (0, y_offset, 0); - - let update = { - let mut chunk = match temper_world::World::get_or_generate_mut( - &state.0.world, - other_pos.chunk(), - Dimension::Overworld, - ) { - Ok(c) => c, - Err(e) => { - error!("Failed to load chunk for door other half toggle: {:?}", e); - continue; - } - }; - - let other_state = chunk.get_block(other_pos.chunk_block_pos()); - let InteractionResult::Toggled(other_new) = try_interact(other_state) else { - continue; - }; - - chunk.set_block(other_pos.chunk_block_pos(), other_new); - debug!( - "Door other half: toggled ({}, {}, {}) to {}", - other_pos.pos.x, - other_pos.pos.y, - other_pos.pos.z, - other_new.raw() - ); - - BlockUpdate { - location: NetworkPosition { - x: other_pos.pos.x, - y: other_pos.pos.y as i16, - z: other_pos.pos.z, - }, - block_state_id: VarInt::from(other_new), - } - }; // chunk lock released here - - let block_chunk = other_pos.chunk(); - let (block_cx, block_cz) = (block_chunk.x(), block_chunk.z()); - let render_distance = state.0.config.chunk_render_distance as i32; - - for (conn, player_pos) in query.iter() { - let pchunk = player_pos.chunk(); - let (pcx, pcz) = (pchunk.x(), pchunk.z()); - - if (block_cx - pcx).abs() <= render_distance - && (block_cz - pcz).abs() <= render_distance - && let Err(e) = conn.send_packet_ref(&update) - { - error!("Failed to send door half block update: {:?}", e); - } - } - } -} diff --git a/src/game_systems/src/interactions/src/interaction_listener.rs b/src/game_systems/src/interactions/src/interaction_listener.rs deleted file mode 100644 index ba6c9f97..00000000 --- a/src/game_systems/src/interactions/src/interaction_listener.rs +++ /dev/null @@ -1,140 +0,0 @@ -use bevy_ecs::prelude::*; -use std::collections::HashMap; -use std::time::{Duration, Instant}; -use temper_codec::net_types::network_position::NetworkPosition; -use temper_codec::net_types::var_int::VarInt; -use temper_components::interaction::InteractionCooldown; -use temper_components::player::position::Position; -use temper_core::pos::BlockPos; -use temper_messages::{BlockInteractMessage, BlockToggledEvent, DoorToggledEvent}; -use temper_net_runtime::connection::StreamWriter; -use temper_protocol::outgoing::block_change_ack::BlockChangeAck; -use temper_protocol::outgoing::block_update::BlockUpdate; -use temper_state::GlobalStateResource; -use temper_world::Dimension; -use tracing::{debug, error}; - -use crate::block_interactions::{InteractionResult, try_interact}; -use crate::door_interaction::{door_other_half_y_offset, is_open}; - -pub fn handle_block_interact( - mut events: MessageReader, - state: Res, - query: Query<(Entity, &StreamWriter, &Position)>, - mut toggled_writer: MessageWriter, - mut door_toggled_writer: MessageWriter, - mut cooldowns: Local>, -) { - let cooldown_duration = Duration::from_millis(InteractionCooldown::default().cooldown_ms); - - for event in events.read() { - let pos = event.position; - - // Ignore rapid repeated interactions on the same block - if cooldowns - .get(&pos) - .is_some_and(|t| t.elapsed() < cooldown_duration) - { - if let Ok((_, conn, _)) = query.get(event.player) { - let ack = BlockChangeAck { - sequence: event.sequence, - }; - if let Err(e) = conn.send_packet_ref(&ack) { - error!("Failed to send BlockChangeAck (cooldown): {:?}", e); - } - } - continue; - } - cooldowns.insert(pos, Instant::now()); - - // Load the chunk and get current block state - let mut chunk = match temper_world::World::get_or_generate_mut( - &state.0.world, - pos.chunk(), - Dimension::Overworld, - ) { - Ok(c) => c, - Err(e) => { - error!("Failed to load chunk for block interaction: {:?}", e); - continue; - } - }; - - let (updates, is_active, new_state) = { - let block_state = chunk.get_block(pos.chunk_block_pos()); - - // Try to interact (toggle) the block - let new_state = match try_interact(block_state) { - InteractionResult::Toggled(new) => new, - _ => continue, - }; - - chunk.set_block(pos.chunk_block_pos(), new_state); - debug!( - "Block interact: toggled ({}, {}, {}) from {} to {}", - pos.pos.x, - pos.pos.y, - pos.pos.z, - block_state.raw(), - new_state.raw() - ); - - let updates = vec![BlockUpdate { - location: NetworkPosition { - x: pos.pos.x, - y: pos.pos.y as i16, - z: pos.pos.z, - }, - block_state_id: VarInt::from(new_state), - }]; - - let is_active = is_open(new_state).unwrap_or(false); - (updates, is_active, new_state) - }; // chunk lock released here - - // If it's a door, let the door handler toggle the other half - if door_other_half_y_offset(new_state).is_some() { - door_toggled_writer.write(DoorToggledEvent { - position: pos, - new_state, - }); - } - - // Emit BlockToggledEvent for other systems to react - toggled_writer.write(BlockToggledEvent { - player: event.player, - position: pos, - is_active, - }); - - // Send BlockChangeAck to the player - if let Ok((_, conn, _)) = query.get(event.player) { - let ack = BlockChangeAck { - sequence: event.sequence, - }; - if let Err(e) = conn.send_packet_ref(&ack) { - error!("Failed to send BlockChangeAck: {:?}", e); - } - } - - // Broadcast BlockUpdate to all players within render distance - let block_chunk = pos.chunk(); - let (block_cx, block_cz) = (block_chunk.x(), block_chunk.z()); - let render_distance = state.0.config.chunk_render_distance as i32; - - for (_, conn, player_pos) in query.iter() { - let pchunk = player_pos.chunk(); - let (pcx, pcz) = (pchunk.x(), pchunk.z()); - - if (block_cx - pcx).abs() <= render_distance - && (block_cz - pcz).abs() <= render_distance - { - for update in &updates { - if let Err(e) = conn.send_packet_ref(update) { - error!("Failed to send block update: {:?}", e); - } - } - } - } - } -} diff --git a/src/game_systems/src/interactions/src/lib.rs b/src/game_systems/src/interactions/src/lib.rs index 7daa3fda..428c6fae 100644 --- a/src/game_systems/src/interactions/src/lib.rs +++ b/src/game_systems/src/interactions/src/lib.rs @@ -7,10 +7,149 @@ //! ## How it works //! //! 1. Player right-clicks on a block (PlaceBlock packet) -//! 2. `try_interact()` checks if the block is interactive -//! 3. If yes, toggles the relevant property and returns the new state +//! 2. The `interact()` method is called on the blockstate +//! 3. `interact()` will change the state to the new state and also return any other blocks to update //! 4. The packet handler updates the chunk and broadcasts to players -pub mod block_interactions; -pub mod door_interaction; -pub mod interaction_listener; +use bevy_ecs::change_detection::Res; +use bevy_ecs::entity::Entity; +use bevy_ecs::message::MessageReader; +use bevy_ecs::prelude::{Local, Query}; +use fastcache::Cache; +use std::time::{Duration, Instant}; +use temper_blocks::BlockDispatch; +use temper_codec::net_types::network_position::NetworkPosition; +use temper_codec::net_types::var_int::VarInt; +use temper_components::InteractionCooldown; +use temper_components::player::client_information::ClientInformationComponent; +use temper_components::player::position::Position; +use temper_core::dimension::Dimension; +use temper_core::pos::BlockPos; +use temper_messages::BlockInteractMessage; +use temper_net_runtime::connection::StreamWriter; +use temper_protocol::outgoing::block_change_ack::BlockChangeAck; +use temper_protocol::outgoing::block_update::BlockUpdate; +use temper_state::GlobalStateResource; +use tracing::{debug, error}; + +pub fn handle_block_interact( + mut events: MessageReader, + state: Res, + query: Query<( + Entity, + &StreamWriter, + &Position, + &ClientInformationComponent, + )>, + mut cooldowns: Local>>, +) { + let cooldown_duration = Duration::from_millis(InteractionCooldown::default().cooldown_ms); + + for event in events.read() { + let pos = event.position; + + // Ignore rapid repeated interactions on the same block + if let Some(cooldowns_map) = cooldowns.as_ref() + && cooldowns_map + .get(&pos) + .is_some_and(|t| t.elapsed() < cooldown_duration) + { + if let Ok((_, conn, _, _)) = query.get(event.player) { + let ack = BlockChangeAck { + sequence: event.sequence, + }; + if let Err(e) = conn.send_packet_ref(&ack) { + error!("Failed to send BlockChangeAck (cooldown): {:?}", e); + } + } + continue; + } + cooldowns + .get_or_insert(Cache::new(512, Duration::from_secs(30))) + .insert(pos, Instant::now()); + + // Load the chunk and get current block state + let mut block_state = state + .0 + .world + .get_chunk(pos.chunk(), Dimension::Overworld) + .map(|chunk| chunk.get_block(pos.chunk_block_pos())) + .unwrap(); + let original = block_state; + + let updates = { + let updates = block_state.interact(&state.0.world, pos); + let mut updates = updates.blocks; + updates.insert(pos, block_state); + + debug!( + "Block interact: toggled ({}, {}, {}) from {} to {}", + pos.pos.x, + pos.pos.y, + pos.pos.z, + original.raw(), + block_state.raw() + ); + + for (pos, block_state) in &updates { + if let Err(error) = state + .0 + .world + .get_chunk_mut(pos.chunk(), Dimension::Overworld) + .map(|mut chunk| chunk.set_block(pos.chunk_block_pos(), *block_state)) + { + error!( + "Attempted to update block at {} to {} but failed: {}", + pos, + block_state.raw(), + error + ); + } + } + + updates + .into_iter() + .map(|(pos, state)| BlockUpdate { + location: NetworkPosition { + x: pos.pos.x, + y: pos.pos.y as i16, + z: pos.pos.z, + }, + block_state_id: VarInt::from(state), + }) + .collect::>() + }; // chunk lock released here + + // Send BlockChangeAck to the player + if let Ok((_, conn, _, _)) = query.get(event.player) { + let ack = BlockChangeAck { + sequence: event.sequence, + }; + if let Err(e) = conn.send_packet_ref(&ack) { + error!("Failed to send BlockChangeAck: {:?}", e); + } + } + + // Broadcast BlockUpdate to all players within render distance + let block_chunk = pos.chunk(); + let (block_cx, block_cz) = (block_chunk.x(), block_chunk.z()); + let render_distance = state.0.config.chunk_render_distance; + + for (_, conn, player_pos, client_info) in query.iter() { + let pchunk = player_pos.chunk(); + let (pcx, pcz) = (pchunk.x(), pchunk.z()); + + let player_render_distance = u32::from(client_info.view_distance).min(render_distance); + + if (block_cx - pcx).abs() <= player_render_distance as i32 + && (block_cz - pcz).abs() <= player_render_distance as i32 + { + for update in &updates { + if let Err(e) = conn.send_packet_ref(update) { + error!("Failed to send block update: {:?}", e); + } + } + } + } + } +} diff --git a/src/game_systems/src/lib.rs b/src/game_systems/src/lib.rs index f2a43e60..9ebc6076 100644 --- a/src/game_systems/src/lib.rs +++ b/src/game_systems/src/lib.rs @@ -43,8 +43,7 @@ fn register_tick_systems(schedule: &mut Schedule) { schedule.add_systems(packets::confirm_player_teleport::handle); schedule.add_systems(packets::keep_alive::handle); schedule.add_systems(packets::place_block::handle); - schedule.add_systems(interactions::interaction_listener::handle_block_interact); - schedule.add_systems(interactions::door_interaction::handle_door_toggled); + schedule.add_systems(interactions::handle_block_interact); schedule.add_systems(packets::player_action::handle); schedule.add_systems(packets::player_command::handle); schedule.add_systems(packets::player_input::handle); diff --git a/src/game_systems/src/packets/Cargo.toml b/src/game_systems/src/packets/Cargo.toml index 95eb24de..65c0c69c 100644 --- a/src/game_systems/src/packets/Cargo.toml +++ b/src/game_systems/src/packets/Cargo.toml @@ -17,10 +17,10 @@ temper-commands = { workspace = true } temper-net-runtime = { workspace = true } temper-codec = { workspace = true } temper-config = { workspace = true } +temper-blocks = { workspace = true } once_cell = { workspace = true } serde_json = { workspace = true } temper-macros = { workspace = true } -block-placing = { path = "../../../world/block-placing" } temper-entities = { workspace = true } bevy_math = { workspace = true } temper-world = { workspace = true } diff --git a/src/game_systems/src/packets/src/place_block.rs b/src/game_systems/src/packets/src/place_block.rs index 116ad4ef..383fe20e 100644 --- a/src/game_systems/src/packets/src/place_block.rs +++ b/src/game_systems/src/packets/src/place_block.rs @@ -1,29 +1,34 @@ use bevy_ecs::message::MessageWriter; use bevy_ecs::prelude::{Entity, Query, Res}; -use interactions::block_interactions::is_interactive; use temper_codec::net_types::network_position::NetworkPosition; use temper_components::player::position::Position; use temper_components::{bounds::CollisionBounds, player::sneak::SneakState}; 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_state::GlobalStateResource; -use tracing::{debug, error, trace}; - -use bevy_math::DVec3; +use bevy_math::{DVec3, IVec3}; +use temper_blocks::BlockDispatch; use temper_components::player::rotation::Rotation; +use temper_core::block_state_id::ITEM_TO_BLOCK_MAPPING; use temper_core::dimension::Dimension; use temper_core::mq; -use temper_entities::MobBundle; use temper_inventories::hotbar::Hotbar; use temper_inventories::inventory::Inventory; -use temper_messages::{SpawnMobBundle, world_change::WorldChange}; +use temper_messages::world_change::WorldChange; +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_state::GlobalStateResource; use temper_text::{Color, NamedColor, TextComponentBuilder}; +use tracing::{debug, error, trace}; +// TODO: in the future this should be reworked so that if a block update exits early the client is informed that the block never updated. +// Currently it spawns ghost blocks if this function exits early (ie continues on to the next update) +// +// TODO: also create a better block update propagation system. All block updates should happen (preferably) on the same tick to prevent +// visible slowdowns. When a block is placed, all adjacent blocks should be updated. There's already a built in `update` function on +// BlockBehavior that should return whether or not the block changed (if it did then we should keep updating blocks next to that) pub fn handle( receiver: Res, state: Res, @@ -39,10 +44,9 @@ pub fn handle( pos_q: Query<(&Position, &CollisionBounds)>, mut world_change: MessageWriter, mut block_interact: MessageWriter, - mut mob_bundle_events: MessageWriter, ) { 'ev_loop: for (event, eid) in receiver.0.try_iter() { - let Ok((entity, conn, inventory, hotbar, pos, rot, sneak)) = query.get(eid) else { + let Ok((entity, conn, inventory, hotbar, _pos, rot, sneak)) = query.get(eid) else { debug!("Could not get connection for entity {:?}", eid); continue; }; @@ -60,7 +64,7 @@ pub fn handle( .get_or_generate_chunk(clicked_pos.chunk(), Dimension::Overworld) .expect("Failed to load chunk for interaction check"); let clicked_block = chunk.get_block(clicked_pos.chunk_block_pos()); - if !sneak.is_sneaking && is_interactive(clicked_block) { + if !sneak.is_sneaking && clicked_block.is_interactable() { block_interact.write(BlockInteractMessage { player: entity, position: clicked_pos, @@ -70,6 +74,10 @@ pub fn handle( } } + let ack_packet = BlockChangeAck { + sequence: event.sequence, + }; + match event.hand.0 { 0 => { let Ok(slot) = hotbar.get_selected_item(inventory) else { @@ -109,24 +117,19 @@ pub fn handle( trace!("Block placement out of bounds: {}", block_pos); continue 'ev_loop; } + let offset_pos = block_pos - + match event.face.0 { - 0 => (0, -1, 0), - 1 => (0, 1, 0), - 2 => (0, 0, -1), - 3 => (0, 0, 1), - 4 => (-1, 0, 0), - 5 => (1, 0, 0), - _ => (0, 0, 0), - }; + + IVec3::new( + (event.cursor_x * 2.0 - 1.0) as i32, + (event.cursor_y * 2.0 - 1.0) as i32, + (event.cursor_z * 2.0 - 1.0) as i32, + ) + .into(); - let block_clicked = { - let chunk = state - .0 - .world - .get_or_generate_chunk(block_pos.chunk(), Dimension::Overworld) - .expect("Failed to load or generate chunk"); - chunk.get_block(block_pos.chunk_block_pos()) + let Ok(curr_state) = state.0.world.get_block(offset_pos, Dimension::Overworld) + else { + error!("Can't get block at {}", offset_pos); + continue 'ev_loop; }; // Check if the block collides with any entities @@ -151,101 +154,96 @@ pub fn handle( }) }; - if does_collide { + if does_collide && curr_state.is_solid() { trace!("Block placement collided with entity"); continue 'ev_loop; } - let _block_at_pos = { - let chunk = state - .0 - .world - .get_or_generate_chunk(offset_pos.chunk(), Dimension::Overworld) - .expect("Failed to load or generate chunk"); - chunk.get_block(offset_pos.chunk_block_pos()) + let placement_context = temper_blocks::PlacementContext { + face: event.face, + cursor: DVec3::new( + f64::from(event.cursor_x), + f64::from(event.cursor_y), + f64::from(event.cursor_z), + ), + block_clicked: block_pos, + block_pos: offset_pos, + level: &state.0.world, + dimension: Dimension::Overworld, + player_rotation: rot, }; - let place_result = block_placing::place_item( - state.0.clone(), - block_placing::BlockPlaceContext { - block_clicked, - block_position: offset_pos, - face_clicked: match event.face.0 { - 0 => block_placing::BlockFace::Bottom, - 1 => block_placing::BlockFace::Top, - 2 => block_placing::BlockFace::North, - 3 => block_placing::BlockFace::South, - 4 => block_placing::BlockFace::West, - 5 => block_placing::BlockFace::East, - _ => { - debug!("Invalid block face"); - continue 'ev_loop; - } - }, - click_position: DVec3::new( - f64::from(event.cursor_x), - f64::from(event.cursor_y), - f64::from(event.cursor_z), - ), - player_position: *pos, - player_rotation: *rot, - item_used: item_id, - }, - ); + if !curr_state.can_be_replaced(placement_context.clone()) { + if let Err(err) = conn.send_packet_ref(&ack_packet) { + error!("Failed to send block change ack packet: {:?}", err); + continue 'ev_loop; + } - match place_result { - Ok(block_placing::PlaceResult::SpawnMob(entity_type)) => { - let spawn_pos_vec = Position::new( - f64::from(offset_pos.pos.x) + 0.5, - f64::from(offset_pos.pos.y), - f64::from(offset_pos.pos.z) + 0.5, - ); - mob_bundle_events.write(SpawnMobBundle { - bundle: MobBundle::new(entity_type, spawn_pos_vec), - persist: true, - }); + if let Err(err) = conn.send_packet(BlockUpdate { + location: NetworkPosition { + x: offset_pos.pos.x, + y: offset_pos.pos.y as i16, + z: offset_pos.pos.z, + }, + block_state_id: curr_state.to_varint(), + }) { + error!("Failed to send block update packet to player: {err}"); + continue 'ev_loop; } - Ok(block_placing::PlaceResult::Placed(placed_blocks)) => { - for (block_pos, block_state) in placed_blocks.blocks { - let block_chunk = block_pos.chunk(); - world_change.write(WorldChange { - chunk: Some(block_chunk), - }); - let chunk_packet = BlockUpdate { - location: NetworkPosition { - x: block_pos.pos.x, - y: block_pos.pos.y as i16, - z: block_pos.pos.z, - }, - block_state_id: block_state.to_varint(), - }; + continue 'ev_loop; + } + + let mut block_state = ITEM_TO_BLOCK_MAPPING + .get() + .unwrap() + .get(&(item_id.as_u32() as i32)) + .copied() + .unwrap(); + + let mut placed_blocks = block_state.get_placement_state(placement_context); + + if placed_blocks.place_original { + placed_blocks.blocks.insert(offset_pos, block_state); + } + + for (block_pos, block_state) in placed_blocks.blocks.iter() { + state + .0 + .world + .set_block(*block_pos, Dimension::Overworld, *block_state) + .unwrap_or_else(|_| error!("Failed to update block {}", block_pos)); + + let block_chunk = block_pos.chunk(); + world_change.write(WorldChange { + chunk: Some(block_chunk), + }); + + let chunk_packet = BlockUpdate { + location: NetworkPosition { + x: block_pos.pos.x, + y: block_pos.pos.y as i16, + z: block_pos.pos.z, + }, + block_state_id: block_state.to_varint(), + }; - let (block_chunk_x, block_chunk_z) = - (block_chunk.x(), block_chunk.z()); - let render_distance = state.0.config.chunk_render_distance as i32; - for (_, conn, _, _, pos, _, _) in query.iter() { - let chunk = pos.chunk(); - let (chunk_x, chunk_z) = (chunk.x(), chunk.z()); + let (block_chunk_x, block_chunk_z) = (block_chunk.x(), block_chunk.z()); + let render_distance = state.0.config.chunk_render_distance as i32; + for (_, conn, _, _, pos, _, _) in query.iter() { + let chunk = pos.chunk(); + let (chunk_x, chunk_z) = (chunk.x(), chunk.z()); - // Only send block update if the player is within the render distance of the block being updated - if (block_chunk_x - chunk_x).abs() <= render_distance - && (block_chunk_z - chunk_z).abs() <= render_distance - && let Err(err) = conn.send_packet_ref(&chunk_packet) - { - error!("Failed to send block update packet: {:?}", err); - } - } + // Only send block update if the player is within the render distance of the block being updated + if (block_chunk_x - chunk_x).abs() <= render_distance + && (block_chunk_z - chunk_z).abs() <= render_distance + && let Err(err) = conn.send_packet_ref(&chunk_packet) + { + error!("Failed to send block update packet: {:?}", err); } } - Err(err) => { - error!("Block placement failed: {:?}", err); - } } } - let ack_packet = BlockChangeAck { - sequence: event.sequence, - }; if let Err(err) = conn.send_packet_ref(&ack_packet) { error!("Failed to send block change ack packet: {:?}", err); diff --git a/src/game_systems/src/packets/src/player_action.rs b/src/game_systems/src/packets/src/player_action.rs index 0765434c..2ef9fa94 100644 --- a/src/game_systems/src/packets/src/player_action.rs +++ b/src/game_systems/src/packets/src/player_action.rs @@ -3,12 +3,13 @@ use temper_components::player::abilities::PlayerAbilities; use temper_messages::BlockBrokenEvent; use temper_messages::player_digging::*; -use interactions::door_interaction::break_block_with_door_half; +use temper_blocks::BlockDispatch; use temper_codec::net_types::network_position::NetworkPosition; use temper_codec::net_types::var_int::VarInt; use temper_core::block_state_id::BlockStateId; use temper_core::dimension::Dimension; use temper_core::pos::BlockPos; +use temper_macros::block; use temper_net_runtime::connection::StreamWriter; use temper_protocol::PlayerActionReceiver; use temper_protocol::outgoing::block_change_ack::BlockChangeAck; @@ -51,18 +52,30 @@ pub fn handle( // --- CREATIVE MODE LOGIC --- // Only instabreak (status 0) is relevant in creative. if event.status.0 == 0 { - let mut chunk = state - .0 - .world - .get_or_generate_mut(pos.chunk(), Dimension::Overworld) - .expect("Failed to load or generate chunk"); - world_change.write(temper_messages::world_change::WorldChange { chunk: Some(pos.chunk()), }); - let broken_positions = - break_block_with_door_half(&mut chunk, pos, &mut block_break_events); + let Ok(id) = state.0.world.get_block(pos, Dimension::Overworld) else { + error!("Couldn't get block at pos {}", pos); + continue; + }; + + let mut broken_positions = id.try_break(&state.0.world, pos).blocks; + broken_positions.push(pos); + + for pos in &broken_positions { + block_break_events.write(BlockBrokenEvent { position: *pos }); + + if let Err(world_error) = + state + .0 + .world + .set_block(*pos, Dimension::Overworld, block!("air")) + { + error!("Failed to break block at {}: {}", pos, world_error) + } + } // Broadcast the change for (eid, conn) in &broadcast_query { diff --git a/src/game_systems/src/player/Cargo.toml b/src/game_systems/src/player/Cargo.toml index 49de0134..bb4de5b1 100644 --- a/src/game_systems/src/player/Cargo.toml +++ b/src/game_systems/src/player/Cargo.toml @@ -26,6 +26,7 @@ interactions = { workspace = true } rand = { workspace = true } bitcode = { workspace = true } temper-permissions = { workspace = true } +temper-blocks = { workspace = true } [lints] workspace = true diff --git a/src/game_systems/src/player/src/digging_system.rs b/src/game_systems/src/player/src/digging_system.rs index 1904cdbf..6e568782 100644 --- a/src/game_systems/src/player/src/digging_system.rs +++ b/src/game_systems/src/player/src/digging_system.rs @@ -1,7 +1,7 @@ use bevy_ecs::prelude::*; use std::time::{Duration, Instant}; -use interactions::door_interaction::break_block_with_door_half; +use temper_blocks::BlockDispatch; use temper_codec::net_types::network_position::NetworkPosition; use temper_codec::net_types::var_int::VarInt; use temper_components::player::abilities::PlayerAbilities; @@ -10,6 +10,7 @@ use temper_core::block_state_id::BlockStateId; use temper_core::dimension::Dimension; use temper_core::pos::BlockPos; use temper_data::blocks::types::Block; +use temper_macros::block; use temper_messages::player_digging::*; use temper_messages::world_change::WorldChange; use temper_net_runtime::connection::StreamWriter; @@ -291,19 +292,31 @@ fn break_block( world_change: &mut MessageWriter, ) { let pos: BlockPos = position.clone().into(); - let mut chunk = state - .0 - .world - .get_or_generate_mut(pos.chunk(), Dimension::Overworld) - .expect("Failed to load or generate chunk"); - debug!("Sending BlockBrokenEvent for block at {:?}", pos.pos); + let Ok(id) = state.0.world.get_block(pos, Dimension::Overworld) else { + error!("Failed to get block at {}", pos); + return; + }; + + let mut broken_positions = id.try_break(&state.0.world, pos).blocks; + broken_positions.push(pos); - let broken_positions = break_block_with_door_half(&mut chunk, pos, block_break_writer); world_change.write(WorldChange { chunk: Some(pos.chunk()), }); + for pos in &broken_positions { + block_break_writer.write(temper_messages::BlockBrokenEvent { position: *pos }); + + if let Err(world_error) = state + .0 + .world + .set_block(*pos, Dimension::Overworld, block!("air")) + { + error!("Failed to break block at {}: {}", pos, world_error) + } + } + // Broadcast the block break to all players let block_update_packet = BlockUpdate { location: position.clone(), diff --git a/src/game_systems/tests/interactions.rs b/src/game_systems/tests/interactions.rs deleted file mode 100644 index 35b38d68..00000000 --- a/src/game_systems/tests/interactions.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[path = "interactions/block_interactions.rs"] -mod block_interactions; diff --git a/src/game_systems/tests/interactions/block_interactions.rs b/src/game_systems/tests/interactions/block_interactions.rs deleted file mode 100644 index 8e887fbb..00000000 --- a/src/game_systems/tests/interactions/block_interactions.rs +++ /dev/null @@ -1,75 +0,0 @@ -use interactions::block_interactions::{ - get_interaction_type, is_interactive, try_interact, InteractionResult, InteractionType, -}; -use std::collections::BTreeMap; -use temper_core::block_data::BlockData; -use temper_core::block_state_id::BlockStateId; -use temper_macros::block; - -#[test] -fn door_detection() { - let door_data = BlockData { - name: "minecraft:oak_door".to_string(), - properties: Some(BTreeMap::from([ - ("facing".to_string(), "north".to_string()), - ("open".to_string(), "false".to_string()), - ("half".to_string(), "lower".to_string()), - ("hinge".to_string(), "left".to_string()), - ])), - }; - - assert!(matches!( - get_interaction_type(&door_data), - Some(InteractionType::Toggleable("open")) - )); -} - -#[test] -fn try_interact_opens_door() { - let closed_door = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); - - let result = try_interact(closed_door); - let InteractionResult::Toggled(new_id) = result else { - panic!("Expected Toggled, got {:?}", result); - }; - - let new_data = new_id - .to_block_data() - .expect("new state ID should be valid"); - let props = new_data.properties.expect("door should have properties"); - assert_eq!(props["open"], "true"); -} - -#[test] -fn try_interact_closes_door() { - let open_door = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: true, powered: false }); - - let result = try_interact(open_door); - let InteractionResult::Toggled(new_id) = result else { - panic!("Expected Toggled, got {:?}", result); - }; - - let new_data = new_id - .to_block_data() - .expect("new state ID should be valid"); - let props = new_data.properties.expect("door should have properties"); - assert_eq!(props["open"], "false"); -} - -#[test] -fn try_interact_not_interactive() { - let stone = block!("stone"); - assert!(matches!( - try_interact(stone), - InteractionResult::NotInteractive - )); -} - -#[test] -fn is_interactive_reports_doors_only() { - let door = block!("oak_door", { facing: "north", half: "lower", hinge: "left", open: false, powered: false }); - let stone = block!("stone"); - - assert!(is_interactive(door)); - assert!(!is_interactive(stone)); -} diff --git a/src/messages/src/block_interaction.rs b/src/messages/src/block_interaction.rs index 162f00d9..dc8c1020 100644 --- a/src/messages/src/block_interaction.rs +++ b/src/messages/src/block_interaction.rs @@ -1,6 +1,5 @@ use bevy_ecs::prelude::{Entity, Message}; use temper_codec::net_types::var_int::VarInt; -use temper_core::block_state_id::BlockStateId; use temper_core::pos::BlockPos; /// Message sent when a player right-clicks an interactive block (door, lever, etc.) @@ -13,20 +12,3 @@ pub struct BlockInteractMessage { pub position: BlockPos, pub sequence: VarInt, } - -/// Emitted when a block has been toggled (door opened/closed, etc.). -/// -/// Fired by the interaction listener after a successful toggle. -#[derive(Message, Clone, Debug)] -pub struct BlockToggledEvent { - pub player: Entity, - pub position: BlockPos, - pub is_active: bool, -} - -/// Emitted when a door block is toggled, so the door system can toggle the other half. -#[derive(Message, Clone, Debug)] -pub struct DoorToggledEvent { - pub position: BlockPos, - pub new_state: BlockStateId, -} diff --git a/src/messages/src/lib.rs b/src/messages/src/lib.rs index bc2db266..4a5ad313 100644 --- a/src/messages/src/lib.rs +++ b/src/messages/src/lib.rs @@ -52,7 +52,7 @@ use crate::particle::SendParticle; use crate::save_chunk_entities::SaveChunkEntities; use crate::teleport_player::TeleportPlayer; pub use block_break::BlockBrokenEvent; -pub use block_interaction::{BlockInteractMessage, BlockToggledEvent, DoorToggledEvent}; +pub use block_interaction::BlockInteractMessage; use temper_commands::messages::{CommandDispatched, ResolvedCommandDispatched}; use world_change::WorldChange; @@ -83,8 +83,6 @@ pub fn register_messages(world: &mut World) { MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); - MessageRegistry::register_message::(world); - MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); diff --git a/src/net/protocol/src/incoming/place_block.rs b/src/net/protocol/src/incoming/place_block.rs index c3692543..703e88d1 100644 --- a/src/net/protocol/src/incoming/place_block.rs +++ b/src/net/protocol/src/incoming/place_block.rs @@ -1,5 +1,6 @@ use temper_codec::net_types::network_position::NetworkPosition; use temper_codec::net_types::var_int::VarInt; +use temper_core::block_face::BlockFace; use temper_macros::{NetDecode, packet}; #[derive(NetDecode, Debug)] @@ -7,7 +8,7 @@ use temper_macros::{NetDecode, packet}; pub struct PlaceBlock { pub hand: VarInt, pub position: NetworkPosition, - pub face: VarInt, + pub face: BlockFace, pub cursor_x: f32, pub cursor_y: f32, pub cursor_z: f32, diff --git a/src/world/block-placing/Cargo.toml b/src/world/block-placing/Cargo.toml deleted file mode 100644 index c447f49c..00000000 --- a/src/world/block-placing/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "block-placing" -version = "0.1.0" -edition = "2024" - -[dependencies] -temper-core = { workspace = true } -temper-world = { workspace = true } -bevy_ecs = { workspace = true } -bevy_math = { workspace = true } -temper-components = { workspace = true } -temper-inventories = { workspace = true } -temper-codec = { workspace = true } -temper-macros = { workspace = true } -temper-state = { workspace = true } -temper-entities = { workspace = true } -tracing = { workspace = true } -thiserror = { workspace = true } - -[lints] -workspace = true diff --git a/src/world/block-placing/src/blocks/door.rs b/src/world/block-placing/src/blocks/door.rs deleted file mode 100644 index 1117b470..00000000 --- a/src/world/block-placing/src/blocks/door.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::errors::BlockPlaceError; -use crate::{BlockFace, BlockStateId}; -use crate::{BlockPlaceContext, PlacableBlock, PlacedBlocks}; -use bevy_math::IVec3; -use std::collections::BTreeMap; -use temper_core::block_data::BlockData; -use temper_core::dimension::Dimension; -use temper_macros::match_block; -use temper_state::GlobalState; -use tracing::error; - -pub(crate) struct PlaceableDoor; - -impl PlacableBlock for PlaceableDoor { - 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 block_above = { - 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.pos + IVec3::new(0, 1, 0)).into()) - }; - if !(match_block!("air", block_above) || match_block!("cave_air", block_above)) { - return Err(BlockPlaceError::TargetBlockNotEmpty( - context.block_position + IVec3::new(0, 1, 0).into(), - )); - }; - let facing = match context.face_clicked { - BlockFace::North => "south", - BlockFace::South => "north", - BlockFace::East => "west", - BlockFace::West => "east", - BlockFace::Top => { - // Facing is determined by player rotation when placing on top face - let yaw = (context.player_rotation.yaw + 180.0) % 360.0; - if (45.0..135.0).contains(&yaw) { - "east" - } else if (135.0..225.0).contains(&yaw) { - "south" - } else if (225.0..315.0).contains(&yaw) { - "west" - } else { - "north" - } - } - _ => return Err(BlockPlaceError::InvalidBlockFace(context.face_clicked)), - }; - let bottom_block = BlockData { - name: name.to_string(), - properties: Some(BTreeMap::from([ - ("facing".to_string(), facing.to_string()), - ("half".to_string(), "lower".to_string()), - ("hinge".to_string(), "left".to_string()), - ("open".to_string(), "false".to_string()), - ("powered".to_string(), "false".to_string()), - ])), - }; - let Some(bottom_block_id) = bottom_block.try_to_block_state_id() else { - error!("Block data '{bottom_block}' could not be converted to a block state ID"); - return Err(BlockPlaceError::BlockNotMappedToBlockStateId(bottom_block)); - }; - { - 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(), bottom_block_id); - } - - let upper_block = BlockData { - name: name.to_string(), - properties: Some(BTreeMap::from([ - ("facing".to_string(), facing.to_string()), - ("half".to_string(), "upper".to_string()), - ("hinge".to_string(), "left".to_string()), - ("open".to_string(), "false".to_string()), - ("powered".to_string(), "false".to_string()), - ])), - }; - - let Some(upper_block_id) = upper_block.clone().try_to_block_state_id() else { - error!("Block data '{upper_block}' could not be converted to a block state ID"); - return Err(BlockPlaceError::BlockNotMappedToBlockStateId(upper_block)); - }; - { - state - .world - .get_or_generate_mut(context.block_position.chunk(), Dimension::Overworld) - .expect("Could not load chunk") - .set_block( - (context.block_position + (0, 1, 0)).chunk_block_pos(), - upper_block_id, - ); - } - - Ok(PlacedBlocks { - blocks: std::collections::HashMap::from([ - (context.block_position, bottom_block_id), - (context.block_position + (0, 1, 0), upper_block_id), - ]), - take_item: true, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::BlockPlaceContext; - use temper_components::player::rotation::Rotation; - use temper_core::dimension::Dimension; - use temper_core::pos::BlockPos; - use temper_macros::{block, item}; - - #[test] - fn test_place_door() { - let (state, _) = temper_state::create_test_state(); - let context = BlockPlaceContext { - block_clicked: Default::default(), - block_position: BlockPos::of(0, 64, 0), - face_clicked: BlockFace::Top, - click_position: Default::default(), - item_used: item!("oak_door"), - player_rotation: Rotation { - yaw: 90.0, - pitch: 0.0, - }, - player_position: Default::default(), - }; - let result = PlaceableDoor::place(context, state.0.clone()); - assert!(result.is_ok()); - let placed_blocks = result.unwrap(); - assert_eq!(placed_blocks.blocks.len(), 2); - let bottom_block_id = placed_blocks - .blocks - .get(&BlockPos::of(0, 64, 0)) - .expect("Bottom block not placed"); - let upper_block_id = placed_blocks - .blocks - .get(&BlockPos::of(0, 65, 0)) - .expect("Upper block not placed"); - assert_eq!( - state - .0 - .world - .get_or_generate_chunk(BlockPos::of(0, 64, 0).chunk(), Dimension::Overworld) - .expect("Could not load chunk") - .get_block(BlockPos::of(0, 64, 0).chunk_block_pos()), - *bottom_block_id - ); - assert_eq!( - state - .0 - .world - .get_or_generate_chunk(BlockPos::of(0, 65, 0).chunk(), Dimension::Overworld) - .expect("Could not load chunk") - .get_block(BlockPos::of(0, 65, 0).chunk_block_pos()), - *upper_block_id - ); - } - - #[test] - fn test_place_door_with_block_above() { - let (state, _) = temper_state::create_test_state(); - // Place a block above the door 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, 65, 0).chunk_block_pos(), block!("stone")); // Some solid block - } - let context = BlockPlaceContext { - block_clicked: Default::default(), - block_position: BlockPos::of(0, 64, 0), - face_clicked: BlockFace::Top, - click_position: Default::default(), - item_used: item!("oak_door"), - player_rotation: Rotation { - yaw: 90.0, - pitch: 0.0, - }, - player_position: Default::default(), - }; - let result = PlaceableDoor::place(context, state.0.clone()); - assert!( - result.is_err(), - "Placing a door with a block above should return an error" - ); - } - - #[test] - fn test_place_door_on_invalid_face() { - let (state, _) = temper_state::create_test_state(); - let context = BlockPlaceContext { - block_clicked: Default::default(), - block_position: BlockPos::of(0, 64, 0), - face_clicked: BlockFace::Bottom, // Invalid face for door placement - click_position: Default::default(), - item_used: item!("oak_door"), - player_rotation: Rotation { - yaw: 90.0, - pitch: 0.0, - }, - player_position: Default::default(), - }; - let result = PlaceableDoor::place(context, state.0.clone()); - assert!( - result.is_err(), - "Placing a door on an invalid face should return an error" - ); - match result.err().unwrap() { - BlockPlaceError::InvalidBlockFace(face) => assert_eq!(face, BlockFace::Bottom), - _ => panic!("Expected InvalidBlockFace error"), - } - // Check that no blocks were placed - 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()), - block!("air") - ); - } -} diff --git a/src/world/block-placing/src/blocks/fence.rs b/src/world/block-placing/src/blocks/fence.rs deleted file mode 100644 index c565480d..00000000 --- a/src/world/block-placing/src/blocks/fence.rs +++ /dev/null @@ -1,202 +0,0 @@ -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, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::BlockFace; - use temper_core::pos::BlockPos; - use temper_macros::item; - use temper_state::create_test_state; - - #[test] - fn test_place_fence() { - 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() { - 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" - ); - } -} diff --git a/src/world/block-placing/src/blocks/logs.rs b/src/world/block-placing/src/blocks/logs.rs deleted file mode 100644 index b078b725..00000000 --- a/src/world/block-placing/src/blocks/logs.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::BlockStateId; -use crate::errors::BlockPlaceError; -use crate::{BlockFace, BlockPlaceContext, PlacableBlock, PlacedBlocks}; -use std::collections::BTreeMap; -use temper_core::block_data::BlockData; -use temper_macros::block; -use temper_state::GlobalState; - -pub(crate) struct PlacableLog; - -impl PlacableBlock for PlacableLog { - fn place( - 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.to_name() { - Some(name) => name, - None => return Err(BlockPlaceError::ItemIdHasNoNameMapping(context.item_used)), - }; - - let block_data = BlockData { - 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, - ) - .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 deleted file mode 100644 index 117addcc..00000000 --- a/src/world/block-placing/src/blocks/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -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/blocks/slab.rs b/src/world/block-placing/src/blocks/slab.rs deleted file mode 100644 index 57305bd9..00000000 --- a/src/world/block-placing/src/blocks/slab.rs +++ /dev/null @@ -1,314 +0,0 @@ -use crate::errors::BlockPlaceError; -use crate::{BlockFace, 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) -> &'static str { - match face { - crate::BlockFace::Top => "bottom", - crate::BlockFace::Bottom => "top", - _ => { - if click_position.y > 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 slab_type = clicked_block_data - .properties - .as_ref() - .and_then(|p| p.get("type")) - .map(|value| value.as_str()); - - let face_is_horizontal = context.face_clicked.is_horizontal(); - let above = context.click_position.y > 0.5; - - let should_combine = if !is_same_slab_block(&clicked_block_data, &block_name) { - false - } else if slab_type == Some("bottom") { - context.face_clicked == BlockFace::Top || above && face_is_horizontal - } else { - context.face_clicked == BlockFace::Bottom || !above && face_is_horizontal - }; - - 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); - - 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::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() { - 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() { - 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() { - 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 deleted file mode 100644 index 2521d85a..00000000 --- a/src/world/block-placing/src/blocks/torch.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::errors::BlockPlaceError; -use crate::{BlockFace, BlockPlaceContext}; -use crate::{BlockStateId, PlacableBlock, PlacedBlocks}; -use std::collections::HashMap; -use temper_core::dimension::Dimension; -use temper_macros::block; -use temper_state::GlobalState; - -pub(crate) struct PlaceableTorch; - -impl PlacableBlock for PlaceableTorch { - fn place( - 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"}), - BlockFace::West => block!("wall_torch", {facing: "west"}), - BlockFace::North => block!("wall_torch", {facing: "north"}), - BlockFace::South => block!("wall_torch", {facing: "south"}), - BlockFace::Bottom => { - return Ok(PlacedBlocks { - blocks: HashMap::new(), - take_item: false, - }); - } // can't place on bottom face - }; - state - .world - .get_or_generate_mut(context.block_position.chunk(), Dimension::Overworld)? - .set_block(context.block_position.chunk_block_pos(), block); - Ok(PlacedBlocks { - blocks: HashMap::from([(context.block_position, block)]), - take_item: true, - }) - } -} diff --git a/src/world/block-placing/src/errors.rs b/src/world/block-placing/src/errors.rs deleted file mode 100644 index 406b24a6..00000000 --- a/src/world/block-placing/src/errors.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::BlockFace; -use temper_core::block_data::BlockData; -use temper_core::pos::BlockPos; -use temper_inventories::item::ItemID; -use temper_world::WorldError; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum BlockPlaceError { - #[error("Invalid block face for placement")] - InvalidBlockFace(BlockFace), - #[error("Target block is not empty")] - TargetBlockNotEmpty(BlockPos), - #[error("Item cannot be placed as a block")] - ItemNotPlaceable(ItemID), - #[error("World Error: {0}")] - WorldError(#[from] WorldError), - #[error("Item can't be mapped to block")] - 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 deleted file mode 100644 index 79ed21ae..00000000 --- a/src/world/block-placing/src/lib.rs +++ /dev/null @@ -1,112 +0,0 @@ -mod blocks; -mod errors; - -use crate::errors::BlockPlaceError; -use bevy_math::DVec3; -use std::collections::HashMap; -use temper_components::player::position::Position; -use temper_components::player::rotation::Rotation; -use temper_core::block_state_id::{ - BlockStateId, ITEM_TO_BLOCK_MAPPING, create_item_to_block_mapping, -}; -use temper_core::dimension::Dimension; -use temper_core::pos::BlockPos; -use temper_inventories::item::ItemID; -use temper_state::GlobalState; - -pub struct PlacedBlocks { - pub blocks: HashMap, - pub take_item: bool, -} - -pub enum PlaceResult { - Placed(PlacedBlocks), - SpawnMob(temper_entities::entity_types::EntityTypeEnum), -} - -pub trait PlacableBlock { - fn place( - context: BlockPlaceContext, - state: GlobalState, - ) -> Result; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum BlockFace { - Top, - Bottom, - North, - South, - West, - East, -} - -impl BlockFace { - pub fn is_vertical(&self) -> bool { - matches!(self, Self::Top | Self::Bottom) - } - - pub fn is_horizontal(&self) -> bool { - matches!(self, Self::North | Self::South | Self::West | Self::East) - } -} - -pub struct BlockPlaceContext { - pub block_clicked: BlockStateId, - pub block_position: BlockPos, - pub face_clicked: BlockFace, - pub click_position: DVec3, - pub player_position: Position, - pub player_rotation: Rotation, - pub item_used: ItemID, -} - -pub fn place_item( - state: GlobalState, - context: BlockPlaceContext, -) -> Result { - 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 let Some(entity_type) = item_name - .strip_suffix("_spawn_egg") - .and_then(temper_entities::entity_types::EntityTypeEnum::from_snake_case) - { - return Ok(PlaceResult::SpawnMob(entity_type)); - } - - if item_name == "torch" { - blocks::torch::PlaceableTorch::place(context, state).map(PlaceResult::Placed) - } else if item_name.ends_with("_slab") { - blocks::slab::PlaceableSlab::place(context, state).map(PlaceResult::Placed) - } else if item_name.ends_with("_door") { - blocks::door::PlaceableDoor::place(context, state).map(PlaceResult::Placed) - } else if item_name.ends_with("_log") { - blocks::logs::PlacableLog::place(context, state).map(PlaceResult::Placed) - } else if item_name.ends_with("_fence") { - blocks::fence::PlaceableFence::place(context, state).map(PlaceResult::Placed) - } else { - let block_opt = ITEM_TO_BLOCK_MAPPING - .get_or_init(create_item_to_block_mapping) - .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(PlaceResult::Placed(PlacedBlocks { - blocks: HashMap::from([(context.block_position, *block)]), - take_item: true, - })) - } - Err(e) => Err(e.into()), - } - } else { - Err(BlockPlaceError::ItemNotPlaceable(context.item_used)) - } - } -} diff --git a/src/world/src/helpers.rs b/src/world/src/helpers.rs new file mode 100644 index 00000000..b176ef5e --- /dev/null +++ b/src/world/src/helpers.rs @@ -0,0 +1,28 @@ +use crate::World; +use temper_core::block_state_id::BlockStateId; +use temper_core::dimension::Dimension; +use temper_core::pos::BlockPos; +use temper_world_format::errors::WorldError; + +impl World { + /// Attempts to get a block in the world. Returns an error if the chunk is not accessible + pub fn get_block( + &self, + pos: BlockPos, + dimension: Dimension, + ) -> Result { + self.get_chunk(pos.chunk(), dimension) + .map(|chunk| chunk.get_block(pos.chunk_block_pos())) + } + + /// Attempts to set a block in the world. Returns an error if the chunk is not mutable + pub fn set_block( + &self, + pos: BlockPos, + dimension: Dimension, + block_state_id: BlockStateId, + ) -> Result<(), WorldError> { + self.get_chunk_mut(pos.chunk(), dimension) + .map(|mut chunk| chunk.set_block(pos.chunk_block_pos(), block_state_id)) + } +} diff --git a/src/world/src/lib.rs b/src/world/src/lib.rs index 48e431c3..482b4761 100644 --- a/src/world/src/lib.rs +++ b/src/world/src/lib.rs @@ -1,4 +1,5 @@ mod db_wrap; +mod helpers; mod importing; pub mod player;