1
0
Fork 0
mirror of https://codeberg.org/icewind/vbsp.git synced 2026-06-03 18:54:05 +02:00

derive macros for entity parsing

This commit is contained in:
Robin Appelman 2022-02-27 17:40:01 +01:00
commit 6ced393424
7 changed files with 384 additions and 158 deletions

View file

@ -19,6 +19,7 @@ lzma-rs = "0.2.0"
binrw = "0.8.0"
static_assertions = "1.1.0"
num_enum = "0.5.6"
vbsp-derive = { path = "derive", version = "*" }
[dev-dependencies]
obj = "0.10"

21
derive/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "vbsp-derive"
version = "0.1.0"
authors = ["Robin Appelman <robin@icewind.nl>"]
homepage = "https://github.com/icewind1991/vbsp"
repository = "https://github.com/icewind1991/vbsp"
description = "Derive macros used by vbsp."
license = "MIT"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "1.0.82"
quote = "1.0.10"
proc-macro2 = "1.0.33"
syn_util = "0.4.2"
[features]
__vbsp_as_self = []

190
derive/src/entity.rs Normal file
View file

@ -0,0 +1,190 @@
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
Data, DataEnum, DataStruct, DeriveInput, Field, GenericParam, Path, PathArguments, PathSegment,
Type, TypePath, Variant,
};
use syn_util::{contains_attribute, get_attribute_value};
type Result<T, E = String> = std::result::Result<T, E>;
pub fn derive_entity(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
if input.generics.lifetimes().count() > 1 {
return Err("Can't derive Entity on structs or entities with more than 1 lifetime".into());
}
let source_lifetime = input
.generics
.params
.iter()
.find(|param| matches!(param, GenericParam::Lifetime(_)));
match &input.data {
Data::Struct(data) => derive_entity_struct(&input, data, source_lifetime),
Data::Enum(data) => derive_entity_enum(&input, data, source_lifetime),
_ => Err("Can only derive Entity for structs and enums".into()),
}
}
fn derive_entity_enum(
input: &DeriveInput,
data: &DataEnum,
source_lifetime: Option<&GenericParam>,
) -> Result<proc_macro2::TokenStream> {
let variants = data
.variants
.iter()
.filter(|variant| !contains_attribute(&variant.attrs, &["entity", "default"]))
.map(EntityVariant::try_from)
.collect::<Result<Vec<_>, _>>()?;
let default = &data
.variants
.iter()
.find(|variant| contains_attribute(&variant.attrs, &["entity", "default"]))
.ok_or("Enum must have one variant with `#[entity(default)]` set")?
.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let name = &input.ident;
let lifetime_or_default = LifetimeOrAnonymous(source_lifetime);
Ok(quote! {
impl #impl_generics TryFrom<crate::RawEntity<#lifetime_or_default>> for #name #ty_generics #where_clause {
type Error = crate::error::EntityParseError;
fn try_from(raw: crate::RawEntity<#source_lifetime>) -> Result<Self, Self::Error> {
let class = raw.prop("classname")?;
Ok(match class {
#(#variants)*
_ => Self::#default(raw),
})
}
}
})
}
fn derive_entity_struct(
input: &DeriveInput,
data: &DataStruct,
source_lifetime: Option<&GenericParam>,
) -> Result<proc_macro2::TokenStream> {
let fields = data
.fields
.iter()
.map(EntityField::try_from)
.collect::<Result<Vec<_>, _>>()?;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let name = &input.ident;
let lifetime_or_default = LifetimeOrAnonymous(source_lifetime);
Ok(quote! {
impl #impl_generics TryFrom<crate::RawEntity<#lifetime_or_default>> for #name #ty_generics #where_clause {
type Error = crate::error::EntityParseError;
fn try_from(raw: crate::RawEntity<#source_lifetime>) -> Result<Self, Self::Error> {
Ok(#name {
#(#fields)*
})
}
}
})
}
struct LifetimeOrAnonymous<'a>(Option<&'a GenericParam>);
impl ToTokens for LifetimeOrAnonymous<'_> {
fn to_tokens(&self, stream: &mut TokenStream) {
match &self.0 {
Some(params) => params.to_tokens(stream),
None => stream.append_all(quote! {'_}),
}
}
}
struct EntityField<'a> {
field: &'a Ident,
name: String,
default: bool,
}
impl<'a> TryFrom<&'a Field> for EntityField<'a> {
type Error = String;
fn try_from(field: &'a Field) -> std::result::Result<Self, Self::Error> {
let ident = &field
.ident
.as_ref()
.ok_or_else(|| format!("Can't derive Entity on structs with unnamed fields"))?;
let name = get_attribute_value(&field.attrs, &["entity", "name"])
.unwrap_or_else(|| ident.to_string());
let default = contains_attribute(&field.attrs, &["entity", "default"]);
Ok(EntityField {
field: ident,
name,
default,
})
}
}
impl ToTokens for EntityField<'_> {
fn to_tokens(&self, stream: &mut TokenStream) {
let EntityField { field, name, .. } = &self;
let tokens = if self.default {
quote! {#field: raw.prop_parse(#name).unwrap_or_default(),}
} else {
quote! {#field: raw.prop_parse(#name)?,}
};
stream.append_all(tokens);
}
}
struct EntityVariant<'a> {
name: String,
variant: &'a Ident,
ty: &'a Path,
}
impl<'a> TryFrom<&'a Variant> for EntityVariant<'a> {
type Error = String;
fn try_from(value: &'a Variant) -> std::result::Result<Self, Self::Error> {
let name = get_attribute_value(&value.attrs, &["entity", "name"]).ok_or_else(|| {
"All variants must have the `#[entity(name)]` or `#[entity(default)]` attribute set"
.to_string()
})?;
if value.fields.len() != 1 {
return Err("All enum variants must have exactly one field".into());
}
let field = value.fields.iter().next().unwrap();
let path = match &field.ty {
Type::Path(TypePath { path, .. }) => path,
_ => return Err("Varients can only contain plain types".into()),
};
Ok(EntityVariant {
name,
variant: &value.ident,
ty: &path,
})
}
}
impl ToTokens for EntityVariant<'_> {
fn to_tokens(&self, stream: &mut TokenStream) {
let EntityVariant { name, variant, ty } = &self;
// strip lifetime params
let ty = Path {
leading_colon: ty.leading_colon.clone(),
segments: ty
.segments
.iter()
.map(|segment| PathSegment {
ident: segment.ident.clone(),
arguments: PathArguments::None,
})
.collect(),
};
stream.append_all(quote! {#name => Self::#variant(#ty::try_from(raw)?),});
}
}

19
derive/src/lib.rs Normal file
View file

@ -0,0 +1,19 @@
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
mod entity;
#[proc_macro_derive(Entity, attributes(entity))]
pub fn derive_entity(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
derive_wrapper(input, entity::derive_entity)
}
fn derive_wrapper<F: Fn(DeriveInput) -> Result<proc_macro2::TokenStream, String>>(
input: proc_macro::TokenStream,
derive: F,
) -> proc_macro::TokenStream {
match derive(parse_macro_input!(input as DeriveInput)) {
Ok(tokens) => tokens.into(),
Err(e) => quote!(compile_error!(#e)).into(),
}
}

27
derive/tests/expand.rs Normal file
View file

@ -0,0 +1,27 @@
use vbsp::RawEntity;
use vbsp_derive::Entity;
#[derive(Entity)]
pub struct SpotLight {
pub angles: [f32; 3],
#[entity(name = "render_color")]
pub color: [u8; 3],
pub cone: u8,
#[entity(default)]
pub optional: bool,
}
#[derive(Entity)]
pub struct Lifetime<'a> {
pub model: &'a str,
}
#[derive(Entity)]
pub enum Entity<'a> {
// #[entity(name = "spot")]
// SpotLight(SpotLight),
#[entity(name = "bar")]
Foo(Lifetime<'a>),
#[entity(default)]
Unknown(RawEntity<'a>),
}

View file

@ -3,6 +3,7 @@ use crate::Vector;
use std::fmt;
use std::fmt::Debug;
use std::str::FromStr;
use vbsp_derive::Entity;
#[derive(Clone)]
pub struct Entities {
@ -101,260 +102,227 @@ impl<'a> RawEntity<'a> {
.ok_or(EntityParseError::NoSuchProperty(key))
}
fn prop_parse<T: FromStr>(&self, key: &'static str) -> Result<T, EntityParseError>
fn prop_parse<T: EntityProp<'a>>(&self, key: &'static str) -> Result<T, EntityParseError> {
T::parse(self.prop(key)?)
}
pub fn parse(&self) -> Result<Entity<'a>, EntityParseError> {
self.clone().try_into()
}
}
trait EntityProp<'a>: Sized {
fn parse(raw: &'a str) -> Result<Self, EntityParseError>;
}
trait FromStrProp: FromStr {}
impl FromStrProp for u8 {}
impl FromStrProp for f32 {}
impl FromStrProp for u32 {}
impl FromStrProp for Vector {}
impl<T: FromStrProp> EntityProp<'_> for T
where
EntityParseError: From<<T as FromStr>::Err>,
{
Ok(self.prop(key)?.parse()?)
fn parse(raw: &'_ str) -> Result<Self, EntityParseError> {
Ok(raw.parse()?)
}
}
fn prop_parse_space_seperated<T: FromStr + Default, const N: usize>(
&self,
key: &'static str,
) -> Result<[T; N], EntityParseError>
impl<T: FromStrProp, const N: usize> EntityProp<'_> for [T; N]
where
EntityParseError: From<<T as FromStr>::Err>,
[T; N]: Default,
{
let prop = self.prop(key)?;
let mut values = prop.split(" ").map(T::from_str);
fn parse(raw: &'_ str) -> Result<Self, EntityParseError> {
let mut values = raw.split(" ").map(T::from_str);
let mut result = <[T; N]>::default();
for i in 0..N {
result[i] = values.next().ok_or(EntityParseError::ElementCount)??;
}
Ok(result)
}
pub fn parse(&self) -> Result<Entity<'a>, EntityParseError> {
let class = self.prop("classname")?;
match class {
"prop_dynamic" => Ok(Entity::PropDynamic(PropDynamic {
angles: self.prop_parse_space_seperated("angles")?,
disable_receive_shadows: self
.prop_parse::<u8>("disablereceiveshadows")
.unwrap_or_default()
> 0,
disable_shadows: self
.prop_parse::<u8>("disablereceiveshadows")
.unwrap_or_default()
> 0,
scale: self.prop_parse("modelscale")?,
model: self.prop("model")?,
origin: self.prop_parse("angles")?,
color: self.prop_parse_space_seperated("rendercolor")?,
name: self.prop("targetname").ok(),
parent: self.prop("parentname").ok(),
})),
"prop_physics_multiplayer" => Ok(Entity::PropPhysics(PropDynamic {
angles: self.prop_parse_space_seperated("angles")?,
disable_receive_shadows: self
.prop_parse::<u8>("disablereceiveshadows")
.unwrap_or_default()
> 0,
disable_shadows: self
.prop_parse::<u8>("disablereceiveshadows")
.unwrap_or_default()
> 0,
scale: self.prop_parse("modelscale")?,
model: self.prop("model")?,
origin: self.prop_parse("angles")?,
color: self.prop_parse_space_seperated("rendercolor")?,
name: self.prop("targetname").ok(),
parent: self.prop("parentname").ok(),
})),
"light_spot" => Ok(Entity::SpotLight(SpotLight {
origin: self.prop_parse("origin")?,
angles: self.prop_parse_space_seperated("angles")?,
color: self.prop_parse_space_seperated("_light")?,
cone: self.prop_parse("_cone")?,
})),
"point_spotlight" => Ok(Entity::SpotLight(SpotLight {
origin: self.prop_parse("origin")?,
angles: self.prop_parse_space_seperated("angles")?,
color: self.prop_parse_space_seperated("rendercolor")?,
cone: self.prop_parse("spotlightwidth")?,
})),
"env_sprite" => Ok(Entity::EnvSprite(EnvSprite {
origin: self.prop_parse("origin")?,
scale: self.prop_parse("scale")?,
model: self.prop("model")?,
color: self.prop_parse_space_seperated("rendercolor")?,
})),
"info_player_teamspawn" => Ok(Entity::Spawn(Spawn {
origin: self.prop_parse("origin")?,
angles: self.prop_parse_space_seperated("angles")?,
target: self.prop("targetname").ok(),
control_point: self.prop("controlpoint").ok(),
start_disabled: self.prop_parse::<u8>("StartDisabled").unwrap_or_default() > 0,
team: self.prop_parse("TeamNum")?,
})),
"func_door" => Ok(Entity::Door(Door {
origin: self.prop_parse("origin")?,
target: self.prop("targetname")?,
speed: self.prop_parse("speed")?,
force_closed: self.prop_parse::<u8>("forceclosed").unwrap_or_default() > 0,
move_direction: self.prop_parse("movedir")?,
model: self.prop("model")?,
})),
"item_ammopack_small" => Ok(Entity::AmmoPack(AmmoPack {
origin: self.prop_parse("origin")?,
ty: PackType::Small,
})),
"item_ammopack_medium" => Ok(Entity::AmmoPack(AmmoPack {
origin: self.prop_parse("origin")?,
ty: PackType::Medium,
})),
"item_ammopack_large" => Ok(Entity::AmmoPack(AmmoPack {
origin: self.prop_parse("origin")?,
ty: PackType::Large,
})),
"item_healthkit_small" => Ok(Entity::HealthPack(HealthPack {
origin: self.prop_parse("origin")?,
ty: PackType::Small,
})),
"item_healthkit_medium" => Ok(Entity::HealthPack(HealthPack {
origin: self.prop_parse("origin")?,
ty: PackType::Medium,
})),
"item_healthkit_large" => Ok(Entity::HealthPack(HealthPack {
origin: self.prop_parse("origin")?,
ty: PackType::Large,
})),
"worldspawn" => Ok(Entity::WorldSpawn(WorldSpawn {
min: self.prop_parse("world_mins")?,
max: self.prop_parse("world_maxs")?,
detail_vbsp: self.prop("detailvbsp")?,
detail_material: self.prop("detailmaterial")?,
comment: self.prop("comment").ok(),
skybox: self.prop("skyname")?,
version: self.prop_parse("mapversion")?,
})),
"info_observer_point" => Ok(Entity::ObserverPoint(ObserverPoint {
start_disabled: self.prop_parse::<u8>("StartDisabled").unwrap_or_default() > 0,
angles: self.prop_parse_space_seperated("angles")?,
origin: self.prop_parse("origin")?,
target: self.prop("targetname").ok(),
parent: self.prop("parentname").ok(),
})),
"func_brush" => Ok(Entity::Brush(BrushEntity {
model: self.prop("model")?,
start_disabled: self.prop_parse::<u8>("StartDisabled").unwrap_or_default() > 0,
origin: self.prop_parse("origin")?,
color: self.prop_parse_space_seperated("rendercolor")?,
})),
_ => Ok(Entity::Unknown(self.clone())),
}
impl<'a> EntityProp<'a> for &'a str {
fn parse(raw: &'a str) -> Result<Self, EntityParseError> {
Ok(raw)
}
}
#[derive(Debug, Clone)]
impl EntityProp<'_> for bool {
fn parse(raw: &'_ str) -> Result<Self, EntityParseError> {
Ok(raw != "0")
}
}
impl<'a, T: EntityProp<'a>> EntityProp<'a> for Option<T> {
fn parse(raw: &'a str) -> Result<Self, EntityParseError> {
Ok(Some(T::parse(raw)?))
}
}
#[derive(Debug, Clone, Entity)]
pub enum Entity<'a> {
#[entity(name = "point_spotlight")]
SpotLight(SpotLight),
#[entity(name = "light_spot")]
LightSpot(LightSpot),
#[entity(name = "prop_dynamic")]
PropDynamic(PropDynamic<'a>),
#[entity(name = "prop_physics_multiplayer")]
PropPhysics(PropDynamic<'a>),
#[entity(name = "env_sprite")]
EnvSprite(EnvSprite<'a>),
#[entity(name = "info_player_teamspawn")]
Spawn(Spawn<'a>),
#[entity(name = "func_door")]
Door(Door<'a>),
AmmoPack(AmmoPack),
HealthPack(HealthPack),
#[entity(name = "worldspawn")]
WorldSpawn(WorldSpawn<'a>),
#[entity(name = "info_observer_point")]
ObserverPoint(ObserverPoint<'a>),
#[entity(name = "func_brush")]
Brush(BrushEntity<'a>),
#[entity(name = "item_ammopack_small")]
AmmoPackSmall(AmmoPack),
#[entity(name = "item_ammopack_medium")]
AmmoPackMedium(AmmoPack),
#[entity(name = "item_ammopack_large")]
HealthPackLarge(HealthPack),
#[entity(name = "item_healthkit_small")]
HealthPackSmall(HealthPack),
#[entity(name = "item_healthkit_medium")]
HealthPackMedium(HealthPack),
#[entity(name = "item_healthkit_large")]
AmmoPackLarge(AmmoPack),
#[entity(default)]
Unknown(RawEntity<'a>),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct SpotLight {
pub origin: Vector,
pub angles: [f32; 3],
#[entity(name = "rendercolor")]
pub color: [u8; 3],
#[entity(name = "spotlightwidth")]
pub cone: u8,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct LightSpot {
pub origin: Vector,
pub angles: [f32; 3],
#[entity(name = "_light")]
pub color: [u8; 3],
#[entity(name = "_cone")]
pub cone: u8,
}
#[derive(Debug, Clone, Entity)]
pub struct PropDynamic<'a> {
pub angles: [f32; 3],
#[entity(name = "disablereceiveshadows", default)]
pub disable_receive_shadows: bool,
#[entity(name = "disableshadows", default)]
pub disable_shadows: bool,
#[entity(name = "modelscale")]
pub scale: f32,
pub model: &'a str,
pub origin: Vector,
#[entity(name = "rendercolor")]
pub color: [u8; 3],
#[entity(name = "targetname", default)]
pub name: Option<&'a str>,
#[entity(name = "parentname", default)]
pub parent: Option<&'a str>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct EnvSprite<'a> {
pub origin: Vector,
pub scale: f32,
pub model: &'a str,
#[entity(name = "rendercolor")]
pub color: [u8; 3],
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct Spawn<'a> {
pub origin: Vector,
pub angles: [f32; 3],
#[entity(name = "targetname", default)]
pub target: Option<&'a str>,
#[entity(name = "controlpoint", default)]
pub control_point: Option<&'a str>,
#[entity(name = "StartDisabled", default)]
pub start_disabled: bool,
#[entity(name = "TeamNum")]
pub team: u8,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct Door<'a> {
pub origin: Vector,
#[entity(name = "targetname", default)]
pub target: &'a str,
pub speed: f32,
#[entity(name = "forceclosed", default)]
pub force_closed: bool,
#[entity(name = "movedir")]
pub move_direction: Vector,
pub model: &'a str,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct AmmoPack {
pub origin: Vector,
pub ty: PackType,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct HealthPack {
pub origin: Vector,
pub ty: PackType,
}
#[derive(Debug, Clone)]
pub enum PackType {
Small,
Medium,
Large,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct WorldSpawn<'a> {
#[entity(name = "world_mins")]
pub min: Vector,
#[entity(name = "world_mins")]
pub max: Vector,
#[entity(name = "detailvbsp")]
pub detail_vbsp: &'a str,
#[entity(name = "detailmaterial")]
pub detail_material: &'a str,
#[entity(default)]
pub comment: Option<&'a str>,
#[entity(name = "skyname")]
pub skybox: &'a str,
#[entity(name = "mapversion")]
pub version: u32,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct ObserverPoint<'a> {
#[entity(name = "StartDisabled", default)]
pub start_disabled: bool,
pub angles: [f32; 3],
pub origin: Vector,
#[entity(name = "targetname", default)]
pub target: Option<&'a str>,
#[entity(name = "parentname", default)]
pub parent: Option<&'a str>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Entity)]
pub struct BrushEntity<'a> {
pub model: &'a str,
pub origin: Vector,
#[entity(name = "StartDisabled", default)]
pub start_disabled: bool,
#[entity(name = "rendercolor")]
pub color: [f32; 3],
}

View file

@ -1,13 +1,13 @@
mod bspfile;
pub mod data;
mod error;
pub mod error;
mod handle;
mod reader;
use crate::bspfile::LumpType;
pub use crate::data::TextureFlags;
pub use crate::data::Vector;
use crate::data::*;
pub use crate::data::*;
use crate::error::ValidationError;
pub use crate::handle::Handle;
use binrw::io::Cursor;