Skip to content

Commit 38fa10d

Browse files
authored
Merge pull request #215 from lexeyOK/feature/block-macro
fix: Use iterators instead of loops
2 parents 4efcec7 + 93685e8 commit 38fa10d

File tree

3 files changed

+136
-165
lines changed

3 files changed

+136
-165
lines changed

src/lib/derive_macros/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ craftflow-nbt = { workspace = true }
2222
indexmap = { workspace = true, features = ["serde"] }
2323
bitcode = { workspace = true }
2424
simd-json = { workspace = true }
25+
26+
[dev-dependencies]
27+
ferrumc-world = { workspace = true }

src/lib/derive_macros/src/block/mod.rs

Lines changed: 127 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use proc_macro::TokenStream;
22
use quote::quote;
33
use simd_json::prelude::{ValueAsObject, ValueAsScalar, ValueObjectAccess};
4+
use simd_json::{OwnedValue, StaticNode};
45
use syn::parse::{Parse, ParseStream};
56
use syn::punctuated::Punctuated;
6-
use syn::{braced, Expr, Ident, LitStr, Result, Token};
7+
use syn::{braced, Expr, Ident, Lit, LitStr, Result, Token};
78

89
const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json");
910

@@ -73,179 +74,147 @@ pub fn block(input: TokenStream) -> TokenStream {
7374
.as_object()
7475
.unwrap()
7576
.iter()
76-
.filter(|v| {
77-
let name_in_json = v.1.get("name");
78-
if let Some(resolved_name) = name_in_json {
79-
resolved_name.as_str() == Some(&name_str)
80-
} else {
81-
false
82-
}
83-
})
84-
.map(|v| (v.0.parse::<u32>().unwrap(), v.1))
77+
.filter(|(_, v)| v.get("name").as_str() == Some(&name_str))
78+
.map(|(k, v)| (k.parse::<u32>().unwrap(), v))
8579
.collect::<Vec<_>>();
86-
if let Some(opts) = opts {
87-
let mut props = vec![];
88-
for kv in opts.pairs.iter() {
89-
let key = kv.key.to_string();
90-
let value = match &kv.value {
91-
Expr::Lit(expr_lit) => {
92-
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
93-
lit_str.value()
94-
} else if let syn::Lit::Bool(lit_bool) = &expr_lit.lit {
95-
lit_bool.value.to_string()
96-
} else if let syn::Lit::Int(lit_int) = &expr_lit.lit {
97-
lit_int.base10_digits().to_string()
98-
} else {
99-
return syn::Error::new_spanned(
80+
81+
let Some(opts) = opts else {
82+
if filtered_names.is_empty() {
83+
return syn::Error::new_spanned(
84+
name.clone(),
85+
format!("block '{}' not found in blockstates.json", name_str),
86+
)
87+
.to_compile_error()
88+
.into();
89+
}
90+
if filtered_names.len() > 1 {
91+
let properties = get_properties(&filtered_names);
92+
return syn::Error::new_spanned(
93+
name_str.clone(),
94+
format!(
95+
"block '{}' has multiple variants, please specify properties. Available properties: {}",
96+
name_str, pretty_print_props(&properties)
97+
),
98+
)
99+
.to_compile_error()
100+
.into();
101+
}
102+
let first = filtered_names.iter().next().unwrap().0;
103+
return quote! { BlockId(#first) }.into();
104+
};
105+
106+
let props = opts
107+
.pairs
108+
.iter()
109+
.map(|kv| {
110+
Ok((
111+
kv.key.to_string(),
112+
match &kv.value {
113+
Expr::Lit(v) => match &v.lit {
114+
Lit::Str(v) => v.value(),
115+
Lit::Bool(v) => v.value.to_string(),
116+
Lit::Int(v) => v.base10_digits().to_string(),
117+
_ => return Err(syn::Error::new_spanned(
100118
&kv.value,
101119
"only string, bool, and int literals are supported as property values",
102-
)
103-
.to_compile_error()
104-
.into();
105-
}
106-
}
107-
_ => {
108-
return syn::Error::new_spanned(
109-
&kv.value,
110-
"only string, bool, and int literals are supported as property values",
111-
)
112-
.to_compile_error()
113-
.into();
114-
}
115-
};
116-
props.push((key, value));
117-
}
118-
let mut matched = vec![];
119-
'outer: for (id, v) in filtered_names.clone() {
120-
if let Some(obj) = v.as_object() {
121-
if let Some(block_props) = obj.get("properties").and_then(|p| p.as_object()) {
122-
for (k, v) in block_props.iter() {
123-
// Convert to a string
124-
let converted = if let Some(s) = v.as_str() {
125-
s.to_string()
126-
} else if let Some(b) = v.as_bool() {
127-
b.to_string()
128-
} else if let Some(n) = v.as_i64() {
129-
n.to_string()
130-
} else {
131-
// unsupported property type
132-
continue 'outer;
133-
};
134-
// property is a single string
135-
if let Some((_, val)) = props.iter().find(|(key, _)| key == k) {
136-
if converted != *val {
137-
continue 'outer; // no match
138-
}
139-
} else {
140-
continue 'outer; // property not specified
141-
}
120+
)),
121+
},
122+
_ => {
123+
return Err(syn::Error::new_spanned(
124+
&kv.value,
125+
"only string, bool, and int literals are supported as property values",
126+
))
142127
}
143-
matched.push(id);
144-
} else if !props.is_empty() {
145-
continue 'outer; // properties specified but block has none
146-
} else {
147-
matched.push(id); // no properties to match, accept this block
128+
},
129+
))
130+
})
131+
.collect::<Result<std::collections::HashMap<String, String>>>();
132+
133+
let props = match props {
134+
Ok(props) => props,
135+
Err(err) => return err.to_compile_error().into(),
136+
};
137+
138+
let matched = filtered_names
139+
.iter()
140+
.filter(|(_, v)| {
141+
if let Some(map) = v.get("properties").as_object() {
142+
// eq impl from halfbwown
143+
if map.len() != props.len() {
144+
return false;
148145
}
146+
return map.iter().all(|(key, value)| {
147+
let converted = match value {
148+
OwnedValue::Static(StaticNode::Bool(v)) => v.to_string(),
149+
OwnedValue::Static(StaticNode::I64(v)) => v.to_string(),
150+
OwnedValue::String(v) => v.to_string(),
151+
_ => return false,
152+
};
153+
props.get(key).is_some_and(|v| *converted == *v)
154+
});
149155
}
150-
}
151-
if matched.len() > 1 {
152-
syn::Error::new_spanned(
156+
false
157+
})
158+
.map(|(id, _)| *id)
159+
.collect::<Vec<u32>>();
160+
161+
if matched.is_empty() {
162+
let properties = get_properties(&filtered_names);
163+
if properties.is_empty() {
164+
return syn::Error::new_spanned(
153165
name_str.clone(),
154-
format!("block '{}' with specified properties has multiple variants, please refine properties", name_str),
155-
)
156-
.to_compile_error()
157-
.into()
158-
} else if matched.is_empty() {
159-
if get_properties_for_id(name_str.clone()).is_empty() {
160-
syn::Error::new_spanned(
166+
format!(
167+
"block '{}' has no properties but the following properties were given: {}",
161168
name_str.clone(),
162-
format!(
163-
"block '{}' has no properties but the following properties were given: {}",
164-
name_str.clone(),
165-
pretty_print_given_props(opts)
166-
),
167-
)
168-
.to_compile_error()
169-
.into()
170-
} else {
171-
syn::Error::new_spanned(
169+
pretty_print_given_props(opts)
170+
),
171+
)
172+
.to_compile_error()
173+
.into();
174+
} else {
175+
return syn::Error::new_spanned(
172176
name_str.clone(),
173177
format!(
174178
"no variant of block '{}' matches the specified properties. Available properties: {}",
175-
name_str.clone(), pretty_print_props(&get_properties_for_id(name_str))
179+
name_str.clone(), pretty_print_props(&properties)
176180
),
177181
)
178182
.to_compile_error()
179-
.into()
180-
}
181-
} else {
182-
let res = matched[0];
183-
quote! { BlockId(#res) }.into()
183+
.into();
184184
}
185-
} else if filtered_names.len() > 1 {
186-
syn::Error::new_spanned(
187-
name_str.clone(),
188-
format!(
189-
"block '{}' has multiple variants, please specify properties. Available properties: {}",
190-
name_str.clone(), pretty_print_props(&get_properties_for_id(name_str))
191-
),
192-
)
193-
.to_compile_error()
194-
.into()
195-
} else if filtered_names.is_empty() {
196-
syn::Error::new_spanned(
197-
name.clone(),
198-
format!("block '{}' not found in blockstates.json", name_str),
199-
)
200-
.to_compile_error()
201-
.into()
202-
} else {
203-
let first = filtered_names[0].0;
204-
quote! { BlockId(#first) }.into()
205185
}
186+
if matched.len() > 1 {
187+
return syn::Error::new_spanned(
188+
name_str.clone(),
189+
format!("block '{}' with specified properties has multiple variants, please refine properties", name_str),
190+
)
191+
.to_compile_error()
192+
.into();
193+
}
194+
195+
let res = matched[0];
196+
quote! { BlockId(#res) }.into()
206197
}
207198

208-
fn get_properties_for_id(id: String) -> Vec<(String, String)> {
209-
// Iterate all blocks and find all with this as the "name" field
210-
let mut buf = JSON_FILE.to_vec();
211-
let v = simd_json::to_owned_value(&mut buf).unwrap();
212-
let filtered_names = v
213-
.as_object()
214-
.unwrap()
199+
fn get_properties(filtered_names: &[(u32, &OwnedValue)]) -> Vec<(String, String)> {
200+
filtered_names
215201
.iter()
216-
.filter(|v| {
217-
let name_in_json = v.1.get("name");
218-
if let Some(resolved_name) = name_in_json {
219-
resolved_name.as_str() == Some(&id)
220-
} else {
221-
false
222-
}
202+
.filter_map(|(_, v)| v.get("properties").and_then(|v| v.as_object()))
203+
.flat_map(|v| {
204+
v.iter().filter_map(|(k, v)| {
205+
let converted = match v {
206+
OwnedValue::Static(StaticNode::Bool(v)) => v.to_string(),
207+
OwnedValue::Static(StaticNode::I64(v)) => v.to_string(),
208+
OwnedValue::String(v) => v.to_string(),
209+
_ => return None,
210+
};
211+
Some((k.clone(), converted))
212+
})
223213
})
224-
.map(|v| (v.0.parse::<u32>().unwrap(), v.1))
225-
.collect::<Vec<_>>();
226-
let mut all_props = vec![];
227-
for (_, v) in filtered_names {
228-
if let Some(obj) = v.as_object() {
229-
if let Some(block_props) = obj.get("properties").and_then(|p| p.as_object()) {
230-
for (k, v) in block_props.iter() {
231-
let converted = if let Some(s) = v.as_str() {
232-
s.to_string()
233-
} else if let Some(b) = v.as_bool() {
234-
b.to_string()
235-
} else if let Some(n) = v.as_i64() {
236-
n.to_string()
237-
} else {
238-
continue; // unsupported property type
239-
};
240-
all_props.push((k.clone(), converted));
241-
}
242-
}
243-
}
244-
}
245-
all_props
214+
.collect()
246215
}
247216

248-
fn pretty_print_props(props: &Vec<(String, String)>) -> String {
217+
fn pretty_print_props(props: &[(String, String)]) -> String {
249218
let mut s = String::new();
250219
for (k, v) in props {
251220
s.push_str(&format!("{}: {}, ", k, v));
@@ -258,17 +227,12 @@ fn pretty_print_given_props(props: Opts) -> String {
258227
for kv in props.pairs.iter() {
259228
let key = kv.key.to_string();
260229
let value = match &kv.value {
261-
Expr::Lit(expr_lit) => {
262-
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
263-
lit_str.value()
264-
} else if let syn::Lit::Bool(lit_bool) = &expr_lit.lit {
265-
lit_bool.value.to_string()
266-
} else if let syn::Lit::Int(lit_int) = &expr_lit.lit {
267-
lit_int.base10_digits().to_string()
268-
} else {
269-
"unsupported".to_string()
270-
}
271-
}
230+
Expr::Lit(v) => match &v.lit {
231+
Lit::Str(v) => v.value(),
232+
Lit::Bool(v) => v.value.to_string(),
233+
Lit::Int(v) => v.base10_digits().to_string(),
234+
_ => "unsupported".to_string(),
235+
},
272236
_ => "unsupported".to_string(),
273237
};
274238
s.push_str(&format!("{}: {}, ", key, value));

src/lib/derive_macros/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,15 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream {
101101
/// A macro to lookup block IDs at compile time.
102102
///
103103
/// Feed in the block name as a string literal, and an optional set of properties as a map.
104-
/// It will output a `BlockId` struct with the correct ID for that block and properties.
104+
/// It will output a [`ferrumc_world::block_id::BlockId`] struct with the correct ID for that block and properties.
105105
/// Usage:
106-
/// ```ignore
106+
/// ```
107+
/// # use ferrumc_world::block_id::BlockId;
108+
/// # use ferrumc_macros::block;
107109
/// let block_id = block!("stone");
108110
/// let another_block_id = block!("minecraft:grass_block", {snowy: true});
111+
/// assert_eq!(block_id, BlockId(1));
112+
/// assert_eq!(another_block_id, BlockId(8));
109113
/// ```
110114
/// Unfortunately, due to current limitations in Rust's proc macros, you will need to import the
111115
/// `BlockId` struct manually.

0 commit comments

Comments
 (0)