Bloxel sandbox game similar to Minecraft "Classic" (2009) written in Rust with Bevy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

134 lines
3.8 KiB

use std::borrow::Cow;
use std::marker::PhantomData;
use std::path::Path;
use bevy::prelude::*;
use bevy::asset::{UntypedAssetId, VisitAssetDependencies};
use bevy::platform::collections::HashMap;
use derive_where::derive_where;
use serde::{Deserialize, Serialize};
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<fn(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)
}
}
#[derive(Deserialize, Serialize, Reflect, Deref, DerefMut)]
#[derive_where(Default)]
pub struct IdentifierMap<T, V>(HashMap<Identifier<T>, V>);
impl<T, V: Clone> Clone for IdentifierMap<T, V> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T, A: Asset> VisitAssetDependencies for IdentifierMap<T, Handle<A>> {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
for dependency in self.values() {
visit(dependency.id().untyped());
}
}
}