From 5169a056f5311277896bd3afbaa211acd9b1d338 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:48:24 +0500 Subject: [PATCH 01/23] fix: speed up the block! and block_match! macro this commit adds build.rs which makes phf map allowing to speed up the calculation of the blockid --- src/lib/derive_macros/Cargo.toml | 5 + src/lib/derive_macros/build.rs | 58 +++++++ src/lib/derive_macros/src/block/matches.rs | 47 ++--- src/lib/derive_macros/src/block/mod.rs | 189 ++++++++------------- 4 files changed, 151 insertions(+), 148 deletions(-) create mode 100644 src/lib/derive_macros/build.rs diff --git a/src/lib/derive_macros/Cargo.toml b/src/lib/derive_macros/Cargo.toml index c39f26384..80514fffd 100644 --- a/src/lib/derive_macros/Cargo.toml +++ b/src/lib/derive_macros/Cargo.toml @@ -22,6 +22,11 @@ craftflow-nbt = { workspace = true } indexmap = { workspace = true, features = ["serde"] } bitcode = { workspace = true } simd-json = { workspace = true } +phf = "0.13.1" [dev-dependencies] ferrumc-world = { workspace = true } + +[build-dependencies] +phf_codegen = "0.13.1" +simd-json = { workspace = true } diff --git a/src/lib/derive_macros/build.rs b/src/lib/derive_macros/build.rs new file mode 100644 index 000000000..4420123cb --- /dev/null +++ b/src/lib/derive_macros/build.rs @@ -0,0 +1,58 @@ +use std::{ + collections::HashMap, + env, + fs::File, + io::{BufWriter, Write}, + path::Path, +}; + +use phf_codegen; +use simd_json::{ + self, + base::{ValueAsObject, ValueAsScalar}, + derived::ValueTryAsObject, +}; + +const JSON_FILE: &[u8] = include_bytes!("../../../assets/data/blockstates.json"); + +fn main() { + let mut buf = JSON_FILE.to_owned(); + let v = simd_json::to_borrowed_value(&mut buf).unwrap(); + let mut map = phf_codegen::Map::new(); + + let mut rev_map: HashMap> = HashMap::new(); + for (key, value) in v.try_as_object().expect("object value") { + let id = key.parse::().expect("integer value"); + let block = value.as_object().expect("object value"); + let name = block + .get("name") + .expect("all block states have names") + .as_str() + .expect("names are strings") + .split_once("minecraft:") + .expect("names start with \"minecraft:\"") + .1; + let props = block.get("properties"); + rev_map.entry(name.to_owned()).or_default().push(id); + if let Some(props) = props { + for (prop_key, prop_val) in props.as_object().expect("properties is object") { + let map_key = format!("{}:{}", prop_key, prop_val); + rev_map.entry(map_key).or_default().push(id); + } + } + } + for (k, v) in rev_map.iter_mut() { + v.sort(); + map.entry(k.clone(), format!("&{:?}", v)); + } + let map = map.build(); + let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + write!( + &mut file, + "static BLOCK_STATES: phf::Map<&'static str, &[u32]> = {};", + map + ) + .expect("able to write to file"); + println!("created {}", &path.to_string_lossy()); +} diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index 9f8f3168e..91bc471d0 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -1,8 +1,6 @@ -use crate::block::JSON_FILE; +use crate::block::BLOCK_STATES; use proc_macro::TokenStream; use quote::quote; -use simd_json::base::{ValueAsObject, ValueAsScalar}; -use simd_json::derived::ValueObjectAccess; struct Input { name: String, @@ -25,23 +23,15 @@ impl syn::parse::Parse for Input { // match_block!("dirt", block_state_id); -> "if block_name == BlockStateId( { ... } pub fn matches_block(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as Input); - let block_name = &input.name; - let block_name = if block_name.starts_with("minecraft:") { - block_name.to_string() - } else { - format!("minecraft:{}", block_name) - }; - let block_state_id_var = &input.id_var; - let mut buf = JSON_FILE.to_vec(); - let v = simd_json::to_owned_value(&mut buf).unwrap(); - let filtered_names = v - .as_object() - .unwrap() - .iter() - .filter(|(_, v)| v.get("name").as_str() == Some(&block_name)) - .map(|(k, v)| (k.parse::().unwrap(), v)) - .collect::>(); - if filtered_names.is_empty() { + let block_name = input + .name + .split_once("minecraft:") + .map(|x| x.1) + .unwrap_or(input.name.as_str()); + let block_id_var = &input.id_var; + + let states = BLOCK_STATES.get(block_name); + if states.is_none_or(|x| x.is_empty()) { return syn::Error::new_spanned( &input.id_var, format!("Block name '{}' not found in registry", block_name), @@ -49,14 +39,13 @@ pub fn matches_block(input: TokenStream) -> TokenStream { .to_compile_error() .into(); } - let mut arms = Vec::new(); - for (id, _) in filtered_names { - arms.push(quote! { - #block_state_id_var == BlockStateId(#id) - }); - } - let joined = quote! { - #(#arms)||* + let &states = states.unwrap(); + + let matched = quote! { + match #block_id_var { + #(BlockStateId(#states) => true),*, + _ => false + } }; - joined.into() + matched.into() } diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index 610531fb2..c633a71a1 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -1,14 +1,14 @@ use proc_macro::TokenStream; use quote::quote; -use simd_json::prelude::{ValueAsObject, ValueAsScalar, ValueObjectAccess}; -use simd_json::{OwnedValue, StaticNode}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::{braced, Expr, Ident, Lit, LitStr, Result, Token}; pub(crate) mod matches; -const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json"); +// const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json"); + +include!(concat!(env!("OUT_DIR"), "/codegen.rs")); struct Input { name: LitStr, @@ -63,47 +63,42 @@ impl Parse for Input { pub fn block(input: TokenStream) -> TokenStream { let Input { name, opts } = syn::parse_macro_input!(input as Input); - - let name_str = if name.value().starts_with("minecraft:") { - name.value() + let name_value = name.value(); + let name_str = if let Some((_, name)) = name_value.split_once("minecraft:") { + name } else { - format!("minecraft:{}", name.value()) + &name_value }; - let mut buf = JSON_FILE.to_vec(); - let v = simd_json::to_owned_value(&mut buf).unwrap(); - let filtered_names = v - .as_object() - .unwrap() - .iter() - .filter(|(_, v)| v.get("name").as_str() == Some(&name_str)) - .map(|(k, v)| (k.parse::().unwrap(), v)) - .collect::>(); + let states = BLOCK_STATES.get(name_str); + if states.is_none_or(|x| x.is_empty()) { + return syn::Error::new_spanned( + name.clone(), + format!("block '{}' not found in blockstates.json", name_value), + ) + .to_compile_error() + .into(); + } - let Some(opts) = opts else { - if filtered_names.is_empty() { - return syn::Error::new_spanned( - name.clone(), - format!("block '{}' not found in blockstates.json", name_str), - ) - .to_compile_error() - .into(); - } - if filtered_names.len() > 1 { - let properties = get_properties(&filtered_names); + let &states = states.unwrap(); + + if opts.is_none() { + if states.len() > 1 { return syn::Error::new_spanned( - name_str.clone(), + name, format!( - "block '{}' has multiple variants, please specify properties. Available properties: {}", - name_str, pretty_print_props(&properties) + "block '{}' has multiple variants, please specify properties.", + name_value ), ) .to_compile_error() .into(); } - let first = filtered_names.first().unwrap().0; - return quote! { BlockStateId(#first) }.into(); - }; + let id = states[0]; + return quote! { BlockStateId(#id) }.into(); + } + + let opts = opts.unwrap(); let props = opts .pairs @@ -130,114 +125,70 @@ pub fn block(input: TokenStream) -> TokenStream { }, )) }) - .collect::>>(); + .collect::>>(); let props = match props { Ok(props) => props, Err(err) => return err.to_compile_error().into(), }; - let matched = filtered_names - .iter() - .filter(|(_, v)| { - if let Some(map) = v.get("properties").as_object() { - // eq impl from halfbwown - if map.len() != props.len() { - return false; + fn intersect(a: &[u32], b: &[u32]) -> Vec { + let mut v = vec![]; + let mut i = 0; + let mut j = 0; + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + std::cmp::Ordering::Less => i += 1, + std::cmp::Ordering::Equal => { + v.push(a[i]); + i += 1; + j += 1; } - return map.iter().all(|(key, value)| { - let converted = match value { - OwnedValue::Static(StaticNode::Bool(v)) => v.to_string(), - OwnedValue::Static(StaticNode::I64(v)) => v.to_string(), - OwnedValue::String(v) => v.to_string(), - _ => return false, - }; - props.get(key).is_some_and(|v| *converted == *v) - }); + std::cmp::Ordering::Greater => j += 1, } - false - }) - .map(|(id, _)| *id) - .collect::>(); + } + v + } + + let mut states = states.to_owned(); + + for (k, v) in props { + let key = format!("{}:{}", k, v); + let prop_states = BLOCK_STATES.get(&key); + if prop_states.is_none_or(|x| x.is_empty()) { + return syn::Error::new_spanned( + name.clone(), + format!("No block has properties '{}'.", key), + ) + .to_compile_error() + .into(); + } - if matched.is_empty() { - let properties = get_properties(&filtered_names); - if properties.is_empty() { + let &prop_states = prop_states.unwrap(); + states = intersect(&states, prop_states); + if states.is_empty() { return syn::Error::new_spanned( - name_str.clone(), + name, format!( - "block '{}' has no properties but the following properties were given: {}", - name_str.clone(), - pretty_print_given_props(opts) + "No variant of block '{}' matches the specified properties.", + name_value ), ) .to_compile_error() .into(); - } else { - return syn::Error::new_spanned( - name_str.clone(), - format!( - "no variant of block '{}' matches the specified properties. Available properties: {}", - name_str.clone(), pretty_print_props(&properties) - ), - ) - .to_compile_error() - .into(); } } - if matched.len() > 1 { + + if states.len() > 1 { return syn::Error::new_spanned( - name_str.clone(), - format!("block '{}' with specified properties has multiple variants, please refine properties", name_str), + name, + format!("Block '{}' with specified properties has multiple variants ({:?}), please refine properties", name_value, states), ) .to_compile_error() .into(); } - let res = matched[0]; - quote! { BlockStateId(#res) }.into() -} + let id = states[0]; -fn get_properties(filtered_names: &[(u32, &OwnedValue)]) -> Vec<(String, String)> { - filtered_names - .iter() - .filter_map(|(_, v)| v.get("properties").and_then(|v| v.as_object())) - .flat_map(|v| { - v.iter().filter_map(|(k, v)| { - let converted = match v { - OwnedValue::Static(StaticNode::Bool(v)) => v.to_string(), - OwnedValue::Static(StaticNode::I64(v)) => v.to_string(), - OwnedValue::String(v) => v.to_string(), - _ => return None, - }; - Some((k.clone(), converted)) - }) - }) - .collect() -} - -fn pretty_print_props(props: &[(String, String)]) -> String { - let mut s = String::new(); - for (k, v) in props { - s.push_str(&format!("{}: {}, ", k, v)); - } - s.trim_end_matches(", ").to_string() -} - -fn pretty_print_given_props(props: Opts) -> String { - let mut s = String::new(); - for kv in props.pairs.iter() { - let key = kv.key.to_string(); - let value = match &kv.value { - Expr::Lit(v) => match &v.lit { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => "unsupported".to_string(), - }, - _ => "unsupported".to_string(), - }; - s.push_str(&format!("{}: {}, ", key, value)); - } - s.trim_end_matches(", ").to_string() + quote! { BlockStateId(#id) }.into() } From ec075084f7481df56224b45007e6e0f2ebc33344 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:48:51 +0500 Subject: [PATCH 02/23] placeholders and multi-value this commit adds placeholder feature and restructures the parsing to allow _ and ident in property's value. --- Cargo.toml | 1 + src/lib/derive_macros/build.rs | 32 +- src/lib/derive_macros/src/block/matches.rs | 7 +- src/lib/derive_macros/src/block/mod.rs | 420 +++++++++++++++------ src/lib/derive_macros/src/lib.rs | 28 +- src/tests/Cargo.toml | 3 + src/tests/build.rs | 90 +++++ src/tests/src/block/mod.rs | 8 + src/tests/src/lib.rs | 5 +- 9 files changed, 463 insertions(+), 131 deletions(-) create mode 100644 src/tests/build.rs create mode 100644 src/tests/src/block/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 1a0528d62..8fec320b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "src/lib/messages", "src/lib/data", "src/lib/entities", + "src/tests", ] [workspace.package] diff --git a/src/lib/derive_macros/build.rs b/src/lib/derive_macros/build.rs index 4420123cb..0fa808b3d 100644 --- a/src/lib/derive_macros/build.rs +++ b/src/lib/derive_macros/build.rs @@ -1,12 +1,11 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, env, fs::File, io::{BufWriter, Write}, path::Path, }; -use phf_codegen; use simd_json::{ self, base::{ValueAsObject, ValueAsScalar}, @@ -19,8 +18,9 @@ fn main() { let mut buf = JSON_FILE.to_owned(); let v = simd_json::to_borrowed_value(&mut buf).unwrap(); let mut map = phf_codegen::Map::new(); - + let mut prop_map = phf_codegen::Map::new(); let mut rev_map: HashMap> = HashMap::new(); + let mut rev_prop: HashMap> = HashMap::new(); for (key, value) in v.try_as_object().expect("object value") { let id = key.parse::().expect("integer value"); let block = value.as_object().expect("object value"); @@ -34,9 +34,18 @@ fn main() { .1; let props = block.get("properties"); rev_map.entry(name.to_owned()).or_default().push(id); + rev_prop.entry(name.to_string()).or_default(); if let Some(props) = props { for (prop_key, prop_val) in props.as_object().expect("properties is object") { let map_key = format!("{}:{}", prop_key, prop_val); + rev_prop + .entry(prop_key.to_string()) + .or_default() + .insert(prop_val.to_string()); + rev_prop + .entry(name.to_string()) + .or_default() + .insert(prop_key.to_string()); rev_map.entry(map_key).or_default().push(id); } } @@ -45,14 +54,27 @@ fn main() { v.sort(); map.entry(k.clone(), format!("&{:?}", v)); } + + for (k, v) in rev_prop.iter() { + let mut v: Vec = v.iter().cloned().collect(); + v.sort(); + prop_map.entry(k.clone(), format!("&{:?}", v)); + } let map = map.build(); + let prop_map = prop_map.build(); let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs"); let mut file = BufWriter::new(File::create(&path).unwrap()); - write!( + writeln!( &mut file, - "static BLOCK_STATES: phf::Map<&'static str, &[u32]> = {};", + "static BLOCK_STATES: phf::Map<&'static str, &[u16]> = {};", map ) .expect("able to write to file"); + writeln!( + &mut file, + "static PROP_PARTS: phf::Map<&'static str, &[&'static str]> = {};", + prop_map + ) + .expect("able to write to file"); println!("created {}", &path.to_string_lossy()); } diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index 91bc471d0..943d57bbb 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -19,8 +19,6 @@ impl syn::parse::Parse for Input { } } -// match_block!("stone", block_state_id); -> "if block_state_id == BlockStateId(1) { ... } -// match_block!("dirt", block_state_id); -> "if block_name == BlockStateId( { ... } pub fn matches_block(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as Input); let block_name = input @@ -39,11 +37,12 @@ pub fn matches_block(input: TokenStream) -> TokenStream { .to_compile_error() .into(); } - let &states = states.unwrap(); + + let states = states.unwrap().iter().map(|&x| x as u32); let matched = quote! { match #block_id_var { - #(BlockStateId(#states) => true),*, + BlockId(#(#states)|*) => true, _ => false } }; diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index c633a71a1..fb4265c75 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -2,12 +2,10 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{braced, Expr, Ident, Lit, LitStr, Result, Token}; +use syn::{braced, Error, Ident, Lit, LitStr, Result, Token}; pub(crate) mod matches; -// const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json"); - include!(concat!(env!("OUT_DIR"), "/codegen.rs")); struct Input { @@ -18,11 +16,56 @@ struct Input { struct Opts { pairs: Punctuated, } - +#[derive(Debug)] struct Kv { key: Ident, _colon: Token![:], - value: Expr, // accept bools, strings, ints, calls, etc. + value: Value, +} + +impl Kv { + fn to_string_pair(&self) -> (String, String) { + ( + match &self.key.to_string() { + v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), + v => v.clone(), + }, + match &self.value { + Value::Static(v) => match v { + Lit::Str(v) => v.value(), + Lit::Bool(v) => v.value.to_string(), + Lit::Int(v) => v.base10_digits().to_string(), + _ => unreachable!(), + }, + Value::Any(_) => "_".into(), + Value::Ident(v) => v.to_string(), + }, + ) + } +} +#[derive(Debug)] +enum Value { + Static(Lit), + Any(Token![_]), + Ident(Ident), +} + +impl Parse for Value { + fn parse(input: ParseStream) -> Result { + if input.peek(Token![_]) { + return Ok(Self::Any(input.parse()?)); + } + if input.peek(Ident) { + return Ok(Self::Ident(input.parse()?)); + } + if input.peek(Lit) { + let lit = input.parse()?; + if matches!(lit, Lit::Str(_) | Lit::Bool(_) | Lit::Int(_)) { + return Ok(Self::Static(lit)); + } + } + Err(input.error("the property value must be _, ident or literal string, bool or int")) + } } impl Parse for Kv { @@ -61,134 +104,285 @@ impl Parse for Input { } } -pub fn block(input: TokenStream) -> TokenStream { - let Input { name, opts } = syn::parse_macro_input!(input as Input); - let name_value = name.value(); - let name_str = if let Some((_, name)) = name_value.split_once("minecraft:") { - name - } else { - &name_value - }; +impl quote::ToTokens for Input { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.name.to_tokens(tokens); + self.opts.to_tokens(tokens); + } +} - let states = BLOCK_STATES.get(name_str); - if states.is_none_or(|x| x.is_empty()) { - return syn::Error::new_spanned( - name.clone(), - format!("block '{}' not found in blockstates.json", name_value), - ) - .to_compile_error() - .into(); +impl quote::ToTokens for Opts { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.pairs.to_tokens(tokens); } +} - let &states = states.unwrap(); +impl quote::ToTokens for Kv { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.key.to_tokens(tokens); + self._colon.to_tokens(tokens); + self.value.to_tokens(tokens); + } +} - if opts.is_none() { - if states.len() > 1 { - return syn::Error::new_spanned( - name, - format!( - "block '{}' has multiple variants, please specify properties.", - name_value - ), - ) - .to_compile_error() - .into(); +impl quote::ToTokens for Value { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Value::Static(lit) => lit.to_tokens(tokens), + Value::Any(underscore) => underscore.to_tokens(tokens), + Value::Ident(ident) => ident.to_tokens(tokens), } - let id = states[0]; - return quote! { BlockStateId(#id) }.into(); } +} - let opts = opts.unwrap(); +pub fn block(input: TokenStream) -> TokenStream { + let out = match syn::parse_macro_input!(input as Input) { + Input { name, opts: None } => static_block(name), + Input { + name, + opts: Some(opts), + } => block_with_props(name, opts), + }; + match out { + Ok(v) => v, + Err(v) => v.into_compile_error().into(), + } +} + +fn block_with_props(name: LitStr, opts: Opts) -> Result { + let name_value = parse_name(&name); + let props = PROP_PARTS.get(&name_value); + if props.is_none() { + return Err(Error::new_spanned( + &name, + format!( + "the block `{}` is not found in the blockstates.json file (PROP_PARTS is not populated)", + name_value + ), + )); + } + if props.is_some_and(|x| x.is_empty()) { + return Err(Error::new_spanned( + &name, + format!("the block `{}` has no properties", name_value), + )); + } + let props = props.unwrap().to_vec(); - let props = opts + let opts_strings = opts .pairs .iter() - .map(|kv| { - Ok(( - kv.key.to_string(), - match &kv.value { - Expr::Lit(v) => match &v.lit { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => return Err(syn::Error::new_spanned( - &kv.value, - "only string, bool, and int literals are supported as property values", - )), - }, - _ => { - return Err(syn::Error::new_spanned( - &kv.value, - "only string, bool, and int literals are supported as property values", - )) - } - }, - )) - }) - .collect::>>(); - - let props = match props { - Ok(props) => props, - Err(err) => return err.to_compile_error().into(), - }; + .map(Kv::to_string_pair) + .zip(opts.pairs.iter()) + .collect::>(); + let mut unknown_props = Vec::new(); + let mut missing_props = Vec::new(); + for prop in &props { + if !opts_strings.iter().any(|((k, _), _)| k == prop) { + missing_props.push(prop.to_string()); + } + } + for ((k, _), _) in &opts_strings { + if !props.contains(&k.as_str()) { + unknown_props.push(k.clone()); + } + } + if !unknown_props.is_empty() { + return Err(Error::new_spanned( + &name, + format!( + "the block {name_value} has no properties with names: [{}]", + unknown_props.join(", ") + ), + )); + } + if !missing_props.is_empty() { + return Err(Error::new_spanned( + name, + format!( + "the block `{name_value}` is missing these properties: [{}]", + missing_props.join(", ") + ), + )); + } - fn intersect(a: &[u32], b: &[u32]) -> Vec { - let mut v = vec![]; - let mut i = 0; - let mut j = 0; - while i < a.len() && j < b.len() { - match a[i].cmp(&b[j]) { - std::cmp::Ordering::Less => i += 1, - std::cmp::Ordering::Equal => { - v.push(a[i]); - i += 1; - j += 1; + let mut block_states = BLOCK_STATES + .get(&name_value) + .expect(&format!("block name {name_value} should be present")) + .to_vec(); + for ((k, v), kv) in &opts_strings { + let property_filter = match &kv.value { + Value::Static(_) => BLOCK_STATES + .get(&format!("{k}:{v}")) + .ok_or(Error::new_spanned( + kv, + format!( + "the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", + PROP_PARTS + .get(k) + .expect("the key `{k}` exists in PROP_PARTS") + .join(", ") + ), + ))? + .to_vec(), + Value::Any(_) => { + let vs = PROP_PARTS + .get(k) + .ok_or( + Error::new_spanned( + &kv.key, + format!("the property `{k}` is not present in the blockstates.json file (PROP_PARTS is not populated)") + ) + )? + .iter() + .map(|v| BLOCK_STATES + .get(&format!("{k}:{v}")) + .ok_or( + Error::new_spanned( + kv, + format!("the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", PROP_PARTS + .get(k) + .expect(&format!("the key `{k}` exists in PROP_PARTS")).join(", ") + ) + )) + ) + .collect::>>()?; + let mut combined_block_states = Vec::new(); + for bs in vs { + combined_block_states = combine(&combined_block_states, bs); } - std::cmp::Ordering::Greater => j += 1, + combined_block_states } - } - v + Value::Ident(_) => { + return Err(Error::new_spanned(&kv.key, "Ident keys are not supported")) + } + }; + block_states = intersect(&block_states, &property_filter); } - let mut states = states.to_owned(); + if block_states.is_empty() { + return Err(Error::new_spanned( + Input { + name, + opts: Some(opts), + }, + "no block state corresponds to this combination of properties", + )); + } + let block_ids = block_states.iter().map(|x| *x as u32); + Ok(quote! {BlockId(#(#block_ids)|*)}.into()) +} - for (k, v) in props { - let key = format!("{}:{}", k, v); - let prop_states = BLOCK_STATES.get(&key); - if prop_states.is_none_or(|x| x.is_empty()) { - return syn::Error::new_spanned( - name.clone(), - format!("No block has properties '{}'.", key), - ) - .to_compile_error() - .into(); - } +fn static_block(name: LitStr) -> Result { + let name_value = parse_name(&name); + let props = PROP_PARTS.get(&name_value); + if props.is_none() { + return Err(Error::new_spanned( + &name, + format!( + "the block `{name_value}` not found in blockstates.json (PROP_PARTS is not populated)", + ), + )); + } + if props.is_some_and(|x| !x.is_empty()) { + let &props = props.unwrap(); + return Err(Error::new_spanned( + &name, + format!( + "the block `{name_value}` has these properties: [{}], please refine properties", + props.join(", ") + ), + )); + } + let block_states = BLOCK_STATES.get(&name_value); + if block_states.is_none_or(|x| x.is_empty()) { + return Err(Error::new_spanned( + &name, + format!( + "the block `{name_value}` not found in the blockstates.json file (BLOCK_STATE is not populated)", + ), + )); + } + let block_states = block_states.unwrap(); + if block_states.len() > 1 { + return Err(Error::new_spanned( + name, + format!( + "the block `{name_value}` has multiple block states: [{}], but only one expected", + block_states + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ") + ), + )); + } - let &prop_states = prop_states.unwrap(); - states = intersect(&states, prop_states); - if states.is_empty() { - return syn::Error::new_spanned( - name, - format!( - "No variant of block '{}' matches the specified properties.", - name_value - ), - ) - .to_compile_error() - .into(); - } + let id = block_states[0] as u32; + Ok(quote! { BlockId(#id) }.into()) +} + +fn parse_name(name: &LitStr) -> String { + let name_value = name.value(); + match name_value.split_once("minecraft:") { + Some((_, name)) => name.to_string(), + None => name_value, } +} - if states.len() > 1 { - return syn::Error::new_spanned( - name, - format!("Block '{}' with specified properties has multiple variants ({:?}), please refine properties", name_value, states), - ) - .to_compile_error() - .into(); +fn intersect(a: &[u16], b: &[u16]) -> Vec { + let mut v = vec![]; + let mut i = 0; + let mut j = 0; + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + std::cmp::Ordering::Less => i += 1, + std::cmp::Ordering::Equal => { + v.push(a[i]); + i += 1; + j += 1; + } + std::cmp::Ordering::Greater => j += 1, + } } + v +} - let id = states[0]; +fn combine(a: &[u16], b: &[u16]) -> Vec { + let mut v = vec![]; + let mut i = 0; + let mut j = 0; + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + std::cmp::Ordering::Less => { + v.push(a[i]); + i += 1; + } + std::cmp::Ordering::Equal => { + v.push(a[i]); + i += 1; + j += 1; + } + std::cmp::Ordering::Greater => { + v.push(b[j]); + j += 1; + } + } + } + if i < a.len() || j < b.len() { + if i < a.len() { + v.extend_from_slice(&a[i..]); + } else { + v.extend_from_slice(&b[j..]); + } + } + v +} - quote! { BlockStateId(#id) }.into() +#[test] +fn simple_combine() { + let a = [2, 4, 6, 8]; + let b = [1, 3, 5, 7]; + let c = combine(&a, &b); + assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], c); } diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index bee92b183..7a23fda7e 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -107,10 +107,16 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream { /// ``` /// # use ferrumc_world::block_state_id::BlockStateId; /// # use ferrumc_macros::block; -/// let block_state_id = block!("stone"); -/// let another_block_state_id = block!("minecraft:grass_block", {snowy: true}); -/// assert_eq!(block_state_id, BlockStateId(1)); -/// assert_eq!(another_block_state_id, BlockStateId(8)); +/// assert_eq!(BlockStateId(1), block!("stone")); +/// assert_eq!(BlockStateId(8) ,block!("minecraft:grass_block", {snowy: true})); +/// for i in 581..1730 { +/// assert!( +/// matches!( +/// BlockStateId(i), +/// block!("note_block", { note: _, powered: _, instrument: _}) +/// ) +/// ); +/// } /// ``` /// Unfortunately, due to current limitations in Rust's proc macros, you will need to import the /// `BlockStateId` struct manually. @@ -119,6 +125,13 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream { /// /// If the block or properties are invalid, a compile-time error will be thrown that should hopefully /// explain the issue. +/// # Static block +/// gives a single block pat. Mainly used with +/// `matches!` or `match`. `if let block!("stone" = bs_id) { .. }`, +/// `match bs_id { block!("stone" => { .. }), }` and `block!(expr)` +/// are not implemented and will panic at compile time. Panics if block state has any properties. +/// # Expr with properties +/// any part of this can be a placeholder or a literal, variables are not allowed. Be careful as this can bloat the code. #[proc_macro] pub fn block(input: TokenStream) -> TokenStream { block::block(input) @@ -129,9 +142,10 @@ pub fn block(input: TokenStream) -> TokenStream { /// ``` /// # use ferrumc_macros::{match_block}; /// # use ferrumc_world::block_state_id::BlockStateId; -/// let block_state_id = BlockStateId(1); -/// if match_block!("stone", block_state_id) { -/// // do something +/// let block_id = BlockStateId(1); +/// assert!(match_block!("stone", block_id)); +/// for i in 581..1730 { +/// assert!(match_block!("note_block", BlockStateId(i))); /// } /// ``` /// Unfortunately, due to current limitations in Rust's proc macros, you will need to import the diff --git a/src/tests/Cargo.toml b/src/tests/Cargo.toml index ccfa87792..a707501be 100644 --- a/src/tests/Cargo.toml +++ b/src/tests/Cargo.toml @@ -16,3 +16,6 @@ maplit = { workspace = true } [lints] workspace = true + +[build-dependencies] +simd-json = { workspace = true } diff --git a/src/tests/build.rs b/src/tests/build.rs new file mode 100644 index 000000000..d25e1c6be --- /dev/null +++ b/src/tests/build.rs @@ -0,0 +1,90 @@ +use std::{ + env, + fs::File, + io::{BufWriter, Write}, + path::Path, +}; + +use simd_json::{ + self, + base::{ValueAsObject, ValueAsScalar}, + derived::ValueTryAsObject, +}; + +const JSON_FILE: &[u8] = include_bytes!("../../assets/data/blockstates.json"); + +fn main() { + let mut buf = JSON_FILE.to_owned(); + let v = simd_json::to_borrowed_value(&mut buf).unwrap(); + + let mut out = vec![]; + for (k, v) in v.try_as_object().expect("object value") { + let id = k; + let block = v.as_object().unwrap(); + let name = block.get("name").unwrap().as_str().unwrap(); + let props = block.get("properties"); + if let Some(props) = props { + out.push(( + id.parse::().unwrap(), + format!( + " assert_eq!(block!(\"{}\", {}), BlockId({}));", + name, + format_props(props), + id + ), + )); + } else { + out.push(( + id.parse::().unwrap(), + format!(" assert_eq!(block!(\"{}\"), BlockId({}));", name, id), + )); + } + } + + out.sort_by_key(|(k, _)| *k); + let out = out.into_iter().map(|(_, v)| v).collect::>(); + let path = + Path::new(&env::var("OUT_DIR").expect("OUT_DIR env varible set")).join("block_test.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + for (i, chunk) in out.chunks(40).enumerate() { + write!( + &mut file, + "#[ignore]\n#[test]\nfn all_the_blocks_{i}() {{\n{}\n}}\n\n", + chunk.join("\n") + ) + .expect("able to write to file"); + } + println!("created {}", &path.to_string_lossy()); +} + +fn format_props(props: &simd_json::BorrowedValue) -> String { + let props_str = props + .as_object() + .unwrap() + .iter() + .map(|(k, v)| { + let k_str = match k.as_ref() { + "type" => "r#type".to_string(), + _ => k.to_string(), + }; + let v_str = match v { + simd_json::BorrowedValue::Static(static_node) => match static_node { + simd_json::StaticNode::I64(v) => v.to_string(), + simd_json::StaticNode::U64(v) => v.to_string(), + simd_json::StaticNode::F64(v) => v.to_string(), + simd_json::StaticNode::Bool(v) => (match v { + true => "true", + false => "false", + }) + .to_string(), + simd_json::StaticNode::Null => "\"null\"".to_string(), + }, + simd_json::BorrowedValue::String(cow) => format!("\"{}\"", cow), + _ => unreachable!(), + }; + format!(" {}: {}", k_str, v_str) + }) + .collect::>() + .join(","); + format!("{{{}}}", props_str) +} diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs new file mode 100644 index 000000000..1fb450350 --- /dev/null +++ b/src/tests/src/block/mod.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod test { + use ferrumc_macros::block; + #[derive(Debug, PartialEq, Eq)] + struct BlockId(u32); + #[cfg(false)] + include!(concat!(env!("OUT_DIR"), "/block_test.rs")); +} diff --git a/src/tests/src/lib.rs b/src/tests/src/lib.rs index fcbce9378..bb79e2753 100644 --- a/src/tests/src/lib.rs +++ b/src/tests/src/lib.rs @@ -1,4 +1,5 @@ #![cfg(test)] -mod nbt; -mod net; +mod block; +//mod nbt; +//mod net; From faaea4a0aca389ec97587620b69abeccd909709b Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:20:24 +0500 Subject: [PATCH 03/23] add .. to allow skip missing properties this follows Struct patterns more closely --- src/lib/derive_macros/src/block/mod.rs | 148 ++++++++++++++++--------- src/tests/build.rs | 2 +- 2 files changed, 98 insertions(+), 52 deletions(-) diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index fb4265c75..0e2456243 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{braced, Error, Ident, Lit, LitStr, Result, Token}; +use syn::{braced, token::Brace, Error, Ident, Lit, LitStr, Result, Token}; pub(crate) mod matches; @@ -14,8 +14,11 @@ struct Input { } struct Opts { + _brace: Brace, pairs: Punctuated, + _dot2: Option, } + #[derive(Debug)] struct Kv { key: Ident, @@ -24,25 +27,27 @@ struct Kv { } impl Kv { - fn to_string_pair(&self) -> (String, String) { - ( - match &self.key.to_string() { - v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), - v => v.clone(), - }, - match &self.value { - Value::Static(v) => match v { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => unreachable!(), - }, - Value::Any(_) => "_".into(), - Value::Ident(v) => v.to_string(), + fn key_str(&self) -> String { + match &self.key.to_string() { + v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), + v => v.clone(), + } + } + + fn value_str(&self) -> String { + match &self.value { + Value::Static(v) => match v { + Lit::Str(v) => v.value(), + Lit::Bool(v) => v.value.to_string(), + Lit::Int(v) => v.base10_digits().to_string(), + _ => unreachable!(), }, - ) + Value::Any(_) => "_".into(), + Value::Ident(v) => v.to_string(), + } } } + #[derive(Debug)] enum Value { Static(Lit), @@ -81,9 +86,27 @@ impl Parse for Kv { impl Parse for Opts { fn parse(input: ParseStream) -> Result { let content; - braced!(content in input); + let _brace = braced!(content in input); + + let mut pairs = Punctuated::new(); + let mut _dot2 = None; + while !content.is_empty() { + if content.peek(Token![..]) { + _dot2 = Some(content.parse()?); + break; + } + pairs.push_value(content.call(Kv::parse)?); + if content.is_empty() { + break; + } + let punct: Token![,] = content.parse()?; + pairs.push_punct(punct); + } + Ok(Self { - pairs: content.parse_terminated(Kv::parse, Token![,])?, + _brace, + pairs, + _dot2, }) } } @@ -107,13 +130,23 @@ impl Parse for Input { impl quote::ToTokens for Input { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { self.name.to_tokens(tokens); + if self.opts.is_some() { + ::default().to_tokens(tokens); + } self.opts.to_tokens(tokens); } } impl quote::ToTokens for Opts { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.pairs.to_tokens(tokens); + self._brace.surround(tokens, |tokens| { + self.pairs.to_tokens(tokens); + // NOTE: We need a comma before the dot2 token if it is present. + if !self.pairs.empty_or_trailing() && self._dot2.is_some() { + ::default().to_tokens(tokens); + } + self._dot2.to_tokens(tokens); + }); } } @@ -156,15 +189,14 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { return Err(Error::new_spanned( &name, format!( - "the block `{}` is not found in the blockstates.json file (PROP_PARTS is not populated)", - name_value + "the block `{name_value}` is not found in the blockstates.json file (PROP_PARTS is not populated)" ), )); } if props.is_some_and(|x| x.is_empty()) { return Err(Error::new_spanned( &name, - format!("the block `{}` has no properties", name_value), + format!("the block `{name_value}` has no properties"), )); } let props = props.unwrap().to_vec(); @@ -172,33 +204,34 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { let opts_strings = opts .pairs .iter() - .map(Kv::to_string_pair) + .map(Kv::key_str) .zip(opts.pairs.iter()) .collect::>(); let mut unknown_props = Vec::new(); - let mut missing_props = Vec::new(); - for prop in &props { - if !opts_strings.iter().any(|((k, _), _)| k == prop) { - missing_props.push(prop.to_string()); - } - } - for ((k, _), _) in &opts_strings { + for (k, _) in &opts_strings { if !props.contains(&k.as_str()) { unknown_props.push(k.clone()); } } if !unknown_props.is_empty() { return Err(Error::new_spanned( - &name, + &opts, format!( - "the block {name_value} has no properties with names: [{}]", + "the block `{name_value}` has no properties with names: [{}]", unknown_props.join(", ") ), )); } - if !missing_props.is_empty() { + + let mut missing_props = Vec::new(); + for prop in &props { + if !opts_strings.iter().any(|(k, _)| k == prop) { + missing_props.push(prop.to_string()); + } + } + if opts._dot2.is_none() && !missing_props.is_empty() { return Err(Error::new_spanned( - name, + opts, format!( "the block `{name_value}` is missing these properties: [{}]", missing_props.join(", ") @@ -210,16 +243,18 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .get(&name_value) .expect(&format!("block name {name_value} should be present")) .to_vec(); - for ((k, v), kv) in &opts_strings { + for kv in &opts.pairs { + let k = kv.key_str(); + let v = kv.value_str(); let property_filter = match &kv.value { Value::Static(_) => BLOCK_STATES .get(&format!("{k}:{v}")) - .ok_or(Error::new_spanned( + .ok_or_else(|| Error::new_spanned( kv, format!( "the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", PROP_PARTS - .get(k) + .get(&k) .expect("the key `{k}` exists in PROP_PARTS") .join(", ") ), @@ -227,8 +262,8 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .to_vec(), Value::Any(_) => { let vs = PROP_PARTS - .get(k) - .ok_or( + .get(&k) + .ok_or_else(|| Error::new_spanned( &kv.key, format!("the property `{k}` is not present in the blockstates.json file (PROP_PARTS is not populated)") @@ -237,18 +272,10 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .iter() .map(|v| BLOCK_STATES .get(&format!("{k}:{v}")) - .ok_or( - Error::new_spanned( - kv, - format!("the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", PROP_PARTS - .get(k) - .expect(&format!("the key `{k}` exists in PROP_PARTS")).join(", ") - ) - )) - ) - .collect::>>()?; + .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + ); let mut combined_block_states = Vec::new(); - for bs in vs { + for &bs in vs { combined_block_states = combine(&combined_block_states, bs); } combined_block_states @@ -260,6 +287,25 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { block_states = intersect(&block_states, &property_filter); } + if opts._dot2.is_some() { + for k in missing_props { + let missing_block_states = PROP_PARTS + .get(&k) + .expect(&format!("the key {k} exists in PROP_PARTS")) + .iter() + .map(|v| { + BLOCK_STATES + .get(&format!("{k}:{v}")) + .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + }); + let mut combined = Vec::new(); + for &bs in missing_block_states { + combined = combine(&combined, bs); + } + block_states = intersect(&block_states, &combined); + } + } + if block_states.is_empty() { return Err(Error::new_spanned( Input { diff --git a/src/tests/build.rs b/src/tests/build.rs index d25e1c6be..7c7e64588 100644 --- a/src/tests/build.rs +++ b/src/tests/build.rs @@ -49,7 +49,7 @@ fn main() { for (i, chunk) in out.chunks(40).enumerate() { write!( &mut file, - "#[ignore]\n#[test]\nfn all_the_blocks_{i}() {{\n{}\n}}\n\n", + "#[test]\nfn all_the_blocks_{i}() {{\n{}\n}}\n\n", chunk.join("\n") ) .expect("able to write to file"); From 5490552abc65a87d55bf513cbdaf8d4383033f03 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:20:11 +0500 Subject: [PATCH 04/23] add _ in props position and use `block!` macro in worldgen crate --- src/lib/derive_macros/src/block/matches.rs | 2 +- src/lib/derive_macros/src/block/mod.rs | 246 +++++++++++---------- src/lib/world/src/vanilla_chunk_format.rs | 4 +- src/tests/build.rs | 7 +- src/tests/src/block/mod.rs | 2 +- 5 files changed, 144 insertions(+), 117 deletions(-) diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index 943d57bbb..ff70b142a 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -42,7 +42,7 @@ pub fn matches_block(input: TokenStream) -> TokenStream { let matched = quote! { match #block_id_var { - BlockId(#(#states)|*) => true, + BlockStateId(#(#states)|*) => true, _ => false } }; diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index 0e2456243..a1de38811 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -13,77 +13,62 @@ struct Input { opts: Option, } -struct Opts { - _brace: Brace, - pairs: Punctuated, - _dot2: Option, -} - -#[derive(Debug)] -struct Kv { - key: Ident, - _colon: Token![:], - value: Value, -} - -impl Kv { - fn key_str(&self) -> String { - match &self.key.to_string() { - v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), - v => v.clone(), +impl Parse for Input { + fn parse(input: ParseStream) -> Result { + let name: LitStr = input.parse()?; + let opts = if input.peek(Token![,]) { + let _comma: Token![,] = input.parse()?; + Some(input.parse::()?) + } else { + None + }; + if !input.is_empty() { + return Err(input.error("unexpected tokens after optional { ... }")); } + Ok(Self { name, opts }) } +} - fn value_str(&self) -> String { - match &self.value { - Value::Static(v) => match v { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => unreachable!(), - }, - Value::Any(_) => "_".into(), - Value::Ident(v) => v.to_string(), +impl quote::ToTokens for Input { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.name.to_tokens(tokens); + if self.opts.is_some() { + ::default().to_tokens(tokens); } + self.opts.to_tokens(tokens); } } -#[derive(Debug)] -enum Value { - Static(Lit), +enum Opts { Any(Token![_]), - Ident(Ident), + Pairs(Pairs), } -impl Parse for Value { +impl Parse for Opts { fn parse(input: ParseStream) -> Result { if input.peek(Token![_]) { return Ok(Self::Any(input.parse()?)); } - if input.peek(Ident) { - return Ok(Self::Ident(input.parse()?)); - } - if input.peek(Lit) { - let lit = input.parse()?; - if matches!(lit, Lit::Str(_) | Lit::Bool(_) | Lit::Int(_)) { - return Ok(Self::Static(lit)); - } - } - Err(input.error("the property value must be _, ident or literal string, bool or int")) + Ok(Self::Pairs(input.parse()?)) } } -impl Parse for Kv { - fn parse(input: ParseStream) -> Result { - Ok(Self { - key: input.parse()?, - _colon: input.parse()?, - value: input.parse()?, - }) +impl quote::ToTokens for Opts { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Opts::Any(underscore) => underscore.to_tokens(tokens), + Opts::Pairs(pairs) => pairs.to_tokens(tokens), + } } } -impl Parse for Opts { +struct Pairs { + _brace: Brace, + pairs: Punctuated, + _dot2: Option, +} + +impl Parse for Pairs { fn parse(input: ParseStream) -> Result { let content; let _brace = braced!(content in input); @@ -102,8 +87,7 @@ impl Parse for Opts { let punct: Token![,] = content.parse()?; pairs.push_punct(punct); } - - Ok(Self { + Ok(Pairs { _brace, pairs, _dot2, @@ -111,33 +95,7 @@ impl Parse for Opts { } } -impl Parse for Input { - fn parse(input: ParseStream) -> Result { - let name: LitStr = input.parse()?; - let opts = if input.peek(Token![,]) { - let _comma: Token![,] = input.parse()?; - Some(input.parse::()?) - } else { - None - }; - if !input.is_empty() { - return Err(input.error("unexpected tokens after optional { ... }")); - } - Ok(Self { name, opts }) - } -} - -impl quote::ToTokens for Input { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.name.to_tokens(tokens); - if self.opts.is_some() { - ::default().to_tokens(tokens); - } - self.opts.to_tokens(tokens); - } -} - -impl quote::ToTokens for Opts { +impl quote::ToTokens for Pairs { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { self._brace.surround(tokens, |tokens| { self.pairs.to_tokens(tokens); @@ -149,6 +107,43 @@ impl quote::ToTokens for Opts { }); } } +struct Kv { + key: Ident, + _colon: Token![:], + value: Value, +} + +impl Kv { + fn key_str(&self) -> String { + match &self.key.to_string() { + v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), + v => v.clone(), + } + } + + fn value_str(&self) -> String { + match &self.value { + Value::Static(v) => match v { + Lit::Str(v) => v.value(), + Lit::Bool(v) => v.value.to_string(), + Lit::Int(v) => v.base10_digits().to_string(), + _ => unreachable!(), + }, + Value::Any(_) => "_".into(), + Value::Ident(v) => v.to_string(), + } + } +} + +impl Parse for Kv { + fn parse(input: ParseStream) -> Result { + Ok(Self { + key: input.parse()?, + _colon: input.parse()?, + value: input.parse()?, + }) + } +} impl quote::ToTokens for Kv { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { @@ -158,6 +153,30 @@ impl quote::ToTokens for Kv { } } +enum Value { + Static(Lit), + Any(Token![_]), + Ident(Ident), +} + +impl Parse for Value { + fn parse(input: ParseStream) -> Result { + if input.peek(Token![_]) { + return Ok(Self::Any(input.parse()?)); + } + if input.peek(Ident) { + return Ok(Self::Ident(input.parse()?)); + } + if input.peek(Lit) { + let lit = input.parse()?; + if matches!(lit, Lit::Str(_) | Lit::Bool(_) | Lit::Int(_)) { + return Ok(Self::Static(lit)); + } + } + Err(input.error("the property value must be _, ident or literal string, bool or int")) + } +} + impl quote::ToTokens for Value { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { match self { @@ -173,8 +192,12 @@ pub fn block(input: TokenStream) -> TokenStream { Input { name, opts: None } => static_block(name), Input { name, - opts: Some(opts), - } => block_with_props(name, opts), + opts: Some(Opts::Pairs(pairs)), + } => block_with_props(name, pairs), + Input { + name, + opts: Some(Opts::Any(_)), + } => block_with_any_props(name), }; match out { Ok(v) => v, @@ -182,24 +205,22 @@ pub fn block(input: TokenStream) -> TokenStream { } } -fn block_with_props(name: LitStr, opts: Opts) -> Result { +fn block_with_props(name: LitStr, opts: Pairs) -> Result { let name_value = parse_name(&name); - let props = PROP_PARTS.get(&name_value); - if props.is_none() { - return Err(Error::new_spanned( + let props = PROP_PARTS.get(&name_value).ok_or_else(|| + Error::new_spanned( &name, format!( "the block `{name_value}` is not found in the blockstates.json file (PROP_PARTS is not populated)" - ), - )); - } - if props.is_some_and(|x| x.is_empty()) { + )))?; + + if props.is_empty() { return Err(Error::new_spanned( &name, format!("the block `{name_value}` has no properties"), )); } - let props = props.unwrap().to_vec(); + let props = props.to_vec(); let opts_strings = opts .pairs @@ -241,7 +262,7 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { let mut block_states = BLOCK_STATES .get(&name_value) - .expect(&format!("block name {name_value} should be present")) + .unwrap_or_else(|| panic!("block name {name_value} should be present")) .to_vec(); for kv in &opts.pairs { let k = kv.key_str(); @@ -272,7 +293,7 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .iter() .map(|v| BLOCK_STATES .get(&format!("{k}:{v}")) - .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + .unwrap_or_else(|| panic!("the key {k}:{v} exists in BLOCK_STATES")) ); let mut combined_block_states = Vec::new(); for &bs in vs { @@ -291,12 +312,12 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { for k in missing_props { let missing_block_states = PROP_PARTS .get(&k) - .expect(&format!("the key {k} exists in PROP_PARTS")) + .unwrap_or_else(|| panic!("the key {k} exists in PROP_PARTS")) .iter() .map(|v| { BLOCK_STATES .get(&format!("{k}:{v}")) - .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + .unwrap_or_else(|| panic!("the key {k}:{v} exists in BLOCK_STATES")) }); let mut combined = Vec::new(); for &bs in missing_block_states { @@ -310,28 +331,34 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { return Err(Error::new_spanned( Input { name, - opts: Some(opts), + opts: Some(Opts::Pairs(opts)), }, "no block state corresponds to this combination of properties", )); } let block_ids = block_states.iter().map(|x| *x as u32); - Ok(quote! {BlockId(#(#block_ids)|*)}.into()) + Ok(quote! {BlockStateId(#(#block_ids)|*)}.into()) } +fn block_with_any_props(name: LitStr) -> Result { + let name_value = parse_name(&name); + let block_ids = BLOCK_STATES + .get(&name_value) + .filter(|&&x| !x.is_empty()) + .ok_or_else(|| Error::new_spanned(&name, format!("the block `{name_value}` is not found in blockstates.json (BLOCK_STATES is not populated)")))? + .iter() + .map(|&x| x as u32); + Ok(quote! {BlockStateId(#(#block_ids)|*)}.into()) +} fn static_block(name: LitStr) -> Result { let name_value = parse_name(&name); - let props = PROP_PARTS.get(&name_value); - if props.is_none() { - return Err(Error::new_spanned( + let &props = PROP_PARTS.get(&name_value).ok_or_else(|| Error::new_spanned( &name, format!( "the block `{name_value}` not found in blockstates.json (PROP_PARTS is not populated)", ), - )); - } - if props.is_some_and(|x| !x.is_empty()) { - let &props = props.unwrap(); + ))?; + if !props.is_empty() { return Err(Error::new_spanned( &name, format!( @@ -340,16 +367,13 @@ fn static_block(name: LitStr) -> Result { ), )); } - let block_states = BLOCK_STATES.get(&name_value); - if block_states.is_none_or(|x| x.is_empty()) { - return Err(Error::new_spanned( + let &block_states = BLOCK_STATES.get(&name_value).filter(|&&x| !x.is_empty()).ok_or_else(|| Error::new_spanned( &name, format!( "the block `{name_value}` not found in the blockstates.json file (BLOCK_STATE is not populated)", ), - )); - } - let block_states = block_states.unwrap(); + ))?; + if block_states.len() > 1 { return Err(Error::new_spanned( name, @@ -365,7 +389,7 @@ fn static_block(name: LitStr) -> Result { } let id = block_states[0] as u32; - Ok(quote! { BlockId(#id) }.into()) + Ok(quote! { BlockStateId(#id) }.into()) } fn parse_name(name: &LitStr) -> String { diff --git a/src/lib/world/src/vanilla_chunk_format.rs b/src/lib/world/src/vanilla_chunk_format.rs index 95b01f95c..b35ad197a 100644 --- a/src/lib/world/src/vanilla_chunk_format.rs +++ b/src/lib/world/src/vanilla_chunk_format.rs @@ -100,9 +100,9 @@ pub(crate) struct BlockStates { /// Information about a block's name and properties. /// -/// This should be used sparingly, as it's much more efficient to use [BlockId] where possible. +/// This should be used sparingly, as it's much more efficient to use [BlockStateId] where possible. /// -/// If you want to use it as a literal and the convert to a BlockId, use the [ferrumc_macros::block_data!] macro. +/// If you want to use it as a literal and the convert to a [[[BlockStateId], use the [ferrumc_macros::block_data!] macro. #[apply(ChunkDerives)] #[derive(deepsize::DeepSizeOf, Hash)] pub struct BlockData { diff --git a/src/tests/build.rs b/src/tests/build.rs index 7c7e64588..76b98342d 100644 --- a/src/tests/build.rs +++ b/src/tests/build.rs @@ -27,7 +27,7 @@ fn main() { out.push(( id.parse::().unwrap(), format!( - " assert_eq!(block!(\"{}\", {}), BlockId({}));", + " assert_eq!(block!(\"{}\", {}), BlockStateId({}));", name, format_props(props), id @@ -36,7 +36,10 @@ fn main() { } else { out.push(( id.parse::().unwrap(), - format!(" assert_eq!(block!(\"{}\"), BlockId({}));", name, id), + format!( + " assert_eq!(block!(\"{}\"), BlockStateId({}));", + name, id + ), )); } } diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs index 1fb450350..9dd04e878 100644 --- a/src/tests/src/block/mod.rs +++ b/src/tests/src/block/mod.rs @@ -2,7 +2,7 @@ mod test { use ferrumc_macros::block; #[derive(Debug, PartialEq, Eq)] - struct BlockId(u32); + struct BlockStateId(u32); #[cfg(false)] include!(concat!(env!("OUT_DIR"), "/block_test.rs")); } From 39ceafc43a2b2e6114630b50c1b326b9a297d93a Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:51:08 +0500 Subject: [PATCH 05/23] updete phf dependency updates phf to 0.13 and makes block makro use workspace version --- Cargo.toml | 4 ++-- src/lib/derive_macros/Cargo.toml | 4 ++-- src/lib/registry/build.rs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8fec320b5..a17a64478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,5 +240,5 @@ criterion = { version = "0.7.0", features = ["html_reports"] } bitcode = { git = "https://github.com/SoftbearStudios/bitcode" } bitcode_derive = { git = "https://github.com/SoftbearStudios/bitcode" } -phf = { version = "0.11", features = ["macros"] } -phf_codegen = { version = "0.11" } +phf = { version = "0.13", features = ["macros"] } +phf_codegen = { version = "0.13" } diff --git a/src/lib/derive_macros/Cargo.toml b/src/lib/derive_macros/Cargo.toml index 80514fffd..5c13e8366 100644 --- a/src/lib/derive_macros/Cargo.toml +++ b/src/lib/derive_macros/Cargo.toml @@ -22,11 +22,11 @@ craftflow-nbt = { workspace = true } indexmap = { workspace = true, features = ["serde"] } bitcode = { workspace = true } simd-json = { workspace = true } -phf = "0.13.1" +phf = { workspace = true } [dev-dependencies] ferrumc-world = { workspace = true } [build-dependencies] -phf_codegen = "0.13.1" +phf_codegen = { workspace = true } simd-json = { workspace = true } diff --git a/src/lib/registry/build.rs b/src/lib/registry/build.rs index f99b54802..5acabd3e7 100644 --- a/src/lib/registry/build.rs +++ b/src/lib/registry/build.rs @@ -76,7 +76,7 @@ fn main() { let mut item_map = Map::new(); for (name, entry) in ®istry.item.entries { // Use & to borrow - item_map.entry(name, &entry.protocol_id.to_string()); + item_map.entry(name, entry.protocol_id.to_string()); } writeln!(file, "{};\n", item_map.build()).unwrap(); @@ -102,7 +102,7 @@ fn main() { let mut bs_map = Map::new(); for (id, data) in blockstates { // Use `entry` to build a valid raw string literal for the value - bs_map.entry(id, &format!("r#\"{}\"#", data.name)); + bs_map.entry(id, format!("r#\"{}\"#", data.name)); } writeln!(file, "{};\n", bs_map.build()).unwrap(); @@ -114,7 +114,7 @@ fn main() { .unwrap(); let mut i2b_map = Map::new(); for (item_id_str, block_state_id_str) in item_to_block { - i2b_map.entry(item_id_str, &format!("r#\"{}\"#", block_state_id_str)); + i2b_map.entry(item_id_str, format!("r#\"{}\"#", block_state_id_str)); } writeln!(file, "{};\n", i2b_map.build()).unwrap(); @@ -129,7 +129,7 @@ fn main() { // (PHF maps don't like f32). We'll convert it back at runtime. let hardness_f32 = entry.hardness.unwrap_or(0.0); let hardness_u32 = hardness_f32.to_bits(); - hardness_map.entry(name, &hardness_u32.to_string()); + hardness_map.entry(name, hardness_u32.to_string()); } writeln!(file, "{};\n", hardness_map.build()).unwrap(); } From 6f3a75a9dc56ea8a6bf9f423791ce121116f1aba Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:51:08 +0500 Subject: [PATCH 06/23] updete phf dependency updates phf to 0.13 and makes block makro use workspace version --- src/lib/registry/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/registry/build.rs b/src/lib/registry/build.rs index 5acabd3e7..b08d49ef4 100644 --- a/src/lib/registry/build.rs +++ b/src/lib/registry/build.rs @@ -89,7 +89,7 @@ fn main() { let mut item_id_map = Map::new(); for (name, entry) in ®istry.item.entries { // Here, the key is the i32 protocol_id - item_id_map.entry(entry.protocol_id, &format!("r#\"{}\"#", name)); + item_id_map.entry(entry.protocol_id, format!("r#\"{}\"#", name)); } writeln!(file, "{};\n", item_id_map.build()).unwrap(); From 1ea558ebafcbcfdbbb1b7114def3b1dbc20e988b Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:04:56 +0500 Subject: [PATCH 07/23] unused import --- src/tests/src/block/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs index 9dd04e878..1a1b40ef2 100644 --- a/src/tests/src/block/mod.rs +++ b/src/tests/src/block/mod.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test { use ferrumc_macros::block; + #[expect(unused_imports)] #[derive(Debug, PartialEq, Eq)] struct BlockStateId(u32); #[cfg(false)] From 1e1e271976736e222531fd9fcc254fdbea38b7bc Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:08:15 +0500 Subject: [PATCH 08/23] remove skipped generated bock! tests --- src/tests/src/block/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs index 1a1b40ef2..4fa71fabf 100644 --- a/src/tests/src/block/mod.rs +++ b/src/tests/src/block/mod.rs @@ -1,7 +1,6 @@ -#[cfg(test)] +#[cfg(false)] mod test { use ferrumc_macros::block; - #[expect(unused_imports)] #[derive(Debug, PartialEq, Eq)] struct BlockStateId(u32); #[cfg(false)] From 967de82347bf42b7b436008ee674a2398c7f9c76 Mon Sep 17 00:00:00 2001 From: Martin <66840021+Mcrtin@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:35:53 +0100 Subject: [PATCH 09/23] introduce Position structs for the Chunk impl (#284) * introduce Position structs for the Chunk impl # Conflicts: # src/lib/world/src/edits.rs # Conflicts: # src/bin/src/main.rs # src/lib/net/src/conn_init/login.rs * add section pos * fix doc tests * add a comment and remove unnecessary parentheses * protect BlockStateId * add min_y field in chunk * remove y from section * refactor: move code from Chunk to Section * make edit_batch more ownership friendly * fix clippy * protect Pos fields # Conflicts: # src/bin/src/main.rs * fix: fix world gen in neg chunks * make chunk gen use ChunkPos * fix chunk pos * correctly pack chunkpos * fix --- src/bin/src/launch.rs | 6 +- src/bin/src/main.rs | 7 +- .../play_packets/pick_item_from_block.rs | 5 +- .../play_packets/place_block.rs | 79 ++--- .../play_packets/player_action.rs | 25 +- .../play_packets/player_loaded.rs | 8 +- src/bin/src/systems/chunk_sending.rs | 9 +- .../src/systems/listeners/digging_system.rs | 65 ++-- src/lib/derive_macros/src/block/matches.rs | 2 +- src/lib/derive_macros/src/block/mod.rs | 4 +- src/lib/derive_macros/src/lib.rs | 8 +- src/lib/inventories/src/item.rs | 6 +- src/lib/net/benches/packets.rs | 10 +- src/lib/net/src/conn_init/login.rs | 6 +- .../packets/outgoing/chunk_and_light_data.rs | 15 +- src/lib/world/Cargo.toml | 1 + src/lib/world/src/benches/cache.rs | 45 ++- src/lib/world/src/benches/edit_bench.rs | 46 +-- src/lib/world/src/block_state_id.rs | 14 +- src/lib/world/src/chunk_format.rs | 84 +++-- src/lib/world/src/db_functions.rs | 97 +++--- src/lib/world/src/edit_batch.rs | 88 ++--- src/lib/world/src/edits.rs | 229 +++++-------- src/lib/world/src/errors.rs | 2 +- src/lib/world/src/importing.rs | 8 +- src/lib/world/src/lib.rs | 6 +- src/lib/world/src/pos.rs | 301 ++++++++++++++++++ src/lib/world_gen/src/biomes/plains.rs | 53 +-- src/lib/world_gen/src/lib.rs | 18 +- 29 files changed, 768 insertions(+), 479 deletions(-) create mode 100644 src/lib/world/src/pos.rs diff --git a/src/bin/src/launch.rs b/src/bin/src/launch.rs index 4452c5956..d3800dc61 100644 --- a/src/bin/src/launch.rs +++ b/src/bin/src/launch.rs @@ -8,6 +8,7 @@ use ferrumc_state::player_cache::PlayerCache; use ferrumc_state::player_list::PlayerList; use ferrumc_state::{GlobalState, ServerState}; use ferrumc_threadpool::ThreadPool; +use ferrumc_world::pos::ChunkPos; use ferrumc_world::World; use ferrumc_world_gen::WorldGenerator; use std::sync::Arc; @@ -44,14 +45,15 @@ pub fn generate_spawn_chunks(state: GlobalState) -> Result<(), BinaryError> { for (x, z) in chunks { let state_clone = state.clone(); batch.execute(move || { + let pos = ChunkPos::new(x, z); let chunk = state_clone .terrain_generator - .generate_chunk(x, z) + .generate_chunk(pos) .map(Arc::new); match chunk { Ok(chunk) => { - if let Err(e) = state_clone.world.save_chunk(chunk) { + if let Err(e) = state_clone.world.save_chunk(pos, "overworld", chunk) { error!("Error saving chunk ({}, {}): {:?}", x, z, e); } } diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 13edcb222..63046d61d 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -4,6 +4,7 @@ use crate::cli::{CLIArgs, Command}; use crate::errors::BinaryError; use clap::Parser; use ferrumc_config::whitelist::create_whitelist; +use ferrumc_world::pos::ChunkPos; use std::sync::Arc; use std::time::Instant; use tracing::{error, info}; @@ -72,8 +73,10 @@ fn entry(start_time: Instant) -> Result<(), BinaryError> { let global_state = Arc::new(state); create_whitelist(); - - if !global_state.world.chunk_exists(0, 0, "overworld")? { + if !global_state + .world + .chunk_exists(ChunkPos::new(0, 0), "overworld")? + { launch::generate_spawn_chunks(global_state.clone())?; } diff --git a/src/bin/src/packet_handlers/play_packets/pick_item_from_block.rs b/src/bin/src/packet_handlers/play_packets/pick_item_from_block.rs index aadd87457..0cc30a933 100644 --- a/src/bin/src/packet_handlers/play_packets/pick_item_from_block.rs +++ b/src/bin/src/packet_handlers/play_packets/pick_item_from_block.rs @@ -43,10 +43,9 @@ pub fn handle( ); // 2. Get block from world + let pos = packet.location.clone().into(); let block_state_id = match state.0.world.get_block_and_fetch( - packet.location.x, - packet.location.y as i32, - packet.location.z, + pos, "overworld", // TODO: Remove overworld hard coding for the dimension ) { Ok(id) => id, diff --git a/src/bin/src/packet_handlers/play_packets/place_block.rs b/src/bin/src/packet_handlers/play_packets/place_block.rs index d19f9cf81..25a438d39 100644 --- a/src/bin/src/packet_handlers/play_packets/place_block.rs +++ b/src/bin/src/packet_handlers/play_packets/place_block.rs @@ -10,6 +10,7 @@ use ferrumc_net::PlaceBlockReceiver; use ferrumc_net_codec::net_types::network_position::NetworkPosition; use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_state::GlobalStateResource; +use ferrumc_world::pos::BlockPos; use tracing::{debug, error, trace}; use ferrumc_inventories::hotbar::Hotbar; @@ -21,12 +22,17 @@ use std::str::FromStr; const ITEM_TO_BLOCK_MAPPING_FILE: &str = include_str!("../../../../../assets/data/item_to_block_mapping.json"); -static ITEM_TO_BLOCK_MAPPING: Lazy> = Lazy::new(|| { +static ITEM_TO_BLOCK_MAPPING: Lazy> = Lazy::new(|| { let str_form: HashMap = serde_json::from_str(ITEM_TO_BLOCK_MAPPING_FILE) .expect("Failed to parse item_to_block_mapping.json"); str_form .into_iter() - .map(|(k, v)| (i32::from_str(&k).unwrap(), i32::from_str(&v).unwrap())) + .map(|(k, v)| { + ( + i32::from_str(&k).unwrap(), + BlockStateId::new(u32::from_str(&v).unwrap()), + ) + }) .collect() }); @@ -65,41 +71,31 @@ pub fn handle( "Placing block with item ID: {}, mapped to block state ID: {}", item_id.0, mapped_block_state_id ); - let mut chunk = match state.0.world.load_chunk_owned( - event.position.x >> 4, - event.position.z >> 4, - "overworld", - ) { + let pos: BlockPos = event.position.into(); + let mut chunk = match state.0.world.load_chunk_owned(pos.chunk(), "overworld") { Ok(chunk) => chunk, Err(e) => { debug!("Failed to load chunk: {:?}", e); continue 'ev_loop; } }; - let Ok(block_clicked) = chunk.get_block( - event.position.x, - event.position.y as i32, - event.position.z, - ) else { - debug!("Failed to get block at position: {:?}", event.position); + let Ok(block_clicked) = chunk.get_block(pos.chunk_block_pos()) else { + debug!("Failed to get block at position: {}", pos); continue 'ev_loop; }; trace!("Block clicked: {:?}", block_clicked); // Use the face to determine the offset of the block to place - let (x_block_offset, y_block_offset, z_block_offset) = match event.face.0 { - 0 => (0, -1, 0), - 1 => (0, 1, 0), - 2 => (0, 0, -1), - 3 => (0, 0, 1), - 4 => (-1, 0, 0), - 5 => (1, 0, 0), - _ => (0, 0, 0), - }; - let (x, y, z) = ( - event.position.x + x_block_offset, - event.position.y + y_block_offset, - event.position.z + z_block_offset, - ); + let offset_pos = pos + + match event.face.0 { + 0 => (0, -1, 0), + 1 => (0, 1, 0), + 2 => (0, 0, -1), + 3 => (0, 0, 1), + 4 => (-1, 0, 0), + 5 => (1, 0, 0), + _ => (0, 0, 0), + }; + // Check if the block collides with any entities let does_collide = { pos_q.into_iter().any(|(pos, bounds)| { @@ -113,7 +109,11 @@ pub fn handle( z_offset_start: 0.0, z_offset_end: 1.0, }, - (x as f64, y as f64, z as f64), + ( + offset_pos.pos.x as f64, + offset_pos.pos.y as f64, + offset_pos.pos.z as f64, + ), ) }) }; @@ -129,12 +129,8 @@ pub fn handle( continue 'ev_loop; } - if let Err(err) = chunk.set_block( - x & 0xF, - y as i32, - z & 0xF, - BlockStateId(*mapped_block_state_id as u32), - ) { + if let Err(err) = chunk.set_block(pos.chunk_block_pos(), *mapped_block_state_id) + { error!("Failed to set block: {:?}", err); continue 'ev_loop; } @@ -143,7 +139,11 @@ pub fn handle( }; let chunk_packet = BlockUpdate { - location: NetworkPosition { x, y, z }, + location: NetworkPosition { + x: offset_pos.pos.x, + y: offset_pos.pos.y as i16, + z: offset_pos.pos.z, + }, block_state_id: VarInt::from(*mapped_block_state_id), }; if let Err(err) = conn.send_packet_ref(&chunk_packet) { @@ -155,10 +155,15 @@ pub fn handle( continue 'ev_loop; } - if let Err(err) = state.0.world.save_chunk(Arc::from(chunk)) { + if let Err(err) = + state + .0 + .world + .save_chunk(pos.chunk(), "overworld", Arc::from(chunk)) + { error!("Failed to save chunk after block placement: {:?}", err); } else { - trace!("Block placed at ({}, {}, {})", x, y, z); + trace!("Block placed at {}", offset_pos); } } } diff --git a/src/bin/src/packet_handlers/play_packets/player_action.rs b/src/bin/src/packet_handlers/play_packets/player_action.rs index ec3d63e31..d6b5449b6 100644 --- a/src/bin/src/packet_handlers/play_packets/player_action.rs +++ b/src/bin/src/packet_handlers/play_packets/player_action.rs @@ -11,7 +11,7 @@ use ferrumc_net::packets::outgoing::block_update::BlockUpdate; use ferrumc_net::PlayerActionReceiver; use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_state::GlobalStateResource; -use ferrumc_world::block_state_id::BlockStateId; +use ferrumc_world::{block_state_id::BlockStateId, pos::BlockPos}; use tracing::{error, trace, warn}; pub fn handle( @@ -34,16 +34,18 @@ pub fn handle( continue; }; + let pos: BlockPos = event.location.clone().into(); if abilities.creative_mode { // --- CREATIVE MODE LOGIC --- // Only instabreak (status 0) is relevant in creative. if event.status.0 == 0 { let res: Result<(), BinaryError> = try { - let mut chunk = match state.0.clone().world.load_chunk_owned( - event.location.x >> 4, - event.location.z >> 4, - "overworld", - ) { + let mut chunk = match state + .0 + .clone() + .world + .load_chunk_owned(pos.chunk(), "overworld") + { Ok(chunk) => chunk, Err(e) => { trace!("Chunk not found, generating new chunk: {:?}", e); @@ -51,23 +53,18 @@ pub fn handle( .0 .clone() .terrain_generator - .generate_chunk(event.location.x >> 4, event.location.z >> 4) + .generate_chunk(pos.chunk()) .map_err(BinaryError::WorldGen)? } }; - let (relative_x, relative_y, relative_z) = ( - event.location.x.abs() % 16, - event.location.y as i32, - event.location.z.abs() % 16, - ); chunk - .set_block(relative_x, relative_y, relative_z, BlockStateId::default()) + .set_block(pos.chunk_block_pos(), BlockStateId::default()) .map_err(BinaryError::World)?; state .0 .world - .save_chunk(Arc::new(chunk)) + .save_chunk(pos.chunk(), "overworld", Arc::new(chunk)) .map_err(BinaryError::World)?; // Broadcast the change diff --git a/src/bin/src/packet_handlers/play_packets/player_loaded.rs b/src/bin/src/packet_handlers/play_packets/player_loaded.rs index f70ff51c5..f19f058db 100644 --- a/src/bin/src/packet_handlers/play_packets/player_loaded.rs +++ b/src/bin/src/packet_handlers/play_packets/player_loaded.rs @@ -1,10 +1,12 @@ use bevy_ecs::prelude::{Entity, Query, Res}; use ferrumc_core::transform::position::Position; +use ferrumc_macros::block; use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::synchronize_player_position::SynchronizePlayerPositionPacket; use ferrumc_net::PlayerLoadedReceiver; use ferrumc_state::GlobalStateResource; use ferrumc_world::block_state_id::BlockStateId; +use ferrumc_world::pos::BlockPos; use tracing::warn; pub fn handle( @@ -24,14 +26,14 @@ pub fn handle( ); continue; } - let head_block = state.0.world.get_block_and_fetch( + let pos = BlockPos::of( player_pos.x as i32, player_pos.y as i32, player_pos.z as i32, - "overworld", ); + let head_block = state.0.world.get_block_and_fetch(pos, "overworld"); if let Ok(head_block) = head_block { - if head_block == BlockStateId(0) { + if head_block == block!("air") { tracing::info!( "Player {} loaded at position: ({}, {}, {})", player, diff --git a/src/bin/src/systems/chunk_sending.rs b/src/bin/src/systems/chunk_sending.rs index 2e09f0f24..3afcd1892 100644 --- a/src/bin/src/systems/chunk_sending.rs +++ b/src/bin/src/systems/chunk_sending.rs @@ -11,6 +11,7 @@ use ferrumc_net::packets::outgoing::chunk_batch_start::ChunkBatchStart; use ferrumc_net::packets::outgoing::set_center_chunk::SetCenterChunk; use ferrumc_net_codec::encode::NetEncodeOpts; use ferrumc_state::GlobalStateResource; +use ferrumc_world::pos::ChunkPos; use std::sync::atomic::Ordering; // Just take the needed chunks from the ChunkReceiver and send them @@ -68,7 +69,7 @@ pub fn handle( }) .expect("Failed to send SetCenterChunk"); - for coordinates in needed_chunks { + for coordinates in needed_chunks.into_iter().map(|c| ChunkPos::new(c.0, c.1)) { let state = state.clone(); let is_compressed = conn.compress.load(Ordering::Relaxed); batch.execute({ @@ -76,16 +77,16 @@ pub fn handle( let chunk = state .0 .world - .load_chunk(coordinates.0, coordinates.1, "overworld") + .load_chunk(coordinates, "overworld") .unwrap_or( state .0 .terrain_generator - .generate_chunk(coordinates.0, coordinates.1) + .generate_chunk(coordinates) .expect("Could not generate chunk") .into(), ); - let packet = ChunkAndLightData::from_chunk(&chunk) + let packet = ChunkAndLightData::from_chunk(coordinates, &chunk) .expect("Failed to create ChunkAndLightData"); compress_packet( &packet, diff --git a/src/bin/src/systems/listeners/digging_system.rs b/src/bin/src/systems/listeners/digging_system.rs index e6aba06a1..99a06a650 100644 --- a/src/bin/src/systems/listeners/digging_system.rs +++ b/src/bin/src/systems/listeners/digging_system.rs @@ -1,4 +1,5 @@ use bevy_ecs::prelude::*; +use ferrumc_world::pos::BlockPos; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -32,10 +33,9 @@ pub fn handle_start_digging( ); // --- 1. Get BlockStateId from the world --- + let pos = event.position.clone().into(); let block_state_id = match state.0.world.get_block_and_fetch( - event.position.x, - event.position.y as i32, - event.position.z, + pos, "overworld", // TODO: remove hardcoded dimension ) { Ok(id) => id, @@ -56,13 +56,11 @@ pub fn handle_start_digging( }; // --- 3. Get Hardness --- - let block_id_u32 = block_state_id.0; - // Get Hardness directly using the ID - let Some(block_data) = Block::by_id(block_id_u32) else { + let Some(block_data) = Block::by_id(block_state_id.raw()) else { warn!( "Could not find block data for BlockStateId: {}", - block_id_u32 + block_state_id ); continue; }; @@ -216,12 +214,8 @@ pub fn handle_finish_digging( digging.break_time.as_millis() ); - let real_block_state = match state.0.world.get_block_and_fetch( - event.position.x, - event.position.y as i32, - event.position.z, - "overworld", - ) { + let pos = event.position.clone().into(); + let real_block_state = match state.0.world.get_block_and_fetch(pos, "overworld") { Ok(id) => id, Err(e) => { error!( @@ -277,36 +271,31 @@ fn break_block( broadcast_query: &Query<(Entity, &StreamWriter)>, position: &ferrumc_net_codec::net_types::network_position::NetworkPosition, ) -> Result<(), BinaryError> { - let mut chunk = - match state - .0 - .clone() - .world - .load_chunk_owned(position.x >> 4, position.z >> 4, "overworld") - { - Ok(chunk) => chunk, - Err(e) => { - trace!("Chunk not found, generating new chunk: {:?}", e); - state - .0 - .clone() - .terrain_generator - .generate_chunk(position.x >> 4, position.z >> 4) - .map_err(BinaryError::WorldGen)? - } - }; - let (relative_x, relative_y, relative_z) = ( - position.x.abs() % 16, - position.y as i32, - position.z.abs() % 16, - ); + let pos: BlockPos = position.clone().into(); + let mut chunk = match state + .0 + .clone() + .world + .load_chunk_owned(pos.chunk(), "overworld") + { + Ok(chunk) => chunk, + Err(e) => { + trace!("Chunk not found, generating new chunk: {:?}", e); + state + .0 + .clone() + .terrain_generator + .generate_chunk(pos.chunk()) + .map_err(BinaryError::WorldGen)? + } + }; chunk - .set_block(relative_x, relative_y, relative_z, BlockStateId::default()) + .set_block(pos.chunk_block_pos(), BlockStateId::default()) .map_err(BinaryError::World)?; state .0 .world - .save_chunk(Arc::new(chunk)) + .save_chunk(pos.chunk(), "overworld", Arc::new(chunk)) .map_err(BinaryError::World)?; // Broadcast the block break to all players diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index 9f8f3168e..07fe3b392 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -52,7 +52,7 @@ pub fn matches_block(input: TokenStream) -> TokenStream { let mut arms = Vec::new(); for (id, _) in filtered_names { arms.push(quote! { - #block_state_id_var == BlockStateId(#id) + #block_state_id_var == BlockStateId::new(#id) }); } let joined = quote! { diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index 610531fb2..f11093b2d 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -102,7 +102,7 @@ pub fn block(input: TokenStream) -> TokenStream { .into(); } let first = filtered_names.first().unwrap().0; - return quote! { BlockStateId(#first) }.into(); + return quote! { BlockStateId::new(#first) }.into(); }; let props = opts @@ -195,7 +195,7 @@ pub fn block(input: TokenStream) -> TokenStream { } let res = matched[0]; - quote! { BlockStateId(#res) }.into() + quote! { BlockStateId::new(#res) }.into() } fn get_properties(filtered_names: &[(u32, &OwnedValue)]) -> Vec<(String, String)> { diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index bee92b183..ee9903925 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -109,8 +109,8 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream { /// # use ferrumc_macros::block; /// let block_state_id = block!("stone"); /// let another_block_state_id = block!("minecraft:grass_block", {snowy: true}); -/// assert_eq!(block_state_id, BlockStateId(1)); -/// assert_eq!(another_block_state_id, BlockStateId(8)); +/// assert_eq!(block_state_id, BlockStateId::new(1)); +/// assert_eq!(another_block_state_id, BlockStateId::new(8)); /// ``` /// Unfortunately, due to current limitations in Rust's proc macros, you will need to import the /// `BlockStateId` struct manually. @@ -127,9 +127,9 @@ pub fn block(input: TokenStream) -> TokenStream { /// A macro to check if a block state ID matches a given block name at compile time. /// Usage: /// ``` -/// # use ferrumc_macros::{match_block}; +/// # use ferrumc_macros::{match_block, block}; /// # use ferrumc_world::block_state_id::BlockStateId; -/// let block_state_id = BlockStateId(1); +/// let block_state_id = block!("stone"); /// if match_block!("stone", block_state_id) { /// // do something /// } diff --git a/src/lib/inventories/src/item.rs b/src/lib/inventories/src/item.rs index 65bc229b1..5b4611f86 100644 --- a/src/lib/inventories/src/item.rs +++ b/src/lib/inventories/src/item.rs @@ -138,7 +138,7 @@ mod tests { #[test] fn test_item_id_from_block_state_stone() { // BlockStateId(1) is "minecraft:stone" - let block_state_id = BlockStateId(1); + let block_state_id = BlockStateId::new(1); let item_id = ItemID::from_block_state(block_state_id); assert!( @@ -159,7 +159,7 @@ mod tests { #[test] fn test_item_id_from_block_state_grass() { // BlockStateId(9) is "minecraft:grass_block" with snowy=false - let block_state_id = BlockStateId(9); + let block_state_id = BlockStateId::new(9); let item_id = ItemID::from_block_state(block_state_id); assert!( @@ -180,7 +180,7 @@ mod tests { #[test] fn test_item_id_from_block_state_air() { // BlockStateId(0) is "minecraft:air" - let block_state_id = BlockStateId(0); + let block_state_id = BlockStateId::new(0); let item_id = ItemID::from_block_state(block_state_id); assert!( diff --git a/src/lib/net/benches/packets.rs b/src/lib/net/benches/packets.rs index 1362f25e4..626e7a1fe 100644 --- a/src/lib/net/benches/packets.rs +++ b/src/lib/net/benches/packets.rs @@ -1,5 +1,6 @@ use criterion::measurement::WallTime; use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts}; +use ferrumc_world::pos::ChunkPos; use std::hint::black_box; pub fn bench_packets(c: &mut criterion::BenchmarkGroup) { @@ -8,11 +9,14 @@ pub fn bench_packets(c: &mut criterion::BenchmarkGroup) { fn bench_chunk_packet(c: &mut criterion::BenchmarkGroup) { let chunk = ferrumc_world_gen::WorldGenerator::new(0) - .generate_chunk(0, 0) + .generate_chunk(ChunkPos::new(0, 0)) .unwrap(); let chunk_packet = black_box( - ferrumc_net::packets::outgoing::chunk_and_light_data::ChunkAndLightData::from_chunk(&chunk) - .unwrap(), + ferrumc_net::packets::outgoing::chunk_and_light_data::ChunkAndLightData::from_chunk( + ChunkPos::new(0, 0), + &chunk, + ) + .unwrap(), ); c.bench_function("chunk_and_light_data", |b| { b.iter(|| { diff --git a/src/lib/net/src/conn_init/login.rs b/src/lib/net/src/conn_init/login.rs index c80713edc..bc15e49e4 100644 --- a/src/lib/net/src/conn_init/login.rs +++ b/src/lib/net/src/conn_init/login.rs @@ -22,6 +22,7 @@ use ferrumc_net_encryption::errors::NetEncryptionError; use ferrumc_net_encryption::get_encryption_keys; use ferrumc_net_encryption::read::EncryptedReader; use ferrumc_state::GlobalState; +use ferrumc_world::pos::ChunkPos; use crate::packets::incoming::ack_finish_configuration::AckFinishConfigurationPacket; use crate::packets::incoming::client_information::ClientInformation; @@ -468,15 +469,16 @@ fn send_initial_chunks( batch.execute({ let state = state.clone(); move || -> Result, NetError> { - let chunk = state.world.load_chunk(x, z, "overworld").unwrap_or( + let chunk = state.world.load_chunk(ChunkPos::new(x,z), "overworld").unwrap_or( state .terrain_generator - .generate_chunk(x, z) + .generate_chunk(ChunkPos::new(x, z)) .expect("Could not generate chunk") .into(), ); let chunk_data = crate::packets::outgoing::chunk_and_light_data::ChunkAndLightData::from_chunk( + ChunkPos::new(x,z), &chunk, )?; compress_packet(&chunk_data, compressed, &NetEncodeOpts::WithLength, 64) diff --git a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs index b7bf647b2..39761e211 100644 --- a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs +++ b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs @@ -6,6 +6,7 @@ use ferrumc_net_codec::net_types::byte_array::ByteArray; use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_world::chunk_format::{Chunk, PaletteType}; +use ferrumc_world::pos::ChunkPos; use std::io::Cursor; use std::ops::Not; use tracing::warn; @@ -71,15 +72,15 @@ impl ChunkAndLightData { } } - pub fn from_chunk(chunk: &Chunk) -> Result { + pub fn from_chunk(pos: ChunkPos, chunk: &Chunk) -> Result { let mut raw_data = Cursor::new(Vec::new()); let mut sky_light_data = Vec::new(); let mut block_light_data = Vec::new(); for section in &chunk.sections { let section_sky_light_data = if section.sky_light.len() != 2048 { warn!( - "Sky light data for section at {}, {} is not 2048 bytes long", - chunk.x, chunk.z + "Sky light data for section at {} is not 2048 bytes long", + pos ); vec![255; 2048] } else { @@ -88,8 +89,8 @@ impl ChunkAndLightData { sky_light_data.push(section_sky_light_data); let section_block_light_data = if section.block_light.len() != 2048 { warn!( - "Block light data for section at {}, {} is not 2048 bytes long", - chunk.x, chunk.z + "Block light data for section at {} is not 2048 bytes long", + pos ); vec![255; 2048] } else { @@ -175,8 +176,8 @@ impl ChunkAndLightData { ]; Ok(ChunkAndLightData { - chunk_x: chunk.x, - chunk_z: chunk.z, + chunk_x: pos.x(), + chunk_z: pos.z(), heightmaps: LengthPrefixedVec::new(heightmaps), data: ByteArray::new(raw_data.into_inner()), block_entities: LengthPrefixedVec::new(Vec::new()), diff --git a/src/lib/world/Cargo.toml b/src/lib/world/Cargo.toml index e6c497d59..b63734cda 100644 --- a/src/lib/world/Cargo.toml +++ b/src/lib/world/Cargo.toml @@ -29,6 +29,7 @@ moka = { workspace = true, features = ["sync"] } ahash = { workspace = true } yazi = { workspace = true } ferrumc-threadpool = { workspace = true } +bevy_math = { workspace = true } [dev-dependencies] diff --git a/src/lib/world/src/benches/cache.rs b/src/lib/world/src/benches/cache.rs index 2819b1cd8..384cccc1b 100644 --- a/src/lib/world/src/benches/cache.rs +++ b/src/lib/world/src/benches/cache.rs @@ -1,7 +1,10 @@ use std::hint::black_box; use criterion::Criterion; -use ferrumc_world::World; +use ferrumc_world::{ + pos::{BlockPos, ChunkPos}, + World, +}; pub(crate) fn bench_cache(c: &mut Criterion) { let backend_path = std::env::current_dir() @@ -11,14 +14,24 @@ pub(crate) fn bench_cache(c: &mut Criterion) { group.bench_function("Load chunk 1,1 uncached", |b| { b.iter_batched( || World::new(&backend_path), - |world| world.load_chunk(black_box(1), black_box(1), black_box("overworld")), + |world| { + world.load_chunk( + ChunkPos::new(black_box(1), black_box(1)), + black_box("overworld"), + ) + }, criterion::BatchSize::PerIteration, ); }); group.bench_function("Load chunk 1,1 uncached, owned", |b| { b.iter_batched( || World::new(&backend_path), - |world| world.load_chunk_owned(black_box(1), black_box(1), black_box("overworld")), + |world| { + world.load_chunk_owned( + ChunkPos::new(black_box(1), black_box(1)), + black_box("overworld"), + ) + }, criterion::BatchSize::PerIteration, ); }); @@ -27,9 +40,7 @@ pub(crate) fn bench_cache(c: &mut Criterion) { || World::new(&backend_path), |world| { world.get_block_and_fetch( - black_box(1), - black_box(1), - black_box(1), + BlockPos::of(black_box(1), black_box(1), black_box(1)), black_box("overworld"), ) }, @@ -37,25 +48,33 @@ pub(crate) fn bench_cache(c: &mut Criterion) { ); }); let world = World::new(backend_path); - let load_chunk = || { - world.load_chunk(1, 1, "overworld").expect( + let load_chunk = || -> std::sync::Arc { + world.load_chunk(ChunkPos::new(1, 1), "overworld").expect( "Failed to load chunk. If it's a bitcode error, chances are the chunk format \ has changed since last generating a world so you'll need to regenerate", ) }; _ = load_chunk(); group.bench_function("Load chunk 1,1 cached", |b| { - b.iter(|| world.load_chunk(black_box(1), black_box(1), black_box("overworld"))) + b.iter(|| { + world.load_chunk( + ChunkPos::new(black_box(1), black_box(1)), + black_box("overworld"), + ) + }) }); group.bench_function("Load chunk 1,1 cached, owned", |b| { - b.iter(|| world.load_chunk_owned(black_box(1), black_box(1), black_box("overworld"))) + b.iter(|| { + world.load_chunk_owned( + ChunkPos::new(black_box(1), black_box(1)), + black_box("overworld"), + ) + }) }); group.bench_function("Load block 1,1 cached", |b| { b.iter(|| { world.get_block_and_fetch( - black_box(1), - black_box(1), - black_box(1), + BlockPos::of(black_box(1), black_box(1), black_box(1)), black_box("overworld"), ) }); diff --git a/src/lib/world/src/benches/edit_bench.rs b/src/lib/world/src/benches/edit_bench.rs index c16f3e19d..7bb181fa4 100644 --- a/src/lib/world/src/benches/edit_bench.rs +++ b/src/lib/world/src/benches/edit_bench.rs @@ -19,20 +19,25 @@ pub(crate) fn bench_edits(c: &mut Criterion) { read_group.throughput(Throughput::Elements(1)); read_group.bench_function("Read 0,0,0", |b| { - b.iter(|| black_box(chunk.get_block(0, 0, 0))); + b.iter(|| black_box(chunk.get_block((0, 0, 0).into()))); }); read_group.bench_function("Read 8,8,150", |b| { - b.iter(|| black_box(chunk.get_block(8, 8, 150))); + b.iter(|| black_box(chunk.get_block((8, 150, 8).into()))); }); read_group.bench_function("Read rand", |b| { b.iter(|| { - black_box(chunk.get_block( - get_rand_in_range(0, 15), - get_rand_in_range(0, 15), - get_rand_in_range(0, 255), - )) + black_box( + chunk.get_block( + ( + get_rand_in_range(0, 15) as u8, + get_rand_in_range(0, 255) as i16, + get_rand_in_range(0, 15) as u8, + ) + .into(), + ), + ) }); }); @@ -45,26 +50,31 @@ pub(crate) fn bench_edits(c: &mut Criterion) { write_group.bench_with_input("Write 0,0,0", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.set_block(0, 0, 0, block!("bricks"))).unwrap(); + black_box(chunk.set_block((0, 0, 0).into(), block!("bricks"))).unwrap(); }); }); write_group.bench_with_input("Write 8,8,150", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.set_block(8, 8, 150, block!("bricks"))).unwrap(); + black_box(chunk.set_block((8, 150, 8).into(), block!("bricks"))).unwrap(); }); }); write_group.bench_with_input("Write rand", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.set_block( - get_rand_in_range(0, 15), - get_rand_in_range(0, 15), - get_rand_in_range(0, 255), - block!("bricks"), - )) + black_box( + chunk.set_block( + ( + get_rand_in_range(0, 15) as u8, + get_rand_in_range(0, 255) as i16, + get_rand_in_range(0, 15) as u8, + ) + .into(), + block!("bricks"), + ), + ) .unwrap(); }); }); @@ -84,7 +94,7 @@ pub(crate) fn bench_edits(c: &mut Criterion) { for x in 0..16 { for y in 0..256 { for z in 0..16 { - black_box(chunk.set_block(x, y, z, block!("bricks"))).unwrap(); + black_box(chunk.set_block((x, y, z).into(), block!("bricks"))).unwrap(); } } } @@ -98,7 +108,7 @@ pub(crate) fn bench_edits(c: &mut Criterion) { for x in 0..16 { for y in 0..256 { for z in 0..16 { - batch.set_block(x, y, z, black_box(block!("bricks"))); + batch.set_block((x, y, z).into(), black_box(block!("bricks"))); } } } @@ -118,7 +128,7 @@ pub(crate) fn bench_edits(c: &mut Criterion) { } else { block!("stone") }; - batch.set_block(x, y, z, black_box(block)); + batch.set_block((x as u8, y, z as u8).into(), black_box(block)); } } } diff --git a/src/lib/world/src/block_state_id.rs b/src/lib/world/src/block_state_id.rs index b3730c35d..f29e675c2 100644 --- a/src/lib/world/src/block_state_id.rs +++ b/src/lib/world/src/block_state_id.rs @@ -48,9 +48,15 @@ lazy_static! { /// This should be used over `BlockData` in most cases, as it's much more efficient to store and pass around. /// You can also generate a block's id at runtime with the [ferrumc_macros::block!] macro. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Encode, Decode, DeepSizeOf)] -pub struct BlockStateId(pub u32); +pub struct BlockStateId(u32); impl BlockStateId { + /// Do NOT use this by yourself. Instead use the block macro `block!("stone")` the item to block + /// map + pub fn new(id: u32) -> Self { + Self(id) + } + /// Given a BlockData, return a BlockStateId. Does not clone, should be quite fast. pub fn from_block_data(block_data: &BlockData) -> Self { let id = BLOCK2ID @@ -72,6 +78,12 @@ impl BlockStateId { pub fn to_varint(&self) -> VarInt { VarInt(self.0 as i32) } + + /// Do Not use this by yourself. This is only useful for apis that use this as an index or key + /// to get additionally information about this state. + pub fn raw(&self) -> u32 { + self.0 + } } impl Display for BlockStateId { diff --git a/src/lib/world/src/chunk_format.rs b/src/lib/world/src/chunk_format.rs index 5e833d54d..2ed13a126 100644 --- a/src/lib/world/src/chunk_format.rs +++ b/src/lib/world/src/chunk_format.rs @@ -1,4 +1,5 @@ use crate::block_state_id::{BlockStateId, BLOCK2ID}; +use crate::pos::ChunkHeight; use crate::vanilla_chunk_format; use crate::vanilla_chunk_format::VanillaChunk; use crate::{errors::WorldError, vanilla_chunk_format::VanillaHeightmaps}; @@ -23,9 +24,7 @@ use tracing::error; #[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] // This is a placeholder for the actual chunk format pub struct Chunk { - pub x: i32, - pub z: i32, - pub dimension: String, + pub min_y: i16, pub sections: Vec
, pub heightmaps: Heightmaps, } @@ -41,7 +40,6 @@ pub struct Heightmaps { } #[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] pub struct Section { - pub y: i8, pub block_states: BlockStates, pub biome_states: BiomeStates, pub block_light: Vec, @@ -119,7 +117,28 @@ impl From for Heightmaps { impl VanillaChunk { pub fn to_custom_format(&self) -> Result { - let mut sections = Vec::new(); + let height = if self.dimension.as_ref().is_none_or(|s| s == "overworld") { + ChunkHeight::new(-64, 384) + } else { + ChunkHeight::new(0, 256) + }; + let mut sections = vec![ + Section { + block_states: BlockStates { + non_air_blocks: 0, + block_data: PaletteType::Single(VarInt::from(0)), + block_counts: HashMap::from([(BlockStateId::default(), 4096)]), + }, + biome_states: BiomeStates { + bits_per_biome: 0, + data: vec![], + palette: vec![VarInt::from(0)], + }, + block_light: vec![255; 2048], + sky_light: vec![255; 2048], + }; + height.height as usize >> 4 + ]; for section in self.sections.as_ref().unwrap() { let y = section.y; let raw_block_data = section @@ -196,23 +215,18 @@ impl VanillaChunk { palette: vec![VarInt::from(0); 1], }; let section = Section { - y, block_states, biome_states, block_light, sky_light, }; - sections.push(section); + sections[(y - (height.min_y >> 4) as i8) as usize] = section; } - let dimension = self.dimension.clone().unwrap_or("overworld".to_string()); - let heightmaps: Heightmaps = self.heightmaps.clone().map(Into::into).unwrap_or_default(); Ok(Chunk { - x: self.x_pos, - z: self.z_pos, - dimension, + min_y: height.min_y, sections, heightmaps, }) @@ -220,10 +234,10 @@ impl VanillaChunk { } impl Chunk { - pub fn new(x: i32, z: i32, dimension: String) -> Self { - let mut sections: Vec
= (-4..20) - .map(|y| Section { - y: y as i8, + pub fn new(height: ChunkHeight) -> Self { + let mut sections: Vec
= (height.min_y.div_euclid(16) + ..height.max_y().div_euclid(16)) + .map(|_| Section { block_states: BlockStates { non_air_blocks: 0, block_data: PaletteType::Single(VarInt::from(0)), @@ -241,15 +255,22 @@ impl Chunk { for section in &mut sections { section.optimise().expect("Failed to optimise section"); } - block!("stone"); Chunk { - x, - z, - dimension, + min_y: height.min_y, sections, heightmaps: Heightmaps::new(), } } + + pub fn get_section_mut(&mut self, section: i8) -> Option<&mut Section> { + self.sections + .get_mut((section - (self.min_y >> 4) as i8) as usize) + } + + pub fn get_section(&self, section: i8) -> Option<&Section> { + self.sections + .get((section - (self.min_y >> 4) as i8) as usize) + } } #[cfg(test)] @@ -259,15 +280,15 @@ mod tests { #[test] fn test_chunk_set_block() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let block = block!("stone"); - chunk.set_block(0, 0, 0, block).unwrap(); - assert_eq!(chunk.get_block(0, 0, 0).unwrap(), block); + chunk.set_block((0, 0, 0).into(), block).unwrap(); + assert_eq!(chunk.get_block((0, 0, 0).into()).unwrap(), block); } #[test] fn test_chunk_fill() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let stone_block = block!("stone"); chunk.fill(stone_block).unwrap(); for section in &chunk.sections { @@ -281,7 +302,6 @@ mod tests { #[test] fn test_section_fill() { let mut section = Section { - y: 0, block_states: BlockStates { non_air_blocks: 0, block_data: PaletteType::Single(VarInt::from(0)), @@ -309,18 +329,18 @@ mod tests { #[test] fn test_false_positive() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let block = block!("stone"); - chunk.set_block(0, 0, 0, block).unwrap(); - assert_ne!(chunk.get_block(0, 1, 0).unwrap(), block); + chunk.set_block((0, 0, 0).into(), block).unwrap(); + assert_ne!(chunk.get_block((0, 1, 0).into()).unwrap(), block); } #[test] fn test_doesnt_fail() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let block = block!("stone"); - assert!(chunk.set_block(0, 0, 0, block).is_ok()); - assert!(chunk.set_block(0, 0, 0, block).is_ok()); - assert!(chunk.get_block(0, 0, 0).is_ok()); + assert!(chunk.set_block((0, 0, 0).into(), block).is_ok()); + assert!(chunk.set_block((0, 0, 0).into(), block).is_ok()); + assert!(chunk.get_block((0, 0, 0).into()).is_ok()); } } diff --git a/src/lib/world/src/db_functions.rs b/src/lib/world/src/db_functions.rs index c2af0de26..77cb2630a 100644 --- a/src/lib/world/src/db_functions.rs +++ b/src/lib/world/src/db_functions.rs @@ -1,6 +1,7 @@ use crate::chunk_format::Chunk; use crate::errors::WorldError; use crate::errors::WorldError::CorruptedChunkData; +use crate::pos::ChunkPos; // db_functions.rs use crate::warn; use crate::World; @@ -15,30 +16,34 @@ impl World { /// /// This function will save a chunk to the storage backend and update the cache with the new /// chunk data. If the chunk already exists in the cache, it will be updated with the new data. - pub fn save_chunk(&self, chunk: Arc) -> Result<(), WorldError> { - let ret = save_chunk_internal(self, &chunk); - self.cache - .insert((chunk.x, chunk.z, chunk.dimension.clone()), chunk); + pub fn save_chunk( + &self, + pos: ChunkPos, + dimension: &str, + chunk: Arc, + ) -> Result<(), WorldError> { + let ret = save_chunk_internal(self, pos, dimension, &chunk); + self.cache.insert((pos, dimension.to_string()), chunk); ret } /// Load a chunk from the storage backend. If the chunk is in the cache, it will be returned /// from the cache instead of the storage backend. If the chunk is not in the cache, it will be /// loaded from the storage backend and inserted into the cache. - pub fn load_chunk(&self, x: i32, z: i32, dimension: &str) -> Result, WorldError> { - if let Some(chunk) = self.cache.get(&(x, z, dimension.to_string())) { + pub fn load_chunk(&self, pos: ChunkPos, dimension: &str) -> Result, WorldError> { + if let Some(chunk) = self.cache.get(&(pos, dimension.to_string())) { return Ok(chunk); } - let chunk = load_chunk_internal(self, x, z, dimension); + let chunk = load_chunk_internal(self, pos, dimension); if let Ok(ref chunk) = chunk { self.cache - .insert((x, z, dimension.to_string()), Arc::from(chunk.clone())); + .insert((pos, dimension.to_string()), Arc::from(chunk.clone())); } chunk.map(Arc::new) } - pub fn load_chunk_owned(&self, x: i32, z: i32, dimension: &str) -> Result { - self.load_chunk(x, z, dimension).map(|c| c.as_ref().clone()) + pub fn load_chunk_owned(&self, pos: ChunkPos, dimension: &str) -> Result { + self.load_chunk(pos, dimension).map(|c| c.as_ref().clone()) } /// Check if a chunk exists in the storage backend. @@ -46,19 +51,19 @@ impl World { /// It will first check if the chunk is in the cache and if it is, it will return true. If the /// chunk is not in the cache, it will check the storage backend for the chunk, returning true /// if it exists and false if it does not. - pub fn chunk_exists(&self, x: i32, z: i32, dimension: &str) -> Result { - if self.cache.contains_key(&(x, z, dimension.to_string())) { + pub fn chunk_exists(&self, pos: ChunkPos, dimension: &str) -> Result { + if self.cache.contains_key(&(pos, dimension.to_string())) { return Ok(true); } - chunk_exists_internal(self, x, z, dimension) + chunk_exists_internal(self, pos, dimension) } /// Delete a chunk from the storage backend. /// /// This function will remove the chunk from the cache and delete it from the storage backend. - pub fn delete_chunk(&self, x: i32, z: i32, dimension: &str) -> Result<(), WorldError> { - self.cache.remove(&(x, z, dimension.to_string())); - delete_chunk_internal(self, x, z, dimension) + pub fn delete_chunk(&self, pos: ChunkPos, dimension: &str) -> Result<(), WorldError> { + self.cache.remove(&(pos, dimension.to_string())); + delete_chunk_internal(self, pos, dimension) } /// Sync the storage backend. @@ -68,8 +73,8 @@ impl World { /// to ensure that the data is properly saved to disk. pub fn sync(&self) -> Result<(), WorldError> { for (k, v) in self.cache.iter() { - trace!("Syncing chunk: {:?}", (k.0, k.1)); - save_chunk_internal(self, &v)?; + trace!("Syncing chunk: {:?}", k.0); + save_chunk_internal(self, k.0, &k.1, &v)?; } sync_internal(self) } @@ -81,22 +86,22 @@ impl World { /// returned as a vector. pub fn load_chunk_batch( &self, - coords: &[(i32, i32, &str)], + coords: &[(ChunkPos, &str)], ) -> Result>, WorldError> { let mut found_chunks = Vec::new(); let mut missing_chunks = Vec::new(); for coord in coords { - if let Some(chunk) = self.cache.get(&(coord.0, coord.1, coord.2.to_string())) { + if let Some(chunk) = self.cache.get(&(coord.0, coord.1.to_string())) { found_chunks.push(chunk); } else { missing_chunks.push(*coord); } } let fetched = load_chunk_batch_internal(self, &missing_chunks)?; - for chunk in fetched { + for (chunk, (pos, dimension)) in fetched.into_iter().zip(missing_chunks) { let chunk = Arc::new(chunk); self.cache - .insert((chunk.x, chunk.z, chunk.dimension.clone()), chunk.clone()); + .insert((pos, dimension.to_string()), chunk.clone()); found_chunks.push(chunk); } Ok(found_chunks) @@ -107,17 +112,22 @@ impl World { /// This function will load a chunk from the storage backend and insert it into the cache /// without returning the chunk. This is useful for preloading chunks into the cache before /// they are needed. - pub fn pre_cache(&self, x: i32, z: i32, dimension: &str) -> Result<(), WorldError> { - if self.cache.get(&(x, z, dimension.to_string())).is_none() { - let chunk = load_chunk_internal(self, x, z, dimension)?; + pub fn pre_cache(&self, pos: ChunkPos, dimension: &str) -> Result<(), WorldError> { + if self.cache.get(&(pos, dimension.to_string())).is_none() { + let chunk = load_chunk_internal(self, pos, dimension)?; self.cache - .insert((x, z, dimension.to_string()), Arc::new(chunk)); + .insert((pos, dimension.to_string()), Arc::new(chunk)); } Ok(()) } } -pub(crate) fn save_chunk_internal(world: &World, chunk: &Chunk) -> Result<(), WorldError> { +pub(crate) fn save_chunk_internal( + world: &World, + pos: ChunkPos, + dimension: &str, + chunk: &Chunk, +) -> Result<(), WorldError> { if !world.storage_backend.table_exists("chunks".to_string())? { world.storage_backend.create_table("chunks".to_string())?; } @@ -126,7 +136,7 @@ pub(crate) fn save_chunk_internal(world: &World, chunk: &Chunk) -> Result<(), Wo yazi::Format::Zlib, CompressionLevel::BestSpeed, )?; - let digest = create_key(chunk.dimension.as_str(), chunk.x, chunk.z); + let digest = create_key(dimension, pos); world .storage_backend .upsert("chunks".to_string(), digest, as_bytes)?; @@ -135,11 +145,10 @@ pub(crate) fn save_chunk_internal(world: &World, chunk: &Chunk) -> Result<(), Wo pub(crate) fn load_chunk_internal( world: &World, - x: i32, - z: i32, + pos: ChunkPos, dimension: &str, ) -> Result { - let digest = create_key(dimension, x, z); + let digest = create_key(dimension, pos); match world.storage_backend.get("chunks".to_string(), digest)? { Some(compressed) => { let (data, checksum) = yazi::decompress(compressed.as_slice(), yazi::Format::Zlib)?; @@ -163,11 +172,11 @@ pub(crate) fn load_chunk_internal( pub(crate) fn load_chunk_batch_internal( world: &World, - coords: &[(i32, i32, &str)], + coords: &[(ChunkPos, &str)], ) -> Result, WorldError> { let digests = coords .iter() - .map(|&(x, z, dim)| create_key(dim, x, z)) + .map(|&(pos, dim)| create_key(dim, pos)) .collect(); world .storage_backend @@ -197,24 +206,22 @@ pub(crate) fn load_chunk_batch_internal( pub(crate) fn chunk_exists_internal( world: &World, - x: i32, - z: i32, + pos: ChunkPos, dimension: &str, ) -> Result { if !world.storage_backend.table_exists("chunks".to_string())? { return Ok(false); } - let digest = create_key(dimension, x, z); + let digest = create_key(dimension, pos); Ok(world.storage_backend.exists("chunks".to_string(), digest)?) } pub(crate) fn delete_chunk_internal( world: &World, - x: i32, - z: i32, + pos: ChunkPos, dimension: &str, ) -> Result<(), WorldError> { - let digest = create_key(dimension, x, z); + let digest = create_key(dimension, pos); world.storage_backend.delete("chunks".to_string(), digest)?; Ok(()) } @@ -224,18 +231,10 @@ pub(crate) fn sync_internal(world: &World) -> Result<(), WorldError> { Ok(()) } -fn create_key(dimension: &str, x: i32, z: i32) -> u128 { - let mut key = 0u128; +fn create_key(dimension: &str, pos: ChunkPos) -> u128 { let mut hasher = wyhash::WyHash::with_seed(0); hasher.write(dimension.as_bytes()); hasher.write_u8(0xFF); let dim_hash = hasher.finish(); - // Insert the dimension hash into the key as the first 32 bits - key |= (dim_hash as u128) << 96; - // Convert the x coordinate to a 48 bit integer and insert it into the key - key |= ((x as u128) & 0x0000_0000_FFFF_FFFF) << 48; - // Convert the z coordinate to a 48 bit integer and insert it into the key - key |= (z as u128) & 0x0000_0000_FFFF_FFFF; - - key + (dim_hash as u128) << 96 | pos.pack() as u128 } diff --git a/src/lib/world/src/edit_batch.rs b/src/lib/world/src/edit_batch.rs index 53683df24..d9324729a 100644 --- a/src/lib/world/src/edit_batch.rs +++ b/src/lib/world/src/edit_batch.rs @@ -1,11 +1,11 @@ use crate::block_state_id::BlockStateId; -use crate::chunk_format::{BiomeStates, BlockStates, Chunk, PaletteType}; +use crate::chunk_format::{Chunk, PaletteType}; +use crate::pos::ChunkBlockPos; use crate::WorldError; use ahash::{AHashMap, AHashSet, AHasher}; use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; use ferrumc_general_purpose::data_packing::u32::write_nbit_u32; use ferrumc_net_codec::net_types::var_int::VarInt; -use std::collections::HashMap; use std::hash::{Hash, Hasher}; /// A batched block editing utility for a single Minecraft chunk. @@ -20,10 +20,12 @@ use std::hash::{Hash, Hasher}; /// # use ferrumc_world::chunk_format::Chunk; /// # use ferrumc_world::edit_batch::EditBatch; /// # use ferrumc_world::vanilla_chunk_format::BlockData; -/// # let mut chunk = Chunk::new(0, 0, "overworld".to_string()); +/// # use ferrumc_world::pos::ChunkBlockPos; +/// # use ferrumc_world::pos::ChunkHeight; +/// # let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); /// let mut batch = EditBatch::new(&mut chunk); -/// batch.set_block(1, 64, 1, block!("stone")); -/// batch.set_block(2, 64, 1, block!("stone")); +/// batch.set_block(ChunkBlockPos::new(1, 64, 1), block!("stone")); +/// batch.set_block(ChunkBlockPos::new(2, 64, 1), block!("stone")); /// batch.apply().unwrap(); /// ``` /// @@ -37,14 +39,11 @@ pub struct EditBatch<'a> { pub(crate) edits: Vec, chunk: &'a mut Chunk, tmp_palette_map: AHashMap, - used: bool, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct Edit { - pub(crate) x: i32, - pub(crate) y: i32, - pub(crate) z: i32, + pub(crate) pos: ChunkBlockPos, pub(crate) block: BlockStateId, } @@ -66,27 +65,21 @@ impl<'a> EditBatch<'a> { edits: Vec::new(), chunk, tmp_palette_map: AHashMap::with_capacity(map_capacity), - used: false, } } /// Sets a block at the given chunk-relative coordinates. /// /// This won't have any effect until `apply()` is called. - pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: BlockStateId) { - self.edits.push(Edit { x, y, z, block }); + pub fn set_block(&mut self, pos: ChunkBlockPos, block: BlockStateId) { + self.edits.push(Edit { pos, block }); } /// Applies all edits in the batch to the chunk. /// /// This will modify the chunk in place and clear the batch. - /// Will return an error if the batch has already been used or if there are no edits. - pub fn apply(&mut self) -> Result<(), WorldError> { - if self.used { - return Err(WorldError::InvalidBatchingOperation( - "EditBatch has already been used".to_string(), - )); - } + /// Will return an error if there are no edits. + pub fn apply(mut self) -> Result<(), WorldError> { if self.edits.is_empty() { return Err(WorldError::InvalidBatchingOperation( "No edits to apply".to_string(), @@ -99,9 +92,9 @@ impl<'a> EditBatch<'a> { // Convert edits into per-section sparse arrays (Vec>), // using block index (0..4095) as the key instead of hashing 3D coords for edit in &self.edits { - let section_index = (edit.y >> 4) as i8; + let section_index = edit.pos.section(); // Compute linear index within section (16x16x16 = 4096 blocks) - let index = ((edit.y & 0xf) * 256 + (edit.z & 0xf) * 16 + (edit.x & 0xf)) as usize; + let index = edit.pos.section_block_pos().pack() as usize; let section_vec = section_edits .entry(section_index) .or_insert_with(|| vec![None; 4096]); @@ -113,7 +106,6 @@ impl<'a> EditBatch<'a> { if edits_vec.is_empty() || edits_vec.iter().all(|e| e.is_none()) { continue; } - let section_maybe = self.chunk.sections.iter_mut().find(|s| s.y == section_y); // let first_edit = edits_vec // .iter() // .find(|e| e.is_some()) @@ -122,38 +114,10 @@ impl<'a> EditBatch<'a> { // .unwrap(); let mut block_count_adds = AHashMap::new(); let mut block_count_removes = AHashMap::new(); - - let section = match section_maybe { - Some(section) => { - // If the section exists, we can just use it - section - } - None => &mut { - // If the section doesn't exist, create it - let new_section = crate::chunk_format::Section { - y: section_y, - block_states: BlockStates { - non_air_blocks: 0, - block_data: PaletteType::Single(VarInt::default()), - block_counts: HashMap::from([(BlockStateId::default(), 4096)]), - }, - // Biomes don't really matter for this, so we can just use empty data - biome_states: BiomeStates { - bits_per_biome: 0, - data: vec![], - palette: vec![], - }, - block_light: vec![255; 2048], - sky_light: vec![255; 2048], - }; - self.chunk.sections.push(new_section); - self.chunk - .sections - .iter_mut() - .find(|s| s.y == section_y) - .expect("Section should exist after push") - }, - }; + let section = self + .chunk + .get_section_mut(section_y) + .ok_or(WorldError::SectionOutOfBounds(section_y as i32))?; // // check if all the edits in 1 section are the same // let all_same = edits_vec @@ -211,7 +175,7 @@ impl<'a> EditBatch<'a> { for maybe_edit in edits_vec.iter() { let Some(edit) = maybe_edit else { continue }; - let index = ((edit.y & 0xf) * 256 + (edit.z & 0xf) * 16 + (edit.x & 0xf)) as usize; + let index = edit.pos.section_block_pos().pack() as usize; let palette_index = if let Some(&idx) = self.tmp_palette_map.get(&edit.block) { idx @@ -302,7 +266,6 @@ impl<'a> EditBatch<'a> { // Clear edits after applying self.edits.clear(); - self.used = true; Ok(()) } @@ -312,6 +275,7 @@ impl<'a> EditBatch<'a> { mod tests { use super::*; use crate::chunk_format::Chunk; + use crate::pos::ChunkHeight; use crate::vanilla_chunk_format::BlockData; fn make_test_block(name: &str) -> BlockStateId { @@ -324,20 +288,20 @@ mod tests { #[test] fn test_single_block_edit() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let block = make_test_block("minecraft:stone"); let mut batch = EditBatch::new(&mut chunk); - batch.set_block(1, 1, 1, block); + batch.set_block((1, 1, 1).into(), block); batch.apply().unwrap(); - let got = chunk.get_block(1, 1, 1).unwrap(); + let got = chunk.get_block((1, 1, 1).into()).unwrap(); assert_eq!(got, block); } #[test] fn test_multi_block_edits() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let stone = make_test_block("minecraft:stone"); let dirt = make_test_block("minecraft:dirt"); @@ -346,7 +310,7 @@ mod tests { for y in 0..4 { for z in 0..4 { let block = if (x + y + z) % 2 == 0 { stone } else { dirt }; - batch.set_block(x, y, z, block); + batch.set_block((x as u8, y, z as u8).into(), block); } } } @@ -356,7 +320,7 @@ mod tests { for y in 0..4 { for z in 0..4 { let expected = if (x + y + z) % 2 == 0 { &stone } else { &dirt }; - let got = chunk.get_block(x, y, z).unwrap(); + let got = chunk.get_block((x as u8, y, z as u8).into()).unwrap(); assert_eq!(&got, expected); } } diff --git a/src/lib/world/src/edits.rs b/src/lib/world/src/edits.rs index 56d9a2fa7..da21bf964 100644 --- a/src/lib/world/src/edits.rs +++ b/src/lib/world/src/edits.rs @@ -1,8 +1,10 @@ -use crate::block_state_id::{BlockStateId, ID2BLOCK}; +use crate::block_state_id::BlockStateId; use crate::chunk_format::{BlockStates, Chunk, PaletteType, Section}; use crate::errors::WorldError; +use crate::pos::{BlockPos, ChunkBlockPos, SectionBlockPos}; use crate::World; use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; +use ferrumc_macros::block; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::Arc; @@ -32,15 +34,11 @@ impl World { /// * `WorldError::InvalidBlockStateData` - If the block state data is invalid. pub fn get_block_and_fetch( &self, - x: i32, - y: i32, - z: i32, + pos: BlockPos, dimension: &str, ) -> Result { - let chunk_x = x >> 4; - let chunk_z = z >> 4; - let chunk = self.load_chunk(chunk_x, chunk_z, dimension)?; - chunk.get_block(x, y, z) + let chunk = self.load_chunk(pos.chunk(), dimension)?; + chunk.get_block(pos.chunk_block_pos()) } /// Sets the block data at the specified coordinates in the given dimension. @@ -61,29 +59,21 @@ impl World { /// * `Err(WorldError)` - If an error occurs while setting the block data. pub fn set_block_and_fetch( &self, - x: i32, - y: i32, - z: i32, + pos: BlockPos, dimension: &str, block: BlockStateId, ) -> Result<(), WorldError> { - if ID2BLOCK.get(block.0 as usize).is_none() { - return Err(WorldError::InvalidBlockStateId(block.0)); - }; - // Get chunk - let chunk_x = x >> 4; - let chunk_z = z >> 4; - let mut chunk = self.load_chunk_owned(chunk_x, chunk_z, dimension)?; + let mut chunk = self.load_chunk_owned(pos.chunk(), dimension)?; - debug!("Chunk: {}, {}", chunk_x, chunk_z); + debug!("Chunk: {}", pos.chunk()); - chunk.set_block(x, y, z, block)?; + chunk.set_block(pos.chunk_block_pos(), block)?; for section in &mut chunk.sections { section.optimise()?; } // Save chunk - self.save_chunk(Arc::new(chunk))?; + self.save_chunk(pos.chunk(), dimension, Arc::new(chunk))?; Ok(()) } } @@ -197,41 +187,73 @@ impl Chunk { /// If the block is the same as the old block, nothing happens. /// If the block is not in the palette, it is added. /// If the palette is in single block mode, it is converted to palette'd mode. + pub fn set_block(&mut self, pos: ChunkBlockPos, block: BlockStateId) -> Result<(), WorldError> { + let section = self + .get_section_mut(pos.section()) + .ok_or(WorldError::SectionOutOfBounds(pos.section() as i32))?; + section.set_block(pos.section_block_pos(), block)?; + + section.optimise()?; + + Ok(()) + } + + pub fn get_block(&self, pos: ChunkBlockPos) -> Result { + let section = self + .get_section(pos.section()) + .ok_or(WorldError::SectionOutOfBounds(pos.section() as i32))?; + section.get_block(pos.section_block_pos()) + } + + /// Sets the section at the specified index to the specified block data. + /// If the section is out of bounds, an error is returned. /// /// # Arguments /// - /// * `x` - The x-coordinate of the block. - /// * `y` - The y-coordinate of the block. - /// * `z` - The z-coordinate of the block. - /// * `block` - The block data to set the block to. + /// * `section` - The index of the section to set. + /// * `block` - The block data to set the section to. /// /// # Returns /// - /// * `Ok(())` - If the block was successfully set. - /// * `Err(WorldError)` - If an error occurs while setting the block. + /// * `Ok(())` - If the section was successfully set. + /// * `Err(WorldError)` - If an error occurs while setting the section. + pub fn set_section(&mut self, section_y: i8, block: BlockStateId) -> Result<(), WorldError> { + if let Some(section) = self.get_section_mut(section_y) { + section.fill(block) + } else { + Err(WorldError::SectionOutOfBounds(section_y as i32)) + } + } + + /// Fills the chunk with the specified block. + /// + /// # Arguments /// - /// ### Note - /// The positions are modulo'd by 16 to get the block index in the section anyway, so converting - /// the coordinates to section coordinates isn't really necessary, but you should probably do it - /// anyway for readability's sake. + /// * `block` - The block data to fill the chunk with. + /// + /// # Returns + /// + /// * `Ok(())` - If the chunk was successfully filled. + /// * `Err(WorldError)` - If an error occurs while filling the chunk. + pub fn fill(&mut self, block: BlockStateId) -> Result<(), WorldError> { + for section in &mut self.sections { + section.fill(block)?; + } + Ok(()) + } +} + +impl Section { pub fn set_block( &mut self, - x: i32, - y: i32, - z: i32, + pos: SectionBlockPos, block: BlockStateId, ) -> Result<(), WorldError> { - let old_block = self.get_block(x, y, z)?; + let old_block = self.get_block(pos)?; if old_block == block { return Ok(()); } - let section = self - .sections - .iter_mut() - .find(|section| section.y == (y >> 4) as i8) - .ok_or(WorldError::SectionOutOfBounds(y >> 4))?; - // from single palette to indirect palette if needed let mut converted = false; let mut new_contents = PaletteType::Indirect { @@ -239,7 +261,7 @@ impl Chunk { data: vec![], palette: vec![], }; - if let PaletteType::Single(val) = §ion.block_states.block_data { + if let PaletteType::Single(val) = &self.block_states.block_data { new_contents = PaletteType::Indirect { bits_per_block: 4, data: vec![0; 256], @@ -248,10 +270,10 @@ impl Chunk { converted = true; } if converted { - section.block_states.block_data = new_contents; + self.block_states.block_data = new_contents; } - let (block_palette_index, needs_resize, target_bits) = match &mut section + let (block_palette_index, needs_resize, target_bits) = match &mut self .block_states .block_data { @@ -263,39 +285,26 @@ impl Chunk { palette, .. } => { - match section.block_states.block_counts.entry(old_block) { + match self.block_states.block_counts.entry(old_block) { Entry::Occupied(mut occ_entry) => { let count = occ_entry.get_mut(); if *count <= 0 { - return match old_block.to_block_data() { - Some(block_data) => { - error!("Block count is zero for block: {:?}", block_data); - Err(WorldError::InvalidBlockStateData(format!( - "Block count is zero for block: {block_data:?}" - ))) - } - None => { - error!( - "Block count is zero for unknown block state ID: {}", - old_block.0 - ); - Err(WorldError::InvalidBlockStateId(old_block.0)) - } - }; + error!("Block count is zero for block state {old_block}"); + return Err(WorldError::InvalidBlockStateId(old_block)); } *count -= 1; } Entry::Vacant(empty_entry) => { - warn!("Block not found in block counts: {:?}", old_block); + warn!("Block not found in block counts: {old_block}"); empty_entry.insert(0); } } // Add new block to counts - if let Some(e) = section.block_states.block_counts.get(&block) { - section.block_states.block_counts.insert(block, e + 1); + if let Some(e) = self.block_states.block_counts.get(&block) { + self.block_states.block_counts.insert(block, e + 1); } else { - section.block_states.block_counts.insert(block, 1); + self.block_states.block_counts.insert(block, 1); } // find in palette @@ -320,18 +329,17 @@ impl Chunk { if needs_resize { // debug!("Resizing section block states to {} bits", target_bits); - section.block_states.resize(target_bits as usize)?; + self.block_states.resize(target_bits as usize)?; } - match &mut section.block_states.block_data { + match &mut self.block_states.block_data { PaletteType::Indirect { bits_per_block, data, .. } => { let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; - let index = - ((y.abs() & 0xf) * 256 + (z.abs() & 0xf) * 16 + (x.abs() & 0xf)) as usize; + let index = pos.pack() as usize; let i64_index = index / blocks_per_i64; let packed_u64 = @@ -360,43 +368,21 @@ impl Chunk { } } - section.block_states.non_air_blocks = section + self.block_states.non_air_blocks = self .block_states .block_counts .iter() - .filter(|(block, _)| ![0, 12958, 12959].contains(&block.0)) + .filter(|(block, _)| { + [block!("air"), block!("cave_air"), block!("void_air")].contains(block) + }) .map(|(_, count)| *count as u16) .sum(); - section.optimise()?; - Ok(()) } - /// Gets the block at the specified coordinates. - /// - /// # Arguments - /// - /// * `x` - The x-coordinate of the block. - /// * `y` - The y-coordinate of the block. - /// * `z` - The z-coordinate of the block. - /// - /// # Returns - /// - /// * `Ok(BlockData)` - The block data at the specified coordinates. - /// * `Err(WorldError)` - If an error occurs while retrieving the block data. - /// - /// ### Note - /// The positions are modulo'd by 16 to get the block index in the section anyway, so converting - /// the coordinates to section coordinates isn't really necessary, but you should probably do it - /// anyway for readability's sake. - pub fn get_block(&self, x: i32, y: i32, z: i32) -> Result { - let section = self - .sections - .iter() - .find(|section| section.y == (y / 16) as i8) - .ok_or(WorldError::SectionOutOfBounds(y >> 4))?; - match §ion.block_states.block_data { + pub fn get_block(&self, pos: SectionBlockPos) -> Result { + match &self.block_states.block_data { PaletteType::Single(val) => Ok(BlockStateId::from_varint(*val)), PaletteType::Indirect { bits_per_block, @@ -407,7 +393,7 @@ impl Chunk { return Ok(BlockStateId::from_varint(palette[0])); } let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; - let index = ((y & 0xf) * 256 + (z & 0xf) * 16 + (x & 0xf)) as usize; + let index = pos.pack() as usize; let i64_index = index / blocks_per_i64; let packed_u64 = data .get(i64_index) @@ -427,49 +413,6 @@ impl Chunk { } } - /// Sets the section at the specified index to the specified block data. - /// If the section is out of bounds, an error is returned. - /// - /// # Arguments - /// - /// * `section` - The index of the section to set. - /// * `block` - The block data to set the section to. - /// - /// # Returns - /// - /// * `Ok(())` - If the section was successfully set. - /// * `Err(WorldError)` - If an error occurs while setting the section. - pub fn set_section(&mut self, section_y: i8, block: BlockStateId) -> Result<(), WorldError> { - if let Some(section) = self - .sections - .iter_mut() - .find(|section| section.y == section_y) - { - section.fill(block) - } else { - Err(WorldError::SectionOutOfBounds(section_y as i32)) - } - } - - /// Fills the chunk with the specified block. - /// - /// # Arguments - /// - /// * `block` - The block data to fill the chunk with. - /// - /// # Returns - /// - /// * `Ok(())` - If the chunk was successfully filled. - /// * `Err(WorldError)` - If an error occurs while filling the chunk. - pub fn fill(&mut self, block: BlockStateId) -> Result<(), WorldError> { - for section in &mut self.sections { - section.fill(block)?; - } - Ok(()) - } -} - -impl Section { /// Fills the section with the specified block. /// /// # Arguments @@ -484,7 +427,7 @@ impl Section { self.block_states.block_data = PaletteType::Single(block.to_varint()); self.block_states.block_counts = HashMap::from([(block, 4096)]); // Air, void air and cave air respectively - if [0, 12958, 12959].contains(&block.0) { + if [block!("air"), block!("cave_air"), block!("void_air")].contains(&block) { self.block_states.non_air_blocks = 0; } else { self.block_states.non_air_blocks = 4096; @@ -497,6 +440,8 @@ impl Section { /// 1. Removes any palette entries that are not used in the block states data. /// /// 2. If there is only one block in the palette, it converts the palette to single block mode. + /// + /// So it may only useful to call if a block has been removed from the section. pub fn optimise(&mut self) -> Result<(), WorldError> { match &mut self.block_states.block_data { PaletteType::Single(_) => { @@ -516,7 +461,7 @@ impl Section { if let Some(index) = index { remove_indexes.push(index); } else { - return Err(WorldError::InvalidBlockStateId(block.0)); + return Err(WorldError::InvalidBlockStateId(*block)); } } } diff --git a/src/lib/world/src/errors.rs b/src/lib/world/src/errors.rs index 4072085dc..9d8b96983 100644 --- a/src/lib/world/src/errors.rs +++ b/src/lib/world/src/errors.rs @@ -51,7 +51,7 @@ pub enum WorldError { #[error("Invalid batching operation: {0}")] InvalidBatchingOperation(String), #[error("Invalid block state ID: {0}")] - InvalidBlockStateId(u32), + InvalidBlockStateId(BlockStateId), #[error("World generation error: {0}")] WorldGenerationError(String), #[error("Compression error: {0}")] diff --git a/src/lib/world/src/importing.rs b/src/lib/world/src/importing.rs index 2f37cbdc7..20d14f75c 100644 --- a/src/lib/world/src/importing.rs +++ b/src/lib/world/src/importing.rs @@ -1,4 +1,5 @@ use crate::errors::WorldError; +use crate::pos::ChunkPos; use crate::vanilla_chunk_format::VanillaChunk; use crate::World; use ferrumc_anvil::load_anvil_file; @@ -91,8 +92,11 @@ impl World { let self_clone = arc_self.clone(); let progress = progress.clone(); move || { - let res = - self_clone.save_chunk(vanilla_chunk.to_custom_format()?.into()); + let res = self_clone.save_chunk( + ChunkPos::new(vanilla_chunk.x_pos, vanilla_chunk.z_pos), + vanilla_chunk.dimension.as_deref().unwrap_or("overworld"), + vanilla_chunk.to_custom_format()?.into(), + ); progress.inc(1); if index == location_count - 1 { self_clone.storage_backend.flush()?; diff --git a/src/lib/world/src/lib.rs b/src/lib/world/src/lib.rs index ad604d240..71f1993f6 100644 --- a/src/lib/world/src/lib.rs +++ b/src/lib/world/src/lib.rs @@ -5,10 +5,12 @@ pub mod edit_batch; pub mod edits; pub mod errors; mod importing; +pub mod pos; pub mod vanilla_chunk_format; use crate::chunk_format::Chunk; use crate::errors::WorldError; +use crate::pos::ChunkPos; use deepsize::DeepSizeOf; use ferrumc_config::server_config::get_global_config; use ferrumc_general_purpose::paths::get_root_path; @@ -24,7 +26,7 @@ use tracing::{error, trace, warn}; #[derive(Clone)] pub struct World { storage_backend: LmdbBackend, - cache: Cache<(i32, i32, String), Arc>, + cache: Cache<(ChunkPos, String), Arc>, } fn check_config_validity() -> Result<(), WorldError> { @@ -128,7 +130,7 @@ mod tests { .unwrap() .join("../../../target/debug/world"), ); - let chunk = world.load_chunk(1, 1, "overworld").expect( + let chunk = world.load_chunk(ChunkPos::new(1, 1), "overworld").expect( "Failed to load chunk. If it's a bitcode error, chances are the chunk format \ has changed since last generating a world so you'll need to regenerate", ); diff --git a/src/lib/world/src/pos.rs b/src/lib/world/src/pos.rs new file mode 100644 index 000000000..9fb49a937 --- /dev/null +++ b/src/lib/world/src/pos.rs @@ -0,0 +1,301 @@ +use std::fmt::Display; +use std::fmt::Formatter; +use std::fmt::Result; +use std::ops::Add; +use std::ops::Range; + +use bevy_math::I16Vec3; +use bevy_math::IVec2; +use bevy_math::IVec3; +use bevy_math::U8Vec2; +use bevy_math::U8Vec3; +use bevy_math::Vec2Swizzles; +use bevy_math::Vec3Swizzles; +use ferrumc_net_codec::net_types::network_position::NetworkPosition; + +#[derive(Clone, Copy)] +pub struct BlockPos { + /// (i26, i12, i26) + pub pos: IVec3, +} + +impl BlockPos { + pub fn of(x: i32, y: i32, z: i32) -> Self { + Self { + pos: IVec3::new(x, y, z), + } + } + + pub fn column(&self) -> ColumnPos { + ColumnPos { pos: self.pos.xz() } + } + + pub fn chunk(&self) -> ChunkPos { + self.column().chunk() + } + + pub fn chunk_block_pos(self) -> ChunkBlockPos { + ChunkBlockPos::new( + self.pos.x.rem_euclid(16) as u8, + self.pos.y as i16, + self.pos.z.rem_euclid(16) as u8, + ) + } + + pub fn section(&self) -> SectionPos { + SectionPos { + pos: self.pos.div_euclid((16, 16, 16).into()) * 16, + } + } + + pub fn section_block_pos(&self) -> SectionBlockPos { + SectionBlockPos { + pos: self.pos.rem_euclid((16, 16, 16).into()).as_u8vec3(), + } + } +} + +impl From for BlockPos { + fn from(value: NetworkPosition) -> Self { + Self { + pos: IVec3::new(value.x, value.y as i32, value.z), + } + } +} + +impl From for NetworkPosition { + fn from(value: BlockPos) -> Self { + Self::new(value.pos.x, value.pos.y as i16, value.pos.z) + } +} + +impl Display for BlockPos { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + self.pos.fmt(f) + } +} + +impl Add<(i32, i32, i32)> for BlockPos { + type Output = BlockPos; + + fn add(self, rhs: (i32, i32, i32)) -> Self::Output { + Self { + pos: self.pos + IVec3::from(rhs), + } + } +} + +#[derive(Clone, Copy)] +pub struct ChunkHeight { + pub min_y: i16, + pub height: u16, +} + +impl ChunkHeight { + pub const fn new(min_y: i16, height: u16) -> Self { + assert!(min_y % 16 == 0); + assert!(height.is_multiple_of(16)); + Self { min_y, height } + } + + pub fn iter(self) -> Range { + self.min_y..self.max_y() + } + pub const fn max_y(self) -> i16 { + self.min_y + self.height as i16 + } +} + +#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkPos { + pub pos: IVec2, +} + +impl ChunkPos { + pub const fn new(x: i32, z: i32) -> Self { + assert!(x < 1 << 22); + assert!(z < 1 << 22); + Self { + pos: IVec2::new(x * 16, z * 16), + } + } + + pub fn center(&self) -> ColumnPos { + self.column_pos((8, 8).into()) + } + + pub fn origin(&self) -> ColumnPos { + self.column_pos((0, 0).into()) + } + + pub fn column_pos(&self, pos: ChunkColumnPos) -> ColumnPos { + (self.pos + pos.pos.as_ivec2()).into() + } + + pub fn chunk_block(&self, pos: ChunkBlockPos) -> BlockPos { + self.column_pos(pos.pos.xz().as_u8vec2().into()) + .block(i32::from(pos.pos.y)) + } + + pub fn block_offset(&self, x: i32, y: i32, z: i32) -> BlockPos { + ColumnPos::from(self.pos + IVec2::new(x, z)).block(y) + } + + pub fn column_offset(&self, x: i32, z: i32) -> ColumnPos { + ColumnPos::from(self.pos + IVec2::new(x, z)) + } + + pub fn x(&self) -> i32 { + self.pos.x >> 4 + } + + pub fn z(&self) -> i32 { + self.pos.y >> 4 + } + + pub fn pack(&self) -> u64 { + (((self.z() as u64) & ((1 << 22) - 1)) << 22) | ((self.x() as u64) & ((1 << 22) - 1)) + } +} + +impl Display for ChunkPos { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + IVec2::fmt(&(self.pos >> 4), f) + } +} + +impl Add<(i32, i32)> for ChunkPos { + type Output = ChunkPos; + + fn add(self, rhs: (i32, i32)) -> Self::Output { + let pos = self.pos + IVec2::from(rhs) * 16; + Self::Output { pos } + } +} + +pub struct ChunkColumnPos { + pos: U8Vec2, +} + +impl ChunkColumnPos { + pub const fn new(x: u8, z: u8) -> Self { + assert!(x < 16); + assert!(z < 16); + Self { + pos: U8Vec2::new(x, z), + } + } +} + +impl From for ChunkColumnPos { + fn from(pos: ColumnPos) -> Self { + Self { + pos: pos.pos.rem_euclid((16, 16).into()).as_u8vec2(), + } + } +} + +impl From for ChunkColumnPos { + fn from(pos: U8Vec2) -> Self { + assert!(pos.x < 16); + assert!(pos.y < 16); + Self { pos } + } +} + +impl From<(u8, u8)> for ChunkColumnPos { + fn from(pos: (u8, u8)) -> Self { + assert!(pos.0 < 16); + assert!(pos.1 < 16); + Self { pos: pos.into() } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ChunkBlockPos { + pos: I16Vec3, +} + +impl From<(u8, i16, u8)> for ChunkBlockPos { + fn from(pos: (u8, i16, u8)) -> Self { + Self::new(pos.0, pos.1, pos.2) + } +} + +impl ChunkBlockPos { + pub const fn new(x: u8, y: i16, z: u8) -> Self { + assert!(x < 16); + assert!(z < 16); + Self { + pos: I16Vec3::new(x as i16, y, z as i16), + } + } + + pub fn section_block_pos(&self) -> SectionBlockPos { + SectionBlockPos { + pos: self.pos.rem_euclid((16, 16, 16).into()).as_u8vec3(), + } + } + + pub fn section(&self) -> i8 { + self.pos.y.div_euclid(16) as i8 + } +} + +#[derive(Clone, Copy)] +pub struct ColumnPos { + pos: IVec2, +} + +impl ColumnPos { + pub fn new(x: i32, z: i32) -> Self { + Self { pos: (x, z).into() } + } + + pub fn block(self, y: i32) -> BlockPos { + BlockPos { + pos: self.pos.xxy().with_y(y), + } + } + + pub fn chunk(self) -> ChunkPos { + ChunkPos::new(self.pos.x.div_euclid(16), self.pos.y.div_euclid(16)) + } + + pub fn x(&self) -> i32 { + self.pos.x + } + + pub fn z(&self) -> i32 { + self.pos.y + } +} + +impl From for ColumnPos { + fn from(pos: IVec2) -> Self { + Self { pos } + } +} + +pub struct SectionPos { + pos: IVec3, +} + +impl SectionPos { + pub fn chunk(&self) -> ChunkPos { + ChunkPos { pos: self.pos.xz() } + } +} + +#[derive(Clone, Copy)] +pub struct SectionBlockPos { + pos: U8Vec3, +} + +impl SectionBlockPos { + /// Packed representation (big endian): 0x0yzx + /// So the max value is 0xfff or 4095 + pub fn pack(&self) -> u16 { + (self.pos.y as u16) << 8 | (self.pos.z as u16) << 4 | self.pos.x as u16 + } +} diff --git a/src/lib/world_gen/src/biomes/plains.rs b/src/lib/world_gen/src/biomes/plains.rs index 02c827473..191c0881b 100644 --- a/src/lib/world_gen/src/biomes/plains.rs +++ b/src/lib/world_gen/src/biomes/plains.rs @@ -4,6 +4,7 @@ use ferrumc_macros::block; use ferrumc_world::block_state_id::BlockStateId; use ferrumc_world::chunk_format::Chunk; use ferrumc_world::edit_batch::EditBatch; +use ferrumc_world::pos::{BlockPos, ChunkColumnPos, ChunkHeight, ChunkPos}; pub(crate) struct PlainsBiome; @@ -18,11 +19,10 @@ impl BiomeGenerator for PlainsBiome { fn generate_chunk( &self, - x: i32, - z: i32, + pos: ChunkPos, noise: &NoiseGenerator, ) -> Result { - let mut chunk = Chunk::new(x, z, "overworld".to_string()); + let mut chunk = Chunk::new(ChunkHeight::new(-64, 384)); let mut heights = vec![]; let stone = block!("stone"); // just to test the macro @@ -32,11 +32,12 @@ impl BiomeGenerator for PlainsBiome { } // Then generate some heights - for chunk_x in 0..16i64 { - for chunk_z in 0..16i64 { - let global_x = i64::from(x) * 16 + chunk_x; - let global_z = i64::from(z) * 16 + chunk_z; - let height = noise.get_noise(global_x as f64, global_z as f64); + for chunk_x in 0..16 { + for chunk_z in 0..16 { + let curr_pos = pos.column_pos(ChunkColumnPos::new(chunk_x, chunk_z)); + let global_x = curr_pos.x(); + let global_z = curr_pos.z(); + let height = noise.get_noise(f64::from(global_x), f64::from(global_z)); let height = (height * 64.0) as i32 + 64; heights.push((global_x, global_z, height)); } @@ -57,16 +58,14 @@ impl BiomeGenerator for PlainsBiome { for y in 0..height { if y + above_filled_sections <= 64 { batch.set_block( - global_x as i32 & 0xF, - y + above_filled_sections, - global_z as i32 & 0xF, + BlockPos::of(global_x, y + above_filled_sections, global_z) + .chunk_block_pos(), block!("sand"), ); } else { batch.set_block( - global_x as i32 & 0xF, - y + above_filled_sections, - global_z as i32 & 0xF, + BlockPos::of(global_x, y + above_filled_sections, global_z) + .chunk_block_pos(), block!("grass_block", {snowy: false}), ); } @@ -88,7 +87,11 @@ mod test { fn test_is_ok() { let generator = PlainsBiome {}; let noise = NoiseGenerator::new(0); - assert!(generator.generate_chunk(0, 0, &noise).is_ok()); + assert!( + generator + .generate_chunk(ChunkPos::new(0, 0), &noise) + .is_ok() + ); } #[test] @@ -96,9 +99,13 @@ mod test { let generator = PlainsBiome {}; let noise = NoiseGenerator::new(0); for _ in 0..100 { - let x = rand::random::(); - let z = rand::random::(); - assert!(generator.generate_chunk(x, z, &noise).is_ok()); + let x = rand::random::() & ((1 << 22) - 1); + let z = rand::random::() & ((1 << 22) - 1); + assert!( + generator + .generate_chunk(ChunkPos::new(x, z), &noise) + .is_ok() + ); } } @@ -108,12 +115,12 @@ mod test { let noise = NoiseGenerator::new(0); assert!( generator - .generate_chunk(1610612735, 1610612735, &noise) + .generate_chunk(ChunkPos::new((1 << 22) - 1, (1 << 22) - 1), &noise) .is_ok() ); assert!( generator - .generate_chunk(-1610612735, -1610612735, &noise) + .generate_chunk(ChunkPos::new(-((1 << 22) - 1), -((1 << 22) - 1)), &noise) .is_ok() ); } @@ -124,7 +131,11 @@ mod test { let generator = PlainsBiome {}; let seed = rand::random::(); let noise = NoiseGenerator::new(seed); - assert!(generator.generate_chunk(0, 0, &noise).is_ok()); + assert!( + generator + .generate_chunk(ChunkPos::new(0, 0), &noise) + .is_ok() + ); } } } diff --git a/src/lib/world_gen/src/lib.rs b/src/lib/world_gen/src/lib.rs index 30bbbb61e..2d9a3a53d 100644 --- a/src/lib/world_gen/src/lib.rs +++ b/src/lib/world_gen/src/lib.rs @@ -2,7 +2,7 @@ mod biomes; pub mod errors; use crate::errors::WorldGenError; -use ferrumc_world::chunk_format::Chunk; +use ferrumc_world::{chunk_format::Chunk, pos::ChunkPos}; use noise::{Clamp, NoiseFn, OpenSimplex}; /// Trait for generating a biome @@ -11,12 +11,8 @@ use noise::{Clamp, NoiseFn, OpenSimplex}; pub(crate) trait BiomeGenerator { fn _biome_id(&self) -> u8; fn _biome_name(&self) -> String; - fn generate_chunk( - &self, - x: i32, - z: i32, - noise: &NoiseGenerator, - ) -> Result; + fn generate_chunk(&self, pos: ChunkPos, noise: &NoiseGenerator) + -> Result; } pub(crate) struct NoiseGenerator { @@ -57,13 +53,13 @@ impl WorldGenerator { } } - fn get_biome(&self, _x: i32, _z: i32) -> Box { + fn get_biome(&self, _pos: ChunkPos) -> Box { // Implement biome selection here Box::new(biomes::plains::PlainsBiome) } - pub fn generate_chunk(&self, x: i32, z: i32) -> Result { - let biome = self.get_biome(x, z); - biome.generate_chunk(x, z, &self.noise_generator) + pub fn generate_chunk(&self, pos: ChunkPos) -> Result { + let biome = self.get_biome(pos); + biome.generate_chunk(pos, &self.noise_generator) } } From a46fb5ce7a5b89e820a421505d2f242e4cf44422 Mon Sep 17 00:00:00 2001 From: Tongue_chaude <145228731+Tonguechaude@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:57:17 +0100 Subject: [PATCH 10/23] feat: allow player to swim (#290) * feat: allow player to swim * fix : use block!() instead of hard coded value * fix : remove OOP nitpicks * fix : get_block_and_fetch_call * fix : use DVec3 and floor after * fix : use match_block! instead of a match! with a block! thx @ReCore-sys --- src/bin/src/systems/mod.rs | 2 + src/bin/src/systems/new_connections.rs | 2 + src/bin/src/systems/player_swimming.rs | 87 +++++++++++++++++++ src/lib/components/src/player/mod.rs | 1 + .../components/src/player/player_bundle.rs | 5 +- src/lib/components/src/player/swimming.rs | 7 ++ .../src/packets/outgoing/entity_metadata.rs | 24 +++++ 7 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/bin/src/systems/player_swimming.rs create mode 100644 src/lib/components/src/player/swimming.rs diff --git a/src/bin/src/systems/mod.rs b/src/bin/src/systems/mod.rs index 91346ee90..a310d7e14 100644 --- a/src/bin/src/systems/mod.rs +++ b/src/bin/src/systems/mod.rs @@ -6,6 +6,7 @@ pub mod lan_pinger; pub mod listeners; mod mq; pub mod new_connections; +mod player_swimming; pub mod shutdown_systems; pub mod world_sync; @@ -15,6 +16,7 @@ pub fn register_game_systems(schedule: &mut bevy_ecs::schedule::Schedule) { schedule.add_systems(chunk_calculator::handle); schedule.add_systems(chunk_sending::handle); schedule.add_systems(mq::process); + schedule.add_systems(player_swimming::detect_player_swimming); // Should always be last schedule.add_systems(connection_killer::connection_killer); diff --git a/src/bin/src/systems/new_connections.rs b/src/bin/src/systems/new_connections.rs index c9f1f8509..3a86bc6dc 100644 --- a/src/bin/src/systems/new_connections.rs +++ b/src/bin/src/systems/new_connections.rs @@ -10,6 +10,7 @@ use ferrumc_components::{ gameplay_state::ender_chest::EnderChest, hunger::Hunger, player_bundle::PlayerBundle, + swimming::SwimmingState, }, }; use ferrumc_core::{ @@ -102,6 +103,7 @@ pub fn accept_new_connections( hunger, experience, active_effects, + swimming: SwimmingState::default(), }; // --- 3. Spawn the PlayerBundle, then .insert() the network components --- diff --git a/src/bin/src/systems/player_swimming.rs b/src/bin/src/systems/player_swimming.rs new file mode 100644 index 000000000..e15e2efb4 --- /dev/null +++ b/src/bin/src/systems/player_swimming.rs @@ -0,0 +1,87 @@ +use bevy_ecs::prelude::*; +use bevy_math::DVec3; +use ferrumc_components::player::swimming::SwimmingState; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_core::transform::position::Position; +use ferrumc_macros::match_block; +use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::outgoing::entity_metadata::{EntityMetadata, EntityMetadataPacket}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::GlobalStateResource; +use ferrumc_world::block_state_id::BlockStateId; +use ferrumc_world::pos::BlockPos; +use tracing::error; + +/// Height of player's eyes from feet (blocks) +const PLAYER_EYE_HEIGHT: f64 = 1.62; + +/// Check if a player is in water by testing at eye level +fn is_player_in_water(state: &ferrumc_state::GlobalState, pos: &Position) -> bool { + let eye_pos = DVec3::new(pos.x, pos.y + PLAYER_EYE_HEIGHT, pos.z) + .floor() + .as_ivec3(); + + let pos = BlockPos::of(eye_pos.x, eye_pos.y, eye_pos.z); + + state + .world + .get_block_and_fetch(pos, "overworld") + .map(|current_block| match_block!("water", current_block)) + .unwrap_or(false) +} + +/// System that detects when players enter/exit water and updates their swimming state +/// Also broadcasts the swimming pose to all connected clients +pub fn detect_player_swimming( + mut swimmers: Query<(&PlayerIdentity, &Position, &mut SwimmingState)>, + all_connections: Query<(Entity, &StreamWriter)>, + state: Res, +) { + for (identity, pos, mut swimming_state) in swimmers.iter_mut() { + let in_water = is_player_in_water(&state.0, pos); + + if in_water && !swimming_state.is_swimming { + swimming_state.is_swimming = true; + + let entity_id = VarInt::new(identity.short_uuid); + let packet = EntityMetadataPacket::new( + entity_id, + [ + EntityMetadata::entity_swimming_state(), + EntityMetadata::entity_swimming_pose(), + ], + ); + + broadcast_metadata(&packet, &all_connections, &state); + } else if !in_water && swimming_state.is_swimming { + swimming_state.is_swimming = false; + + let entity_id = VarInt::new(identity.short_uuid); + let packet = EntityMetadataPacket::new( + entity_id, + [ + EntityMetadata::entity_clear_state(), + EntityMetadata::entity_standing(), + ], + ); + + broadcast_metadata(&packet, &all_connections, &state); + } + } +} + +/// Helper function to broadcast entity metadata to all connected players +fn broadcast_metadata( + packet: &EntityMetadataPacket, + connections: &Query<(Entity, &StreamWriter)>, + state: &GlobalStateResource, +) { + for (entity, conn) in connections { + if !state.0.players.is_connected(entity) { + continue; + } + if let Err(err) = conn.send_packet_ref(packet) { + error!("Failed to send entity metadata packet: {:?}", err); + } + } +} diff --git a/src/lib/components/src/player/mod.rs b/src/lib/components/src/player/mod.rs index a0fbb6b22..baad4f574 100644 --- a/src/lib/components/src/player/mod.rs +++ b/src/lib/components/src/player/mod.rs @@ -5,4 +5,5 @@ pub mod gamemode; pub mod gameplay_state; pub mod hunger; pub mod player_bundle; +pub mod swimming; pub mod view_distance; diff --git a/src/lib/components/src/player/player_bundle.rs b/src/lib/components/src/player/player_bundle.rs index b474e71f4..9a08355d3 100644 --- a/src/lib/components/src/player/player_bundle.rs +++ b/src/lib/components/src/player/player_bundle.rs @@ -3,7 +3,7 @@ use crate::{ health::Health, player::{ abilities::PlayerAbilities, experience::Experience, gamemode::GameModeComponent, - gameplay_state::ender_chest::EnderChest, hunger::Hunger, + gameplay_state::ender_chest::EnderChest, hunger::Hunger, swimming::SwimmingState, }, }; use bevy_ecs::prelude::Bundle; @@ -40,4 +40,7 @@ pub struct PlayerBundle { pub hunger: Hunger, pub experience: Experience, pub active_effects: ActiveEffects, + + // Movement State + pub swimming: SwimmingState, } diff --git a/src/lib/components/src/player/swimming.rs b/src/lib/components/src/player/swimming.rs new file mode 100644 index 000000000..22535fb69 --- /dev/null +++ b/src/lib/components/src/player/swimming.rs @@ -0,0 +1,7 @@ +use bevy_ecs::prelude::Component; + +/// Component tracking whether a player is currently swimming +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct SwimmingState { + pub is_swimming: bool, +} diff --git a/src/lib/net/src/packets/outgoing/entity_metadata.rs b/src/lib/net/src/packets/outgoing/entity_metadata.rs index 96ad59232..25d03c0e9 100644 --- a/src/lib/net/src/packets/outgoing/entity_metadata.rs +++ b/src/lib/net/src/packets/outgoing/entity_metadata.rs @@ -89,6 +89,30 @@ pub mod constructors { EntityMetadataValue::Entity6(EntityPose::Standing), ) } + + /// Entity state with swimming bit set + pub fn entity_swimming_state() -> Self { + Self::new( + EntityMetadataIndexType::Byte, + EntityMetadataValue::Entity0(EntityStateMask::from_state(EntityState::Swimming)), + ) + } + + /// Entity in swimming pose + pub fn entity_swimming_pose() -> Self { + Self::new( + EntityMetadataIndexType::Pose, + EntityMetadataValue::Entity6(EntityPose::Swimming), + ) + } + + /// Entity state with all flags cleared (default state) + pub fn entity_clear_state() -> Self { + Self::new( + EntityMetadataIndexType::Byte, + EntityMetadataValue::Entity0(EntityStateMask::new()), + ) + } } } From dbbd06a3c1deb11b76aff678bf7687741d57129a Mon Sep 17 00:00:00 2001 From: Outspending Date: Sun, 21 Dec 2025 22:54:32 -0700 Subject: [PATCH 11/23] feat: Server Statistics (#296) * core components * config + clippy fix * fixed peak memory * Refactor TPS and performance monitoring APIs Replaces fixed-window TPS and percentile methods with parameterized versions using Duration and percentile arguments. Updates command and resource registration code to use new APIs, and adds system support checks for performance statistics. Also adds Kilobytes to MemoryUnit and updates dependencies. * Refactor ServerPerformance::new to return Self instead of Option Changed ServerPerformance::new to always return a ServerPerformance instance instead of an Option. Updated resource registration to match the new signature, simplifying resource insertion and removing unnecessary conditional logic. --- .etc/example-config.toml | 1 - Cargo.toml | 3 + src/bin/Cargo.toml | 1 + src/bin/src/game_loop.rs | 18 ++++ src/bin/src/register_resources.rs | 3 + src/lib/default_commands/Cargo.toml | 1 + src/lib/default_commands/src/lib.rs | 1 + src/lib/default_commands/src/tps.rs | 136 ++++++++++++++++++++++++++++ src/lib/performance/Cargo.toml | 13 +++ src/lib/performance/src/lib.rs | 56 ++++++++++++ src/lib/performance/src/memory.rs | 70 ++++++++++++++ src/lib/performance/src/tick.rs | 35 +++++++ src/lib/performance/src/tps.rs | 112 +++++++++++++++++++++++ 13 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 src/lib/default_commands/src/tps.rs create mode 100644 src/lib/performance/Cargo.toml create mode 100644 src/lib/performance/src/lib.rs create mode 100644 src/lib/performance/src/memory.rs create mode 100644 src/lib/performance/src/tick.rs create mode 100644 src/lib/performance/src/tps.rs diff --git a/.etc/example-config.toml b/.etc/example-config.toml index bbf7870c2..27f96bc26 100644 --- a/.etc/example-config.toml +++ b/.etc/example-config.toml @@ -39,4 +39,3 @@ map_size = 1_000 cache_ttl = 60 # How big the cache can be in kb. cache_capacity = 20_000 - diff --git a/Cargo.toml b/Cargo.toml index 1a0528d62..e3e207d26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "src/lib/messages", "src/lib/data", "src/lib/entities", + "src/lib/performance", ] [workspace.package] @@ -104,6 +105,7 @@ ferrumc-logging = { path = "src/lib/utils/logging" } ferrumc-macros = { path = "src/lib/derive_macros" } ferrumc-nbt = { path = "src/lib/adapters/nbt" } ferrumc-net = { path = "src/lib/net" } +ferrumc-performance = { path = "src/lib/performance" } ferrumc-net-codec = { path = "src/lib/net/crates/codec" } ferrumc-net-encryption = { path = "src/lib/net/crates/encryption" } ferrumc-plugins = { path = "src/lib/plugins" } @@ -227,6 +229,7 @@ typename = "0.1.2" bevy_ecs = { version = "0.17.2", features = ["multi_threaded", "trace"] } bevy_math = "0.16.1" once_cell = "1.21.3" +sysinfo = "0.37.2" # I/O memmap2 = "0.9.8" diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 85d01dc61..6511ad19d 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -17,6 +17,7 @@ ferrumc-registry = { workspace = true } ferrumc-messages = { workspace = true } ferrumc-components = { workspace = true } ferrumc-net = { workspace = true } +ferrumc-performance = { workspace = true } ferrumc-net-codec = { workspace = true } ferrumc-plugins = { workspace = true } ferrumc-storage = { workspace = true } diff --git a/src/bin/src/game_loop.rs b/src/bin/src/game_loop.rs index bae60041f..910f7275b 100644 --- a/src/bin/src/game_loop.rs +++ b/src/bin/src/game_loop.rs @@ -22,6 +22,8 @@ use ferrumc_config::server_config::get_global_config; use ferrumc_net::connection::{handle_connection, NewConnection}; use ferrumc_net::server::create_server_listener; use ferrumc_net::PacketSender; +use ferrumc_performance::tick::TickData; +use ferrumc_performance::ServerPerformance; use ferrumc_scheduler::MissedTickBehavior; use ferrumc_scheduler::{drain_registered_schedules, Scheduler, TimedSchedule}; use ferrumc_state::{GlobalState, GlobalStateResource}; @@ -114,11 +116,14 @@ pub fn start_game_loop(global_state: GlobalState) -> Result<(), BinaryError> { // This prevents starvation if we fall behind (e.g., after a lag spike). const MAX_GLOBAL_CATCH_UP: usize = 64; + let tick_zero = Instant::now(); + // Main loop - runs until shutdown flag is set (e.g., via Ctrl+C or /stop command) while !global_state .shut_down .load(std::sync::atomic::Ordering::Relaxed) { + let tick_start = Instant::now(); let mut ran_any = false; let mut ran_count = 0; @@ -182,9 +187,22 @@ pub fn start_game_loop(global_state: GlobalState) -> Result<(), BinaryError> { ran_count += 1; } + let tick_duration = tick_start.elapsed(); + // If no schedules were ready, sleep until the next one is due + // If schedules were ran, store tick data. if !ran_any { timed.park_until_next_due(); + } else { + let tick_data = TickData { + start_ns: tick_zero.elapsed().as_nanos(), + duration_ns: tick_duration.as_nanos(), + entity_count: 0, + ran_count, + }; + + let mut performance = ecs_world.resource_mut::(); + performance.tps.record_tick(tick_data); } } diff --git a/src/bin/src/register_resources.rs b/src/bin/src/register_resources.rs index e94e1194c..be7c20b49 100644 --- a/src/bin/src/register_resources.rs +++ b/src/bin/src/register_resources.rs @@ -1,8 +1,10 @@ use crate::systems::new_connections::NewConnectionRecv; use bevy_ecs::prelude::World; use crossbeam_channel::Receiver; +use ferrumc_config::server_config::get_global_config; use ferrumc_core::chunks::world_sync_tracker::WorldSyncTracker; use ferrumc_net::connection::NewConnection; +use ferrumc_performance::ServerPerformance; use ferrumc_state::GlobalStateResource; pub fn register_resources( @@ -15,4 +17,5 @@ pub fn register_resources( world.insert_resource(WorldSyncTracker { last_synced: std::time::Instant::now(), }); + world.insert_resource(ServerPerformance::new(get_global_config().tps)); } diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index d2c55e067..38f6963c3 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -11,6 +11,7 @@ ferrumc-macros = { workspace = true } ferrumc-text = { workspace = true } ferrumc-core = { workspace = true } ferrumc-net = { workspace = true } +ferrumc-performance = { workspace = true } ferrumc-entities = { workspace = true } ctor = { workspace = true } diff --git a/src/lib/default_commands/src/lib.rs b/src/lib/default_commands/src/lib.rs index c79c5be15..06d499074 100644 --- a/src/lib/default_commands/src/lib.rs +++ b/src/lib/default_commands/src/lib.rs @@ -3,6 +3,7 @@ pub mod fly; pub mod gamemode; pub mod nested; pub mod spawn; +pub mod tps; /// Static library initialisation shenanigans. pub fn init() {} diff --git a/src/lib/default_commands/src/tps.rs b/src/lib/default_commands/src/tps.rs new file mode 100644 index 000000000..ad81338aa --- /dev/null +++ b/src/lib/default_commands/src/tps.rs @@ -0,0 +1,136 @@ +use std::time::Duration; + +use bevy_ecs::system::ResMut; +use ferrumc_commands::Sender; +use ferrumc_macros::command; +use ferrumc_performance::{memory::MemoryUnit, ServerPerformance}; +use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder}; + +#[command("tps")] +fn tps_command(#[sender] sender: Sender, performance_res: ResMut) { + let performance = performance_res.into_inner(); + + let tps = &performance.tps; + let (current, peak) = performance.memory.get_memory(MemoryUnit::Megabytes); + + sender.send_message( + TextComponentBuilder::new("Server Performance\n") + .color(NamedColor::Gray) + // TPS section + .extra( + TextComponentBuilder::new("TPS ") + .color(NamedColor::Gray) + .build(), + ) + .extra(get_tps_component(tps.tps(Duration::from_secs(1)))) + .extra( + TextComponentBuilder::new("| ") + .color(NamedColor::DarkGray) + .build(), + ) + .extra(get_tps_component(tps.tps(Duration::from_secs(5)))) + .extra( + TextComponentBuilder::new("| ") + .color(NamedColor::DarkGray) + .build(), + ) + .extra(get_tps_component(tps.tps(Duration::from_secs(15)))) + .extra( + TextComponentBuilder::new(" (1s / 5s / 15s)\n\n") + .color(NamedColor::Gray) + .build(), + ) + // Tick duration section + .extra( + TextComponentBuilder::new("Tick Time (ms)\n") + .color(NamedColor::Gray) + .build(), + ) + .extra( + TextComponentBuilder::new("avg ") + .color(NamedColor::Gray) + .build(), + ) + .extra(get_percentile_component(tps.avg_tick_ms())) + .extra( + TextComponentBuilder::new("p50 ") + .color(NamedColor::Gray) + .build(), + ) + .extra(get_percentile_component(tps.tick_duration(0.50))) + .extra( + TextComponentBuilder::new("p95 ") + .color(NamedColor::Gray) + .build(), + ) + .extra(get_percentile_component(tps.tick_duration(0.95))) + .extra( + TextComponentBuilder::new("p99 ") + .color(NamedColor::Gray) + .build(), + ) + .extra(get_percentile_component(tps.tick_duration(0.99))) + // Memory section + .extra(TextComponent::from("\n\n")) + .extra( + TextComponentBuilder::new("Memory (MB)\n") + .color(NamedColor::Gray) + .build(), + ) + .extra( + TextComponentBuilder::new("used ") + .color(NamedColor::Gray) + .build(), + ) + .extra( + TextComponentBuilder::new(format!("{}MB", current)) + .color(NamedColor::White) + .build(), + ) + .extra( + TextComponentBuilder::new(" | ") + .color(NamedColor::DarkGray) + .build(), + ) + .extra( + TextComponentBuilder::new("peak ") + .color(NamedColor::Gray) + .build(), + ) + .extra( + TextComponentBuilder::new(format!("{}MB", peak)) + .color(NamedColor::White) + .build(), + ) + .build(), + false, + ); +} + +fn get_tps_component(tps: f32) -> TextComponent { + let color = if tps < 14.0 { + NamedColor::Red + } else if tps < 16.0 { + NamedColor::Yellow + } else { + NamedColor::Green + }; + + TextComponentBuilder::new(format!("{:.2} ", tps)) + .color(color) + .build() +} + +fn get_percentile_component(percentile: f64) -> TextComponent { + let color = if percentile > 100.0 { + NamedColor::Red + } else if percentile > 50.0 { + NamedColor::Yellow + } else { + NamedColor::Green + }; + + TextComponentBuilder::new(format!("{:.2}ms ", percentile)) + .color(color) + .build() +} diff --git a/src/lib/performance/Cargo.toml b/src/lib/performance/Cargo.toml new file mode 100644 index 000000000..2816eda71 --- /dev/null +++ b/src/lib/performance/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ferrumc-performance" +description = "Stores all performance variables and calculates it." +edition.workspace = true +version.workspace = true + +[dependencies] +bevy_ecs = { workspace = true } +sysinfo = { workspace = true } +tracing = { workspace = true } + +[lints] +workspace = true diff --git a/src/lib/performance/src/lib.rs b/src/lib/performance/src/lib.rs new file mode 100644 index 000000000..103e96605 --- /dev/null +++ b/src/lib/performance/src/lib.rs @@ -0,0 +1,56 @@ +use bevy_ecs::resource::Resource; +use tracing::warn; + +use crate::{memory::MemoryUsage, tps::TPSMonitor}; + +pub mod memory; +pub mod tick; +pub mod tps; + +pub const WINDOW_SECONDS: usize = 60; + +/// Core ECS resource for all server performance metrics. +/// +/// This resource is updated once per tick by the main scheduler +/// loop and can be queried by commands, debug tools, or plugins. +/// As shown below. +/// +/// For grabbing Memory Usage it has to be mutable. +/// +/// ```rs +/// fn test(performance: Res) { +/// let tps = &performance.tps; +/// +/// tps.tps(Duration::from_secs(1)); +/// tps.tick_duration(0.50); +/// } +/// ``` +/// +/// ```rs +/// fn test(performance: ResMut) { +/// let (current, peak) = performance.memory.get_memory(MemoryUnit::Megabytes); +/// } +/// ``` +/// +/// Currently tracks: +/// - Tick durations +/// - Rolling TPS (1s / 5s / 15s windows) +/// - Memory Usage (Current / Peak) +#[derive(Resource)] +pub struct ServerPerformance { + pub tps: TPSMonitor, + pub memory: MemoryUsage, +} + +impl ServerPerformance { + pub fn new(tps: u32) -> Self { + if !sysinfo::IS_SUPPORTED_SYSTEM { + warn!("System does not support 'sysinfo', disabling server performance statisics."); + } + + Self { + tps: TPSMonitor::new(tps), + memory: MemoryUsage::default(), + } + } +} diff --git a/src/lib/performance/src/memory.rs b/src/lib/performance/src/memory.rs new file mode 100644 index 000000000..3496a573f --- /dev/null +++ b/src/lib/performance/src/memory.rs @@ -0,0 +1,70 @@ +use sysinfo::{Pid, ProcessesToUpdate, System}; + +pub enum MemoryUnit { + Bytes, + Kilobytes, + Megabytes, + Gigabytes, +} + +pub struct MemoryUsage { + sys: System, + process_pid: Pid, + max_rss_bytes: u64, +} + +impl Default for MemoryUsage { + fn default() -> Self { + Self { + sys: System::new(), + process_pid: sysinfo::get_current_pid().ok().unwrap(), + max_rss_bytes: 0, + } + } +} + +impl MemoryUsage { + fn get_usage_internal(&mut self) -> Option { + self.sys + .refresh_processes(ProcessesToUpdate::Some(&[self.process_pid]), true); + + let process = self.sys.process(self.process_pid)?; + + let rss_bytes = { + let raw = process.memory(); + + #[cfg(windows)] + { + raw + } + + #[cfg(not(windows))] + { + raw * 1024 + } + }; + + self.max_rss_bytes = self.max_rss_bytes.max(rss_bytes); + Some(rss_bytes) + } + + fn convert(&self, bytes: u64, unit: &MemoryUnit) -> u64 { + match unit { + MemoryUnit::Bytes => bytes, + MemoryUnit::Kilobytes => bytes / 1024, + MemoryUnit::Megabytes => bytes / 1024 / 1024, + MemoryUnit::Gigabytes => bytes / 1024 / 1024 / 1024, + } + } + + pub fn get_memory(&mut self, unit: MemoryUnit) -> (u64, u64) { + // updates peak memory, so this has to go first. + let usage = match self.get_usage_internal() { + Some(bytes) => self.convert(bytes, &unit), + None => 0, + }; + let max = self.convert(self.max_rss_bytes, &unit); + + (usage, max) + } +} diff --git a/src/lib/performance/src/tick.rs b/src/lib/performance/src/tick.rs new file mode 100644 index 000000000..ea6056694 --- /dev/null +++ b/src/lib/performance/src/tick.rs @@ -0,0 +1,35 @@ +use std::collections::VecDeque; + +#[derive(Debug)] +pub struct TickData { + pub start_ns: u128, + pub duration_ns: u128, + pub entity_count: u32, + pub ran_count: usize, +} + +pub(crate) struct TickHistory { + buffer: VecDeque, + capacity: usize, +} + +impl TickHistory { + pub(crate) fn new(capacity: usize) -> Self { + Self { + buffer: VecDeque::with_capacity(capacity), + capacity, + } + } + + pub(crate) fn record(&mut self, data: TickData) { + if self.buffer.len().eq(&self.capacity) { + self.buffer.pop_front(); + } + self.buffer.push_back(data); + } + + /// Iterate newest → oldest + pub(crate) fn iter_rev(&self) -> impl Iterator { + self.buffer.iter().rev() + } +} diff --git a/src/lib/performance/src/tps.rs b/src/lib/performance/src/tps.rs new file mode 100644 index 000000000..f9d58d1ce --- /dev/null +++ b/src/lib/performance/src/tps.rs @@ -0,0 +1,112 @@ +use std::time::Duration; + +use crate::{ + tick::{TickData, TickHistory}, + WINDOW_SECONDS, +}; + +pub struct TPSMonitor { + targetted_tps: u32, + history: TickHistory, +} + +impl TPSMonitor { + pub fn new(tps: u32) -> Self { + Self { + targetted_tps: tps, + history: TickHistory::new(tps as usize * WINDOW_SECONDS), + } + } + + #[inline] + pub fn record_tick(&mut self, tick: TickData) { + self.history.record(tick); + } + + /// Core TPS calculation (time-windowed) + fn tps_window_ns(&self, window_ns: u128) -> f32 { + let mut elapsed_ns: u128 = 0; + let mut ticks: u64 = 0; + + for tick in self.history.iter_rev() { + elapsed_ns += tick.duration_ns; + ticks += 1; + + if elapsed_ns >= window_ns { + break; + } + } + + if ticks == 0 || elapsed_ns == 0 { + return self.targetted_tps as f32; + } + + let tps = (ticks as f64) / (elapsed_ns as f64 / 1_000_000_000.0); + tps.clamp(0.0, f64::from(self.targetted_tps)) as f32 + } + + fn collect_window_ns(&self, window_ns: u128) -> Vec { + let mut total = 0u128; + let mut out = Vec::with_capacity(32); + + for tick in self.history.iter_rev() { + total += tick.duration_ns; + out.push(tick.duration_ns); + + if total >= window_ns { + break; + } + } + + out + } + + fn percentile_ns(&self, mut samples: Vec, percentile: f64) -> Option { + if samples.is_empty() { + return None; + } + + let len = samples.len(); + let rank = ((percentile * len as f64).ceil() as usize) + .saturating_sub(1) + .min(len - 1); + + let (_, value, _) = samples.select_nth_unstable(rank); + Some(*value) + } + + fn percentile_ms(&self, percentile: f64, window_ns: u128) -> Option { + let samples = self.collect_window_ns(window_ns); + let ns = self.percentile_ns(samples, percentile)?; + Some(ns as f64 / 1_000_000.0) + } + + /// Average tick duration over the last second (ms) + pub fn avg_tick_ms(&self) -> f64 { + let mut elapsed_ns = 0u128; + let mut ticks = 0; + + for tick in self.history.iter_rev() { + elapsed_ns += tick.duration_ns; + ticks += 1; + + if elapsed_ns >= 1_000_000_000 { + break; + } + } + + if ticks == 0 { + return 0.0; + } + + (elapsed_ns as f64 / f64::from(ticks)) / 1_000_000.0 + } + + pub fn tps(&self, duration: Duration) -> f32 { + self.tps_window_ns(duration.as_nanos()) + } + + pub fn tick_duration(&self, percentile: f64) -> f64 { + self.percentile_ms(percentile, 1_000_000_000).unwrap_or(0.0) + } +} From f4613eee0a55c4d4450e268077eabd63524ffc87 Mon Sep 17 00:00:00 2001 From: Tongue_chaude <145228731+Tonguechaude@users.noreply.github.com> Date: Fri, 26 Dec 2025 13:03:07 +0100 Subject: [PATCH 12/23] feat : add gravity/collisions system (#276) * feat : add gravity/collisions system * fix : use bevy maths for vec operations * clean : tuple + refacto + add const for vertical water friction * fmt : cargo fmt * test : add tests for collision and lastsynced * Add example gravity and drag * Fleshed out drag * Added AABB and velocity * Moved physics over to individual bevy systems * Added proper gravity and some half assed collisions * Bump deps + clippy * Unit tests * fix : use axis based collision detection system * fix : use bevymaths instead of my shitty code * Properly lined up collisions * feat : add HasCollision markers * clippy * fmt + removed unneeded file * Removed tests * Fix deps * Fix broken rand/rsa --------- Co-authored-by: ReCore <66930138+ReCore-sys@users.noreply.github.com> --- Cargo.toml | 65 ++++--- docs/protocol-flow.md | 49 ++--- src/bin/Cargo.toml | 1 + src/bin/src/game_loop.rs | 2 + src/bin/src/register_messages.rs | 2 + src/bin/src/systems/listeners/entity_spawn.rs | 11 +- src/bin/src/systems/mobs/mod.rs | 1 + src/bin/src/systems/mobs/pig.rs | 9 + src/bin/src/systems/mod.rs | 5 + src/bin/src/systems/physics/collisions.rs | 142 +++++++++++++++ src/bin/src/systems/physics/drag.rs | 36 ++++ src/bin/src/systems/physics/gravity.rs | 66 +++++++ src/bin/src/systems/physics/mod.rs | 17 ++ src/bin/src/systems/physics/velocity.rs | 171 ++++++++++++++++++ src/bin/src/systems/send_entity_updates.rs | 93 ++++++++++ src/lib/core/src/identity/entity_identity.rs | 2 +- src/lib/core/src/transform/mod.rs | 1 + src/lib/core/src/transform/position.rs | 7 + src/lib/core/src/transform/velocity.rs | 59 ++++++ src/lib/default_commands/Cargo.toml | 4 +- src/lib/default_commands/src/spawn.rs | 41 +++-- src/lib/entities/Cargo.toml | 1 + src/lib/entities/src/bundles/pig.rs | 86 ++------- src/lib/entities/src/collision.rs | 1 + .../src/components/last_synced_position.rs | 93 ++++++++++ src/lib/entities/src/components/metadata.rs | 2 +- src/lib/entities/src/components/mod.rs | 2 + src/lib/entities/src/components/physical.rs | 62 +++++-- src/lib/entities/src/lib.rs | 4 +- src/lib/entities/src/markers.rs | 18 ++ src/lib/messages/src/entity_spawn.rs | 2 +- src/lib/messages/src/entity_update.rs | 4 + src/lib/messages/src/lib.rs | 2 + src/lib/net/crates/encryption/Cargo.toml | 2 +- src/lib/net/crates/encryption/src/lib.rs | 4 +- src/lib/physics/Cargo.toml | 10 + src/lib/physics/src/lib.rs | 16 ++ src/lib/utils/Cargo.toml | 3 +- src/lib/world/src/pos.rs | 24 ++- 39 files changed, 953 insertions(+), 167 deletions(-) create mode 100644 src/bin/src/systems/mobs/mod.rs create mode 100644 src/bin/src/systems/mobs/pig.rs create mode 100644 src/bin/src/systems/physics/collisions.rs create mode 100644 src/bin/src/systems/physics/drag.rs create mode 100644 src/bin/src/systems/physics/gravity.rs create mode 100644 src/bin/src/systems/physics/mod.rs create mode 100644 src/bin/src/systems/physics/velocity.rs create mode 100644 src/bin/src/systems/send_entity_updates.rs create mode 100644 src/lib/core/src/transform/velocity.rs create mode 100644 src/lib/entities/src/collision.rs create mode 100644 src/lib/entities/src/components/last_synced_position.rs create mode 100644 src/lib/entities/src/markers.rs create mode 100644 src/lib/messages/src/entity_update.rs create mode 100644 src/lib/physics/Cargo.toml create mode 100644 src/lib/physics/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e3e207d26..0f4fcf49a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "src/lib/messages", "src/lib/data", "src/lib/entities", + "src/lib/physics", "src/lib/performance", ] @@ -123,7 +124,8 @@ ferrumc-inventories = { path = "src/lib/inventories" } ferrumc-components = { path = "src/lib/components" } ferrumc-messages = { path = "src/lib/messages" } ferrumc-data = { path = "src/lib/data" } -ferrumc-entities = { path = "src/lib/entities"} +ferrumc-entities = { path = "src/lib/entities" } +ferrumc-physics = { path = "src/lib/physics" } # Asynchronous tokio = { version = "1.48.0", features = [ @@ -137,10 +139,10 @@ tokio = { version = "1.48.0", features = [ ], default-features = false } # Logging -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } -tracing-appender = "0.2.3" -log = "0.4.28" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +tracing-appender = "0.2.4" +log = "0.4.29" # Concurrency/Parallelism parking_lot = "0.12.5" @@ -150,18 +152,18 @@ rusty_pool = "0.7.0" crossbeam-queue = "0.3.12" # Network -ureq = "3.1.2" -reqwest = { version = "0.12.24", features = ["json"] } +ureq = "3.1.4" +reqwest = { version = "0.12.28", features = ["json"] } # Error handling thiserror = "2.0.17" # Cryptography -rand = "0.10.0-rc.0" +rand = "0.9.2" fnv = "1.0.7" wyhash = "0.6.0" ahash = "0.8.12" -rsa = { version = "0.10.0-rc.10" } +rsa = { version = "0.9.9" } aes = { version = "0.8.4" } cfb8 = "0.8.1" sha1 = "0.10.6" @@ -169,14 +171,12 @@ num-bigint = "0.4.6" # Encoding/Serialization serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" +serde_json = "1.0.146" serde_derive = "1.0.228" base64 = "0.22.1" -# When if bitcode's latest version has been changed, see if clippy still complains about -# src/lib/net/src/packets/outgoing/chunk_and_light_data.rs -#bitcode = "0.6.7" -#bitcode_derive = "0.6.7" +bitcode = "0.6.9" +bitcode_derive = "0.6.9" toml = "0.9.8" craftflow-nbt = "2.1.0" figment = { version = "0.10.19", features = ["toml", "env"] } @@ -187,60 +187,57 @@ byteorder = "1.5.0" # Data types dashmap = "7.0.0-rc2" -uuid = { version = "1.18.1", features = ["v4", "v3", "serde"] } -indexmap = { version = "2.12.0", features = ["serde"] } +uuid = { version = "1.19.0", features = ["v4", "v3", "serde"] } +indexmap = { version = "2.12.1", features = ["serde"] } +bimap = "0.6.3" # Macros lazy_static = "1.5.0" -quote = "1.0.41" -syn = "2.0.106" -proc-macro2 = "1.0.101" +quote = "1.0.42" +syn = "2.0.111" +proc-macro2 = "1.0.103" paste = "1.0.15" maplit = "1.0.2" macro_rules_attribute = "0.2.2" # Magic dhat = "0.3.3" -ctor = "0.6.0" +ctor = "0.6.3" # Compression/Decompression -flate2 = { version = "1.1.4", features = ["zlib"], default-features = false } +flate2 = { version = "1.1.5", features = ["zlib"], default-features = false } lzzzz = "2.0.0" yazi = "0.2.1" # Database -heed = "0.22.0" -moka = "0.12.11" +heed = "0.22.1-nested-rtxns-6" +moka = "0.12.12" # CLI -clap = "4.5.49" -indicatif = "0.18.0" +clap = "4.5.53" +indicatif = "0.18.3" colored = "3.0.0" # Misc deepsize = "0.2.0" page_size = "0.6.0" -enum-ordinalize = "4.3.0" +enum-ordinalize = "4.3.2" regex = "1.12.2" noise = "0.9.0" -ctrlc = "3.5.0" +ctrlc = "3.5.1" num_cpus = "1.17.0" typename = "0.1.2" -bevy_ecs = { version = "0.17.2", features = ["multi_threaded", "trace"] } -bevy_math = "0.16.1" +bevy_ecs = { version = "0.17.3", features = ["multi_threaded", "trace"] } +bevy_math = "0.17.3" once_cell = "1.21.3" sysinfo = "0.37.2" # I/O -memmap2 = "0.9.8" +memmap2 = "0.9.9" tempfile = "3.23.0" # Benchmarking criterion = { version = "0.7.0", features = ["html_reports"] } - -bitcode = { git = "https://github.com/SoftbearStudios/bitcode" } -bitcode_derive = { git = "https://github.com/SoftbearStudios/bitcode" } - phf = { version = "0.11", features = ["macros"] } phf_codegen = { version = "0.11" } diff --git a/docs/protocol-flow.md b/docs/protocol-flow.md index fbf24262e..e32325977 100644 --- a/docs/protocol-flow.md +++ b/docs/protocol-flow.md @@ -20,13 +20,13 @@ stateDiagram-v2 ## Connection States -| State | Purpose | Code Reference | -|-------|---------|----------------| -| **Handshake** | Initial packet determines intent | `ConnState::Handshake` in [`lib.rs`](../src/lib/net/src/lib.rs) | -| **Status** | Server list ping (MOTD, player count) | `ConnState::Status` | -| **Login** | Authentication, compression setup | `ConnState::Login` | -| **Configuration** | Registry data, resource packs | `ConnState::Configuration` | -| **Play** | Actual gameplay packets | `ConnState::Play` | +| State | Purpose | Code Reference | +|-------------------|---------------------------------------|-----------------------------------------------------------------| +| **Handshake** | Initial packet determines intent | `ConnState::Handshake` in [`lib.rs`](../src/lib/net/src/lib.rs) | +| **Status** | Server list ping (MOTD, player count) | `ConnState::Status` | +| **Login** | Authentication, compression setup | `ConnState::Login` | +| **Configuration** | Registry data, resource packs | `ConnState::Configuration` | +| **Play** | Actual gameplay packets | `ConnState::Play` | --- @@ -125,14 +125,14 @@ sequenceDiagram ### Code Flow -| Step | Function | File | -|------|----------|------| -| 1. TCP Accept | `tcp_conn_acceptor()` | [`game_loop.rs`](../src/bin/src/game_loop.rs#L309) | -| 2. Connection Handler | `handle_connection()` | [`connection.rs`](../src/lib/net/src/connection.rs#L253) | -| 3. Handshake | `handle_handshake()` | [`conn_init/mod.rs`](../src/lib/net/src/conn_init/mod.rs#L60) | -| 4. Login Sequence | `login()` | [`conn_init/login.rs`](../src/lib/net/src/conn_init/login.rs#L50) | -| 5. ECS Registration | `accept_new_connections()` | [`new_connections.rs`](../src/bin/src/systems/new_connections.rs) | -| 6. Packet Loop | `handle_connection()` recv loop | [`connection.rs`](../src/lib/net/src/connection.rs#L358) | +| Step | Function | File | +|-----------------------|---------------------------------|-------------------------------------------------------------------| +| 1. TCP Accept | `tcp_conn_acceptor()` | [`game_loop.rs`](../src/bin/src/game_loop.rs#L309) | +| 2. Connection Handler | `handle_connection()` | [`connection.rs`](../src/lib/net/src/connection.rs#L253) | +| 3. Handshake | `handle_handshake()` | [`conn_init/mod.rs`](../src/lib/net/src/conn_init/mod.rs#L60) | +| 4. Login Sequence | `login()` | [`conn_init/login.rs`](../src/lib/net/src/conn_init/login.rs#L50) | +| 5. ECS Registration | `accept_new_connections()` | [`new_connections.rs`](../src/bin/src/systems/new_connections.rs) | +| 6. Packet Loop | `handle_connection()` recv loop | [`connection.rs`](../src/lib/net/src/connection.rs#L358) | --- @@ -141,6 +141,7 @@ sequenceDiagram All packets follow this format: ### Uncompressed + ``` ┌──────────────┬────────────┬──────────────────┐ │ Length │ Packet ID │ Data │ @@ -149,6 +150,7 @@ All packets follow this format: ``` ### Compressed (when data length > threshold) + ``` ┌──────────────┬────────────────┬────────────────────────┐ │ Packet Len │ Data Length │ Compressed │ @@ -176,19 +178,19 @@ flowchart LR ### Key Components -| Component | Purpose | File | -|-----------|---------|------| -| `StreamWriter` | Async packet output | [`connection.rs`](../src/lib/net/src/connection.rs#L41) | -| `EncryptedReader` | AES-encrypted input | `ferrumc_net_encryption` | -| `PacketSkeleton` | Parsed packet frame | [`packet_skeleton.rs`](../src/lib/net/src/packets/incoming/packet_skeleton.rs) | -| `PacketSender` | Channel to ECS | [`lib.rs`](../src/lib/net/src/lib.rs) | +| Component | Purpose | File | +|-------------------|---------------------|--------------------------------------------------------------------------------| +| `StreamWriter` | Async packet output | [`connection.rs`](../src/lib/net/src/connection.rs#L41) | +| `EncryptedReader` | AES-encrypted input | `ferrumc_net_encryption` | +| `PacketSkeleton` | Parsed packet frame | [`packet_skeleton.rs`](../src/lib/net/src/packets/incoming/packet_skeleton.rs) | +| `PacketSender` | Channel to ECS | [`lib.rs`](../src/lib/net/src/lib.rs) | ### Packet Handler Registration Packets are dispatched via the `setup_packet_handling!` macro: ```rust -// src/lib/net/src/lib.rs +// src/lib/net/src/mod setup_packet_handling!("\\src\\packets\\incoming"); ``` @@ -211,6 +213,7 @@ flowchart TD ``` **Example**: Player join triggers `PlayerJoined` event: + - [`new_connections.rs`](../src/bin/src/systems/new_connections.rs) sends `PlayerJoined` - Listeners in [`register_gameplay_listeners()`](../src/bin/src/systems/listeners/mod.rs) react @@ -248,10 +251,12 @@ See [`game_loop.rs`](../src/bin/src/game_loop.rs) for the threading setup. ## Quick Reference ### Protocol Version + - **Minecraft**: 1.21.8 - **Protocol**: 772 - **Constant**: `PROTOCOL_VERSION_1_21_8` in [`conn_init/mod.rs`](../src/lib/net/src/conn_init/mod.rs#L31) ### External References + - [Minecraft Protocol Wiki](https://minecraft.wiki/w/Java_Edition_protocol/Packets) - [Protocol Version Numbers](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_version_numbers) diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 6511ad19d..1e76982c0 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -37,6 +37,7 @@ ferrumc-threadpool = { workspace = true } ferrumc-inventories = { workspace = true } ferrumc-data = { workspace = true } ferrumc-entities = { workspace = true } +ferrumc-physics = { workspace = true } bevy_math = { workspace = true } once_cell = { workspace = true } serde_json = { workspace = true } diff --git a/src/bin/src/game_loop.rs b/src/bin/src/game_loop.rs index 910f7275b..20c6a9db5 100644 --- a/src/bin/src/game_loop.rs +++ b/src/bin/src/game_loop.rs @@ -12,6 +12,7 @@ use crate::register_messages::register_messages; use crate::register_resources::register_resources; use crate::systems::lan_pinger::LanPinger; use crate::systems::listeners::register_gameplay_listeners; +use crate::systems::physics::register_physics; use crate::systems::register_game_systems; use crate::systems::shutdown_systems::register_shutdown_systems; use bevy_ecs::prelude::World; @@ -250,6 +251,7 @@ fn build_timed_scheduler() -> Scheduler { register_command_systems(s); // Process queued commands register_game_systems(s); // General game logic register_gameplay_listeners(s); // Event listeners for gameplay events + register_physics(s); // Physics systems (movement, collision, etc.) }; let tick_period = Duration::from_secs(1) / get_global_config().tps; timed.register( diff --git a/src/bin/src/register_messages.rs b/src/bin/src/register_messages.rs index 309b2f905..2dc450253 100644 --- a/src/bin/src/register_messages.rs +++ b/src/bin/src/register_messages.rs @@ -3,6 +3,7 @@ use bevy_ecs::prelude::World; use ferrumc_commands::messages::{CommandDispatched, ResolvedCommandDispatched}; use ferrumc_core::conn::force_player_recount_event::ForcePlayerRecount; use ferrumc_messages::chunk_calc::ChunkCalc; +use ferrumc_messages::entity_update::SendEntityUpdate; use ferrumc_messages::{ PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating, PlayerFinishedDigging, PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft, PlayerLeveledUp, @@ -30,4 +31,5 @@ pub fn register_messages(world: &mut World) { MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); } diff --git a/src/bin/src/systems/listeners/entity_spawn.rs b/src/bin/src/systems/listeners/entity_spawn.rs index b63774c30..5b52eabd2 100644 --- a/src/bin/src/systems/listeners/entity_spawn.rs +++ b/src/bin/src/systems/listeners/entity_spawn.rs @@ -4,6 +4,8 @@ use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; use ferrumc_entities::bundles::PigBundle; use ferrumc_entities::components::EntityMetadata; +use ferrumc_entities::markers::entity_types::Pig; +use ferrumc_entities::markers::{HasCollisions, HasGravity}; use ferrumc_messages::{EntityType, SpawnEntityCommand, SpawnEntityEvent}; use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::spawn_entity::SpawnEntityPacket; @@ -104,7 +106,14 @@ pub fn handle_spawn_entity(mut events: MessageReader, mut comm match event.entity_type { EntityType::Pig => { // Spawn the pig entity - let pig_entity = commands.spawn(PigBundle::new(event.position.clone())).id(); + let pig_entity = commands + .spawn(( + PigBundle::new(event.position.clone()), + Pig, + HasGravity, + HasCollisions, + )) + .id(); // Queue a deferred system to broadcast spawn packets after entity is fully spawned commands.queue(move |world: &mut World| { diff --git a/src/bin/src/systems/mobs/mod.rs b/src/bin/src/systems/mobs/mod.rs new file mode 100644 index 000000000..2de43ece3 --- /dev/null +++ b/src/bin/src/systems/mobs/mod.rs @@ -0,0 +1 @@ +mod pig; diff --git a/src/bin/src/systems/mobs/pig.rs b/src/bin/src/systems/mobs/pig.rs new file mode 100644 index 000000000..b4a14193a --- /dev/null +++ b/src/bin/src/systems/mobs/pig.rs @@ -0,0 +1,9 @@ +use bevy_ecs::prelude::{Query, With}; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::velocity::Velocity; +use ferrumc_entities::markers::entity_types::Pig; + +#[expect(dead_code, unused_variables)] +pub fn tick_pig(query: Query<(&Position, &Velocity), With>) { + // Pig AI logic would go here +} diff --git a/src/bin/src/systems/mod.rs b/src/bin/src/systems/mod.rs index a310d7e14..73c784036 100644 --- a/src/bin/src/systems/mod.rs +++ b/src/bin/src/systems/mod.rs @@ -4,9 +4,12 @@ pub mod connection_killer; pub mod keep_alive_system; pub mod lan_pinger; pub mod listeners; +pub mod mobs; mod mq; pub mod new_connections; +pub mod physics; mod player_swimming; +mod send_entity_updates; pub mod shutdown_systems; pub mod world_sync; @@ -18,6 +21,8 @@ pub fn register_game_systems(schedule: &mut bevy_ecs::schedule::Schedule) { schedule.add_systems(mq::process); schedule.add_systems(player_swimming::detect_player_swimming); + schedule.add_systems(send_entity_updates::handle); + // Should always be last schedule.add_systems(connection_killer::connection_killer); } diff --git a/src/bin/src/systems/physics/collisions.rs b/src/bin/src/systems/physics/collisions.rs new file mode 100644 index 000000000..67d38d1d4 --- /dev/null +++ b/src/bin/src/systems/physics/collisions.rs @@ -0,0 +1,142 @@ +use bevy_ecs::message::MessageWriter; +use bevy_ecs::prelude::{DetectChanges, Entity, Query, Res, With}; +use bevy_math::bounding::{Aabb3d, BoundingVolume}; +use bevy_math::{IVec3, Vec3A}; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::velocity::Velocity; +use ferrumc_entities::markers::HasCollisions; +use ferrumc_entities::PhysicalProperties; +use ferrumc_macros::match_block; +use ferrumc_messages::entity_update::SendEntityUpdate; +use ferrumc_state::{GlobalState, GlobalStateResource}; +use ferrumc_world::block_state_id::BlockStateId; +use ferrumc_world::pos::{ChunkBlockPos, ChunkPos}; +use tracing::debug; + +pub fn handle( + query: Query< + ( + Entity, + &mut Velocity, + &mut Position, + &PhysicalProperties, + &mut OnGround, + ), + With, + >, + mut writer: MessageWriter, + state: Res, +) { + for (eid, mut vel, mut pos, physical, mut grounded) in query { + if pos.is_changed() || vel.is_changed() { + // Figure out where the entity is going to be next tick + let next_pos = pos.coords.as_vec3a() + **vel; + let mut collided = false; + let mut hit_blocks = vec![]; + + // Merge the current and next bounding boxes to get the full area the entity will occupy + // This helps catch fast-moving entities that might skip through thin blocks + // At really high speeds this will create a very large bounding box, so further optimizations may be needed + let current_hitbox = Aabb3d { + min: physical.bounding_box.min + pos.coords.as_vec3a(), + max: physical.bounding_box.max + pos.coords.as_vec3a(), + }; + + let next_hitbox = Aabb3d { + min: physical.bounding_box.min + next_pos, + max: physical.bounding_box.max + next_pos, + }; + + let merged_hitbox = current_hitbox.merge(&next_hitbox); + + // Get the block positions that the entity's bounding box will occupy + let min_block_pos = merged_hitbox.min; + let max_block_pos = merged_hitbox.max; + + // Check each block in the bounding box for solidity + for x in min_block_pos.x.floor() as i32..=max_block_pos.x.floor() as i32 { + for y in min_block_pos.y.floor() as i32..=max_block_pos.y.floor() as i32 { + for z in min_block_pos.z.floor() as i32..=max_block_pos.z.floor() as i32 { + let block_pos = IVec3::new(x, y, z); + if is_solid_block(&state.0, block_pos) { + collided = true; + hit_blocks.push(block_pos); + if is_solid_block(&state.0, IVec3::new(x, y - 1, z)) && vel.y <= 0.0 { + grounded.0 = true; + } + } + } + } + } + // If a collision is detected, stop the entity's movement + if collided { + vel.vec = Vec3A::ZERO; + // Find the closest hit block to the entity's next position + hit_blocks.sort_by(|a, b| { + let dist_a = (a.as_vec3a() - next_pos).length_squared(); + let dist_b = (b.as_vec3a() - next_pos).length_squared(); + dist_a.partial_cmp(&dist_b).unwrap() + }); + let first_hit = hit_blocks.first().expect("At least one hit block expected"); + debug!( + "Entity collided at block position: {:?} going {}", + &hit_blocks, vel.vec + ); + + let block_aabb = Aabb3d { + min: first_hit.as_vec3a(), + max: (first_hit + IVec3::ONE).as_vec3a(), + }; + + let translated_bounding_box = Aabb3d { + min: physical.bounding_box.min + pos.coords.as_vec3a(), + max: physical.bounding_box.max + pos.coords.as_vec3a(), + }; + + // Get the closest point on the entity's bounding box to the block's AABB + let entity_collide_point = translated_bounding_box + .closest_point(block_aabb.center().as_dvec3().as_vec3a()); + + if entity_collide_point == block_aabb.center().as_dvec3().as_vec3a() { + continue; + } + + // Then we get the closest point on the block's AABB to the entity's collide point + let block_collide_point = block_aabb.closest_point(entity_collide_point); + + if block_collide_point == entity_collide_point { + continue; + } + + // The difference between these two points tells us how far apart the 2 colliding objects are + let collision_difference = entity_collide_point - block_collide_point; + + // We use this to nudge the entity out of the block along the smallest axis + pos.coords -= collision_difference.as_dvec3(); + } + + writer.write(SendEntityUpdate(eid)); + } + } +} + +pub fn is_solid_block(state: &GlobalState, pos: IVec3) -> bool { + state + .world + .load_chunk(ChunkPos::from(pos.as_dvec3()), "overworld") + .unwrap_or( + state + .terrain_generator + .generate_chunk(ChunkPos::from(pos.as_dvec3())) + .expect("Failed to generate chunk") + .into(), + ) + .get_block(ChunkBlockPos::from(pos)) + .map(|block_state| { + !match_block!("air", block_state) + && !match_block!("void_air", block_state) + && !match_block!("cave_air", block_state) + }) + .unwrap_or(false) +} diff --git a/src/bin/src/systems/physics/drag.rs b/src/bin/src/systems/physics/drag.rs new file mode 100644 index 000000000..19b08b48b --- /dev/null +++ b/src/bin/src/systems/physics/drag.rs @@ -0,0 +1,36 @@ +use bevy_ecs::prelude::{DetectChanges, Query, Res, With}; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::velocity::Velocity; +use ferrumc_entities::markers::HasWaterDrag; +use ferrumc_macros::match_block; +use ferrumc_state::GlobalStateResource; +use ferrumc_world::block_state_id::BlockStateId; +use ferrumc_world::pos::{ChunkBlockPos, ChunkPos}; + +pub fn handle( + mut query: Query<(&mut Velocity, &mut Position), With>, + state: Res, +) { + for (mut vel, pos) in query.iter_mut() { + if pos.is_changed() || vel.is_changed() { + let chunk_pos = ChunkPos::from(pos.coords); + let chunk = state.0.world.load_chunk(chunk_pos, "overworld").unwrap_or( + state + .0 + .terrain_generator + .generate_chunk(chunk_pos) + .expect("Failed to generate chunk") + .into(), + ); + let is_in_water = chunk + .get_block(ChunkBlockPos::from(pos.coords.as_ivec3())) + .map(|block| match_block!("water", block)) + .unwrap_or(false); + if is_in_water { + vel.y *= 0.98; + vel.x *= 0.91; + vel.z *= 0.91; + } + } + } +} diff --git a/src/bin/src/systems/physics/gravity.rs b/src/bin/src/systems/physics/gravity.rs new file mode 100644 index 000000000..f8a62a3b4 --- /dev/null +++ b/src/bin/src/systems/physics/gravity.rs @@ -0,0 +1,66 @@ +use bevy_ecs::prelude::{Query, With}; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::velocity::Velocity; +use ferrumc_entities::markers::HasGravity; +use ferrumc_physics::GRAVITY_ACCELERATION; + +// Just apply gravity to a mob's velocity. Application of velocity is handled elsewhere. +pub(crate) fn handle(mut entities: Query<(&mut Velocity, &OnGround), With>) { + for (mut vel, grounded) in entities.iter_mut() { + if grounded.0 { + continue; + } + // Apply gravity + vel.vec += GRAVITY_ACCELERATION; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy_ecs::prelude::*; + use bevy_math::Vec3A; + use ferrumc_core::transform::grounded::OnGround; + use ferrumc_core::transform::velocity::Velocity; + use ferrumc_entities::markers::HasGravity; + + #[test] + fn test_gravity_application() { + let mut world = World::new(); + let entity = world + .spawn((Velocity { vec: Vec3A::ZERO }, OnGround(false), HasGravity)) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + // Run the gravity system + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert!( + vel.vec.y < 0.0, + "Velocity Y should be negative after gravity application" + ); + } + + #[test] + fn test_no_gravity_when_grounded() { + let mut world = World::new(); + let entity = world + .spawn((Velocity { vec: Vec3A::ZERO }, OnGround(true), HasGravity)) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + // Run the gravity system + schedule.run(&mut world); + + let vel = world.get::(entity).unwrap(); + assert_eq!( + vel.vec.y, 0.0, + "Velocity Y should remain zero when grounded" + ); + } +} diff --git a/src/bin/src/systems/physics/mod.rs b/src/bin/src/systems/physics/mod.rs new file mode 100644 index 000000000..439b8700d --- /dev/null +++ b/src/bin/src/systems/physics/mod.rs @@ -0,0 +1,17 @@ +use bevy_ecs::schedule::IntoScheduleConfigs; +pub mod collisions; +pub mod drag; +pub mod gravity; +pub mod velocity; + +pub fn register_physics(schedule: &mut bevy_ecs::schedule::Schedule) { + schedule.add_systems( + ( + gravity::handle, + drag::handle, + velocity::handle, + collisions::handle, + ) + .chain(), + ); +} diff --git a/src/bin/src/systems/physics/velocity.rs b/src/bin/src/systems/physics/velocity.rs new file mode 100644 index 000000000..17be3e26a --- /dev/null +++ b/src/bin/src/systems/physics/velocity.rs @@ -0,0 +1,171 @@ +use bevy_ecs::prelude::Query; +use bevy_math::Vec3A; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::velocity::Velocity; + +pub fn handle(mut query: Query<(&Velocity, &mut Position)>) { + for (vel, mut pos) in query.iter_mut() { + if **vel == Vec3A::ZERO { + continue; + } + pos.coords += vel.as_dvec3(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy_ecs::message::MessageRegistry; + use bevy_ecs::prelude::*; + use bevy_math::Vec3A; + use ferrumc_core::transform::position::Position; + use ferrumc_core::transform::velocity::Velocity; + use ferrumc_messages::entity_update::SendEntityUpdate; + + #[test] + fn test_velocity_updates_position() { + let mut world = World::new(); + let entity = world + .spawn(( + Velocity { + vec: Vec3A::new(1.0, 2.0, 3.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + MessageRegistry::register_message::(&mut world); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + // Run the velocity system + schedule.run(&mut world); + + let pos = world.get::(entity).unwrap(); + assert_eq!( + pos.coords, + Vec3A::new(1.0, 2.0, 3.0).as_dvec3(), + "Position should be updated based on velocity" + ); + } + + #[test] + fn test_no_update_when_unchanged() { + let mut world = World::new(); + let entity = world + .spawn(( + Velocity { vec: Vec3A::ZERO }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + + MessageRegistry::register_message::(&mut world); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + // Run the velocity system + schedule.run(&mut world); + + assert!( + world.get::(entity).is_some(), + "Entity should exist" + ); + + assert_eq!( + world.get::(entity).unwrap().coords, + Vec3A::ZERO.as_dvec3(), + "Position should remain unchanged" + ); + + let reader = world.get_resource::>().unwrap(); + let mut cursor = reader.get_cursor(); + let mut messages = vec![]; + for msg in cursor.read(reader) { + messages.push(msg); + } + assert_eq!( + messages.len(), + 0, + "No SendEntityUpdate message should be sent when unchanged" + ); + } + + #[test] + fn test_multiple_velocity_steps() { + let mut world = World::new(); + let entity = world + .spawn(( + Velocity { + vec: Vec3A::new(0.5, 0.0, 0.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + // Run the velocity system multiple times + for _ in 0..4 { + schedule.run(&mut world); + } + + let pos = world.get::(entity).unwrap(); + assert_eq!( + pos.coords, + Vec3A::new(2.0, 0.0, 0.0).as_dvec3(), + "Position should be updated correctly after multiple steps" + ); + } + + #[test] + fn test_multiple_entities() { + let mut world = World::new(); + let entity1 = world + .spawn(( + Velocity { + vec: Vec3A::new(1.0, 0.0, 0.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + let entity2 = world + .spawn(( + Velocity { + vec: Vec3A::new(0.0, 1.0, 0.0), + }, + Position { + coords: Vec3A::ZERO.as_dvec3(), + }, + )) + .id(); + + let mut schedule = Schedule::default(); + schedule.add_systems(handle); + + // Run the velocity system + schedule.run(&mut world); + + let pos1 = world.get::(entity1).unwrap(); + let pos2 = world.get::(entity2).unwrap(); + assert_eq!( + pos1.coords, + Vec3A::new(1.0, 0.0, 0.0).as_dvec3(), + "Entity 1 position should be updated correctly" + ); + assert_eq!( + pos2.coords, + Vec3A::new(0.0, 1.0, 0.0).as_dvec3(), + "Entity 2 position should be updated correctly" + ); + } +} diff --git a/src/bin/src/systems/send_entity_updates.rs b/src/bin/src/systems/send_entity_updates.rs new file mode 100644 index 000000000..7dd4b6bc7 --- /dev/null +++ b/src/bin/src/systems/send_entity_updates.rs @@ -0,0 +1,93 @@ +use bevy_ecs::prelude::{MessageReader, Query}; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_core::transform::velocity::Velocity; +use ferrumc_entities::LastSyncedPosition; +use ferrumc_messages::entity_update::SendEntityUpdate; +use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::outgoing::entity_position_sync::TeleportEntityPacket; +use ferrumc_net::packets::outgoing::update_entity_position_and_rotation::UpdateEntityPositionAndRotationPacket; +use ferrumc_net_codec::net_types::angle::NetAngle; +use tracing::{debug, warn}; + +pub fn handle( + mut query: Query<( + &Position, + &Velocity, + &Rotation, + &mut LastSyncedPosition, + &EntityIdentity, + &OnGround, + )>, + mut conn_query: Query<&StreamWriter>, + mut reader: MessageReader, +) { + let mut entities_to_update = vec![]; + for msg in reader.read() { + entities_to_update.push(msg.0); + } + entities_to_update.dedup(); + for entity in entities_to_update { + debug!("Sending entity update for entity {:?}", entity); + if let Ok((pos, vel, rot, mut last_synced, id, grounded)) = query.get_mut(entity) { + if last_synced.0.distance(pos.coords) > 8.0 { + let packet = TeleportEntityPacket { + entity_id: id.entity_id.into(), + x: pos.x, + y: pos.y, + z: pos.z, + vel_x: vel.x as f64, + vel_y: vel.y as f64, + vel_z: vel.z as f64, + yaw: rot.yaw, + pitch: rot.pitch, + on_ground: grounded.0, + }; + for conn in conn_query.iter_mut() { + // TODO: Only send if the client is tracking this entity + if let Err(e) = conn.send_packet_ref(&packet) { + warn!( + "Failed to send teleport packet for entity {:?}: {:?}", + entity, e + ); + } + } + } else { + let (delta_x, delta_y, delta_z) = { + let delta = pos.coords - last_synced.0; + ( + (delta.x * 4096.0).round() as i16, + (delta.y * 4096.0).round() as i16, + (delta.z * 4096.0).round() as i16, + ) + }; + let packet = UpdateEntityPositionAndRotationPacket { + entity_id: id.entity_id.into(), + delta_x, + delta_y, + delta_z, + yaw: NetAngle::from_degrees(rot.yaw.into()), + pitch: NetAngle::from_degrees(rot.pitch.into()), + on_ground: grounded.0, + }; + for conn in conn_query.iter_mut() { + // TODO: Only send if the client is tracking this entity + if let Err(e) = conn.send_packet_ref(&packet) { + warn!( + "Failed to send entity update packet for entity {:?}: {:?}", + entity, e + ); + } + } + }; + *last_synced = LastSyncedPosition(pos.coords); + } else { + warn!( + "Tried to send entity update for non-existent entity: {:?}", + entity + ); + } + } +} diff --git a/src/lib/core/src/identity/entity_identity.rs b/src/lib/core/src/identity/entity_identity.rs index b0dc6eecf..1bd5a42fe 100644 --- a/src/lib/core/src/identity/entity_identity.rs +++ b/src/lib/core/src/identity/entity_identity.rs @@ -10,7 +10,7 @@ static ENTITY_ID_COUNTER: AtomicI32 = AtomicI32::new(1_000_000); /// Identity component for non-player entities. /// -/// Similar to PlayerIdentity but for mobs and other entities. +/// Similar to PlayerIdentity but for physics and other entities. /// Contains a unique network ID and UUID for the entity. /// /// # Examples diff --git a/src/lib/core/src/transform/mod.rs b/src/lib/core/src/transform/mod.rs index effd8b990..9be5a3867 100644 --- a/src/lib/core/src/transform/mod.rs +++ b/src/lib/core/src/transform/mod.rs @@ -1,3 +1,4 @@ pub mod grounded; pub mod position; pub mod rotation; +pub mod velocity; diff --git a/src/lib/core/src/transform/position.rs b/src/lib/core/src/transform/position.rs index 0853125a8..01874d130 100644 --- a/src/lib/core/src/transform/position.rs +++ b/src/lib/core/src/transform/position.rs @@ -1,6 +1,7 @@ use bevy_ecs::prelude::Component; use bevy_math::DVec3; use ferrumc_net_codec::net_types::network_position::NetworkPosition; +use std::ops::DerefMut; use std::{ fmt::{Debug, Display, Formatter}, ops::Deref, @@ -80,6 +81,12 @@ impl Deref for Position { } } +impl DerefMut for Position { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.coords + } +} + // Implementations: impl Default for Position { fn default() -> Self { diff --git a/src/lib/core/src/transform/velocity.rs b/src/lib/core/src/transform/velocity.rs new file mode 100644 index 000000000..ca64799e4 --- /dev/null +++ b/src/lib/core/src/transform/velocity.rs @@ -0,0 +1,59 @@ +use bevy_ecs::prelude::Component; +use bevy_math::{DVec3, Vec3A}; +use std::ops::{Deref, DerefMut}; +use typename::TypeName; + +/// Velocity component representing the rate of change of position. +/// +/// Measured in blocks per tick (at 60 TPS). +/// Positive Y is upward. +#[derive(TypeName, Debug, Component, Clone, Copy)] +pub struct Velocity { + pub vec: Vec3A, +} + +impl Velocity { + pub fn new(x: f64, y: f64, z: f64) -> Self { + Self { + vec: Vec3A::new(x as f32, y as f32, z as f32), + } + } + + pub fn zero() -> Self { + Self { vec: Vec3A::ZERO } + } +} + +impl Default for Velocity { + fn default() -> Self { + Self::zero() + } +} + +impl Deref for Velocity { + type Target = Vec3A; + + fn deref(&self) -> &Self::Target { + &self.vec + } +} + +impl DerefMut for Velocity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.vec + } +} + +impl From for Velocity { + fn from(vec: DVec3) -> Self { + Self { + vec: vec.as_vec3a(), + } + } +} + +impl From for Vec3A { + fn from(velocity: Velocity) -> Self { + velocity.vec + } +} diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index 38f6963c3..d813274cd 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] ferrumc-messages = { workspace = true } -ferrumc-components = { workspace = true} +ferrumc-components = { workspace = true } ferrumc-commands = { workspace = true } ferrumc-macros = { workspace = true } ferrumc-text = { workspace = true } @@ -13,6 +13,8 @@ ferrumc-core = { workspace = true } ferrumc-net = { workspace = true } ferrumc-performance = { workspace = true } ferrumc-entities = { workspace = true } +lazy_static = { workspace = true } +bimap = { workspace = true } ctor = { workspace = true } tracing = { workspace = true } diff --git a/src/lib/default_commands/src/spawn.rs b/src/lib/default_commands/src/spawn.rs index d94cca4c7..8cc4771b7 100644 --- a/src/lib/default_commands/src/spawn.rs +++ b/src/lib/default_commands/src/spawn.rs @@ -1,4 +1,5 @@ use bevy_ecs::prelude::MessageWriter; +use bimap::BiMap; use ferrumc_commands::{ arg::{primitive::PrimitiveArgument, utils::parser_error, CommandArgument, ParserResult}, CommandContext, Sender, Suggestion, @@ -6,25 +7,33 @@ use ferrumc_commands::{ use ferrumc_macros::command; use ferrumc_messages::{EntityType, SpawnEntityCommand}; use ferrumc_text::TextComponent; +use lazy_static::lazy_static; /// Wrapper type for EntityType that implements CommandArgument #[derive(Debug, Clone, Copy)] struct EntityTypeArg(EntityType); +lazy_static! { + static ref MAPPED_ENTITIES: BiMap<&'static str, EntityType> = { + let mut m = BiMap::new(); + + // Add supported entities here + m.insert("pig", EntityType::Pig); + + m + }; +} + impl CommandArgument for EntityTypeArg { fn parse(ctx: &mut CommandContext) -> ParserResult { let str = ctx.input.read_string(); - let value = match &*str.to_lowercase() { - "pig" => EntityType::Pig, - // Add more entity types here as they're implemented - // "cow" => EntityType::Cow, - // "sheep" => EntityType::Sheep, - _ => { - return Err(parser_error(&format!( - "Unknown entity type: '{}'. Currently supported: pig", - str - ))) + let value = match MAPPED_ENTITIES.get_by_left(str.as_str()) { + Some(&entity_type) => entity_type, + None => { + return Err(parser_error( + format!("Unknown entity type: {}", str).as_str(), + )) } }; @@ -39,8 +48,10 @@ impl CommandArgument for EntityTypeArg { fn suggest(ctx: &mut CommandContext) -> Vec { ctx.input.read_string(); - // Only suggest "pig" for now - add more as they're implemented - vec![Suggestion::of("pig")] + MAPPED_ENTITIES + .iter() + .map(|(&name, _)| Suggestion::of(name)) + .collect() } } @@ -63,9 +74,9 @@ fn spawn_command( }); // Get entity name for message - let entity_name = match entity_type.0 { - EntityType::Pig => "Pig", - }; + let entity_name = MAPPED_ENTITIES + .get_by_right(&entity_type.0) + .unwrap_or(&"unknown"); sender.send_message( TextComponent::from(format!("{} spawned!", entity_name)), diff --git a/src/lib/entities/Cargo.toml b/src/lib/entities/Cargo.toml index 582dccfdb..3e4efa49c 100644 --- a/src/lib/entities/Cargo.toml +++ b/src/lib/entities/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] bevy_ecs = { workspace = true } +bevy_math = { workspace = true } ferrumc-core = { workspace = true } ferrumc-data = { workspace = true } diff --git a/src/lib/entities/src/bundles/pig.rs b/src/lib/entities/src/bundles/pig.rs index 1103624c8..d6b8dcaa0 100644 --- a/src/lib/entities/src/bundles/pig.rs +++ b/src/lib/entities/src/bundles/pig.rs @@ -1,97 +1,54 @@ use bevy_ecs::prelude::Bundle; use ferrumc_core::identity::entity_identity::EntityIdentity; -use ferrumc_core::transform::{grounded::OnGround, position::Position, rotation::Rotation}; +use ferrumc_core::transform::{ + grounded::OnGround, position::Position, rotation::Rotation, velocity::Velocity, +}; use ferrumc_data::generated::entities::EntityType as VanillaEntityType; -use crate::components::{CombatProperties, EntityMetadata, PhysicalProperties, SpawnProperties}; +use crate::components::{ + CombatProperties, EntityMetadata, LastSyncedPosition, PhysicalProperties, SpawnProperties, +}; /// Complete bundle to spawn a pig in Bevy ECS. /// -/// This bundle contain all the necessary composantsto represent a pig -/// in the world. It use Vanilla's data from ferrumc-data to correctly +/// This bundle contains all the necessary components to represent a pig +/// in the world. It uses Vanilla's data from ferrumc-data to correctly /// initialize properties. -/// -/// # Examples -/// -/// ```ignore -/// use bevy_ecs::prelude::Commands; -/// use ferrumc_entities::bundles::PigBundle; -/// use ferrumc_core::transform::position::Position; -/// -/// fn spawn_pig(mut commands: Commands) { -/// let position = Position::new(0.0, 64.0, 0.0); -/// commands.spawn(PigBundle::new(position)); -/// } -/// ``` #[derive(Bundle)] pub struct PigBundle { - /// Network identity (entity_id and UUID) pub identity: EntityIdentity, - - /// Immutable vanilla metadatas (protocol_id, resource_name, etc.) pub metadata: EntityMetadata, - - /// Physical properties (bounding box, eye_height, fire_immune) pub physical: PhysicalProperties, - - /// Combat properties (attackable, invulnerability) pub combat: CombatProperties, - - /// Spawn properties (category, saveable, limits) pub spawn: SpawnProperties, - - /// Actual entities position in the world pub position: Position, - - /// Actual rotation (yaw, pitch) pub rotation: Rotation, - - /// True if the entity is on the ground (needed for physics) + pub velocity: Velocity, pub on_ground: OnGround, + pub last_synced_position: LastSyncedPosition, } impl PigBundle { - /// Create a new bundle for the pig for a gived position. - /// - /// Initialize all the components with correct vanilla's values - /// from ferrumc-data. - /// - /// # Examples - /// - /// ```ignore - /// use ferrumc_entities::bundles::PigBundle; - /// use ferrumc_core::transform::position::Position; - /// - /// let pig = PigBundle::new(Position::new(10.0, 64.0, 20.0)); - /// commands.spawn(pig); - /// ``` pub fn new(position: Position) -> Self { - // Create metadata from vanilla data let metadata = EntityMetadata::from_vanilla(&VanillaEntityType::PIG); - - // Create other components from metadata let physical = PhysicalProperties::from_metadata(&metadata); let combat = CombatProperties::from_metadata(&metadata); let spawn = SpawnProperties::from_metadata(&metadata); Self { - // Network identity identity: EntityIdentity::new(), - - // Derived components from vanilla metadata, physical, combat, spawn, - - // Transformation state - position, rotation: Rotation::default(), - on_ground: OnGround(true), // Spawn on the ground + velocity: Velocity::zero(), + on_ground: OnGround(false), + last_synced_position: LastSyncedPosition::from_position(&position), + position, } } - /// Create a pig at the gived position with a custom rotation pub fn with_rotation(position: Position, rotation: Rotation) -> Self { let mut bundle = Self::new(position); bundle.rotation = rotation; @@ -99,17 +56,6 @@ impl PigBundle { } } -impl std::fmt::Debug for PigBundle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PigBundle") - .field("metadata", &self.metadata) - .field("physical", &self.physical) - .field("position", &self.position) - .field("rotation", &self.rotation) - .finish() - } -} - #[cfg(test)] mod tests { use super::*; @@ -128,8 +74,8 @@ mod tests { assert!(pig.metadata.is_mob()); // Verify physical properties (using epsilon for floating point comparison) - assert!((pig.physical.bounding_box.height - 0.9).abs() < EPSILON_F64); - assert!((pig.physical.bounding_box.half_width - 0.45).abs() < EPSILON_F64); + assert!((pig.physical.bounding_box.height() - 0.9).abs() < EPSILON_F64); + assert!((pig.physical.bounding_box.width() - 0.9).abs() < EPSILON_F64); assert!((pig.physical.eye_height - 0.765).abs() < EPSILON_F32); assert!(!pig.physical.fire_immune); diff --git a/src/lib/entities/src/collision.rs b/src/lib/entities/src/collision.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/lib/entities/src/collision.rs @@ -0,0 +1 @@ + diff --git a/src/lib/entities/src/components/last_synced_position.rs b/src/lib/entities/src/components/last_synced_position.rs new file mode 100644 index 000000000..ea53719b2 --- /dev/null +++ b/src/lib/entities/src/components/last_synced_position.rs @@ -0,0 +1,93 @@ +use bevy_ecs::prelude::Component; +use bevy_math::DVec3; +use ferrumc_core::transform::position::Position; + +/// Component that tracks the last position synchronized to clients +#[derive(Component, Debug, Clone, Copy)] +pub struct LastSyncedPosition(pub DVec3); + +impl LastSyncedPosition { + pub fn from_position(pos: &Position) -> Self { + Self(pos.coords) + } + + /// Calculate delta in Minecraft protocol units (1/4096 of a block) + pub fn delta_to(&self, new_pos: &Position) -> (i16, i16, i16) { + const SCALE: f64 = 4096.0; + let delta = new_pos.coords - self.0; + ( + (delta.x * SCALE) as i16, + (delta.y * SCALE) as i16, + (delta.z * SCALE) as i16, + ) + } + + pub fn has_moved(&self, new_pos: &Position) -> bool { + let delta = self.delta_to(new_pos); + delta.0 != 0 || delta.1 != 0 || delta.2 != 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy_math::DVec3; + + #[test] + fn test_from_position() { + let pos = Position::new(10.5, 64.0, -20.3); + let last_synced = LastSyncedPosition::from_position(&pos); + + assert_eq!(last_synced.0, DVec3::new(10.5, 64.0, -20.3)); + } + + #[test] + fn test_delta_to_no_movement() { + let pos = Position::new(10.0, 64.0, 20.0); + let last_synced = LastSyncedPosition::from_position(&pos); + + let delta = last_synced.delta_to(&pos); + assert_eq!(delta, (0, 0, 0)); + } + + #[test] + fn test_delta_to_with_movement() { + let old_pos = Position::new(10.0, 64.0, 20.0); + let new_pos = Position::new(10.5, 64.2, 20.1); + let last_synced = LastSyncedPosition::from_position(&old_pos); + + let delta = last_synced.delta_to(&new_pos); + // Delta in protocol units (1/4096 of a block) + // 0.5 * 4096 = 2048 + // 0.2 * 4096 = 819.2 -> 819 + // 0.1 * 4096 = 409.6 -> 409 + assert_eq!(delta, (2048, 819, 409)); + } + + #[test] + fn test_has_moved_false() { + let pos = Position::new(10.0, 64.0, 20.0); + let last_synced = LastSyncedPosition::from_position(&pos); + + assert!(!last_synced.has_moved(&pos)); + } + + #[test] + fn test_has_moved_true() { + let old_pos = Position::new(10.0, 64.0, 20.0); + let new_pos = Position::new(10.001, 64.0, 20.0); + let last_synced = LastSyncedPosition::from_position(&old_pos); + + assert!(last_synced.has_moved(&new_pos)); + } + + #[test] + fn test_has_moved_small_delta_below_threshold() { + let old_pos = Position::new(10.0, 64.0, 20.0); + // Movement smaller than 1/4096 should round to 0 + let new_pos = Position::new(10.0001, 64.0, 20.0); + let last_synced = LastSyncedPosition::from_position(&old_pos); + + assert!(!last_synced.has_moved(&new_pos)); + } +} diff --git a/src/lib/entities/src/components/metadata.rs b/src/lib/entities/src/components/metadata.rs index 2993d077c..9bb19649c 100644 --- a/src/lib/entities/src/components/metadata.rs +++ b/src/lib/entities/src/components/metadata.rs @@ -70,7 +70,7 @@ impl EntityMetadata { /// Returns true if this entity is a mob. /// /// Mobs are living entities (animals, monsters). - /// Non-mobs include items, projectiles, boats, etc. + /// Non-physics include items, projectiles, boats, etc. pub const fn is_mob(&self) -> bool { self.vanilla_data.mob } diff --git a/src/lib/entities/src/components/mod.rs b/src/lib/entities/src/components/mod.rs index bf0e8db5e..903f62ede 100644 --- a/src/lib/entities/src/components/mod.rs +++ b/src/lib/entities/src/components/mod.rs @@ -1,11 +1,13 @@ // Core entity components based on ferrumc-data pub mod combat; +pub mod last_synced_position; pub mod metadata; pub mod physical; pub mod spawn; // Re-exports pub use combat::CombatProperties; +pub use last_synced_position::LastSyncedPosition; pub use metadata::EntityMetadata; pub use physical::PhysicalProperties; pub use spawn::SpawnProperties; diff --git a/src/lib/entities/src/components/physical.rs b/src/lib/entities/src/components/physical.rs index 2405307f1..44e312062 100644 --- a/src/lib/entities/src/components/physical.rs +++ b/src/lib/entities/src/components/physical.rs @@ -1,5 +1,7 @@ use bevy_ecs::prelude::Component; +use bevy_math::bounding::Aabb3d; use ferrumc_data::generated::entities::EntityType as VanillaEntityType; +use std::ops::{Deref, DerefMut}; /// Entity bounding box (collision box). /// @@ -7,14 +9,21 @@ use ferrumc_data::generated::entities::EntityType as VanillaEntityType; /// Used for collision detection and physics. #[derive(Debug, Clone, Copy, PartialEq)] pub struct BoundingBox { - /// Half-width of the bounding box in blocks. - /// - /// The total width is `half_width * 2`. Stored as half-width - /// to facilitate collision calculations (distance from center to edges). - pub half_width: f64, + aabb: Aabb3d, +} + +impl Deref for BoundingBox { + type Target = Aabb3d; - /// Height of the bounding box in blocks. - pub height: f64, + fn deref(&self) -> &Self::Target { + &self.aabb + } +} + +impl DerefMut for BoundingBox { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.aabb + } } impl BoundingBox { @@ -35,19 +44,31 @@ impl BoundingBox { /// ``` pub const fn from_vanilla_dimension(dimension: [f32; 2]) -> Self { Self { - half_width: (dimension[0] / 2.0) as f64, - height: dimension[1] as f64, + aabb: Aabb3d { + max: bevy_math::Vec3A::new(dimension[0] / 2.0, dimension[1], dimension[0] / 2.0), + min: bevy_math::Vec3A::new(-(dimension[0] / 2.0), 0.0, -(dimension[0] / 2.0)), + }, } } /// Returns the total width of the bounding box. - pub const fn width(&self) -> f64 { - self.half_width * 2.0 + pub fn width(&self) -> f64 { + (self.aabb.max.x - self.aabb.min.x) as f64 + } + + /// Returns the height of the bounding box. + pub fn height(&self) -> f64 { + (self.aabb.max.y - self.aabb.min.y) as f64 + } + + /// Returns the depth of the bounding box. + pub fn depth(&self) -> f64 { + (self.aabb.max.z - self.aabb.min.z) as f64 } /// Returns the volume of the bounding box in cubic blocks. - pub const fn volume(&self) -> f64 { - self.width() * self.width() * self.height + pub fn volume(&self) -> f64 { + self.width() * self.height() * self.depth() } } @@ -128,8 +149,19 @@ impl PhysicalProperties { /// /// * `scale` - Multiplier factor (0.5 for baby, 1.0 for adult) pub fn apply_scale(&mut self, scale: f64) { - self.bounding_box.half_width *= scale; - self.bounding_box.height *= scale; + let width = self.bounding_box.width() * scale; + let height = self.bounding_box.height() * scale; + let depth = self.bounding_box.depth() * scale; + self.bounding_box = BoundingBox { + aabb: Aabb3d { + min: bevy_math::Vec3A::new(-(width as f32) / 2.0, 0.0, -(depth as f32) / 2.0), + max: bevy_math::Vec3A::new( + (width as f32) / 2.0, + height as f32, + (depth as f32) / 2.0, + ), + }, + }; self.eye_height = (self.eye_height as f64 * scale) as f32; } } diff --git a/src/lib/entities/src/lib.rs b/src/lib/entities/src/lib.rs index 08bad974b..d03dd491e 100644 --- a/src/lib/entities/src/lib.rs +++ b/src/lib/entities/src/lib.rs @@ -1,7 +1,9 @@ -// Modules publics pub mod bundles; +pub mod collision; pub mod components; +pub mod markers; // Re-exports to facilitate use pub use bundles::*; pub use components::*; +pub use markers::*; diff --git a/src/lib/entities/src/markers.rs b/src/lib/entities/src/markers.rs new file mode 100644 index 000000000..ba3c5f3c8 --- /dev/null +++ b/src/lib/entities/src/markers.rs @@ -0,0 +1,18 @@ +use bevy_ecs::prelude::Component; + +/// Marker components for entities +#[derive(Component)] +pub struct HasGravity; + +#[derive(Component)] +pub struct HasWaterDrag; + +#[derive(Component)] +pub struct HasCollisions; + +// Entity types +pub mod entity_types { + use super::Component; + #[derive(Component)] + pub struct Pig; +} diff --git a/src/lib/messages/src/entity_spawn.rs b/src/lib/messages/src/entity_spawn.rs index 8f9201464..78fd920ee 100644 --- a/src/lib/messages/src/entity_spawn.rs +++ b/src/lib/messages/src/entity_spawn.rs @@ -2,7 +2,7 @@ use bevy_ecs::prelude::{Entity, Message}; use ferrumc_core::transform::position::Position; /// Type of entity to spawn -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum EntityType { Pig, // Add more entity types here as they're implemented diff --git a/src/lib/messages/src/entity_update.rs b/src/lib/messages/src/entity_update.rs new file mode 100644 index 000000000..20af1bc6e --- /dev/null +++ b/src/lib/messages/src/entity_update.rs @@ -0,0 +1,4 @@ +use bevy_ecs::prelude::{Entity, Message}; + +#[derive(Message)] +pub struct SendEntityUpdate(pub Entity); diff --git a/src/lib/messages/src/lib.rs b/src/lib/messages/src/lib.rs index 7b6c3910d..f18018b35 100644 --- a/src/lib/messages/src/lib.rs +++ b/src/lib/messages/src/lib.rs @@ -22,4 +22,6 @@ pub mod chunk_calc; pub use change_gamemode::*; pub mod entity_spawn; +pub mod entity_update; + pub use entity_spawn::{EntityType, SpawnEntityCommand, SpawnEntityEvent}; diff --git a/src/lib/net/crates/encryption/Cargo.toml b/src/lib/net/crates/encryption/Cargo.toml index f9e137b40..0f42fa7b5 100644 --- a/src/lib/net/crates/encryption/Cargo.toml +++ b/src/lib/net/crates/encryption/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] thiserror = { workspace = true } -rand = { workspace = true } +rand = "0.8.5" tokio = { workspace = true } rsa = { workspace = true } aes = { workspace = true } diff --git a/src/lib/net/crates/encryption/src/lib.rs b/src/lib/net/crates/encryption/src/lib.rs index d178bf81c..7190e55d8 100644 --- a/src/lib/net/crates/encryption/src/lib.rs +++ b/src/lib/net/crates/encryption/src/lib.rs @@ -32,8 +32,8 @@ impl EncryptionKeys { /// # Returns /// - `Self`: A new EncryptionKeys instance with a random RSA key pair. pub fn generate() -> Self { - let private_key = - RsaPrivateKey::new(&mut rand::rng(), 1024).expect("RsaPrivateKey failed to generate"); + let private_key = RsaPrivateKey::new(&mut rand::thread_rng(), 1024) + .expect("RsaPrivateKey failed to generate"); let public_key = RsaPublicKey::from(&private_key); let der = public_key diff --git a/src/lib/physics/Cargo.toml b/src/lib/physics/Cargo.toml new file mode 100644 index 000000000..ac10bf44f --- /dev/null +++ b/src/lib/physics/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ferrumc-physics" +edition.workspace = true +version.workspace = true + +[dependencies] +bevy_math = { workspace = true } + +[lints] +workspace = true diff --git a/src/lib/physics/src/lib.rs b/src/lib/physics/src/lib.rs new file mode 100644 index 000000000..70072db8e --- /dev/null +++ b/src/lib/physics/src/lib.rs @@ -0,0 +1,16 @@ +#[allow(dead_code)] +use bevy_math::Vec3A; + +pub const GRAVITY_ACCELERATION: Vec3A = Vec3A::new(0.0, -0.08, 0.0); + +pub const TERMINAL_VELOCITY_Y: f64 = -3.92; + +// const WATER_BUOYANCY: f64 = 0.09; +// +// const WATER_DRAG: f64 = 0.8; +// +// const WATER_VERTICAL_DRAG: f64 = 0.95; +// +// const GROUND_FRICTION: f64 = 0.85; +// +// const AIR_RESISTANCE: f64 = 0.98; diff --git a/src/lib/utils/Cargo.toml b/src/lib/utils/Cargo.toml index 677c6a306..86f08274b 100644 --- a/src/lib/utils/Cargo.toml +++ b/src/lib/utils/Cargo.toml @@ -8,4 +8,5 @@ edition = "2024" thiserror = { workspace = true } ferrumc-logging = { workspace = true } ferrumc-profiling = { workspace = true } -ferrumc-config = { workspace = true } \ No newline at end of file +ferrumc-config = { workspace = true } +bevy_ecs = { workspace = true } \ No newline at end of file diff --git a/src/lib/world/src/pos.rs b/src/lib/world/src/pos.rs index 9fb49a937..f21e63a92 100644 --- a/src/lib/world/src/pos.rs +++ b/src/lib/world/src/pos.rs @@ -4,13 +4,13 @@ use std::fmt::Result; use std::ops::Add; use std::ops::Range; -use bevy_math::I16Vec3; use bevy_math::IVec2; use bevy_math::IVec3; use bevy_math::U8Vec2; use bevy_math::U8Vec3; use bevy_math::Vec2Swizzles; use bevy_math::Vec3Swizzles; +use bevy_math::{DVec3, I16Vec3}; use ferrumc_net_codec::net_types::network_position::NetworkPosition; #[derive(Clone, Copy)] @@ -111,6 +111,18 @@ pub struct ChunkPos { pub pos: IVec2, } +impl From for ChunkPos { + fn from(pos: DVec3) -> Self { + Self::new(pos.x.div_euclid(16.0) as i32, pos.z.div_euclid(16.0) as i32) + } +} + +impl From for ChunkPos { + fn from(pos: IVec3) -> Self { + Self::new(pos.x.div_euclid(16), pos.z.div_euclid(16)) + } +} + impl ChunkPos { pub const fn new(x: i32, z: i32) -> Self { assert!(x < 1 << 22); @@ -222,6 +234,16 @@ impl From<(u8, i16, u8)> for ChunkBlockPos { } } +impl From for ChunkBlockPos { + fn from(pos: IVec3) -> Self { + Self::new( + pos.x.rem_euclid(16) as u8, + pos.y as i16, + pos.z.rem_euclid(16) as u8, + ) + } +} + impl ChunkBlockPos { pub const fn new(x: u8, y: i16, z: u8) -> Self { assert!(x < 16); From 8317369959722717b273fd92087ba86e79ec91d9 Mon Sep 17 00:00:00 2001 From: ReCore <66930138+ReCore-sys@users.noreply.github.com> Date: Sun, 28 Dec 2025 16:39:04 +1030 Subject: [PATCH 13/23] Add particle packets and systems (#297) * Add particle packets and systems * Glowing pigs! * Collisions patch * Particles track player * Clippy * Return ok for dropped conn * Fix requested changes --- Cargo.toml | 3 + src/bin/Cargo.toml | 1 + src/bin/src/game_loop.rs | 2 + src/bin/src/register_messages.rs | 2 + src/bin/src/systems/connection_killer.rs | 2 +- src/bin/src/systems/listeners/entity_spawn.rs | 2 +- src/bin/src/systems/mobs/mod.rs | 4 + src/bin/src/systems/mobs/pig.rs | 41 +++- src/bin/src/systems/mod.rs | 2 + src/bin/src/systems/particles.rs | 32 ++++ src/bin/src/systems/physics/collisions.rs | 11 +- src/bin/src/systems/send_entity_updates.rs | 3 +- src/lib/core/src/color.rs | 18 ++ src/lib/core/src/lib.rs | 1 + src/lib/core/src/transform/position.rs | 12 +- src/lib/entities/Cargo.toml | 16 -- src/lib/messages/Cargo.toml | 4 +- src/lib/messages/src/lib.rs | 1 + src/lib/messages/src/particle.rs | 21 +++ src/lib/net/Cargo.toml | 1 + src/lib/net/src/conn_init/login.rs | 2 +- src/lib/net/src/connection.rs | 3 +- src/lib/net/src/packets/outgoing/mod.rs | 2 + src/lib/net/src/packets/outgoing/particle.rs | 16 ++ src/lib/particles/Cargo.toml | 15 ++ src/lib/particles/src/lib.rs | 178 ++++++++++++++++++ src/lib/particles/src/net.rs | 92 +++++++++ src/lib/utils/Cargo.toml | 2 +- src/lib/utils/src/lib.rs | 1 + src/lib/utils/src/maths/mod.rs | 1 + src/lib/utils/src/maths/step.rs | 43 +++++ 31 files changed, 495 insertions(+), 39 deletions(-) create mode 100644 src/bin/src/systems/particles.rs create mode 100644 src/lib/core/src/color.rs create mode 100644 src/lib/messages/src/particle.rs create mode 100644 src/lib/net/src/packets/outgoing/particle.rs create mode 100644 src/lib/particles/Cargo.toml create mode 100644 src/lib/particles/src/lib.rs create mode 100644 src/lib/particles/src/net.rs create mode 100644 src/lib/utils/src/maths/mod.rs create mode 100644 src/lib/utils/src/maths/step.rs diff --git a/Cargo.toml b/Cargo.toml index 0f4fcf49a..f8bf819c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ "src/lib/entities", "src/lib/physics", "src/lib/performance", + "src/lib/particles", ] [workspace.package] @@ -126,6 +127,7 @@ ferrumc-messages = { path = "src/lib/messages" } ferrumc-data = { path = "src/lib/data" } ferrumc-entities = { path = "src/lib/entities" } ferrumc-physics = { path = "src/lib/physics" } +ferrumc-particles = { path = "src/lib/particles" } # Asynchronous tokio = { version = "1.48.0", features = [ @@ -199,6 +201,7 @@ proc-macro2 = "1.0.103" paste = "1.0.15" maplit = "1.0.2" macro_rules_attribute = "0.2.2" +enum_discriminant = "1.0.1" # Magic dhat = "0.3.3" diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 1e76982c0..4e941391e 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -38,6 +38,7 @@ ferrumc-inventories = { workspace = true } ferrumc-data = { workspace = true } ferrumc-entities = { workspace = true } ferrumc-physics = { workspace = true } +ferrumc-particles = { workspace = true } bevy_math = { workspace = true } once_cell = { workspace = true } serde_json = { workspace = true } diff --git a/src/bin/src/game_loop.rs b/src/bin/src/game_loop.rs index 20c6a9db5..372b500f6 100644 --- a/src/bin/src/game_loop.rs +++ b/src/bin/src/game_loop.rs @@ -12,6 +12,7 @@ use crate::register_messages::register_messages; use crate::register_resources::register_resources; use crate::systems::lan_pinger::LanPinger; use crate::systems::listeners::register_gameplay_listeners; +use crate::systems::mobs::register_mob_systems; use crate::systems::physics::register_physics; use crate::systems::register_game_systems; use crate::systems::shutdown_systems::register_shutdown_systems; @@ -252,6 +253,7 @@ fn build_timed_scheduler() -> Scheduler { register_game_systems(s); // General game logic register_gameplay_listeners(s); // Event listeners for gameplay events register_physics(s); // Physics systems (movement, collision, etc.) + register_mob_systems(s); // Mob AI and behavior }; let tick_period = Duration::from_secs(1) / get_global_config().tps; timed.register( diff --git a/src/bin/src/register_messages.rs b/src/bin/src/register_messages.rs index 2dc450253..355d65783 100644 --- a/src/bin/src/register_messages.rs +++ b/src/bin/src/register_messages.rs @@ -4,6 +4,7 @@ use ferrumc_commands::messages::{CommandDispatched, ResolvedCommandDispatched}; use ferrumc_core::conn::force_player_recount_event::ForcePlayerRecount; use ferrumc_messages::chunk_calc::ChunkCalc; use ferrumc_messages::entity_update::SendEntityUpdate; +use ferrumc_messages::particle::SendParticle; use ferrumc_messages::{ PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating, PlayerFinishedDigging, PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft, PlayerLeveledUp, @@ -32,4 +33,5 @@ pub fn register_messages(world: &mut World) { MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); } diff --git a/src/bin/src/systems/connection_killer.rs b/src/bin/src/systems/connection_killer.rs index d1b17f560..aec9b2085 100644 --- a/src/bin/src/systems/connection_killer.rs +++ b/src/bin/src/systems/connection_killer.rs @@ -106,7 +106,7 @@ pub fn connection_killer( let data_to_cache = OfflinePlayerData { abilities: *abilities, gamemode: gamemode.0, - position: pos.clone(), + position: *pos, rotation: *rot, inventory: inv.clone(), health: *health, diff --git a/src/bin/src/systems/listeners/entity_spawn.rs b/src/bin/src/systems/listeners/entity_spawn.rs index 5b52eabd2..f8f06b25a 100644 --- a/src/bin/src/systems/listeners/entity_spawn.rs +++ b/src/bin/src/systems/listeners/entity_spawn.rs @@ -108,7 +108,7 @@ pub fn handle_spawn_entity(mut events: MessageReader, mut comm // Spawn the pig entity let pig_entity = commands .spawn(( - PigBundle::new(event.position.clone()), + PigBundle::new(event.position), Pig, HasGravity, HasCollisions, diff --git a/src/bin/src/systems/mobs/mod.rs b/src/bin/src/systems/mobs/mod.rs index 2de43ece3..6c34a4092 100644 --- a/src/bin/src/systems/mobs/mod.rs +++ b/src/bin/src/systems/mobs/mod.rs @@ -1 +1,5 @@ mod pig; + +pub fn register_mob_systems(schedule: &mut bevy_ecs::schedule::Schedule) { + schedule.add_systems(pig::tick_pig); +} diff --git a/src/bin/src/systems/mobs/pig.rs b/src/bin/src/systems/mobs/pig.rs index b4a14193a..9c8db3e20 100644 --- a/src/bin/src/systems/mobs/pig.rs +++ b/src/bin/src/systems/mobs/pig.rs @@ -1,9 +1,40 @@ -use bevy_ecs::prelude::{Query, With}; +use bevy_ecs::prelude::{MessageWriter, Query, With}; +use bevy_math::Vec3A; +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::position::Position; -use ferrumc_core::transform::velocity::Velocity; use ferrumc_entities::markers::entity_types::Pig; +use ferrumc_messages::particle::SendParticle; +use ferrumc_particles::ParticleType; -#[expect(dead_code, unused_variables)] -pub fn tick_pig(query: Query<(&Position, &Velocity), With>) { - // Pig AI logic would go here +pub fn tick_pig( + query: Query<&Position, With>, + players: Query<&Position, With>, + mut msgs: MessageWriter, +) { + for pos in query.iter() { + for player_pos in players.iter() { + let distance_sq = player_pos.as_vec3a().distance_squared(pos.as_vec3a()); + // Only spawn particles if a player is within 256 blocks + if distance_sq > 16.0 * 256.0 { + continue; + } + // Spawn end rod particles from the pig to the player + let steps = ferrumc_utils::maths::step::step_between( + pos.coords.as_vec3a(), + player_pos.coords.as_vec3a(), + 0.5, + ); + // Limit to 32 particles to avoid spamming (16 blocks with a 0.5 step) + for step_pos in steps.iter().take(32) { + let particle_message = SendParticle { + particle_type: ParticleType::EndRod, + position: *step_pos, + offset: Vec3A::new(0.0, 0.0, 0.0), + speed: 0.0, + count: 1, + }; + msgs.write(particle_message); + } + } + } } diff --git a/src/bin/src/systems/mod.rs b/src/bin/src/systems/mod.rs index 73c784036..740dfd090 100644 --- a/src/bin/src/systems/mod.rs +++ b/src/bin/src/systems/mod.rs @@ -7,6 +7,7 @@ pub mod listeners; pub mod mobs; mod mq; pub mod new_connections; +mod particles; pub mod physics; mod player_swimming; mod send_entity_updates; @@ -25,4 +26,5 @@ pub fn register_game_systems(schedule: &mut bevy_ecs::schedule::Schedule) { // Should always be last schedule.add_systems(connection_killer::connection_killer); + schedule.add_systems(particles::handle); } diff --git a/src/bin/src/systems/particles.rs b/src/bin/src/systems/particles.rs new file mode 100644 index 000000000..71b8cf4c8 --- /dev/null +++ b/src/bin/src/systems/particles.rs @@ -0,0 +1,32 @@ +use bevy_ecs::prelude::{MessageReader, Query}; +use ferrumc_core::transform::position::Position; +use ferrumc_messages::particle::SendParticle; +use ferrumc_net::connection::StreamWriter; + +pub fn handle(mut msgs: MessageReader, query: Query<(&Position, &StreamWriter)>) { + for message in msgs.read() { + let double_pos = message.position.as_dvec3(); + let packet = ferrumc_net::packets::outgoing::particle::Particle { + long_distance: false, + always_visible: false, + x: double_pos.x, + y: double_pos.y, + z: double_pos.z, + offset_x: message.offset.x, + offset_y: message.offset.y, + offset_z: message.offset.z, + max_speed: message.speed, + count: message.count, + particle_type: message.particle_type.clone(), + }; + for (pos, writer) in query.iter() { + let distance_sq = pos.as_vec3a().distance_squared(message.position); + // 256 blocks radius + if distance_sq <= 256.0 * 256.0 { + writer + .send_packet_ref(&packet) + .expect("Failed to send particle packet"); + } + } + } +} diff --git a/src/bin/src/systems/physics/collisions.rs b/src/bin/src/systems/physics/collisions.rs index 67d38d1d4..b2d77f04d 100644 --- a/src/bin/src/systems/physics/collisions.rs +++ b/src/bin/src/systems/physics/collisions.rs @@ -12,7 +12,6 @@ use ferrumc_messages::entity_update::SendEntityUpdate; use ferrumc_state::{GlobalState, GlobalStateResource}; use ferrumc_world::block_state_id::BlockStateId; use ferrumc_world::pos::{ChunkBlockPos, ChunkPos}; -use tracing::debug; pub fn handle( query: Query< @@ -72,17 +71,13 @@ pub fn handle( // If a collision is detected, stop the entity's movement if collided { vel.vec = Vec3A::ZERO; - // Find the closest hit block to the entity's next position + // Find the closest hit block to the entity's position hit_blocks.sort_by(|a, b| { - let dist_a = (a.as_vec3a() - next_pos).length_squared(); - let dist_b = (b.as_vec3a() - next_pos).length_squared(); + let dist_a = (a.as_dvec3() - pos.coords).length_squared(); + let dist_b = (b.as_dvec3() - pos.coords).length_squared(); dist_a.partial_cmp(&dist_b).unwrap() }); let first_hit = hit_blocks.first().expect("At least one hit block expected"); - debug!( - "Entity collided at block position: {:?} going {}", - &hit_blocks, vel.vec - ); let block_aabb = Aabb3d { min: first_hit.as_vec3a(), diff --git a/src/bin/src/systems/send_entity_updates.rs b/src/bin/src/systems/send_entity_updates.rs index 7dd4b6bc7..e570c5b5f 100644 --- a/src/bin/src/systems/send_entity_updates.rs +++ b/src/bin/src/systems/send_entity_updates.rs @@ -10,7 +10,7 @@ use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::entity_position_sync::TeleportEntityPacket; use ferrumc_net::packets::outgoing::update_entity_position_and_rotation::UpdateEntityPositionAndRotationPacket; use ferrumc_net_codec::net_types::angle::NetAngle; -use tracing::{debug, warn}; +use tracing::warn; pub fn handle( mut query: Query<( @@ -30,7 +30,6 @@ pub fn handle( } entities_to_update.dedup(); for entity in entities_to_update { - debug!("Sending entity update for entity {:?}", entity); if let Ok((pos, vel, rot, mut last_synced, id, grounded)) = query.get_mut(entity) { if last_synced.0.distance(pos.coords) > 8.0 { let packet = TeleportEntityPacket { diff --git a/src/lib/core/src/color.rs b/src/lib/core/src/color.rs new file mode 100644 index 000000000..f993f6aa8 --- /dev/null +++ b/src/lib/core/src/color.rs @@ -0,0 +1,18 @@ +#[derive(Copy, Clone)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, +} + +impl Color { + pub fn new(r: f32, g: f32, b: f32) -> Self { + Self { r, g, b } + } + + pub fn to_i32(&self) -> i32 { + ((self.r.clamp(0.0, 1.0) * 255.0) as i32) << 16 + | ((self.g.clamp(0.0, 1.0) * 255.0) as i32) << 8 + | ((self.b.clamp(0.0, 1.0) * 255.0) as i32) + } +} diff --git a/src/lib/core/src/lib.rs b/src/lib/core/src/lib.rs index b22441dfc..e3dc39cdc 100644 --- a/src/lib/core/src/lib.rs +++ b/src/lib/core/src/lib.rs @@ -3,6 +3,7 @@ pub mod errors; // Core structs/types. Usually used in ECS Components. pub mod chunks; pub mod collisions; +pub mod color; pub mod conn; pub mod crafting; pub mod identity; diff --git a/src/lib/core/src/transform/position.rs b/src/lib/core/src/transform/position.rs index 01874d130..ab36b26b1 100644 --- a/src/lib/core/src/transform/position.rs +++ b/src/lib/core/src/transform/position.rs @@ -8,7 +8,7 @@ use std::{ }; use typename::TypeName; -#[derive(TypeName, Component, Clone)] +#[derive(TypeName, Component, Clone, Copy)] pub struct Position { pub coords: DVec3, } @@ -19,6 +19,16 @@ impl From for Position { } } +impl From for NetworkPosition { + fn from(pos: Position) -> Self { + NetworkPosition { + x: pos.x.floor() as i32, + y: pos.y.floor() as i16, + z: pos.z.floor() as i32, + } + } +} + // Helper functions: impl Position { pub fn new(x: f64, y: f64, z: f64) -> Self { diff --git a/src/lib/entities/Cargo.toml b/src/lib/entities/Cargo.toml index 3e4efa49c..093dd6b16 100644 --- a/src/lib/entities/Cargo.toml +++ b/src/lib/entities/Cargo.toml @@ -9,19 +9,3 @@ bevy_math = { workspace = true } ferrumc-core = { workspace = true } ferrumc-data = { workspace = true } -ferrumc-macros = { workspace = true } -ferrumc-net = { workspace = true } -ferrumc-net-codec = { workspace = true } -ferrumc-state = { workspace = true } -ferrumc-world = { workspace = true } - -thiserror = { workspace = true } -tracing = { workspace = true } -uuid = {workspace = true } -rand = { workspace = true } -typename = {workspace = true } -once_cell = {workspace = true } -crossbeam-queue = {workspace = true } - -serde = {workspace = true } -serde_derive = { workspace = true } diff --git a/src/lib/messages/Cargo.toml b/src/lib/messages/Cargo.toml index d3cc60545..4c62d7e3c 100644 --- a/src/lib/messages/Cargo.toml +++ b/src/lib/messages/Cargo.toml @@ -6,9 +6,11 @@ description = "Defines all Bevy ECS messages for FerrumC." [dependencies] bevy_ecs = { workspace = true } +bevy_math = { workspace = true } ferrumc-components = { workspace = true } ferrumc-core = { workspace = true } ferrumc-net-codec = { workspace = true } -ferrumc-inventories = {workspace = true } +ferrumc-inventories = { workspace = true } ferrumc-entities = { workspace = true } +ferrumc-particles = { workspace = true } diff --git a/src/lib/messages/src/lib.rs b/src/lib/messages/src/lib.rs index f18018b35..9c881f5a1 100644 --- a/src/lib/messages/src/lib.rs +++ b/src/lib/messages/src/lib.rs @@ -23,5 +23,6 @@ pub use change_gamemode::*; pub mod entity_spawn; pub mod entity_update; +pub mod particle; pub use entity_spawn::{EntityType, SpawnEntityCommand, SpawnEntityEvent}; diff --git a/src/lib/messages/src/particle.rs b/src/lib/messages/src/particle.rs new file mode 100644 index 000000000..18247e21e --- /dev/null +++ b/src/lib/messages/src/particle.rs @@ -0,0 +1,21 @@ +use bevy_ecs::prelude::Message; +use ferrumc_particles::ParticleType; + +/// Represents a message to send particle data in the system. +/// +/// This struct is used to define the properties of a particle effect +/// that can be sent as a message. It includes the type of particle, +/// its position, offset, speed, and the number of particles to generate. +#[derive(Message)] +pub struct SendParticle { + /// The type of particle to be sent. + pub particle_type: ParticleType, + /// The position where the particle effect will be generated. + pub position: bevy_math::Vec3A, + /// The offset applied to the particle's position. + pub offset: bevy_math::Vec3A, + /// The speed at which the particle moves. + pub speed: f32, + /// The number of particles to generate. + pub count: i32, +} diff --git a/src/lib/net/Cargo.toml b/src/lib/net/Cargo.toml index 52fe107f4..abc4e1c88 100644 --- a/src/lib/net/Cargo.toml +++ b/src/lib/net/Cargo.toml @@ -18,6 +18,7 @@ ferrumc-nbt = { workspace = true } ferrumc-core = { workspace = true } ferrumc-text = { workspace = true } ferrumc-world = { workspace = true } +ferrumc-particles = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } diff --git a/src/lib/net/src/conn_init/login.rs b/src/lib/net/src/conn_init/login.rs index bc15e49e4..9c152c4b0 100644 --- a/src/lib/net/src/conn_init/login.rs +++ b/src/lib/net/src/conn_init/login.rs @@ -394,7 +394,7 @@ async fn sync_player_position( // Get spawn position from cache or use defaults let (spawn_pos, spawn_rotation) = if let Some(data) = state.player_cache.get(&player_identity.uuid) { - (data.position.clone(), data.rotation) + (data.position, data.rotation) } else { ( Position::new( diff --git a/src/lib/net/src/connection.rs b/src/lib/net/src/connection.rs index eeb36ba6d..f5db5617a 100644 --- a/src/lib/net/src/connection.rs +++ b/src/lib/net/src/connection.rs @@ -144,9 +144,8 @@ impl StreamWriter { net_encode_opts: &NetEncodeOpts, ) -> Result<(), NetError> { if !self.running.load(Ordering::Relaxed) { - #[cfg(debug_assertions)] warn!("Attempted to send packet on closed connection"); - return Err(NetError::ConnectionDropped); + return Ok(()); } let raw_bytes = compress_packet( diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index 26f67cdd0..ae19c523e 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -50,3 +50,5 @@ pub mod encryption_request; pub mod set_container_content; pub mod set_container_slot; pub mod set_player_inventory_slot; + +pub mod particle; diff --git a/src/lib/net/src/packets/outgoing/particle.rs b/src/lib/net/src/packets/outgoing/particle.rs new file mode 100644 index 000000000..44dbd1ba8 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/particle.rs @@ -0,0 +1,16 @@ +use ferrumc_macros::{packet, NetEncode}; +#[derive(NetEncode)] +#[packet(packet_id = "level_particles", state = "play")] +pub struct Particle { + pub long_distance: bool, + pub always_visible: bool, + pub x: f64, + pub y: f64, + pub z: f64, + pub offset_x: f32, + pub offset_y: f32, + pub offset_z: f32, + pub max_speed: f32, + pub count: i32, + pub particle_type: ferrumc_particles::ParticleType, +} diff --git a/src/lib/particles/Cargo.toml b/src/lib/particles/Cargo.toml new file mode 100644 index 000000000..db4928f3a --- /dev/null +++ b/src/lib/particles/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ferrumc-particles" +edition.workspace = true +version.workspace = true + +[dependencies] +ferrumc-world = { workspace = true } +ferrumc-net-codec = { workspace = true } +ferrumc-core = { workspace = true } +ferrumc-inventories = { workspace = true } +enum_discriminant = { workspace = true } +tokio = { workspace = true } + +[lints] +workspace = true diff --git a/src/lib/particles/src/lib.rs b/src/lib/particles/src/lib.rs new file mode 100644 index 000000000..1d94f95d6 --- /dev/null +++ b/src/lib/particles/src/lib.rs @@ -0,0 +1,178 @@ +mod net; + +use enum_discriminant::discriminant; +use ferrumc_core::color::Color; +use ferrumc_core::transform::position::Position; +use ferrumc_inventories::slot::InventorySlot; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_world::block_state_id::BlockStateId; + +#[discriminant(i32)] +#[derive(Clone)] +/// Enum representing different types of particles in the game. +/// +/// To send to clients, use the `SendParticle` message from `ferrumc_messages::particle`. +pub enum ParticleType { + AngryVillager, + Block { + blockstate: BlockStateId, + }, + BlockMarker { + blockstate: BlockStateId, + }, + Bubble, + Cloud, + Crit, + DamageIndicator, + DragonBreath, + DrippingLava, + FallingLava, + LandingLava, + DrippingWater, + FallingWater, + Dust { + color: Color, + scale: f32, + }, + DustColorTransition { + from: Color, + to: Color, + scale: f32, + }, + Effect, + ElderGuardian, + EnchantedHit, + Enchant, + EndRod, + EntityEffect { + color: Color, + }, + ExplosionEmitter, + Explosion, + Gust, + SmallGust, + GustEmitterLarge, + GustEmitterSmall, + SonicBoom, + FallingDust { + blockstate: BlockStateId, + }, + Firework, + Fishing, + Flame, + Infested, + CherryLeaves, + PaleOakLeaves, + TintedLeaves { + color: Color, + }, + SculkSoul, + SculkCharge { + roll: f32, + }, + SculkChargePop, + SoulFireFlame, + Soul, + Flash, + HappyVillager, + Composter, + Heart, + InstantEffect, + Item { + item: InventorySlot, + }, + + /// Vibration from a block or an entity. + Vibration { + source: VibrationSource, + /// Ticks it takes for the vibration to travel. + ticks: VarInt, + }, + + Trail { + x: f64, + y: f64, + z: f64, + color: Color, + duration: VarInt, + }, + + ItemSlime, + ItemCobweb, + ItemSnowball, + LargeSmoke, + Lava, + Mycelium, + Note, + Poof, + Portal, + Rain, + Smoke, + WhiteSmoke, + Sneeze, + Spit, + SquidInk, + SweepAttack, + TotemOfUndying, + Underwater, + Splash, + Witch, + BubblePop, + CurrentDown, + BubbleColumnUp, + Nautilus, + Dolphin, + CampfireCosySmoke, + CampfireSignalSmoke, + DrippingHoney, + FallingHoney, + LandingHoney, + FallingNectar, + FallingSporeBlossom, + Ash, + CrimsonSpore, + WarpedSpore, + SporeBlossomAir, + DrippingObsidianTear, + FallingObsidianTear, + LandingObsidianTear, + ReversePortal, + WhiteAsh, + SmallFlame, + Snowflake, + DrippingDripstoneLava, + FallingDripstoneLava, + DrippingDripstoneWater, + FallingDripstoneWater, + GlowSquidInk, + Glow, + WaxOn, + WaxOff, + ElectricSpark, + Scrape, + Shriek { + delay: VarInt, + }, + EggCrack, + DustPlume, + TrialSpawnerDetection, + TrialSpawnerDetectionOminous, + VaultConnection, + DustPillar { + blockstate: BlockStateId, + }, + OminousSpawning, + RaidOmen, + TrialOmen, + BlockCrumble { + blockstate: BlockStateId, + }, + Firefly, +} + +/// Source type for the `vibration` particle. +#[derive(Copy, Clone)] +pub enum VibrationSource { + Block { position: Position }, + Entity { entity_id: VarInt, eye_height: f32 }, +} diff --git a/src/lib/particles/src/net.rs b/src/lib/particles/src/net.rs new file mode 100644 index 000000000..2d8081084 --- /dev/null +++ b/src/lib/particles/src/net.rs @@ -0,0 +1,92 @@ +use crate::{ParticleType, VibrationSource}; +use ferrumc_net_codec::encode::errors::NetEncodeError; +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts}; +use ferrumc_net_codec::net_types::network_position::NetworkPosition; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; +use ParticleType::*; + +impl NetEncode for ParticleType { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> Result<(), NetEncodeError> { + VarInt::new(self.discriminant()).encode(writer, opts)?; + match self { + Block { blockstate } | BlockMarker { blockstate } | FallingDust { blockstate } => { + blockstate.to_varint().encode(writer, opts) + } + Dust { color, scale } => { + color.to_i32().encode(writer, opts)?; + writer.write_all(&scale.to_le_bytes())?; + Ok(()) + } + DustColorTransition { from, to, scale } => { + from.to_i32().encode(writer, opts)?; + to.to_i32().encode(writer, opts)?; + writer.write_all(&scale.to_le_bytes())?; + Ok(()) + } + EntityEffect { color } => color.to_i32().encode(writer, opts), + TintedLeaves { color } => color.to_i32().encode(writer, opts), + SculkCharge { roll } => { + writer.write_all(&roll.to_le_bytes())?; + Ok(()) + } + Item { item } => item.encode(writer, opts), + Vibration { source, ticks } => { + source.encode(writer, opts)?; + ticks.encode(writer, opts) + } + Trail { + x, + y, + z, + color, + duration, + } => { + writer.write_all(&x.to_le_bytes())?; + writer.write_all(&y.to_le_bytes())?; + writer.write_all(&z.to_le_bytes())?; + color.to_i32().encode(writer, opts)?; + duration.encode(writer, opts) + } + Shriek { delay } => delay.encode(writer, opts), + DustPillar { blockstate } => blockstate.to_varint().encode(writer, opts), + BlockCrumble { blockstate } => blockstate.to_varint().encode(writer, opts), + + _ => Ok(()), + } + } + async fn encode_async( + &self, + _: &mut W, + _: &NetEncodeOpts, + ) -> Result<(), NetEncodeError> { + unreachable!() + } +} + +impl NetEncode for &VibrationSource { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> Result<(), NetEncodeError> { + match self { + VibrationSource::Block { position } => { + VarInt(0).encode(writer, opts)?; + NetworkPosition::from(*position).encode(writer, opts) + } + VibrationSource::Entity { + entity_id, + eye_height, + } => { + VarInt(1).encode(writer, opts)?; + entity_id.encode(writer, opts)?; + writer.write_all(&eye_height.to_le_bytes())?; + Ok(()) + } + } + } + async fn encode_async( + &self, + _: &mut W, + _: &NetEncodeOpts, + ) -> Result<(), NetEncodeError> { + unreachable!() + } +} diff --git a/src/lib/utils/Cargo.toml b/src/lib/utils/Cargo.toml index 86f08274b..1fab1ecf3 100644 --- a/src/lib/utils/Cargo.toml +++ b/src/lib/utils/Cargo.toml @@ -9,4 +9,4 @@ thiserror = { workspace = true } ferrumc-logging = { workspace = true } ferrumc-profiling = { workspace = true } ferrumc-config = { workspace = true } -bevy_ecs = { workspace = true } \ No newline at end of file +bevy_math = { workspace = true } \ No newline at end of file diff --git a/src/lib/utils/src/lib.rs b/src/lib/utils/src/lib.rs index d497414b7..2aa5086d8 100644 --- a/src/lib/utils/src/lib.rs +++ b/src/lib/utils/src/lib.rs @@ -1,5 +1,6 @@ pub mod errors; pub mod formatting; +pub mod maths; /// Gets the fully qualified path to the root of the project and joins it with the given path. /// diff --git a/src/lib/utils/src/maths/mod.rs b/src/lib/utils/src/maths/mod.rs new file mode 100644 index 000000000..80aa5fd73 --- /dev/null +++ b/src/lib/utils/src/maths/mod.rs @@ -0,0 +1 @@ +pub mod step; diff --git a/src/lib/utils/src/maths/step.rs b/src/lib/utils/src/maths/step.rs new file mode 100644 index 000000000..cbb68eced --- /dev/null +++ b/src/lib/utils/src/maths/step.rs @@ -0,0 +1,43 @@ +/// Generates a series of points between two 3D vectors (`start` and `end`) at a fixed step distance. +/// +/// # Arguments +/// +/// * `start` - A `Vec3A` representing the starting point of the line. +/// * `end` - A `Vec3A` representing the ending point of the line. +/// * `step` - A `f32` value representing the distance between each generated point. +/// +/// # Returns +/// +/// A `Vec` containing the points between `start` and `end`, including the `end` point. +/// +/// # Example +/// +/// ``` +/// use bevy_math::Vec3A; +/// let start = Vec3A::new(0.0, 0.0, 0.0); +/// let end = Vec3A::new(1.0, 1.0, 1.0); +/// let step = 0.5; +/// let points = step_between(start, end, step); +/// assert_eq!(points.len(), 3); +/// ``` +pub fn step_between( + start: bevy_math::Vec3A, + end: bevy_math::Vec3A, + step: f32, +) -> Vec { + let direction = end - start; // Calculate the direction vector from start to end. + let distance = direction.length(); // Calculate the total distance between start and end. + let step_count = (distance / step).ceil() as usize; // Determine the number of steps needed. + let mut points = Vec::with_capacity(step_count + 1); // Preallocate space for the points. + let direction_normalized = direction.normalize(); // Normalize the direction vector. + + let mut current_distance = 0.0; + while current_distance < distance { + // Calculate the next point along the direction vector. + let point = start + direction_normalized * current_distance; + points.push(point); // Add the point to the list. + current_distance += step; // Increment the distance by the step size. + } + points.push(end); // Ensure the end point is included in the list. + points +} From e239b9e18cbcadb4df6fc9267926ef396d91c70f Mon Sep 17 00:00:00 2001 From: Tongue_chaude <145228731+Tonguechaude@users.noreply.github.com> Date: Sun, 28 Dec 2025 10:39:04 +0100 Subject: [PATCH 14/23] feat : add ungrounding system for entities (#298) * feat : add ungrounding system for entities * fix : clippy lint * fix : rustfmt * fix : add some docs + clippy lints --- .../play_packets/player_action.rs | 13 ++- src/bin/src/register_messages.rs | 7 +- .../src/systems/listeners/digging_system.rs | 13 ++- src/bin/src/systems/physics/mod.rs | 2 + src/bin/src/systems/physics/unground.rs | 106 ++++++++++++++++++ src/lib/messages/Cargo.toml | 1 + src/lib/messages/src/block_break.rs | 8 ++ src/lib/messages/src/lib.rs | 3 + 8 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/bin/src/systems/physics/unground.rs create mode 100644 src/lib/messages/src/block_break.rs diff --git a/src/bin/src/packet_handlers/play_packets/player_action.rs b/src/bin/src/packet_handlers/play_packets/player_action.rs index d6b5449b6..c06e141fa 100644 --- a/src/bin/src/packet_handlers/play_packets/player_action.rs +++ b/src/bin/src/packet_handlers/play_packets/player_action.rs @@ -4,6 +4,7 @@ use crate::errors::BinaryError; use bevy_ecs::prelude::{Entity, MessageWriter, Query, Res}; use ferrumc_components::player::abilities::PlayerAbilities; use ferrumc_messages::player_digging::*; +use ferrumc_messages::BlockBrokenEvent; use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::block_change_ack::BlockChangeAck; @@ -19,9 +20,12 @@ pub fn handle( state: Res, broadcast_query: Query<(Entity, &StreamWriter)>, player_query: Query<&PlayerAbilities>, - mut start_dig_events: MessageWriter, - mut cancel_dig_events: MessageWriter, - mut finish_dig_events: MessageWriter, + (mut start_dig_events, mut cancel_dig_events, mut finish_dig_events, mut block_break_events): ( + MessageWriter, + MessageWriter, + MessageWriter, + MessageWriter, + ), ) { // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol?oldid=2773393#Player_Action for (event, trigger_eid) in receiver.0.try_iter() { @@ -67,6 +71,9 @@ pub fn handle( .save_chunk(pos.chunk(), "overworld", Arc::new(chunk)) .map_err(BinaryError::World)?; + // Send block broken event for un-grounding system + block_break_events.write(BlockBrokenEvent { position: pos }); + // Broadcast the change for (eid, conn) in &broadcast_query { if !state.0.players.is_connected(eid) { diff --git a/src/bin/src/register_messages.rs b/src/bin/src/register_messages.rs index 355d65783..b05e9bb09 100644 --- a/src/bin/src/register_messages.rs +++ b/src/bin/src/register_messages.rs @@ -6,9 +6,9 @@ use ferrumc_messages::chunk_calc::ChunkCalc; use ferrumc_messages::entity_update::SendEntityUpdate; use ferrumc_messages::particle::SendParticle; use ferrumc_messages::{ - PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating, PlayerFinishedDigging, - PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft, PlayerLeveledUp, - PlayerStartedDigging, SpawnEntityCommand, SpawnEntityEvent, + BlockBrokenEvent, PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating, + PlayerFinishedDigging, PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft, + PlayerLeveledUp, PlayerStartedDigging, SpawnEntityCommand, SpawnEntityEvent, }; use ferrumc_net::packets::packet_messages::Movement; @@ -34,4 +34,5 @@ pub fn register_messages(world: &mut World) { MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); MessageRegistry::register_message::(world); + MessageRegistry::register_message::(world); } diff --git a/src/bin/src/systems/listeners/digging_system.rs b/src/bin/src/systems/listeners/digging_system.rs index 99a06a650..b60ca8547 100644 --- a/src/bin/src/systems/listeners/digging_system.rs +++ b/src/bin/src/systems/listeners/digging_system.rs @@ -160,6 +160,7 @@ pub fn handle_finish_digging( state: Res, mut player_query: Query, broadcast_query: Query<(Entity, &StreamWriter)>, // For broadcasting the break + mut block_break_writer: MessageWriter, ) { for event in events.read() { let Ok((_player_entity, writer, digging_opt)) = player_query.get_mut(event.player) else { @@ -246,7 +247,12 @@ pub fn handle_finish_digging( // We wrap the block-breaking logic in its own function // to handle the errors cleanly (replaces `try` block). - if let Err(e) = break_block(&state, &broadcast_query, &event.position) { + if let Err(e) = break_block( + &state, + &broadcast_query, + &event.position, + &mut block_break_writer, + ) { error!("Error handling finished digging: {:?}", e); } } @@ -270,6 +276,7 @@ fn break_block( state: &Res, broadcast_query: &Query<(Entity, &StreamWriter)>, position: &ferrumc_net_codec::net_types::network_position::NetworkPosition, + block_break_writer: &mut MessageWriter, ) -> Result<(), BinaryError> { let pos: BlockPos = position.clone().into(); let mut chunk = match state @@ -298,6 +305,10 @@ fn break_block( .save_chunk(pos.chunk(), "overworld", Arc::new(chunk)) .map_err(BinaryError::World)?; + // Send block broken event for un-grounding system + debug!("Sending BlockBrokenEvent for block at {:?}", pos.pos); + block_break_writer.write(ferrumc_messages::BlockBrokenEvent { position: pos }); + // Broadcast the block break to all players let block_update_packet = BlockUpdate { location: position.clone(), diff --git a/src/bin/src/systems/physics/mod.rs b/src/bin/src/systems/physics/mod.rs index 439b8700d..c8582cf4d 100644 --- a/src/bin/src/systems/physics/mod.rs +++ b/src/bin/src/systems/physics/mod.rs @@ -2,11 +2,13 @@ use bevy_ecs::schedule::IntoScheduleConfigs; pub mod collisions; pub mod drag; pub mod gravity; +pub mod unground; pub mod velocity; pub fn register_physics(schedule: &mut bevy_ecs::schedule::Schedule) { schedule.add_systems( ( + unground::handle, gravity::handle, drag::handle, velocity::handle, diff --git a/src/bin/src/systems/physics/unground.rs b/src/bin/src/systems/physics/unground.rs new file mode 100644 index 000000000..8d79f329e --- /dev/null +++ b/src/bin/src/systems/physics/unground.rs @@ -0,0 +1,106 @@ +use bevy_ecs::message::MessageReader; +use bevy_ecs::prelude::{Query, Res, With}; +use bevy_math::IVec3; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::position::Position; +use ferrumc_entities::markers::HasCollisions; +use ferrumc_entities::PhysicalProperties; +use ferrumc_messages::BlockBrokenEvent; +use ferrumc_state::GlobalStateResource; +use tracing::debug; + +use super::collisions::is_solid_block; + +/// System that ungrounds entities when blocks are broken beneath them. +/// This runs only when BlockBrokenEvent messages are received, avoiding +/// the performance cost of checking every entity every tick. +/// The main purpose is to re-enable gravity for entities that lose their ground support. +pub fn handle( + mut events: MessageReader, + mut entities: Query<(&Position, &PhysicalProperties, &mut OnGround), With>, + state: Res, +) { + for event in events.read() { + let broken_block_pos = event.position; + debug!( + "Block broken at {:?}, checking entities for un-grounding", + broken_block_pos.pos + ); + + // Check all entities with collisions + for (pos, physical, mut grounded) in entities.iter_mut() { + // Skip entities that aren't grounded + if !grounded.0 { + continue; + } + + // Calculate entity's feet position + let entity_pos = pos.coords.as_vec3a(); + let feet_min = physical.bounding_box.min + entity_pos; + + // Check if the broken block was supporting this entity + // We check blocks just below the entity's feet + let feet_y = (feet_min.y - 0.01).floor() as i32; + + // Get the horizontal range of blocks the entity occupies + let min_x = feet_min.x.floor() as i32; + let max_x = (physical.bounding_box.max.x + entity_pos.x).floor() as i32; + let min_z = feet_min.z.floor() as i32; + let max_z = (physical.bounding_box.max.z + entity_pos.z).floor() as i32; + + debug!( + "Entity at {:?}, feet_y={}, support area: x=[{}, {}], z=[{}, {}]", + pos.coords, feet_y, min_x, max_x, min_z, max_z + ); + + // Check if the broken block is in the entity's support area + if broken_block_pos.pos.y == feet_y + && broken_block_pos.pos.x >= min_x + && broken_block_pos.pos.x <= max_x + && broken_block_pos.pos.z >= min_z + && broken_block_pos.pos.z <= max_z + { + // Block was broken beneath this entity + // Now check if there are ANY other solid blocks supporting it + let mut has_support = false; + + for x in min_x..=max_x { + for z in min_z..=max_z { + // Skip the broken block + if x == broken_block_pos.pos.x && z == broken_block_pos.pos.z { + continue; + } + + let check_pos = IVec3::new(x, feet_y, z); + if is_solid_block(&state.0, check_pos) { + has_support = true; + break; + } + } + if has_support { + break; + } + } + + // If no support found, unground the entity + if !has_support { + debug!( + "Un-grounding entity at {:?} - no support remaining", + pos.coords + ); + grounded.0 = false; + } else { + debug!( + "Entity at {:?} still has support after block break", + pos.coords + ); + } + } else { + debug!( + "Broken block {:?} not in entity's support area (feet_y={}, x=[{}, {}], z=[{}, {}])", + broken_block_pos.pos, feet_y, min_x, max_x, min_z, max_z + ); + } + } + } +} diff --git a/src/lib/messages/Cargo.toml b/src/lib/messages/Cargo.toml index 4c62d7e3c..470d4f45a 100644 --- a/src/lib/messages/Cargo.toml +++ b/src/lib/messages/Cargo.toml @@ -14,3 +14,4 @@ ferrumc-net-codec = { workspace = true } ferrumc-inventories = { workspace = true } ferrumc-entities = { workspace = true } ferrumc-particles = { workspace = true } +ferrumc-world = { workspace = true } diff --git a/src/lib/messages/src/block_break.rs b/src/lib/messages/src/block_break.rs new file mode 100644 index 000000000..b5fc1345b --- /dev/null +++ b/src/lib/messages/src/block_break.rs @@ -0,0 +1,8 @@ +use bevy_ecs::prelude::Message; +use ferrumc_world::pos::BlockPos; + +/// Message sent when a block is broken in the world +#[derive(Message)] +pub struct BlockBrokenEvent { + pub position: BlockPos, +} diff --git a/src/lib/messages/src/lib.rs b/src/lib/messages/src/lib.rs index 9c881f5a1..470f58630 100644 --- a/src/lib/messages/src/lib.rs +++ b/src/lib/messages/src/lib.rs @@ -26,3 +26,6 @@ pub mod entity_update; pub mod particle; pub use entity_spawn::{EntityType, SpawnEntityCommand, SpawnEntityEvent}; + +pub mod block_break; +pub use block_break::BlockBrokenEvent; From 30ad800540f0c9cdff9c313e1b34d1dd8b77eeb4 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:48:24 +0500 Subject: [PATCH 15/23] fix: speed up the block! and block_match! macro this commit adds build.rs which makes phf map allowing to speed up the calculation of the blockid --- src/lib/derive_macros/Cargo.toml | 5 + src/lib/derive_macros/build.rs | 58 +++++++ src/lib/derive_macros/src/block/matches.rs | 32 ++-- src/lib/derive_macros/src/block/mod.rs | 179 ++++++++------------- 4 files changed, 140 insertions(+), 134 deletions(-) create mode 100644 src/lib/derive_macros/build.rs diff --git a/src/lib/derive_macros/Cargo.toml b/src/lib/derive_macros/Cargo.toml index c39f26384..80514fffd 100644 --- a/src/lib/derive_macros/Cargo.toml +++ b/src/lib/derive_macros/Cargo.toml @@ -22,6 +22,11 @@ craftflow-nbt = { workspace = true } indexmap = { workspace = true, features = ["serde"] } bitcode = { workspace = true } simd-json = { workspace = true } +phf = "0.13.1" [dev-dependencies] ferrumc-world = { workspace = true } + +[build-dependencies] +phf_codegen = "0.13.1" +simd-json = { workspace = true } diff --git a/src/lib/derive_macros/build.rs b/src/lib/derive_macros/build.rs new file mode 100644 index 000000000..4420123cb --- /dev/null +++ b/src/lib/derive_macros/build.rs @@ -0,0 +1,58 @@ +use std::{ + collections::HashMap, + env, + fs::File, + io::{BufWriter, Write}, + path::Path, +}; + +use phf_codegen; +use simd_json::{ + self, + base::{ValueAsObject, ValueAsScalar}, + derived::ValueTryAsObject, +}; + +const JSON_FILE: &[u8] = include_bytes!("../../../assets/data/blockstates.json"); + +fn main() { + let mut buf = JSON_FILE.to_owned(); + let v = simd_json::to_borrowed_value(&mut buf).unwrap(); + let mut map = phf_codegen::Map::new(); + + let mut rev_map: HashMap> = HashMap::new(); + for (key, value) in v.try_as_object().expect("object value") { + let id = key.parse::().expect("integer value"); + let block = value.as_object().expect("object value"); + let name = block + .get("name") + .expect("all block states have names") + .as_str() + .expect("names are strings") + .split_once("minecraft:") + .expect("names start with \"minecraft:\"") + .1; + let props = block.get("properties"); + rev_map.entry(name.to_owned()).or_default().push(id); + if let Some(props) = props { + for (prop_key, prop_val) in props.as_object().expect("properties is object") { + let map_key = format!("{}:{}", prop_key, prop_val); + rev_map.entry(map_key).or_default().push(id); + } + } + } + for (k, v) in rev_map.iter_mut() { + v.sort(); + map.entry(k.clone(), format!("&{:?}", v)); + } + let map = map.build(); + let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + write!( + &mut file, + "static BLOCK_STATES: phf::Map<&'static str, &[u32]> = {};", + map + ) + .expect("able to write to file"); + println!("created {}", &path.to_string_lossy()); +} diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index 07fe3b392..f1235f6e3 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -1,8 +1,6 @@ -use crate::block::JSON_FILE; +use crate::block::BLOCK_STATES; use proc_macro::TokenStream; use quote::quote; -use simd_json::base::{ValueAsObject, ValueAsScalar}; -use simd_json::derived::ValueObjectAccess; struct Input { name: String, @@ -25,23 +23,15 @@ impl syn::parse::Parse for Input { // match_block!("dirt", block_state_id); -> "if block_name == BlockStateId( { ... } pub fn matches_block(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as Input); - let block_name = &input.name; - let block_name = if block_name.starts_with("minecraft:") { - block_name.to_string() - } else { - format!("minecraft:{}", block_name) - }; - let block_state_id_var = &input.id_var; - let mut buf = JSON_FILE.to_vec(); - let v = simd_json::to_owned_value(&mut buf).unwrap(); - let filtered_names = v - .as_object() - .unwrap() - .iter() - .filter(|(_, v)| v.get("name").as_str() == Some(&block_name)) - .map(|(k, v)| (k.parse::().unwrap(), v)) - .collect::>(); - if filtered_names.is_empty() { + let block_name = input + .name + .split_once("minecraft:") + .map(|x| x.1) + .unwrap_or(input.name.as_str()); + let block_id_var = &input.id_var; + + let states = BLOCK_STATES.get(block_name); + if states.is_none_or(|x| x.is_empty()) { return syn::Error::new_spanned( &input.id_var, format!("Block name '{}' not found in registry", block_name), @@ -58,5 +48,5 @@ pub fn matches_block(input: TokenStream) -> TokenStream { let joined = quote! { #(#arms)||* }; - joined.into() + matched.into() } diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index f11093b2d..2adb13828 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -1,14 +1,14 @@ use proc_macro::TokenStream; use quote::quote; -use simd_json::prelude::{ValueAsObject, ValueAsScalar, ValueObjectAccess}; -use simd_json::{OwnedValue, StaticNode}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::{braced, Expr, Ident, Lit, LitStr, Result, Token}; pub(crate) mod matches; -const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json"); +// const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json"); + +include!(concat!(env!("OUT_DIR"), "/codegen.rs")); struct Input { name: LitStr, @@ -63,39 +63,32 @@ impl Parse for Input { pub fn block(input: TokenStream) -> TokenStream { let Input { name, opts } = syn::parse_macro_input!(input as Input); - - let name_str = if name.value().starts_with("minecraft:") { - name.value() + let name_value = name.value(); + let name_str = if let Some((_, name)) = name_value.split_once("minecraft:") { + name } else { - format!("minecraft:{}", name.value()) + &name_value }; - let mut buf = JSON_FILE.to_vec(); - let v = simd_json::to_owned_value(&mut buf).unwrap(); - let filtered_names = v - .as_object() - .unwrap() - .iter() - .filter(|(_, v)| v.get("name").as_str() == Some(&name_str)) - .map(|(k, v)| (k.parse::().unwrap(), v)) - .collect::>(); + let states = BLOCK_STATES.get(name_str); + if states.is_none_or(|x| x.is_empty()) { + return syn::Error::new_spanned( + name.clone(), + format!("block '{}' not found in blockstates.json", name_value), + ) + .to_compile_error() + .into(); + } - let Some(opts) = opts else { - if filtered_names.is_empty() { - return syn::Error::new_spanned( - name.clone(), - format!("block '{}' not found in blockstates.json", name_str), - ) - .to_compile_error() - .into(); - } - if filtered_names.len() > 1 { - let properties = get_properties(&filtered_names); + let &states = states.unwrap(); + + if opts.is_none() { + if states.len() > 1 { return syn::Error::new_spanned( - name_str.clone(), + name, format!( - "block '{}' has multiple variants, please specify properties. Available properties: {}", - name_str, pretty_print_props(&properties) + "block '{}' has multiple variants, please specify properties.", + name_value ), ) .to_compile_error() @@ -105,6 +98,8 @@ pub fn block(input: TokenStream) -> TokenStream { return quote! { BlockStateId::new(#first) }.into(); }; + let opts = opts.unwrap(); + let props = opts .pairs .iter() @@ -130,65 +125,64 @@ pub fn block(input: TokenStream) -> TokenStream { }, )) }) - .collect::>>(); + .collect::>>(); let props = match props { Ok(props) => props, Err(err) => return err.to_compile_error().into(), }; - let matched = filtered_names - .iter() - .filter(|(_, v)| { - if let Some(map) = v.get("properties").as_object() { - // eq impl from halfbwown - if map.len() != props.len() { - return false; + fn intersect(a: &[u32], b: &[u32]) -> Vec { + let mut v = vec![]; + let mut i = 0; + let mut j = 0; + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + std::cmp::Ordering::Less => i += 1, + std::cmp::Ordering::Equal => { + v.push(a[i]); + i += 1; + j += 1; } - return map.iter().all(|(key, value)| { - let converted = match value { - OwnedValue::Static(StaticNode::Bool(v)) => v.to_string(), - OwnedValue::Static(StaticNode::I64(v)) => v.to_string(), - OwnedValue::String(v) => v.to_string(), - _ => return false, - }; - props.get(key).is_some_and(|v| *converted == *v) - }); + std::cmp::Ordering::Greater => j += 1, } - false - }) - .map(|(id, _)| *id) - .collect::>(); + } + v + } + + let mut states = states.to_owned(); - if matched.is_empty() { - let properties = get_properties(&filtered_names); - if properties.is_empty() { + for (k, v) in props { + let key = format!("{}:{}", k, v); + let prop_states = BLOCK_STATES.get(&key); + if prop_states.is_none_or(|x| x.is_empty()) { return syn::Error::new_spanned( - name_str.clone(), + name.clone(), + format!("No block has properties '{}'.", key), + ) + .to_compile_error() + .into(); + } + + let &prop_states = prop_states.unwrap(); + states = intersect(&states, prop_states); + if states.is_empty() { + return syn::Error::new_spanned( + name, format!( - "block '{}' has no properties but the following properties were given: {}", - name_str.clone(), - pretty_print_given_props(opts) + "No variant of block '{}' matches the specified properties.", + name_value ), ) .to_compile_error() .into(); - } else { - return syn::Error::new_spanned( - name_str.clone(), - format!( - "no variant of block '{}' matches the specified properties. Available properties: {}", - name_str.clone(), pretty_print_props(&properties) - ), - ) - .to_compile_error() - .into(); } } - if matched.len() > 1 { + + if states.len() > 1 { return syn::Error::new_spanned( - name_str.clone(), - format!("block '{}' with specified properties has multiple variants, please refine properties", name_str), + name, + format!("Block '{}' with specified properties has multiple variants ({:?}), please refine properties", name_value, states), ) .to_compile_error() .into(); @@ -198,46 +192,5 @@ pub fn block(input: TokenStream) -> TokenStream { quote! { BlockStateId::new(#res) }.into() } -fn get_properties(filtered_names: &[(u32, &OwnedValue)]) -> Vec<(String, String)> { - filtered_names - .iter() - .filter_map(|(_, v)| v.get("properties").and_then(|v| v.as_object())) - .flat_map(|v| { - v.iter().filter_map(|(k, v)| { - let converted = match v { - OwnedValue::Static(StaticNode::Bool(v)) => v.to_string(), - OwnedValue::Static(StaticNode::I64(v)) => v.to_string(), - OwnedValue::String(v) => v.to_string(), - _ => return None, - }; - Some((k.clone(), converted)) - }) - }) - .collect() -} - -fn pretty_print_props(props: &[(String, String)]) -> String { - let mut s = String::new(); - for (k, v) in props { - s.push_str(&format!("{}: {}, ", k, v)); - } - s.trim_end_matches(", ").to_string() -} - -fn pretty_print_given_props(props: Opts) -> String { - let mut s = String::new(); - for kv in props.pairs.iter() { - let key = kv.key.to_string(); - let value = match &kv.value { - Expr::Lit(v) => match &v.lit { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => "unsupported".to_string(), - }, - _ => "unsupported".to_string(), - }; - s.push_str(&format!("{}: {}, ", key, value)); - } - s.trim_end_matches(", ").to_string() + quote! { BlockStateId(#id) }.into() } From 06a0109bbbd9464621f5c21f09da7ba00fb31bc4 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:48:51 +0500 Subject: [PATCH 16/23] placeholders and multi-value this commit adds placeholder feature and restructures the parsing to allow _ and ident in property's value. --- src/lib/derive_macros/build.rs | 32 +- src/lib/derive_macros/src/block/matches.rs | 18 +- src/lib/derive_macros/src/block/mod.rs | 408 +++++++++++++++------ src/lib/derive_macros/src/lib.rs | 28 +- src/tests/Cargo.toml | 3 + src/tests/build.rs | 90 +++++ src/tests/src/block/mod.rs | 8 + src/tests/src/lib.rs | 5 +- 8 files changed, 460 insertions(+), 132 deletions(-) create mode 100644 src/tests/build.rs create mode 100644 src/tests/src/block/mod.rs diff --git a/src/lib/derive_macros/build.rs b/src/lib/derive_macros/build.rs index 4420123cb..0fa808b3d 100644 --- a/src/lib/derive_macros/build.rs +++ b/src/lib/derive_macros/build.rs @@ -1,12 +1,11 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, env, fs::File, io::{BufWriter, Write}, path::Path, }; -use phf_codegen; use simd_json::{ self, base::{ValueAsObject, ValueAsScalar}, @@ -19,8 +18,9 @@ fn main() { let mut buf = JSON_FILE.to_owned(); let v = simd_json::to_borrowed_value(&mut buf).unwrap(); let mut map = phf_codegen::Map::new(); - + let mut prop_map = phf_codegen::Map::new(); let mut rev_map: HashMap> = HashMap::new(); + let mut rev_prop: HashMap> = HashMap::new(); for (key, value) in v.try_as_object().expect("object value") { let id = key.parse::().expect("integer value"); let block = value.as_object().expect("object value"); @@ -34,9 +34,18 @@ fn main() { .1; let props = block.get("properties"); rev_map.entry(name.to_owned()).or_default().push(id); + rev_prop.entry(name.to_string()).or_default(); if let Some(props) = props { for (prop_key, prop_val) in props.as_object().expect("properties is object") { let map_key = format!("{}:{}", prop_key, prop_val); + rev_prop + .entry(prop_key.to_string()) + .or_default() + .insert(prop_val.to_string()); + rev_prop + .entry(name.to_string()) + .or_default() + .insert(prop_key.to_string()); rev_map.entry(map_key).or_default().push(id); } } @@ -45,14 +54,27 @@ fn main() { v.sort(); map.entry(k.clone(), format!("&{:?}", v)); } + + for (k, v) in rev_prop.iter() { + let mut v: Vec = v.iter().cloned().collect(); + v.sort(); + prop_map.entry(k.clone(), format!("&{:?}", v)); + } let map = map.build(); + let prop_map = prop_map.build(); let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs"); let mut file = BufWriter::new(File::create(&path).unwrap()); - write!( + writeln!( &mut file, - "static BLOCK_STATES: phf::Map<&'static str, &[u32]> = {};", + "static BLOCK_STATES: phf::Map<&'static str, &[u16]> = {};", map ) .expect("able to write to file"); + writeln!( + &mut file, + "static PROP_PARTS: phf::Map<&'static str, &[&'static str]> = {};", + prop_map + ) + .expect("able to write to file"); println!("created {}", &path.to_string_lossy()); } diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index f1235f6e3..943d57bbb 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -19,8 +19,6 @@ impl syn::parse::Parse for Input { } } -// match_block!("stone", block_state_id); -> "if block_state_id == BlockStateId(1) { ... } -// match_block!("dirt", block_state_id); -> "if block_name == BlockStateId( { ... } pub fn matches_block(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as Input); let block_name = input @@ -39,14 +37,14 @@ pub fn matches_block(input: TokenStream) -> TokenStream { .to_compile_error() .into(); } - let mut arms = Vec::new(); - for (id, _) in filtered_names { - arms.push(quote! { - #block_state_id_var == BlockStateId::new(#id) - }); - } - let joined = quote! { - #(#arms)||* + + let states = states.unwrap().iter().map(|&x| x as u32); + + let matched = quote! { + match #block_id_var { + BlockId(#(#states)|*) => true, + _ => false + } }; matched.into() } diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index 2adb13828..154d11614 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -2,12 +2,10 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{braced, Expr, Ident, Lit, LitStr, Result, Token}; +use syn::{braced, Error, Ident, Lit, LitStr, Result, Token}; pub(crate) mod matches; -// const JSON_FILE: &[u8] = include_bytes!("../../../../../assets/data/blockstates.json"); - include!(concat!(env!("OUT_DIR"), "/codegen.rs")); struct Input { @@ -18,11 +16,56 @@ struct Input { struct Opts { pairs: Punctuated, } - +#[derive(Debug)] struct Kv { key: Ident, _colon: Token![:], - value: Expr, // accept bools, strings, ints, calls, etc. + value: Value, +} + +impl Kv { + fn to_string_pair(&self) -> (String, String) { + ( + match &self.key.to_string() { + v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), + v => v.clone(), + }, + match &self.value { + Value::Static(v) => match v { + Lit::Str(v) => v.value(), + Lit::Bool(v) => v.value.to_string(), + Lit::Int(v) => v.base10_digits().to_string(), + _ => unreachable!(), + }, + Value::Any(_) => "_".into(), + Value::Ident(v) => v.to_string(), + }, + ) + } +} +#[derive(Debug)] +enum Value { + Static(Lit), + Any(Token![_]), + Ident(Ident), +} + +impl Parse for Value { + fn parse(input: ParseStream) -> Result { + if input.peek(Token![_]) { + return Ok(Self::Any(input.parse()?)); + } + if input.peek(Ident) { + return Ok(Self::Ident(input.parse()?)); + } + if input.peek(Lit) { + let lit = input.parse()?; + if matches!(lit, Lit::Str(_) | Lit::Bool(_) | Lit::Int(_)) { + return Ok(Self::Static(lit)); + } + } + Err(input.error("the property value must be _, ident or literal string, bool or int")) + } } impl Parse for Kv { @@ -61,132 +104,281 @@ impl Parse for Input { } } -pub fn block(input: TokenStream) -> TokenStream { - let Input { name, opts } = syn::parse_macro_input!(input as Input); - let name_value = name.value(); - let name_str = if let Some((_, name)) = name_value.split_once("minecraft:") { - name - } else { - &name_value - }; +impl quote::ToTokens for Input { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.name.to_tokens(tokens); + self.opts.to_tokens(tokens); + } +} - let states = BLOCK_STATES.get(name_str); - if states.is_none_or(|x| x.is_empty()) { - return syn::Error::new_spanned( - name.clone(), - format!("block '{}' not found in blockstates.json", name_value), - ) - .to_compile_error() - .into(); +impl quote::ToTokens for Opts { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.pairs.to_tokens(tokens); } +} - let &states = states.unwrap(); +impl quote::ToTokens for Kv { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.key.to_tokens(tokens); + self._colon.to_tokens(tokens); + self.value.to_tokens(tokens); + } +} - if opts.is_none() { - if states.len() > 1 { - return syn::Error::new_spanned( - name, - format!( - "block '{}' has multiple variants, please specify properties.", - name_value - ), - ) - .to_compile_error() - .into(); +impl quote::ToTokens for Value { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Value::Static(lit) => lit.to_tokens(tokens), + Value::Any(underscore) => underscore.to_tokens(tokens), + Value::Ident(ident) => ident.to_tokens(tokens), } let first = filtered_names.first().unwrap().0; return quote! { BlockStateId::new(#first) }.into(); }; - let opts = opts.unwrap(); +pub fn block(input: TokenStream) -> TokenStream { + let out = match syn::parse_macro_input!(input as Input) { + Input { name, opts: None } => static_block(name), + Input { + name, + opts: Some(opts), + } => block_with_props(name, opts), + }; + match out { + Ok(v) => v, + Err(v) => v.into_compile_error().into(), + } +} + +fn block_with_props(name: LitStr, opts: Opts) -> Result { + let name_value = parse_name(&name); + let props = PROP_PARTS.get(&name_value); + if props.is_none() { + return Err(Error::new_spanned( + &name, + format!( + "the block `{}` is not found in the blockstates.json file (PROP_PARTS is not populated)", + name_value + ), + )); + } + if props.is_some_and(|x| x.is_empty()) { + return Err(Error::new_spanned( + &name, + format!("the block `{}` has no properties", name_value), + )); + } + let props = props.unwrap().to_vec(); - let props = opts + let opts_strings = opts .pairs .iter() - .map(|kv| { - Ok(( - kv.key.to_string(), - match &kv.value { - Expr::Lit(v) => match &v.lit { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => return Err(syn::Error::new_spanned( - &kv.value, - "only string, bool, and int literals are supported as property values", - )), - }, - _ => { - return Err(syn::Error::new_spanned( - &kv.value, - "only string, bool, and int literals are supported as property values", - )) - } - }, - )) - }) - .collect::>>(); - - let props = match props { - Ok(props) => props, - Err(err) => return err.to_compile_error().into(), - }; + .map(Kv::to_string_pair) + .zip(opts.pairs.iter()) + .collect::>(); + let mut unknown_props = Vec::new(); + let mut missing_props = Vec::new(); + for prop in &props { + if !opts_strings.iter().any(|((k, _), _)| k == prop) { + missing_props.push(prop.to_string()); + } + } + for ((k, _), _) in &opts_strings { + if !props.contains(&k.as_str()) { + unknown_props.push(k.clone()); + } + } + if !unknown_props.is_empty() { + return Err(Error::new_spanned( + &name, + format!( + "the block {name_value} has no properties with names: [{}]", + unknown_props.join(", ") + ), + )); + } + if !missing_props.is_empty() { + return Err(Error::new_spanned( + name, + format!( + "the block `{name_value}` is missing these properties: [{}]", + missing_props.join(", ") + ), + )); + } - fn intersect(a: &[u32], b: &[u32]) -> Vec { - let mut v = vec![]; - let mut i = 0; - let mut j = 0; - while i < a.len() && j < b.len() { - match a[i].cmp(&b[j]) { - std::cmp::Ordering::Less => i += 1, - std::cmp::Ordering::Equal => { - v.push(a[i]); - i += 1; - j += 1; + let mut block_states = BLOCK_STATES + .get(&name_value) + .expect(&format!("block name {name_value} should be present")) + .to_vec(); + for ((k, v), kv) in &opts_strings { + let property_filter = match &kv.value { + Value::Static(_) => BLOCK_STATES + .get(&format!("{k}:{v}")) + .ok_or(Error::new_spanned( + kv, + format!( + "the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", + PROP_PARTS + .get(k) + .expect("the key `{k}` exists in PROP_PARTS") + .join(", ") + ), + ))? + .to_vec(), + Value::Any(_) => { + let vs = PROP_PARTS + .get(k) + .ok_or( + Error::new_spanned( + &kv.key, + format!("the property `{k}` is not present in the blockstates.json file (PROP_PARTS is not populated)") + ) + )? + .iter() + .map(|v| BLOCK_STATES + .get(&format!("{k}:{v}")) + .ok_or( + Error::new_spanned( + kv, + format!("the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", PROP_PARTS + .get(k) + .expect(&format!("the key `{k}` exists in PROP_PARTS")).join(", ") + ) + )) + ) + .collect::>>()?; + let mut combined_block_states = Vec::new(); + for bs in vs { + combined_block_states = combine(&combined_block_states, bs); } - std::cmp::Ordering::Greater => j += 1, + combined_block_states } - } - v + Value::Ident(_) => { + return Err(Error::new_spanned(&kv.key, "Ident keys are not supported")) + } + }; + block_states = intersect(&block_states, &property_filter); } - let mut states = states.to_owned(); + if block_states.is_empty() { + return Err(Error::new_spanned( + Input { + name, + opts: Some(opts), + }, + "no block state corresponds to this combination of properties", + )); + } + let block_ids = block_states.iter().map(|x| *x as u32); + Ok(quote! {BlockId(#(#block_ids)|*)}.into()) +} - for (k, v) in props { - let key = format!("{}:{}", k, v); - let prop_states = BLOCK_STATES.get(&key); - if prop_states.is_none_or(|x| x.is_empty()) { - return syn::Error::new_spanned( - name.clone(), - format!("No block has properties '{}'.", key), - ) - .to_compile_error() - .into(); - } +fn static_block(name: LitStr) -> Result { + let name_value = parse_name(&name); + let props = PROP_PARTS.get(&name_value); + if props.is_none() { + return Err(Error::new_spanned( + &name, + format!( + "the block `{name_value}` not found in blockstates.json (PROP_PARTS is not populated)", + ), + )); + } + if props.is_some_and(|x| !x.is_empty()) { + let &props = props.unwrap(); + return Err(Error::new_spanned( + &name, + format!( + "the block `{name_value}` has these properties: [{}], please refine properties", + props.join(", ") + ), + )); + } + let block_states = BLOCK_STATES.get(&name_value); + if block_states.is_none_or(|x| x.is_empty()) { + return Err(Error::new_spanned( + &name, + format!( + "the block `{name_value}` not found in the blockstates.json file (BLOCK_STATE is not populated)", + ), + )); + } + let block_states = block_states.unwrap(); + if block_states.len() > 1 { + return Err(Error::new_spanned( + name, + format!( + "the block `{name_value}` has multiple block states: [{}], but only one expected", + block_states + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ") + ), + )); + } - let &prop_states = prop_states.unwrap(); - states = intersect(&states, prop_states); - if states.is_empty() { - return syn::Error::new_spanned( - name, - format!( - "No variant of block '{}' matches the specified properties.", - name_value - ), - ) - .to_compile_error() - .into(); + let id = block_states[0] as u32; + Ok(quote! { BlockId(#id) }.into()) +} + +fn parse_name(name: &LitStr) -> String { + let name_value = name.value(); + match name_value.split_once("minecraft:") { + Some((_, name)) => name.to_string(), + None => name_value, + } +} + +fn intersect(a: &[u16], b: &[u16]) -> Vec { + let mut v = vec![]; + let mut i = 0; + let mut j = 0; + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + std::cmp::Ordering::Less => i += 1, + std::cmp::Ordering::Equal => { + v.push(a[i]); + i += 1; + j += 1; + } + std::cmp::Ordering::Greater => j += 1, } } + v +} - if states.len() > 1 { - return syn::Error::new_spanned( - name, - format!("Block '{}' with specified properties has multiple variants ({:?}), please refine properties", name_value, states), - ) - .to_compile_error() - .into(); +fn combine(a: &[u16], b: &[u16]) -> Vec { + let mut v = vec![]; + let mut i = 0; + let mut j = 0; + while i < a.len() && j < b.len() { + match a[i].cmp(&b[j]) { + std::cmp::Ordering::Less => { + v.push(a[i]); + i += 1; + } + std::cmp::Ordering::Equal => { + v.push(a[i]); + i += 1; + j += 1; + } + std::cmp::Ordering::Greater => { + v.push(b[j]); + j += 1; + } + } } + if i < a.len() || j < b.len() { + if i < a.len() { + v.extend_from_slice(&a[i..]); + } else { + v.extend_from_slice(&b[j..]); + } + } + v +} let res = matched[0]; quote! { BlockStateId::new(#res) }.into() diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index ee9903925..f2f0784d7 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -107,10 +107,16 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream { /// ``` /// # use ferrumc_world::block_state_id::BlockStateId; /// # use ferrumc_macros::block; -/// let block_state_id = block!("stone"); -/// let another_block_state_id = block!("minecraft:grass_block", {snowy: true}); -/// assert_eq!(block_state_id, BlockStateId::new(1)); -/// assert_eq!(another_block_state_id, BlockStateId::new(8)); +/// assert_eq!(BlockStateId(1), block!("stone")); +/// assert_eq!(BlockStateId(8) ,block!("minecraft:grass_block", {snowy: true})); +/// for i in 581..1730 { +/// assert!( +/// matches!( +/// BlockStateId(i), +/// block!("note_block", { note: _, powered: _, instrument: _}) +/// ) +/// ); +/// } /// ``` /// Unfortunately, due to current limitations in Rust's proc macros, you will need to import the /// `BlockStateId` struct manually. @@ -119,6 +125,13 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream { /// /// If the block or properties are invalid, a compile-time error will be thrown that should hopefully /// explain the issue. +/// # Static block +/// gives a single block pat. Mainly used with +/// `matches!` or `match`. `if let block!("stone" = bs_id) { .. }`, +/// `match bs_id { block!("stone" => { .. }), }` and `block!(expr)` +/// are not implemented and will panic at compile time. Panics if block state has any properties. +/// # Expr with properties +/// any part of this can be a placeholder or a literal, variables are not allowed. Be careful as this can bloat the code. #[proc_macro] pub fn block(input: TokenStream) -> TokenStream { block::block(input) @@ -129,9 +142,10 @@ pub fn block(input: TokenStream) -> TokenStream { /// ``` /// # use ferrumc_macros::{match_block, block}; /// # use ferrumc_world::block_state_id::BlockStateId; -/// let block_state_id = block!("stone"); -/// if match_block!("stone", block_state_id) { -/// // do something +/// let block_id = BlockStateId(1); +/// assert!(match_block!("stone", block_id)); +/// for i in 581..1730 { +/// assert!(match_block!("note_block", BlockStateId(i))); /// } /// ``` /// Unfortunately, due to current limitations in Rust's proc macros, you will need to import the diff --git a/src/tests/Cargo.toml b/src/tests/Cargo.toml index ccfa87792..a707501be 100644 --- a/src/tests/Cargo.toml +++ b/src/tests/Cargo.toml @@ -16,3 +16,6 @@ maplit = { workspace = true } [lints] workspace = true + +[build-dependencies] +simd-json = { workspace = true } diff --git a/src/tests/build.rs b/src/tests/build.rs new file mode 100644 index 000000000..d25e1c6be --- /dev/null +++ b/src/tests/build.rs @@ -0,0 +1,90 @@ +use std::{ + env, + fs::File, + io::{BufWriter, Write}, + path::Path, +}; + +use simd_json::{ + self, + base::{ValueAsObject, ValueAsScalar}, + derived::ValueTryAsObject, +}; + +const JSON_FILE: &[u8] = include_bytes!("../../assets/data/blockstates.json"); + +fn main() { + let mut buf = JSON_FILE.to_owned(); + let v = simd_json::to_borrowed_value(&mut buf).unwrap(); + + let mut out = vec![]; + for (k, v) in v.try_as_object().expect("object value") { + let id = k; + let block = v.as_object().unwrap(); + let name = block.get("name").unwrap().as_str().unwrap(); + let props = block.get("properties"); + if let Some(props) = props { + out.push(( + id.parse::().unwrap(), + format!( + " assert_eq!(block!(\"{}\", {}), BlockId({}));", + name, + format_props(props), + id + ), + )); + } else { + out.push(( + id.parse::().unwrap(), + format!(" assert_eq!(block!(\"{}\"), BlockId({}));", name, id), + )); + } + } + + out.sort_by_key(|(k, _)| *k); + let out = out.into_iter().map(|(_, v)| v).collect::>(); + let path = + Path::new(&env::var("OUT_DIR").expect("OUT_DIR env varible set")).join("block_test.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + for (i, chunk) in out.chunks(40).enumerate() { + write!( + &mut file, + "#[ignore]\n#[test]\nfn all_the_blocks_{i}() {{\n{}\n}}\n\n", + chunk.join("\n") + ) + .expect("able to write to file"); + } + println!("created {}", &path.to_string_lossy()); +} + +fn format_props(props: &simd_json::BorrowedValue) -> String { + let props_str = props + .as_object() + .unwrap() + .iter() + .map(|(k, v)| { + let k_str = match k.as_ref() { + "type" => "r#type".to_string(), + _ => k.to_string(), + }; + let v_str = match v { + simd_json::BorrowedValue::Static(static_node) => match static_node { + simd_json::StaticNode::I64(v) => v.to_string(), + simd_json::StaticNode::U64(v) => v.to_string(), + simd_json::StaticNode::F64(v) => v.to_string(), + simd_json::StaticNode::Bool(v) => (match v { + true => "true", + false => "false", + }) + .to_string(), + simd_json::StaticNode::Null => "\"null\"".to_string(), + }, + simd_json::BorrowedValue::String(cow) => format!("\"{}\"", cow), + _ => unreachable!(), + }; + format!(" {}: {}", k_str, v_str) + }) + .collect::>() + .join(","); + format!("{{{}}}", props_str) +} diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs new file mode 100644 index 000000000..1fb450350 --- /dev/null +++ b/src/tests/src/block/mod.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod test { + use ferrumc_macros::block; + #[derive(Debug, PartialEq, Eq)] + struct BlockId(u32); + #[cfg(false)] + include!(concat!(env!("OUT_DIR"), "/block_test.rs")); +} diff --git a/src/tests/src/lib.rs b/src/tests/src/lib.rs index fcbce9378..bb79e2753 100644 --- a/src/tests/src/lib.rs +++ b/src/tests/src/lib.rs @@ -1,4 +1,5 @@ #![cfg(test)] -mod nbt; -mod net; +mod block; +//mod nbt; +//mod net; From 7fbd8ed38962eb01280e8e0c2e44bdb01f591544 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:20:24 +0500 Subject: [PATCH 17/23] add .. to allow skip missing properties this follows Struct patterns more closely --- src/lib/derive_macros/src/block/mod.rs | 148 ++++++++++++++++--------- src/tests/build.rs | 2 +- 2 files changed, 98 insertions(+), 52 deletions(-) diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index 154d11614..42dd7e46a 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{braced, Error, Ident, Lit, LitStr, Result, Token}; +use syn::{braced, token::Brace, Error, Ident, Lit, LitStr, Result, Token}; pub(crate) mod matches; @@ -14,8 +14,11 @@ struct Input { } struct Opts { + _brace: Brace, pairs: Punctuated, + _dot2: Option, } + #[derive(Debug)] struct Kv { key: Ident, @@ -24,25 +27,27 @@ struct Kv { } impl Kv { - fn to_string_pair(&self) -> (String, String) { - ( - match &self.key.to_string() { - v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), - v => v.clone(), - }, - match &self.value { - Value::Static(v) => match v { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => unreachable!(), - }, - Value::Any(_) => "_".into(), - Value::Ident(v) => v.to_string(), + fn key_str(&self) -> String { + match &self.key.to_string() { + v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), + v => v.clone(), + } + } + + fn value_str(&self) -> String { + match &self.value { + Value::Static(v) => match v { + Lit::Str(v) => v.value(), + Lit::Bool(v) => v.value.to_string(), + Lit::Int(v) => v.base10_digits().to_string(), + _ => unreachable!(), }, - ) + Value::Any(_) => "_".into(), + Value::Ident(v) => v.to_string(), + } } } + #[derive(Debug)] enum Value { Static(Lit), @@ -81,9 +86,27 @@ impl Parse for Kv { impl Parse for Opts { fn parse(input: ParseStream) -> Result { let content; - braced!(content in input); + let _brace = braced!(content in input); + + let mut pairs = Punctuated::new(); + let mut _dot2 = None; + while !content.is_empty() { + if content.peek(Token![..]) { + _dot2 = Some(content.parse()?); + break; + } + pairs.push_value(content.call(Kv::parse)?); + if content.is_empty() { + break; + } + let punct: Token![,] = content.parse()?; + pairs.push_punct(punct); + } + Ok(Self { - pairs: content.parse_terminated(Kv::parse, Token![,])?, + _brace, + pairs, + _dot2, }) } } @@ -107,13 +130,23 @@ impl Parse for Input { impl quote::ToTokens for Input { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { self.name.to_tokens(tokens); + if self.opts.is_some() { + ::default().to_tokens(tokens); + } self.opts.to_tokens(tokens); } } impl quote::ToTokens for Opts { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.pairs.to_tokens(tokens); + self._brace.surround(tokens, |tokens| { + self.pairs.to_tokens(tokens); + // NOTE: We need a comma before the dot2 token if it is present. + if !self.pairs.empty_or_trailing() && self._dot2.is_some() { + ::default().to_tokens(tokens); + } + self._dot2.to_tokens(tokens); + }); } } @@ -157,15 +190,14 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { return Err(Error::new_spanned( &name, format!( - "the block `{}` is not found in the blockstates.json file (PROP_PARTS is not populated)", - name_value + "the block `{name_value}` is not found in the blockstates.json file (PROP_PARTS is not populated)" ), )); } if props.is_some_and(|x| x.is_empty()) { return Err(Error::new_spanned( &name, - format!("the block `{}` has no properties", name_value), + format!("the block `{name_value}` has no properties"), )); } let props = props.unwrap().to_vec(); @@ -173,33 +205,34 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { let opts_strings = opts .pairs .iter() - .map(Kv::to_string_pair) + .map(Kv::key_str) .zip(opts.pairs.iter()) .collect::>(); let mut unknown_props = Vec::new(); - let mut missing_props = Vec::new(); - for prop in &props { - if !opts_strings.iter().any(|((k, _), _)| k == prop) { - missing_props.push(prop.to_string()); - } - } - for ((k, _), _) in &opts_strings { + for (k, _) in &opts_strings { if !props.contains(&k.as_str()) { unknown_props.push(k.clone()); } } if !unknown_props.is_empty() { return Err(Error::new_spanned( - &name, + &opts, format!( - "the block {name_value} has no properties with names: [{}]", + "the block `{name_value}` has no properties with names: [{}]", unknown_props.join(", ") ), )); } - if !missing_props.is_empty() { + + let mut missing_props = Vec::new(); + for prop in &props { + if !opts_strings.iter().any(|(k, _)| k == prop) { + missing_props.push(prop.to_string()); + } + } + if opts._dot2.is_none() && !missing_props.is_empty() { return Err(Error::new_spanned( - name, + opts, format!( "the block `{name_value}` is missing these properties: [{}]", missing_props.join(", ") @@ -211,16 +244,18 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .get(&name_value) .expect(&format!("block name {name_value} should be present")) .to_vec(); - for ((k, v), kv) in &opts_strings { + for kv in &opts.pairs { + let k = kv.key_str(); + let v = kv.value_str(); let property_filter = match &kv.value { Value::Static(_) => BLOCK_STATES .get(&format!("{k}:{v}")) - .ok_or(Error::new_spanned( + .ok_or_else(|| Error::new_spanned( kv, format!( "the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", PROP_PARTS - .get(k) + .get(&k) .expect("the key `{k}` exists in PROP_PARTS") .join(", ") ), @@ -228,8 +263,8 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .to_vec(), Value::Any(_) => { let vs = PROP_PARTS - .get(k) - .ok_or( + .get(&k) + .ok_or_else(|| Error::new_spanned( &kv.key, format!("the property `{k}` is not present in the blockstates.json file (PROP_PARTS is not populated)") @@ -238,18 +273,10 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .iter() .map(|v| BLOCK_STATES .get(&format!("{k}:{v}")) - .ok_or( - Error::new_spanned( - kv, - format!("the value `{v}` is not a valid value for the property `{k}`, available values: [{}]", PROP_PARTS - .get(k) - .expect(&format!("the key `{k}` exists in PROP_PARTS")).join(", ") - ) - )) - ) - .collect::>>()?; + .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + ); let mut combined_block_states = Vec::new(); - for bs in vs { + for &bs in vs { combined_block_states = combine(&combined_block_states, bs); } combined_block_states @@ -261,6 +288,25 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { block_states = intersect(&block_states, &property_filter); } + if opts._dot2.is_some() { + for k in missing_props { + let missing_block_states = PROP_PARTS + .get(&k) + .expect(&format!("the key {k} exists in PROP_PARTS")) + .iter() + .map(|v| { + BLOCK_STATES + .get(&format!("{k}:{v}")) + .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + }); + let mut combined = Vec::new(); + for &bs in missing_block_states { + combined = combine(&combined, bs); + } + block_states = intersect(&block_states, &combined); + } + } + if block_states.is_empty() { return Err(Error::new_spanned( Input { diff --git a/src/tests/build.rs b/src/tests/build.rs index d25e1c6be..7c7e64588 100644 --- a/src/tests/build.rs +++ b/src/tests/build.rs @@ -49,7 +49,7 @@ fn main() { for (i, chunk) in out.chunks(40).enumerate() { write!( &mut file, - "#[ignore]\n#[test]\nfn all_the_blocks_{i}() {{\n{}\n}}\n\n", + "#[test]\nfn all_the_blocks_{i}() {{\n{}\n}}\n\n", chunk.join("\n") ) .expect("able to write to file"); From 47eb7ed30993c05f6eedfa22e22ef2355e4c6ea1 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:20:11 +0500 Subject: [PATCH 18/23] add _ in props position and use `block!` macro in worldgen crate --- src/lib/derive_macros/src/block/matches.rs | 2 +- src/lib/derive_macros/src/block/mod.rs | 246 +++++++++++---------- src/lib/world/src/vanilla_chunk_format.rs | 4 +- src/tests/build.rs | 7 +- src/tests/src/block/mod.rs | 2 +- 5 files changed, 144 insertions(+), 117 deletions(-) diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index 943d57bbb..ff70b142a 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -42,7 +42,7 @@ pub fn matches_block(input: TokenStream) -> TokenStream { let matched = quote! { match #block_id_var { - BlockId(#(#states)|*) => true, + BlockStateId(#(#states)|*) => true, _ => false } }; diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index 42dd7e46a..3e01f485a 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -13,77 +13,62 @@ struct Input { opts: Option, } -struct Opts { - _brace: Brace, - pairs: Punctuated, - _dot2: Option, -} - -#[derive(Debug)] -struct Kv { - key: Ident, - _colon: Token![:], - value: Value, -} - -impl Kv { - fn key_str(&self) -> String { - match &self.key.to_string() { - v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), - v => v.clone(), +impl Parse for Input { + fn parse(input: ParseStream) -> Result { + let name: LitStr = input.parse()?; + let opts = if input.peek(Token![,]) { + let _comma: Token![,] = input.parse()?; + Some(input.parse::()?) + } else { + None + }; + if !input.is_empty() { + return Err(input.error("unexpected tokens after optional { ... }")); } + Ok(Self { name, opts }) } +} - fn value_str(&self) -> String { - match &self.value { - Value::Static(v) => match v { - Lit::Str(v) => v.value(), - Lit::Bool(v) => v.value.to_string(), - Lit::Int(v) => v.base10_digits().to_string(), - _ => unreachable!(), - }, - Value::Any(_) => "_".into(), - Value::Ident(v) => v.to_string(), +impl quote::ToTokens for Input { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.name.to_tokens(tokens); + if self.opts.is_some() { + ::default().to_tokens(tokens); } + self.opts.to_tokens(tokens); } } -#[derive(Debug)] -enum Value { - Static(Lit), +enum Opts { Any(Token![_]), - Ident(Ident), + Pairs(Pairs), } -impl Parse for Value { +impl Parse for Opts { fn parse(input: ParseStream) -> Result { if input.peek(Token![_]) { return Ok(Self::Any(input.parse()?)); } - if input.peek(Ident) { - return Ok(Self::Ident(input.parse()?)); - } - if input.peek(Lit) { - let lit = input.parse()?; - if matches!(lit, Lit::Str(_) | Lit::Bool(_) | Lit::Int(_)) { - return Ok(Self::Static(lit)); - } - } - Err(input.error("the property value must be _, ident or literal string, bool or int")) + Ok(Self::Pairs(input.parse()?)) } } -impl Parse for Kv { - fn parse(input: ParseStream) -> Result { - Ok(Self { - key: input.parse()?, - _colon: input.parse()?, - value: input.parse()?, - }) +impl quote::ToTokens for Opts { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Opts::Any(underscore) => underscore.to_tokens(tokens), + Opts::Pairs(pairs) => pairs.to_tokens(tokens), + } } } -impl Parse for Opts { +struct Pairs { + _brace: Brace, + pairs: Punctuated, + _dot2: Option, +} + +impl Parse for Pairs { fn parse(input: ParseStream) -> Result { let content; let _brace = braced!(content in input); @@ -102,8 +87,7 @@ impl Parse for Opts { let punct: Token![,] = content.parse()?; pairs.push_punct(punct); } - - Ok(Self { + Ok(Pairs { _brace, pairs, _dot2, @@ -111,33 +95,7 @@ impl Parse for Opts { } } -impl Parse for Input { - fn parse(input: ParseStream) -> Result { - let name: LitStr = input.parse()?; - let opts = if input.peek(Token![,]) { - let _comma: Token![,] = input.parse()?; - Some(input.parse::()?) - } else { - None - }; - if !input.is_empty() { - return Err(input.error("unexpected tokens after optional { ... }")); - } - Ok(Self { name, opts }) - } -} - -impl quote::ToTokens for Input { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.name.to_tokens(tokens); - if self.opts.is_some() { - ::default().to_tokens(tokens); - } - self.opts.to_tokens(tokens); - } -} - -impl quote::ToTokens for Opts { +impl quote::ToTokens for Pairs { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { self._brace.surround(tokens, |tokens| { self.pairs.to_tokens(tokens); @@ -149,6 +107,43 @@ impl quote::ToTokens for Opts { }); } } +struct Kv { + key: Ident, + _colon: Token![:], + value: Value, +} + +impl Kv { + fn key_str(&self) -> String { + match &self.key.to_string() { + v if v.starts_with("r#") => v.split_once("r#").unwrap().1.to_string(), + v => v.clone(), + } + } + + fn value_str(&self) -> String { + match &self.value { + Value::Static(v) => match v { + Lit::Str(v) => v.value(), + Lit::Bool(v) => v.value.to_string(), + Lit::Int(v) => v.base10_digits().to_string(), + _ => unreachable!(), + }, + Value::Any(_) => "_".into(), + Value::Ident(v) => v.to_string(), + } + } +} + +impl Parse for Kv { + fn parse(input: ParseStream) -> Result { + Ok(Self { + key: input.parse()?, + _colon: input.parse()?, + value: input.parse()?, + }) + } +} impl quote::ToTokens for Kv { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { @@ -158,6 +153,30 @@ impl quote::ToTokens for Kv { } } +enum Value { + Static(Lit), + Any(Token![_]), + Ident(Ident), +} + +impl Parse for Value { + fn parse(input: ParseStream) -> Result { + if input.peek(Token![_]) { + return Ok(Self::Any(input.parse()?)); + } + if input.peek(Ident) { + return Ok(Self::Ident(input.parse()?)); + } + if input.peek(Lit) { + let lit = input.parse()?; + if matches!(lit, Lit::Str(_) | Lit::Bool(_) | Lit::Int(_)) { + return Ok(Self::Static(lit)); + } + } + Err(input.error("the property value must be _, ident or literal string, bool or int")) + } +} + impl quote::ToTokens for Value { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { match self { @@ -174,8 +193,12 @@ pub fn block(input: TokenStream) -> TokenStream { Input { name, opts: None } => static_block(name), Input { name, - opts: Some(opts), - } => block_with_props(name, opts), + opts: Some(Opts::Pairs(pairs)), + } => block_with_props(name, pairs), + Input { + name, + opts: Some(Opts::Any(_)), + } => block_with_any_props(name), }; match out { Ok(v) => v, @@ -183,24 +206,22 @@ pub fn block(input: TokenStream) -> TokenStream { } } -fn block_with_props(name: LitStr, opts: Opts) -> Result { +fn block_with_props(name: LitStr, opts: Pairs) -> Result { let name_value = parse_name(&name); - let props = PROP_PARTS.get(&name_value); - if props.is_none() { - return Err(Error::new_spanned( + let props = PROP_PARTS.get(&name_value).ok_or_else(|| + Error::new_spanned( &name, format!( "the block `{name_value}` is not found in the blockstates.json file (PROP_PARTS is not populated)" - ), - )); - } - if props.is_some_and(|x| x.is_empty()) { + )))?; + + if props.is_empty() { return Err(Error::new_spanned( &name, format!("the block `{name_value}` has no properties"), )); } - let props = props.unwrap().to_vec(); + let props = props.to_vec(); let opts_strings = opts .pairs @@ -242,7 +263,7 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { let mut block_states = BLOCK_STATES .get(&name_value) - .expect(&format!("block name {name_value} should be present")) + .unwrap_or_else(|| panic!("block name {name_value} should be present")) .to_vec(); for kv in &opts.pairs { let k = kv.key_str(); @@ -273,7 +294,7 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { .iter() .map(|v| BLOCK_STATES .get(&format!("{k}:{v}")) - .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + .unwrap_or_else(|| panic!("the key {k}:{v} exists in BLOCK_STATES")) ); let mut combined_block_states = Vec::new(); for &bs in vs { @@ -292,12 +313,12 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { for k in missing_props { let missing_block_states = PROP_PARTS .get(&k) - .expect(&format!("the key {k} exists in PROP_PARTS")) + .unwrap_or_else(|| panic!("the key {k} exists in PROP_PARTS")) .iter() .map(|v| { BLOCK_STATES .get(&format!("{k}:{v}")) - .expect(&format!("the key {k}:{v} exists in BLOCK_STATES")) + .unwrap_or_else(|| panic!("the key {k}:{v} exists in BLOCK_STATES")) }); let mut combined = Vec::new(); for &bs in missing_block_states { @@ -311,28 +332,34 @@ fn block_with_props(name: LitStr, opts: Opts) -> Result { return Err(Error::new_spanned( Input { name, - opts: Some(opts), + opts: Some(Opts::Pairs(opts)), }, "no block state corresponds to this combination of properties", )); } let block_ids = block_states.iter().map(|x| *x as u32); - Ok(quote! {BlockId(#(#block_ids)|*)}.into()) + Ok(quote! {BlockStateId(#(#block_ids)|*)}.into()) } +fn block_with_any_props(name: LitStr) -> Result { + let name_value = parse_name(&name); + let block_ids = BLOCK_STATES + .get(&name_value) + .filter(|&&x| !x.is_empty()) + .ok_or_else(|| Error::new_spanned(&name, format!("the block `{name_value}` is not found in blockstates.json (BLOCK_STATES is not populated)")))? + .iter() + .map(|&x| x as u32); + Ok(quote! {BlockStateId(#(#block_ids)|*)}.into()) +} fn static_block(name: LitStr) -> Result { let name_value = parse_name(&name); - let props = PROP_PARTS.get(&name_value); - if props.is_none() { - return Err(Error::new_spanned( + let &props = PROP_PARTS.get(&name_value).ok_or_else(|| Error::new_spanned( &name, format!( "the block `{name_value}` not found in blockstates.json (PROP_PARTS is not populated)", ), - )); - } - if props.is_some_and(|x| !x.is_empty()) { - let &props = props.unwrap(); + ))?; + if !props.is_empty() { return Err(Error::new_spanned( &name, format!( @@ -341,16 +368,13 @@ fn static_block(name: LitStr) -> Result { ), )); } - let block_states = BLOCK_STATES.get(&name_value); - if block_states.is_none_or(|x| x.is_empty()) { - return Err(Error::new_spanned( + let &block_states = BLOCK_STATES.get(&name_value).filter(|&&x| !x.is_empty()).ok_or_else(|| Error::new_spanned( &name, format!( "the block `{name_value}` not found in the blockstates.json file (BLOCK_STATE is not populated)", ), - )); - } - let block_states = block_states.unwrap(); + ))?; + if block_states.len() > 1 { return Err(Error::new_spanned( name, @@ -366,7 +390,7 @@ fn static_block(name: LitStr) -> Result { } let id = block_states[0] as u32; - Ok(quote! { BlockId(#id) }.into()) + Ok(quote! { BlockStateId(#id) }.into()) } fn parse_name(name: &LitStr) -> String { diff --git a/src/lib/world/src/vanilla_chunk_format.rs b/src/lib/world/src/vanilla_chunk_format.rs index 95b01f95c..b35ad197a 100644 --- a/src/lib/world/src/vanilla_chunk_format.rs +++ b/src/lib/world/src/vanilla_chunk_format.rs @@ -100,9 +100,9 @@ pub(crate) struct BlockStates { /// Information about a block's name and properties. /// -/// This should be used sparingly, as it's much more efficient to use [BlockId] where possible. +/// This should be used sparingly, as it's much more efficient to use [BlockStateId] where possible. /// -/// If you want to use it as a literal and the convert to a BlockId, use the [ferrumc_macros::block_data!] macro. +/// If you want to use it as a literal and the convert to a [[[BlockStateId], use the [ferrumc_macros::block_data!] macro. #[apply(ChunkDerives)] #[derive(deepsize::DeepSizeOf, Hash)] pub struct BlockData { diff --git a/src/tests/build.rs b/src/tests/build.rs index 7c7e64588..76b98342d 100644 --- a/src/tests/build.rs +++ b/src/tests/build.rs @@ -27,7 +27,7 @@ fn main() { out.push(( id.parse::().unwrap(), format!( - " assert_eq!(block!(\"{}\", {}), BlockId({}));", + " assert_eq!(block!(\"{}\", {}), BlockStateId({}));", name, format_props(props), id @@ -36,7 +36,10 @@ fn main() { } else { out.push(( id.parse::().unwrap(), - format!(" assert_eq!(block!(\"{}\"), BlockId({}));", name, id), + format!( + " assert_eq!(block!(\"{}\"), BlockStateId({}));", + name, id + ), )); } } diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs index 1fb450350..9dd04e878 100644 --- a/src/tests/src/block/mod.rs +++ b/src/tests/src/block/mod.rs @@ -2,7 +2,7 @@ mod test { use ferrumc_macros::block; #[derive(Debug, PartialEq, Eq)] - struct BlockId(u32); + struct BlockStateId(u32); #[cfg(false)] include!(concat!(env!("OUT_DIR"), "/block_test.rs")); } From 1923a5f88612a5249cc6ff3c1ae36aec3ac5afb2 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:51:08 +0500 Subject: [PATCH 19/23] updete phf dependency updates phf to 0.13 and makes block makro use workspace version --- Cargo.toml | 4 ++-- src/lib/derive_macros/Cargo.toml | 4 ++-- src/lib/registry/build.rs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f8bf819c5..1243a336e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -242,5 +242,5 @@ tempfile = "3.23.0" # Benchmarking criterion = { version = "0.7.0", features = ["html_reports"] } -phf = { version = "0.11", features = ["macros"] } -phf_codegen = { version = "0.11" } +phf = { version = "0.13", features = ["macros"] } +phf_codegen = { version = "0.13" } diff --git a/src/lib/derive_macros/Cargo.toml b/src/lib/derive_macros/Cargo.toml index 80514fffd..5c13e8366 100644 --- a/src/lib/derive_macros/Cargo.toml +++ b/src/lib/derive_macros/Cargo.toml @@ -22,11 +22,11 @@ craftflow-nbt = { workspace = true } indexmap = { workspace = true, features = ["serde"] } bitcode = { workspace = true } simd-json = { workspace = true } -phf = "0.13.1" +phf = { workspace = true } [dev-dependencies] ferrumc-world = { workspace = true } [build-dependencies] -phf_codegen = "0.13.1" +phf_codegen = { workspace = true } simd-json = { workspace = true } diff --git a/src/lib/registry/build.rs b/src/lib/registry/build.rs index f99b54802..5acabd3e7 100644 --- a/src/lib/registry/build.rs +++ b/src/lib/registry/build.rs @@ -76,7 +76,7 @@ fn main() { let mut item_map = Map::new(); for (name, entry) in ®istry.item.entries { // Use & to borrow - item_map.entry(name, &entry.protocol_id.to_string()); + item_map.entry(name, entry.protocol_id.to_string()); } writeln!(file, "{};\n", item_map.build()).unwrap(); @@ -102,7 +102,7 @@ fn main() { let mut bs_map = Map::new(); for (id, data) in blockstates { // Use `entry` to build a valid raw string literal for the value - bs_map.entry(id, &format!("r#\"{}\"#", data.name)); + bs_map.entry(id, format!("r#\"{}\"#", data.name)); } writeln!(file, "{};\n", bs_map.build()).unwrap(); @@ -114,7 +114,7 @@ fn main() { .unwrap(); let mut i2b_map = Map::new(); for (item_id_str, block_state_id_str) in item_to_block { - i2b_map.entry(item_id_str, &format!("r#\"{}\"#", block_state_id_str)); + i2b_map.entry(item_id_str, format!("r#\"{}\"#", block_state_id_str)); } writeln!(file, "{};\n", i2b_map.build()).unwrap(); @@ -129,7 +129,7 @@ fn main() { // (PHF maps don't like f32). We'll convert it back at runtime. let hardness_f32 = entry.hardness.unwrap_or(0.0); let hardness_u32 = hardness_f32.to_bits(); - hardness_map.entry(name, &hardness_u32.to_string()); + hardness_map.entry(name, hardness_u32.to_string()); } writeln!(file, "{};\n", hardness_map.build()).unwrap(); } From 03112aee31e33cf07776e0dc4f45d7b7000cb627 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:51:08 +0500 Subject: [PATCH 20/23] updete phf dependency updates phf to 0.13 and makes block makro use workspace version --- src/lib/registry/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/registry/build.rs b/src/lib/registry/build.rs index 5acabd3e7..b08d49ef4 100644 --- a/src/lib/registry/build.rs +++ b/src/lib/registry/build.rs @@ -89,7 +89,7 @@ fn main() { let mut item_id_map = Map::new(); for (name, entry) in ®istry.item.entries { // Here, the key is the i32 protocol_id - item_id_map.entry(entry.protocol_id, &format!("r#\"{}\"#", name)); + item_id_map.entry(entry.protocol_id, format!("r#\"{}\"#", name)); } writeln!(file, "{};\n", item_id_map.build()).unwrap(); From de37469acf4c8497672202d13a3b1bce094e0147 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:04:56 +0500 Subject: [PATCH 21/23] unused import --- src/tests/src/block/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs index 9dd04e878..1a1b40ef2 100644 --- a/src/tests/src/block/mod.rs +++ b/src/tests/src/block/mod.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test { use ferrumc_macros::block; + #[expect(unused_imports)] #[derive(Debug, PartialEq, Eq)] struct BlockStateId(u32); #[cfg(false)] From 4d1fc0be478aff0e219b57fe9f1a4d14cba1a769 Mon Sep 17 00:00:00 2001 From: lexeyOK <35109763+lexeyOK@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:08:15 +0500 Subject: [PATCH 22/23] remove skipped generated bock! tests --- src/tests/src/block/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/src/block/mod.rs b/src/tests/src/block/mod.rs index 1a1b40ef2..4fa71fabf 100644 --- a/src/tests/src/block/mod.rs +++ b/src/tests/src/block/mod.rs @@ -1,7 +1,6 @@ -#[cfg(test)] +#[cfg(false)] mod test { use ferrumc_macros::block; - #[expect(unused_imports)] #[derive(Debug, PartialEq, Eq)] struct BlockStateId(u32); #[cfg(false)] From e9c7340fc07b2b3acabd10ed152e8d0d769cec03 Mon Sep 17 00:00:00 2001 From: ReCore Date: Sun, 28 Dec 2025 20:45:51 +1030 Subject: [PATCH 23/23] Fixes for merge --- Cargo.toml | 5 +---- src/bin/src/systems/physics/collisions.rs | 1 - src/bin/src/systems/physics/drag.rs | 1 - src/bin/src/systems/player_swimming.rs | 1 - src/lib/derive_macros/src/block/matches.rs | 4 ++-- src/lib/derive_macros/src/block/mod.rs | 6 +++--- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94c748b3c..85c9ca542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -243,7 +243,4 @@ tempfile = "3.23.0" criterion = { version = "0.7.0", features = ["html_reports"] } phf = { version = "0.13", features = ["macros"] } -phf_codegen = { version = "0.13" } - -phf_codegen = { version = "0.13" } -phf = { version = "0.13", features = ["macros"] } \ No newline at end of file +phf_codegen = { version = "0.13" } \ No newline at end of file diff --git a/src/bin/src/systems/physics/collisions.rs b/src/bin/src/systems/physics/collisions.rs index b2d77f04d..a85d1fdbc 100644 --- a/src/bin/src/systems/physics/collisions.rs +++ b/src/bin/src/systems/physics/collisions.rs @@ -10,7 +10,6 @@ use ferrumc_entities::PhysicalProperties; use ferrumc_macros::match_block; use ferrumc_messages::entity_update::SendEntityUpdate; use ferrumc_state::{GlobalState, GlobalStateResource}; -use ferrumc_world::block_state_id::BlockStateId; use ferrumc_world::pos::{ChunkBlockPos, ChunkPos}; pub fn handle( diff --git a/src/bin/src/systems/physics/drag.rs b/src/bin/src/systems/physics/drag.rs index 19b08b48b..d58ca3ecd 100644 --- a/src/bin/src/systems/physics/drag.rs +++ b/src/bin/src/systems/physics/drag.rs @@ -4,7 +4,6 @@ use ferrumc_core::transform::velocity::Velocity; use ferrumc_entities::markers::HasWaterDrag; use ferrumc_macros::match_block; use ferrumc_state::GlobalStateResource; -use ferrumc_world::block_state_id::BlockStateId; use ferrumc_world::pos::{ChunkBlockPos, ChunkPos}; pub fn handle( diff --git a/src/bin/src/systems/player_swimming.rs b/src/bin/src/systems/player_swimming.rs index e15e2efb4..0d8e22ea3 100644 --- a/src/bin/src/systems/player_swimming.rs +++ b/src/bin/src/systems/player_swimming.rs @@ -8,7 +8,6 @@ use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::entity_metadata::{EntityMetadata, EntityMetadataPacket}; use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_state::GlobalStateResource; -use ferrumc_world::block_state_id::BlockStateId; use ferrumc_world::pos::BlockPos; use tracing::error; diff --git a/src/lib/derive_macros/src/block/matches.rs b/src/lib/derive_macros/src/block/matches.rs index ff70b142a..b012cc199 100644 --- a/src/lib/derive_macros/src/block/matches.rs +++ b/src/lib/derive_macros/src/block/matches.rs @@ -41,8 +41,8 @@ pub fn matches_block(input: TokenStream) -> TokenStream { let states = states.unwrap().iter().map(|&x| x as u32); let matched = quote! { - match #block_id_var { - BlockStateId(#(#states)|*) => true, + match #block_id_var.raw() { + #(#states)|* => true, _ => false } }; diff --git a/src/lib/derive_macros/src/block/mod.rs b/src/lib/derive_macros/src/block/mod.rs index a1de38811..dc11d6c15 100644 --- a/src/lib/derive_macros/src/block/mod.rs +++ b/src/lib/derive_macros/src/block/mod.rs @@ -337,7 +337,7 @@ fn block_with_props(name: LitStr, opts: Pairs) -> Result { )); } let block_ids = block_states.iter().map(|x| *x as u32); - Ok(quote! {BlockStateId(#(#block_ids)|*)}.into()) + Ok(quote! {BlockStateId::new(#(#block_ids)|*)}.into()) } fn block_with_any_props(name: LitStr) -> Result { @@ -348,7 +348,7 @@ fn block_with_any_props(name: LitStr) -> Result { .ok_or_else(|| Error::new_spanned(&name, format!("the block `{name_value}` is not found in blockstates.json (BLOCK_STATES is not populated)")))? .iter() .map(|&x| x as u32); - Ok(quote! {BlockStateId(#(#block_ids)|*)}.into()) + Ok(quote! {BlockStateId::new(#(#block_ids)|*)}.into()) } fn static_block(name: LitStr) -> Result { let name_value = parse_name(&name); @@ -389,7 +389,7 @@ fn static_block(name: LitStr) -> Result { } let id = block_states[0] as u32; - Ok(quote! { BlockStateId(#id) }.into()) + Ok(quote! { BlockStateId::new(#id) }.into()) } fn parse_name(name: &LitStr) -> String {