diff --git a/Cargo.lock b/Cargo.lock index 2bf31ef..b8d1ea8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,7 +631,9 @@ dependencies = [ "bevy_fix_cursor_unlock_web", "clap", "lightyear", + "ron 0.12.0", "serde", + "thiserror 2.0.17", "web-sys", ] @@ -640,7 +642,9 @@ name = "bevy-bloxel-classic-common" version = "0.1.0" dependencies = [ "bevy", + "derive-where", "lightyear", + "ron 0.12.0", "serde", "thiserror 2.0.17", ] @@ -692,7 +696,7 @@ dependencies = [ "downcast-rs 2.0.2", "either", "petgraph", - "ron", + "ron 0.10.1", "serde", "smallvec", "thiserror 2.0.17", @@ -785,8 +789,9 @@ dependencies = [ "futures-io", "futures-lite", "js-sys", + "notify-debouncer-full", "parking_lot", - "ron", + "ron 0.10.1", "serde", "stackfuture", "thiserror 2.0.17", @@ -2620,6 +2625,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -2882,6 +2898,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -3002,6 +3027,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.31" @@ -3891,6 +3925,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "ktx2" version = "0.4.0" @@ -4719,6 +4773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -4925,6 +4980,43 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "ntapi" version = "0.4.1" @@ -6055,6 +6147,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags 2.10.0", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "roxmltree" version = "0.20.0" diff --git a/Cargo.toml b/Cargo.toml index 56a85d7..3c66cad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,12 @@ opt-level = 1 opt-level = 3 [workspace.dependencies] -bevy = { version = "0.17.2", features = [ "serialize" ] } -# lightyear = { version = "0.25.3", features = [ "netcode", "webtransport" ] } +bevy = { version = "0.17.2", features = [ "serialize", "file_watcher", "embedded_watcher" ] } +# lightyear = { version = "0.25.3", features = [ "netcode", "webtransport", "input_native" ] } + +derive-where = { version = "1.6.0", features = [ "serde" ] } + +ron = "0.12.0" serde = "1.0.228" thiserror = "2.0.17" diff --git a/assets/blocks/default.ron b/assets/blocks/default.ron new file mode 100644 index 0000000..7c355f6 --- /dev/null +++ b/assets/blocks/default.ron @@ -0,0 +1,3 @@ +( + color: (124, 144, 255), +) diff --git a/assets/blocks/platform.ron b/assets/blocks/platform.ron new file mode 100644 index 0000000..e93dcb1 --- /dev/null +++ b/assets/blocks/platform.ron @@ -0,0 +1,3 @@ +( + color: (64, 64, 64), +) diff --git a/client/Cargo.toml b/client/Cargo.toml index e0a9daf..be70ba2 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,7 +9,10 @@ common = { path = "../common", package = "bevy-bloxel-classic-common" } bevy.workspace = true lightyear.workspace = true + +ron.workspace = true serde.workspace = true +thiserror.workspace = true bevy_fix_cursor_unlock_web = "0.2.0" diff --git a/client/src/assets/block.rs b/client/src/assets/block.rs deleted file mode 100644 index 22562da..0000000 --- a/client/src/assets/block.rs +++ /dev/null @@ -1,38 +0,0 @@ -use bevy::prelude::*; -use common::prelude::*; - -pub fn plugin(app: &mut App) { - app.load_resource::(); - app.add_observer(insert_block_visuals); -} - -#[derive(Resource, Asset, Reflect, Clone)] -#[reflect(Resource)] -pub struct BlockAssets { - #[dependency] - mesh: Handle, - #[dependency] - material: Handle, -} - -impl FromWorld for BlockAssets { - fn from_world(world: &mut World) -> Self { - let assets = world.resource::(); - Self { - mesh: assets.add(Cuboid::new(1.0, 1.0, 1.0).into()), - material: assets.add(Color::srgb_u8(124, 144, 255).into()), - } - } -} - -fn insert_block_visuals( - event: On, - block_assets: Res, - mut commands: Commands, -) { - let block = event.entity; - commands.entity(block).insert(( - Mesh3d(block_assets.mesh.clone()), - MeshMaterial3d(block_assets.material.clone()), - )); -} diff --git a/client/src/assets/block_visuals.rs b/client/src/assets/block_visuals.rs new file mode 100644 index 0000000..7332424 --- /dev/null +++ b/client/src/assets/block_visuals.rs @@ -0,0 +1,122 @@ +use bevy::prelude::*; +use common::prelude::*; + +use bevy::asset::{AssetLoader, LoadContext, io::Reader}; +use serde::Deserialize; + +pub fn plugin(app: &mut App) { + let mesh = Mesh::from(Cuboid::new(1.0, 1.0, 1.0)); + let cube = app.world_mut().add_asset(mesh); + app.insert_resource(BuiltinBlockMeshes { cube }); + + app.init_asset::(); + app.init_asset_loader::(); + app.load_resource::(); + + app.add_observer(insert_block_visuals); +} + +#[derive(Resource)] +struct BuiltinBlockMeshes { + cube: Handle, +} + +#[derive(Asset, TypePath)] +struct BlockVisuals { + _id: Identifier, + // mesh: Handle, + material: Handle, +} + +#[derive(Deserialize)] +struct BlockTypeVisualsRaw { + color: (u8, u8, u8), + // ignore unknown fields +} + +#[derive(Default)] +struct BlockVisualsLoader; + +impl AssetLoader for BlockVisualsLoader { + type Asset = BlockVisuals; + type Settings = (); + type Error = BevyError; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let raw = ron::de::from_bytes::(&bytes)?; + + let (r, g, b) = raw.color; + let material = StandardMaterial::from(Color::srgb_u8(r, g, b)); + let material = load_context.add_labeled_asset("material".to_string(), material); + + // TODO: Figure out how to reference a procedural mesh from here. + // let mesh = load_context.load(???); + + let id = load_context.path().try_into()?; + Ok(BlockVisuals { _id: id, material }) + } + + fn extensions(&self) -> &[&str] { + &["ron"] + } +} + +#[derive(Resource, Asset, Reflect, Clone)] +#[reflect(Resource)] +struct BlockVisualsCollection { + #[dependency] + default: Handle, + #[dependency] + platform: Handle, +} + +impl BlockVisualsCollection { + pub fn get(&self, id: &Identifier) -> Option<&Handle> { + match id.as_ref() { + "default" => Some(&self.default), + "platform" => Some(&self.platform), + _ => None, + } + } +} + +impl FromWorld for BlockVisualsCollection { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + default: assets.load("blocks/default.ron"), + platform: assets.load("blocks/platform.ron"), + } + } +} + +fn insert_block_visuals( + event: On, + blocks: Query<&Block>, + visuals_collection: Res, + visuals_assets: Res>, + meshes: Res, + mut commands: Commands, +) { + let block = event.entity; + let id = blocks.get(block).unwrap().id(); + + let Some(visuals) = visuals_collection.get(id) else { + warn!("missing BlockVisuals for id `{id}`"); + return; + }; + // SAFETY: If it's in `BlockVisualsCollection`, it should be in `Assets`. + let visuals = visuals_assets.get(visuals).unwrap(); + + commands.entity(block).insert(( + Mesh3d(meshes.cube.clone()), + MeshMaterial3d(visuals.material.clone()), + )); +} diff --git a/client/src/assets/mod.rs b/client/src/assets/mod.rs index 47f803b..fb01348 100644 --- a/client/src/assets/mod.rs +++ b/client/src/assets/mod.rs @@ -6,10 +6,10 @@ use bevy::prelude::*; -mod block; +mod block_visuals; mod player; pub fn plugin(app: &mut App) { - app.add_plugins(block::plugin); + app.add_plugins(block_visuals::plugin); app.add_plugins(player::plugin); } diff --git a/client/src/input/client_inputs.rs b/client/src/input/client_inputs.rs index 0c2ea77..7e28a63 100644 --- a/client/src/input/client_inputs.rs +++ b/client/src/input/client_inputs.rs @@ -85,7 +85,8 @@ pub fn buffer_action( player.0 = if buttons.just_pressed(MouseButton::Right) { // FIXME: This only works for axis-aligned normals. let offset = hit.normal.normalize().round().as_ivec3(); - Action::PlaceBlock(block_pos + offset) + // TODO: Don't hardcode block type. + Action::PlaceBlock(block_pos + offset, Block::DEFAULT) } else { Action::BreakBlock(block_pos) }; diff --git a/client/src/main.rs b/client/src/main.rs index 71cd607..ba5dd34 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -51,7 +51,7 @@ fn main() -> Result { app.add_plugins(( bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin, - common::asset_loading::plugin, + common::assets::plugin, common::network::plugin, assets::plugin, input::plugin, @@ -105,7 +105,8 @@ fn start_server_or_connect(args: Res, mut commands: Commands) -> Result { fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { for x in -8..8 { for z in -8..8 { - blocks.spawn(IVec3::new(x, 0, z)); + let pos = BlockPos::new(x, 0, z); + blocks.spawn(pos, Block::PLATFORM).unwrap(); } } } diff --git a/client/src/ui/loading_screen.rs b/client/src/ui/loading_screen.rs index a25d4f9..114f7a6 100644 --- a/client/src/ui/loading_screen.rs +++ b/client/src/ui/loading_screen.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use common::asset_loading::ResourceHandles; +use common::assets::asset_tracking::ResourceHandles; use crate::Screen; diff --git a/common/Cargo.toml b/common/Cargo.toml index bdd41e3..37bcc6d 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,5 +6,9 @@ edition = "2024" [dependencies] bevy.workspace = true lightyear.workspace = true + +derive-where.workspace = true + +ron.workspace = true serde.workspace = true thiserror.workspace = true diff --git a/common/src/asset_loading.rs b/common/src/assets/asset_tracking.rs similarity index 98% rename from common/src/asset_loading.rs rename to common/src/assets/asset_tracking.rs index a366a5c..9db688a 100644 --- a/common/src/asset_loading.rs +++ b/common/src/assets/asset_tracking.rs @@ -5,7 +5,7 @@ use std::collections::VecDeque; use bevy::prelude::*; -pub fn plugin(app: &mut App) { +pub(super) fn plugin(app: &mut App) { app.init_resource::(); app.add_systems(PreUpdate, load_resource_assets); } diff --git a/common/src/assets/block_definition.rs b/common/src/assets/block_definition.rs new file mode 100644 index 0000000..9ff450f --- /dev/null +++ b/common/src/assets/block_definition.rs @@ -0,0 +1,88 @@ +use bevy::prelude::*; + +use bevy::asset::{AssetLoader, LoadContext, io::Reader}; +use serde::Deserialize; + +use crate::assets::asset_tracking::LoadResource; +use crate::block::Block; +use crate::prelude::Identifier; + +pub(super) fn plugin(app: &mut App) { + app.init_asset::(); + app.init_asset_loader::(); + app.load_resource::(); +} + +#[derive(Asset, TypePath)] +pub struct BlockDefinition { + id: Identifier, +} + +impl BlockDefinition { + pub fn id(&self) -> &Identifier { + &self.id + } +} + +#[derive(Deserialize)] +struct BlockDefinitionRaw { + // ignore unknown fields +} + +#[derive(Default)] +struct BlockDefinitionLoader; + +impl AssetLoader for BlockDefinitionLoader { + type Asset = BlockDefinition; + type Settings = (); + type Error = BevyError; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + // NOTE: Currently unused, because `BlockDefinition` doesn't have any properties. + // So this basically just makes sure the file parses correctly. + let _raw = ron::de::from_bytes::(&bytes)?; + + let id = load_context.path().try_into()?; + Ok(BlockDefinition { id }) + } + + fn extensions(&self) -> &[&str] { + &["ron"] + } +} + +#[derive(Resource, Asset, Reflect, Clone)] +#[reflect(Resource)] +pub struct BlockDefinitionCollection { + #[dependency] + default: Handle, + #[dependency] + platform: Handle, +} + +impl BlockDefinitionCollection { + pub fn get(&self, id: &Identifier) -> Option<&Handle> { + match id.as_ref() { + "default" => Some(&self.default), + "platform" => Some(&self.platform), + _ => None, + } + } +} + +impl FromWorld for BlockDefinitionCollection { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + default: assets.load("blocks/default.ron"), + platform: assets.load("blocks/platform.ron"), + } + } +} diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs new file mode 100644 index 0000000..e9ee507 --- /dev/null +++ b/common/src/assets/mod.rs @@ -0,0 +1,11 @@ +use bevy::prelude::*; + +pub mod asset_tracking; +pub mod block_definition; + +pub fn plugin(app: &mut App) { + // `tracking::plugin` must come first. + app.add_plugins(asset_tracking::plugin); + + app.add_plugins(block_definition::plugin); +} diff --git a/common/src/block.rs b/common/src/block.rs index f873eec..b0118ca 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -4,7 +4,10 @@ use lightyear::prelude::*; use bevy::ecs::system::SystemParam; use bevy::platform::collections::HashMap; use serde::{Deserialize, Serialize}; +use thiserror::Error; +use crate::assets::block_definition::{BlockDefinition, BlockDefinitionCollection}; +use crate::identifier::Identifier; use crate::math::BlockPos; pub(crate) fn plugin(app: &mut App) { @@ -13,8 +16,24 @@ pub(crate) fn plugin(app: &mut App) { app.add_observer(block_removed); } -#[derive(Component, Deserialize, Serialize, PartialEq)] -pub struct Block; +#[derive(Component, Deserialize, Serialize, Reflect, PartialEq)] +pub struct Block { + pos: BlockPos, + id: Identifier, +} + +impl Block { + pub const DEFAULT: Identifier = Identifier::::new_const("default"); + pub const PLATFORM: Identifier = Identifier::::new_const("platform"); + + pub fn pos(&self) -> BlockPos { + self.pos + } + + pub fn id(&self) -> &Identifier { + &self.id + } +} #[derive(Resource, Default)] pub struct BlockMap(HashMap); @@ -22,58 +41,69 @@ pub struct BlockMap(HashMap); #[derive(SystemParam)] pub struct Blocks<'w, 's> { map: Res<'w, BlockMap>, - blocks: Query<'w, 's, (&'static Block, &'static BlockPos)>, + definition_collection: Res<'w, BlockDefinitionCollection>, + _definition_assets: Res<'w, Assets>, + blocks: Query<'w, 's, &'static Block>, commands: Commands<'w, 's>, } impl Blocks<'_, '_> { /// Gets the block [`Entity`] at the given position, if any. - pub fn get(&self, pos: impl Into) -> Option { - self.map.0.get(&pos.into()).copied() + pub fn get(&self, pos: BlockPos) -> Option { + self.map.0.get(&pos).copied() } /// Gets an [`EntityCommands`] for the block at the given position, if any. - pub fn entity(&mut self, pos: impl Into) -> Option { + pub fn entity(&mut self, pos: BlockPos) -> Option { self.get(pos).map(|block| self.commands.entity(block)) } - /// Spawns a block at the given position. - pub fn spawn(&mut self, pos: impl Into) -> EntityCommands { - self.commands.spawn((Block, pos.into())) - } - - /// Tries to spawn a block at the given position, as long as there isn't one already. - /// If there already is a block, its [`Entity`] is returned as the `Err` variant. - pub fn try_spawn(&mut self, pos: impl Into) -> Result { - let pos = pos.into(); // required because we use it twice + /// Attempts to spawn a block at the given position, returning the + /// [`EntityCommands`] of the resulting spawned entity if successful. + pub fn spawn( + &mut self, + pos: BlockPos, + id: Identifier, + ) -> Result { + if self.definition_collection.get(&id).is_none() { + return Err(SpawnBlockError::UnknownBlockId(id)); + } if let Some(existing) = self.get(pos) { - Err(existing) - } else { - Ok(self.spawn(pos)) + return Err(SpawnBlockError::ExistingBlock(pos, existing)); } + Ok(self.commands.spawn(Block { pos, id })) } /// Despawns the block at the given position, returning if successful. - pub fn despawn(&mut self, pos: impl Into) -> bool { + pub fn despawn(&mut self, pos: BlockPos) -> bool { self.entity(pos).map(|mut block| block.despawn()).is_some() } /// Gets the position of the given block [`Entity`], if it exists and is a [`Block`]. pub fn position(&self, entity: Entity) -> Option { - self.blocks.get(entity).ok().map(|block| *block.1) + self.blocks.get(entity).ok().map(|block| block.pos()) } } +#[derive(Error, Debug)] +pub enum SpawnBlockError { + #[error("Unknown block id `{0}`")] + UnknownBlockId(Identifier), + #[error("Existing block at {0}: {1}")] + ExistingBlock(BlockPos, Entity), +} + fn block_added( event: On, - blocks: Query<(&Block, &BlockPos)>, + blocks: Query<&Block>, mut map: ResMut, server: Option>, mut commands: Commands, ) { let block = event.entity; - let (_, pos) = blocks.get(block).expect("Block is missing BlockPos"); - if map.0.insert(*pos, block).is_some() { + // SAFETY: `Add` trigger guarantees `Block` will be on the entity. + let pos = blocks.get(block).unwrap().pos(); + if map.0.insert(pos, block).is_some() { // TODO: This IS going to happen occasionally. warn!("Duplicate block at pos {pos}"); } @@ -85,12 +115,9 @@ fn block_added( } } -fn block_removed( - event: On, - blocks: Query<(&Block, &BlockPos)>, - mut map: ResMut, -) { +fn block_removed(event: On, blocks: Query<&Block>, mut map: ResMut) { let block = event.entity; - let (_, pos) = blocks.get(block).expect("Block is missing BlockPos"); - map.0.remove(pos); + // SAFETY: `Remove` trigger guarantees `Block` will be on the entity. + let pos = blocks.get(block).unwrap().pos(); + map.0.remove(&pos); } diff --git a/common/src/identifier.rs b/common/src/identifier.rs new file mode 100644 index 0000000..72ea079 --- /dev/null +++ b/common/src/identifier.rs @@ -0,0 +1,113 @@ +use std::borrow::Cow; +use std::marker::PhantomData; +use std::path::Path; + +use bevy::prelude::*; + +use derive_where::derive_where; +use thiserror::Error; + +#[derive(Reflect, Deref)] +#[derive_where(Deserialize, Serialize, Clone, PartialEq, Eq, Hash, Debug)] +#[serde(try_from = "String", into = "String")] +pub struct Identifier { + #[deref] + value: Cow<'static, str>, + #[reflect(ignore)] + identifier_type: PhantomData, +} + +impl Identifier { + pub const unsafe fn new_unsafe(value: Cow<'static, str>) -> Self { + Self { + value, + identifier_type: PhantomData, + } + } + + pub fn new(value: impl Into) -> Result { + let value = value.into(); + Self::validate(&value)?; + Ok(unsafe { Self::new_unsafe(Cow::Owned(value)) }) + } + + pub const fn new_const(value: &'static str) -> Self { + match Self::validate(&value) { + Ok(_) => unsafe { Self::new_unsafe(Cow::Borrowed(value)) }, + Err(_) => panic!("invalid identifier"), + } + } + + const fn validate(value: &str) -> Result<(), IdentifierParseError> { + if value.is_empty() { + return Err(IdentifierParseError::Empty); + } + if value.len() > 32 { + return Err(IdentifierParseError::Length); + } + // if value.chars().any(|c| !c.is_ascii_lowercase()) + // NOTE: This mess is to allow the function to be called in a const context. + let bytes = value.as_bytes(); + let mut i = 0; + while i < value.len() { + if let Some(c) = char::from_u32(bytes[i] as u32) { + if c.is_ascii_lowercase() { + i += 1; + continue; + } + } + return Err(IdentifierParseError::Invalid); + } + Ok(()) + } +} + +#[derive(Error, Debug)] +pub enum IdentifierParseError { + #[error("String is empty")] + Empty, + #[error("String is too long")] + Length, + #[error("String contains invalid characters")] + Invalid, +} + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value) + } +} + +impl From> for String { + fn from(value: Identifier) -> Self { + value.to_string() + } +} + +impl TryFrom for Identifier { + type Error = IdentifierParseError; + fn try_from(value: String) -> std::result::Result { + Self::new(value) + } +} + +impl TryFrom<&Path> for Identifier { + type Error = IdentifierParseError; + fn try_from(value: &Path) -> Result { + // SAFETY: Since this is an asset path, it should never fail. + let file_name = value.file_name().unwrap(); + // SAFETY: If the asset path contains invalid UTF-8, it's someone else's fault. + let file_name_str = file_name.to_str().expect("invalid utf8"); + + let id = if let Some((prefix, _)) = file_name_str.split_once('.') { + if prefix.is_empty() { + panic!("asset file name starts with dot") + } + prefix + } else { + file_name_str + }; + + Self::new(id) + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 8c3e7ef..9affcf8 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,5 +1,6 @@ -pub mod asset_loading; +pub mod assets; pub mod block; +pub mod identifier; pub mod math; pub mod network; pub mod player; @@ -7,11 +8,13 @@ pub mod player; pub mod prelude { pub use crate::network; + pub use crate::assets::block_definition::*; pub use crate::block::*; + pub use crate::identifier::*; pub use crate::math::*; pub use crate::network::protocol::*; pub use crate::player::*; // Allows use of the `App::load_resource` extension trait function. - pub use crate::asset_loading::LoadResource; + pub use crate::assets::asset_tracking::LoadResource; } diff --git a/common/src/math/block_pos.rs b/common/src/math/block_pos.rs index ee37e62..4cc7153 100644 --- a/common/src/math/block_pos.rs +++ b/common/src/math/block_pos.rs @@ -4,7 +4,7 @@ use bevy::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Component, Reflect, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Reflect, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct BlockPos { pub x: i32, pub y: i32, diff --git a/common/src/network/protocol.rs b/common/src/network/protocol.rs index a9136b2..e34713a 100644 --- a/common/src/network/protocol.rs +++ b/common/src/network/protocol.rs @@ -6,6 +6,7 @@ use lightyear::input::native::plugin::InputPlugin; use serde::{Deserialize, Serialize}; use crate::block::Block; +use crate::identifier::Identifier; use crate::math::BlockPos; use crate::player::{HeadOrientation, Player}; @@ -14,11 +15,16 @@ pub(super) fn plugin(app: &mut App) { app.add_plugins(InputPlugin::::default()); app.add_plugins(InputPlugin::::default()); - // marker components - app.register_component::(); - app.register_component::(); - app.register_component::(); + // Some components only need to be replicated once on insert / when the entity spawns. + let replicate_once = ComponentReplicationConfig { + replicate_once: true, + ..default() + }; + app.register_component::() + .with_replication_config(replicate_once.clone()); + app.register_component::() + .with_replication_config(replicate_once.clone()); app.register_component::() .add_prediction() .add_interpolation_with(interpolate_transform); @@ -40,7 +46,7 @@ pub struct Inputs { pub enum Action { #[default] None, - PlaceBlock(BlockPos), + PlaceBlock(BlockPos, Identifier), BreakBlock(BlockPos), } diff --git a/common/src/player.rs b/common/src/player.rs index f56e57e..f0c23c7 100644 --- a/common/src/player.rs +++ b/common/src/player.rs @@ -103,18 +103,18 @@ pub fn placement( mut blocks: Blocks, ) { for mut action in players { - match **action { + match &**action { Action::None => { // do nothing } - Action::PlaceBlock(pos) => { - if let Ok(mut block) = blocks.try_spawn(pos) { + Action::PlaceBlock(pos, id) => { + if let Ok(mut block) = blocks.spawn(*pos, id.clone()) { // TODO: Use `default_with_salt` with the player's `client_id`? block.insert(PreSpawned::default()); } } Action::BreakBlock(pos) => { - blocks.despawn(pos); + blocks.despawn(*pos); } } // HACK: Ensure an action will only run once?