Block definition / visuals assets

- Move `asset_loading` to `assets/asset_tracking`
- Move `assets/block` to `assets/block_visuals`
- Add `Identifier<T>`, a safe wrapper around strings
- `Block` component now has `pos` and `id`
- `BlockPos` is no longer a component
- Add `default` and `platform` block definitions
main
copygirl 6 days ago
parent 536ff0ebb6
commit 994b33dac2
  1. 110
      Cargo.lock
  2. 8
      Cargo.toml
  3. 3
      assets/blocks/default.ron
  4. 3
      assets/blocks/platform.ron
  5. 3
      client/Cargo.toml
  6. 38
      client/src/assets/block.rs
  7. 122
      client/src/assets/block_visuals.rs
  8. 4
      client/src/assets/mod.rs
  9. 3
      client/src/input/client_inputs.rs
  10. 5
      client/src/main.rs
  11. 2
      client/src/ui/loading_screen.rs
  12. 4
      common/Cargo.toml
  13. 2
      common/src/assets/asset_tracking.rs
  14. 88
      common/src/assets/block_definition.rs
  15. 11
      common/src/assets/mod.rs
  16. 87
      common/src/block.rs
  17. 113
      common/src/identifier.rs
  18. 7
      common/src/lib.rs
  19. 2
      common/src/math/block_pos.rs
  20. 16
      common/src/network/protocol.rs
  21. 8
      common/src/player.rs

110
Cargo.lock generated

@ -631,7 +631,9 @@ dependencies = [
"bevy_fix_cursor_unlock_web", "bevy_fix_cursor_unlock_web",
"clap", "clap",
"lightyear", "lightyear",
"ron 0.12.0",
"serde", "serde",
"thiserror 2.0.17",
"web-sys", "web-sys",
] ]
@ -640,7 +642,9 @@ name = "bevy-bloxel-classic-common"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"derive-where",
"lightyear", "lightyear",
"ron 0.12.0",
"serde", "serde",
"thiserror 2.0.17", "thiserror 2.0.17",
] ]
@ -692,7 +696,7 @@ dependencies = [
"downcast-rs 2.0.2", "downcast-rs 2.0.2",
"either", "either",
"petgraph", "petgraph",
"ron", "ron 0.10.1",
"serde", "serde",
"smallvec", "smallvec",
"thiserror 2.0.17", "thiserror 2.0.17",
@ -785,8 +789,9 @@ dependencies = [
"futures-io", "futures-io",
"futures-lite", "futures-lite",
"js-sys", "js-sys",
"notify-debouncer-full",
"parking_lot", "parking_lot",
"ron", "ron 0.10.1",
"serde", "serde",
"stackfuture", "stackfuture",
"thiserror 2.0.17", "thiserror 2.0.17",
@ -2620,6 +2625,17 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.0.1" version = "2.0.1"
@ -2882,6 +2898,15 @@ dependencies = [
"simd-adler32", "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]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.4" version = "0.1.4"
@ -3002,6 +3027,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 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]] [[package]]
name = "futures" name = "futures"
version = "0.3.31" version = "0.3.31"
@ -3891,6 +3925,26 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" 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]] [[package]]
name = "ktx2" name = "ktx2"
version = "0.4.0" version = "0.4.0"
@ -4719,6 +4773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi", "wasi",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@ -4925,6 +4980,43 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" 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]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.1" version = "0.4.1"
@ -6055,6 +6147,20 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.20.0" version = "0.20.0"

@ -11,8 +11,12 @@ opt-level = 1
opt-level = 3 opt-level = 3
[workspace.dependencies] [workspace.dependencies]
bevy = { version = "0.17.2", features = [ "serialize" ] } bevy = { version = "0.17.2", features = [ "serialize", "file_watcher", "embedded_watcher" ] }
# lightyear = { version = "0.25.3", features = [ "netcode", "webtransport" ] } # 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" serde = "1.0.228"
thiserror = "2.0.17" thiserror = "2.0.17"

@ -0,0 +1,3 @@
(
color: (124, 144, 255),
)

@ -0,0 +1,3 @@
(
color: (64, 64, 64),
)

@ -9,7 +9,10 @@ common = { path = "../common", package = "bevy-bloxel-classic-common" }
bevy.workspace = true bevy.workspace = true
lightyear.workspace = true lightyear.workspace = true
ron.workspace = true
serde.workspace = true serde.workspace = true
thiserror.workspace = true
bevy_fix_cursor_unlock_web = "0.2.0" bevy_fix_cursor_unlock_web = "0.2.0"

@ -1,38 +0,0 @@
use bevy::prelude::*;
use common::prelude::*;
pub fn plugin(app: &mut App) {
app.load_resource::<BlockAssets>();
app.add_observer(insert_block_visuals);
}
#[derive(Resource, Asset, Reflect, Clone)]
#[reflect(Resource)]
pub struct BlockAssets {
#[dependency]
mesh: Handle<Mesh>,
#[dependency]
material: Handle<StandardMaterial>,
}
impl FromWorld for BlockAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
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<Add, Block>,
block_assets: Res<BlockAssets>,
mut commands: Commands,
) {
let block = event.entity;
commands.entity(block).insert((
Mesh3d(block_assets.mesh.clone()),
MeshMaterial3d(block_assets.material.clone()),
));
}

@ -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::<BlockVisuals>();
app.init_asset_loader::<BlockVisualsLoader>();
app.load_resource::<BlockVisualsCollection>();
app.add_observer(insert_block_visuals);
}
#[derive(Resource)]
struct BuiltinBlockMeshes {
cube: Handle<Mesh>,
}
#[derive(Asset, TypePath)]
struct BlockVisuals {
_id: Identifier<Block>,
// mesh: Handle<Mesh>,
material: Handle<StandardMaterial>,
}
#[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<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let raw = ron::de::from_bytes::<BlockTypeVisualsRaw>(&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<BlockVisuals>,
#[dependency]
platform: Handle<BlockVisuals>,
}
impl BlockVisualsCollection {
pub fn get(&self, id: &Identifier<Block>) -> Option<&Handle<BlockVisuals>> {
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::<AssetServer>();
Self {
default: assets.load("blocks/default.ron"),
platform: assets.load("blocks/platform.ron"),
}
}
}
fn insert_block_visuals(
event: On<Add, Block>,
blocks: Query<&Block>,
visuals_collection: Res<BlockVisualsCollection>,
visuals_assets: Res<Assets<BlockVisuals>>,
meshes: Res<BuiltinBlockMeshes>,
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<BlockVisuals>`.
let visuals = visuals_assets.get(visuals).unwrap();
commands.entity(block).insert((
Mesh3d(meshes.cube.clone()),
MeshMaterial3d(visuals.material.clone()),
));
}

@ -6,10 +6,10 @@
use bevy::prelude::*; use bevy::prelude::*;
mod block; mod block_visuals;
mod player; mod player;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(block::plugin); app.add_plugins(block_visuals::plugin);
app.add_plugins(player::plugin); app.add_plugins(player::plugin);
} }

@ -85,7 +85,8 @@ pub fn buffer_action(
player.0 = if buttons.just_pressed(MouseButton::Right) { player.0 = if buttons.just_pressed(MouseButton::Right) {
// FIXME: This only works for axis-aligned normals. // FIXME: This only works for axis-aligned normals.
let offset = hit.normal.normalize().round().as_ivec3(); 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 { } else {
Action::BreakBlock(block_pos) Action::BreakBlock(block_pos)
}; };

@ -51,7 +51,7 @@ fn main() -> Result {
app.add_plugins(( app.add_plugins((
bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin, bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin,
common::asset_loading::plugin, common::assets::plugin,
common::network::plugin, common::network::plugin,
assets::plugin, assets::plugin,
input::plugin, input::plugin,
@ -105,7 +105,8 @@ fn start_server_or_connect(args: Res<Args>, mut commands: Commands) -> Result {
fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) { fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) {
for x in -8..8 { for x in -8..8 {
for z 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();
} }
} }
} }

@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use common::asset_loading::ResourceHandles; use common::assets::asset_tracking::ResourceHandles;
use crate::Screen; use crate::Screen;

@ -6,5 +6,9 @@ edition = "2024"
[dependencies] [dependencies]
bevy.workspace = true bevy.workspace = true
lightyear.workspace = true lightyear.workspace = true
derive-where.workspace = true
ron.workspace = true
serde.workspace = true serde.workspace = true
thiserror.workspace = true thiserror.workspace = true

@ -5,7 +5,7 @@ use std::collections::VecDeque;
use bevy::prelude::*; use bevy::prelude::*;
pub fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_resource::<ResourceHandles>(); app.init_resource::<ResourceHandles>();
app.add_systems(PreUpdate, load_resource_assets); app.add_systems(PreUpdate, load_resource_assets);
} }

@ -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::<BlockDefinition>();
app.init_asset_loader::<BlockDefinitionLoader>();
app.load_resource::<BlockDefinitionCollection>();
}
#[derive(Asset, TypePath)]
pub struct BlockDefinition {
id: Identifier<Block>,
}
impl BlockDefinition {
pub fn id(&self) -> &Identifier<Block> {
&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<Self::Asset, Self::Error> {
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::<BlockDefinitionRaw>(&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<BlockDefinition>,
#[dependency]
platform: Handle<BlockDefinition>,
}
impl BlockDefinitionCollection {
pub fn get(&self, id: &Identifier<Block>) -> Option<&Handle<BlockDefinition>> {
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::<AssetServer>();
Self {
default: assets.load("blocks/default.ron"),
platform: assets.load("blocks/platform.ron"),
}
}
}

@ -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);
}

@ -4,7 +4,10 @@ use lightyear::prelude::*;
use bevy::ecs::system::SystemParam; use bevy::ecs::system::SystemParam;
use bevy::platform::collections::HashMap; use bevy::platform::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::assets::block_definition::{BlockDefinition, BlockDefinitionCollection};
use crate::identifier::Identifier;
use crate::math::BlockPos; use crate::math::BlockPos;
pub(crate) fn plugin(app: &mut App) { pub(crate) fn plugin(app: &mut App) {
@ -13,8 +16,24 @@ pub(crate) fn plugin(app: &mut App) {
app.add_observer(block_removed); app.add_observer(block_removed);
} }
#[derive(Component, Deserialize, Serialize, PartialEq)] #[derive(Component, Deserialize, Serialize, Reflect, PartialEq)]
pub struct Block; pub struct Block {
pos: BlockPos,
id: Identifier<Block>,
}
impl Block {
pub const DEFAULT: Identifier<Block> = Identifier::<Block>::new_const("default");
pub const PLATFORM: Identifier<Block> = Identifier::<Block>::new_const("platform");
pub fn pos(&self) -> BlockPos {
self.pos
}
pub fn id(&self) -> &Identifier<Block> {
&self.id
}
}
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct BlockMap(HashMap<BlockPos, Entity>); pub struct BlockMap(HashMap<BlockPos, Entity>);
@ -22,58 +41,69 @@ pub struct BlockMap(HashMap<BlockPos, Entity>);
#[derive(SystemParam)] #[derive(SystemParam)]
pub struct Blocks<'w, 's> { pub struct Blocks<'w, 's> {
map: Res<'w, BlockMap>, map: Res<'w, BlockMap>,
blocks: Query<'w, 's, (&'static Block, &'static BlockPos)>, definition_collection: Res<'w, BlockDefinitionCollection>,
_definition_assets: Res<'w, Assets<BlockDefinition>>,
blocks: Query<'w, 's, &'static Block>,
commands: Commands<'w, 's>, commands: Commands<'w, 's>,
} }
impl Blocks<'_, '_> { impl Blocks<'_, '_> {
/// Gets the block [`Entity`] at the given position, if any. /// Gets the block [`Entity`] at the given position, if any.
pub fn get(&self, pos: impl Into<BlockPos>) -> Option<Entity> { pub fn get(&self, pos: BlockPos) -> Option<Entity> {
self.map.0.get(&pos.into()).copied() self.map.0.get(&pos).copied()
} }
/// Gets an [`EntityCommands`] for the block at the given position, if any. /// Gets an [`EntityCommands`] for the block at the given position, if any.
pub fn entity(&mut self, pos: impl Into<BlockPos>) -> Option<EntityCommands> { pub fn entity(&mut self, pos: BlockPos) -> Option<EntityCommands> {
self.get(pos).map(|block| self.commands.entity(block)) self.get(pos).map(|block| self.commands.entity(block))
} }
/// Spawns a block at the given position. /// Attempts to spawn a block at the given position, returning the
pub fn spawn(&mut self, pos: impl Into<BlockPos>) -> EntityCommands { /// [`EntityCommands`] of the resulting spawned entity if successful.
self.commands.spawn((Block, pos.into())) pub fn spawn(
} &mut self,
pos: BlockPos,
/// Tries to spawn a block at the given position, as long as there isn't one already. id: Identifier<Block>,
/// If there already is a block, its [`Entity`] is returned as the `Err` variant. ) -> Result<EntityCommands, SpawnBlockError> {
pub fn try_spawn(&mut self, pos: impl Into<BlockPos>) -> Result<EntityCommands, Entity> { if self.definition_collection.get(&id).is_none() {
let pos = pos.into(); // required because we use it twice return Err(SpawnBlockError::UnknownBlockId(id));
}
if let Some(existing) = self.get(pos) { if let Some(existing) = self.get(pos) {
Err(existing) return Err(SpawnBlockError::ExistingBlock(pos, existing));
} else {
Ok(self.spawn(pos))
} }
Ok(self.commands.spawn(Block { pos, id }))
} }
/// Despawns the block at the given position, returning if successful. /// Despawns the block at the given position, returning if successful.
pub fn despawn(&mut self, pos: impl Into<BlockPos>) -> bool { pub fn despawn(&mut self, pos: BlockPos) -> bool {
self.entity(pos).map(|mut block| block.despawn()).is_some() 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`]. /// Gets the position of the given block [`Entity`], if it exists and is a [`Block`].
pub fn position(&self, entity: Entity) -> Option<BlockPos> { pub fn position(&self, entity: Entity) -> Option<BlockPos> {
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<Block>),
#[error("Existing block at {0}: {1}")]
ExistingBlock(BlockPos, Entity),
}
fn block_added( fn block_added(
event: On<Add, Block>, event: On<Add, Block>,
blocks: Query<(&Block, &BlockPos)>, blocks: Query<&Block>,
mut map: ResMut<BlockMap>, mut map: ResMut<BlockMap>,
server: Option<Single<&Server>>, server: Option<Single<&Server>>,
mut commands: Commands, mut commands: Commands,
) { ) {
let block = event.entity; let block = event.entity;
let (_, pos) = blocks.get(block).expect("Block is missing BlockPos"); // SAFETY: `Add` trigger guarantees `Block` will be on the entity.
if map.0.insert(*pos, block).is_some() { let pos = blocks.get(block).unwrap().pos();
if map.0.insert(pos, block).is_some() {
// TODO: This IS going to happen occasionally. // TODO: This IS going to happen occasionally.
warn!("Duplicate block at pos {pos}"); warn!("Duplicate block at pos {pos}");
} }
@ -85,12 +115,9 @@ fn block_added(
} }
} }
fn block_removed( fn block_removed(event: On<Remove, Block>, blocks: Query<&Block>, mut map: ResMut<BlockMap>) {
event: On<Remove, Block>,
blocks: Query<(&Block, &BlockPos)>,
mut map: ResMut<BlockMap>,
) {
let block = event.entity; let block = event.entity;
let (_, pos) = blocks.get(block).expect("Block is missing BlockPos"); // SAFETY: `Remove` trigger guarantees `Block` will be on the entity.
map.0.remove(pos); let pos = blocks.get(block).unwrap().pos();
map.0.remove(&pos);
} }

@ -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<T> {
#[deref]
value: Cow<'static, str>,
#[reflect(ignore)]
identifier_type: PhantomData<T>,
}
impl<T> Identifier<T> {
pub const unsafe fn new_unsafe(value: Cow<'static, str>) -> Self {
Self {
value,
identifier_type: PhantomData,
}
}
pub fn new(value: impl Into<String>) -> Result<Self, IdentifierParseError> {
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<T> std::fmt::Display for Identifier<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
impl<T> From<Identifier<T>> for String {
fn from(value: Identifier<T>) -> Self {
value.to_string()
}
}
impl<T> TryFrom<String> for Identifier<T> {
type Error = IdentifierParseError;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
impl<T> TryFrom<&Path> for Identifier<T> {
type Error = IdentifierParseError;
fn try_from(value: &Path) -> Result<Self, Self::Error> {
// 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)
}
}

@ -1,5 +1,6 @@
pub mod asset_loading; pub mod assets;
pub mod block; pub mod block;
pub mod identifier;
pub mod math; pub mod math;
pub mod network; pub mod network;
pub mod player; pub mod player;
@ -7,11 +8,13 @@ pub mod player;
pub mod prelude { pub mod prelude {
pub use crate::network; pub use crate::network;
pub use crate::assets::block_definition::*;
pub use crate::block::*; pub use crate::block::*;
pub use crate::identifier::*;
pub use crate::math::*; pub use crate::math::*;
pub use crate::network::protocol::*; pub use crate::network::protocol::*;
pub use crate::player::*; pub use crate::player::*;
// Allows use of the `App::load_resource` extension trait function. // Allows use of the `App::load_resource` extension trait function.
pub use crate::asset_loading::LoadResource; pub use crate::assets::asset_tracking::LoadResource;
} }

@ -4,7 +4,7 @@ use bevy::prelude::*;
use serde::{Deserialize, Serialize}; 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 struct BlockPos {
pub x: i32, pub x: i32,
pub y: i32, pub y: i32,

@ -6,6 +6,7 @@ use lightyear::input::native::plugin::InputPlugin;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::block::Block; use crate::block::Block;
use crate::identifier::Identifier;
use crate::math::BlockPos; use crate::math::BlockPos;
use crate::player::{HeadOrientation, Player}; use crate::player::{HeadOrientation, Player};
@ -14,11 +15,16 @@ pub(super) fn plugin(app: &mut App) {
app.add_plugins(InputPlugin::<Inputs>::default()); app.add_plugins(InputPlugin::<Inputs>::default());
app.add_plugins(InputPlugin::<Action>::default()); app.add_plugins(InputPlugin::<Action>::default());
// marker components // Some components only need to be replicated once on insert / when the entity spawns.
app.register_component::<Player>(); let replicate_once = ComponentReplicationConfig {
app.register_component::<Block>(); replicate_once: true,
app.register_component::<BlockPos>(); ..default()
};
app.register_component::<Player>()
.with_replication_config(replicate_once.clone());
app.register_component::<Block>()
.with_replication_config(replicate_once.clone());
app.register_component::<Transform>() app.register_component::<Transform>()
.add_prediction() .add_prediction()
.add_interpolation_with(interpolate_transform); .add_interpolation_with(interpolate_transform);
@ -40,7 +46,7 @@ pub struct Inputs {
pub enum Action { pub enum Action {
#[default] #[default]
None, None,
PlaceBlock(BlockPos), PlaceBlock(BlockPos, Identifier<Block>),
BreakBlock(BlockPos), BreakBlock(BlockPos),
} }

@ -103,18 +103,18 @@ pub fn placement(
mut blocks: Blocks, mut blocks: Blocks,
) { ) {
for mut action in players { for mut action in players {
match **action { match &**action {
Action::None => { Action::None => {
// do nothing // do nothing
} }
Action::PlaceBlock(pos) => { Action::PlaceBlock(pos, id) => {
if let Ok(mut block) = blocks.try_spawn(pos) { if let Ok(mut block) = blocks.spawn(*pos, id.clone()) {
// TODO: Use `default_with_salt` with the player's `client_id`? // TODO: Use `default_with_salt` with the player's `client_id`?
block.insert(PreSpawned::default()); block.insert(PreSpawned::default());
} }
} }
Action::BreakBlock(pos) => { Action::BreakBlock(pos) => {
blocks.despawn(pos); blocks.despawn(*pos);
} }
} }
// HACK: Ensure an action will only run once? // HACK: Ensure an action will only run once?

Loading…
Cancel
Save