- 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 definitionsmain
parent
536ff0ebb6
commit
994b33dac2
21 changed files with 548 additions and 91 deletions
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (124, 144, 255), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (64, 64, 64), |
||||||
|
) |
||||||
@ -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()), |
||||||
|
)); |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue