- 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