1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

Use an ECS (#52)

* add EntityData::kind

* start making metadata use hecs

* make entity codegen generate ecs stuff

* fix registry codegen

* get rid of worldhaver

it's not even used

* add bevy_ecs to deps

* rename Component to FormattedText

also start making the metadata use bevy_ecs but bevy_ecs doesn't let you query on Bundles so it's annoying

* generate metadata.rs correctly for bevy_ecs

* start switching more entity stuff to use ecs

* more ecs stuff for entity storage

* ok well it compiles but

it definitely doesn't work

* random fixes

* change a bunch of entity things to use the components

* some ecs stuff in az-client

* packet handler uses the ecs now

and other fun changes

i still need to make ticking use the ecs but that's tricker, i'm considering using bevy_ecs systems for those

bevy_ecs systems can't be async but the only async things in ticking is just sending packets which can just be done as a tokio task so that's not a big deal

* start converting some functions in az-client into systems

committing because i'm about to try something that might go horribly wrong

* start splitting client

i'm probably gonna change it so azalea entity ids are separate from minecraft entity ids next (so stuff like player ids can be consistent and we don't have to wait for the login packet)

* separate minecraft entity ids from azalea entity ids + more ecs stuff

i guess i'm using bevy_app now too huh
it's necessary for plugins and it lets us control the tick rate anyways so it's fine i think

i'm still not 100% sure how packet handling that interacts with the world will work, but i think if i can sneak the ecs world into there it'll be fine. Can't put packet handling in the schedule because that'd make it tick-bound, which it's not (technically it'd still work but it'd be wrong and anticheats might realize).

* packet handling

now it runs the schedule only when we get a tick or packet 😄

also i systemified some more functions and did other random fixes so az-world and az-physics compile

making azalea-client use the ecs is almost done! all the hard parts are done now i hope, i just have to finish writing all the code so it actually works

* start figuring out how functions in Client will work

generally just lifetimes being annoying but i think i can get it all to work

* make writing packets work synchronously*

* huh az-client compiles

* start fixing stuff

* start fixing some packets

* make packet handler work

i still haven't actually tested any of this yet lol but in theory it should all work

i'll probably either actually test az-client and fix all the remaining issues or update the azalea crate next

ok also one thing that i'm not particularly happy with is how the packet handlers are doing ugly queries like
```rs
let local_player = ecs
    .query::<&LocalPlayer>()
    .get_mut(ecs, player_entity)
    .unwrap();
```
i think the right way to solve it would be by putting every packet handler in its own system but i haven't come up with a way to make that not be really annoying yet

* fix warnings

* ok what if i just have a bunch of queries and a single packet handler system

* simple example for azalea-client

* 🐛

* maybe fix deadlock idk

can't test it rn lmao

* make physicsstate its own component

* use the default plugins

* azalea compiles lol

* use systemstate for packet handler

* fix entities

basically moved some stuff from being in the world to just being components

* physics (ticking) works

* try to add a .entity_by function

still doesn't work because i want to make the predicate magic

* try to make entity_by work

well it does work but i couldn't figure out how to make it look not terrible. Will hopefully change in the future

* everything compiles

* start converting swarm to use builder

* continue switching swarm to builder and fix stuff

* make swarm use builder

still have to fix some stuff and make client use builder

* fix death event

* client builder

* fix some warnings

* document plugins a bit

* start trying to fix tests

* azalea-ecs

* azalea-ecs stuff compiles

* az-physics tests pass 🎉

* fix all the tests

* clippy on azalea-ecs-macros

* remove now-unnecessary trait_upcasting feature

* fix some clippy::pedantic warnings lol

* why did cargo fmt not remove the trailing spaces

* FIX ALL THE THINGS

* when i said 'all' i meant non-swarm bugs

* start adding task pool

* fix entity deduplication

* fix pathfinder not stopping

* fix some more random bugs

* fix panic that sometimes happens in swarms

* make pathfinder run in task

* fix some tests

* fix doctests and clippy

* deadlock

* fix systems running in wrong order

* fix non-swarm bots
This commit is contained in:
mat 2023-02-04 19:32:27 -06:00 committed by GitHub
parent 7c7446ab1e
commit a5672815cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 18370 additions and 12922 deletions

786
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ members = [
"azalea-buf", "azalea-buf",
"azalea-physics", "azalea-physics",
"azalea-registry", "azalea-registry",
"azalea-ecs",
] ]
[profile.release] [profile.release]
@ -24,19 +25,9 @@ debug = true
# decoding packets takes forever if we don't do this # decoding packets takes forever if we don't do this
[profile.dev.package.azalea-crypto] [profile.dev.package.azalea-crypto]
opt-level = 3 opt-level = 3
[profile.dev.package.cipher]
opt-level = 3
[profile.dev.package.cfb8] [profile.dev.package.cfb8]
opt-level = 3 opt-level = 3
[profile.dev.package.aes] [profile.dev.package.aes]
opt-level = 3 opt-level = 3
[profile.dev.package.crypto-common]
opt-level = 3
[profile.dev.package.generic-array]
opt-level = 3
[profile.dev.package.typenum]
opt-level = 3
[profile.dev.package.inout]
opt-level = 3
[profile.dev.package.flate2] [profile.dev.package.flate2]
opt-level = 3 opt-level = 3

View file

@ -4,7 +4,7 @@ A port of Mojang's Authlib and launcher authentication.
# Examples # Examples
``` ```no_run
use std::path::PathBuf; use std::path::PathBuf;
#[tokio::main] #[tokio::main]

View file

@ -5,7 +5,9 @@ use uuid::Uuid;
#[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)] #[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)]
pub struct GameProfile { pub struct GameProfile {
/// The UUID of the player.
pub uuid: Uuid, pub uuid: Uuid,
/// The username of the player.
pub name: String, pub name: String,
pub properties: HashMap<String, ProfilePropertyValue>, pub properties: HashMap<String, ProfilePropertyValue>,
} }

View file

@ -73,7 +73,7 @@ impl Parse for PropertyWithNameAndDefault {
is_enum = true; is_enum = true;
property_type = first_ident; property_type = first_ident;
let variant = input.parse::<Ident>()?; let variant = input.parse::<Ident>()?;
property_default.extend(quote! { ::#variant }) property_default.extend(quote! { ::#variant });
} else if first_ident_string == "true" || first_ident_string == "false" { } else if first_ident_string == "true" || first_ident_string == "false" {
property_type = Ident::new("bool", first_ident.span()); property_type = Ident::new("bool", first_ident.span());
} else { } else {
@ -388,7 +388,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
// Ident::new(&property.to_string(), proc_macro2::Span::call_site()); // Ident::new(&property.to_string(), proc_macro2::Span::call_site());
block_struct_fields.extend(quote! { block_struct_fields.extend(quote! {
pub #name: #struct_name, pub #name: #struct_name,
}) });
} }
let block_name_pascal_case = Ident::new( let block_name_pascal_case = Ident::new(
@ -420,8 +420,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
combination combination
.iter() .iter()
.map(|v| v[0..1].to_uppercase() + &v[1..]) .map(|v| v[0..1].to_uppercase() + &v[1..])
.collect::<Vec<String>>() .collect::<String>()
.join("")
), ),
proc_macro2::Span::call_site(), proc_macro2::Span::call_site(),
); );
@ -509,20 +508,20 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
.. ..
} in properties_with_name } in properties_with_name
{ {
block_default_fields.extend(quote! {#name: #property_default,}) block_default_fields.extend(quote! { #name: #property_default, });
} }
let block_behavior = &block.behavior; let block_behavior = &block.behavior;
let block_id = block.name.to_string(); let block_id = block.name.to_string();
let from_block_to_state_match = if !block.properties_and_defaults.is_empty() { let from_block_to_state_match = if block.properties_and_defaults.is_empty() {
quote! { BlockState::#block_name_pascal_case }
} else {
quote! { quote! {
match b { match b {
#from_block_to_state_match_inner #from_block_to_state_match_inner
} }
} }
} else {
quote! { BlockState::#block_name_pascal_case }
}; };
let block_struct = quote! { let block_struct = quote! {

View file

@ -136,7 +136,7 @@ impl<S> CommandDispatcher<S> {
return Ordering::Greater; return Ordering::Greater;
}; };
Ordering::Equal Ordering::Equal
}) });
} }
let best_potential = potentials.into_iter().next().unwrap(); let best_potential = potentials.into_iter().next().unwrap();
return Ok(best_potential); return Ok(best_potential);
@ -195,7 +195,7 @@ impl<S> CommandDispatcher<S> {
let mut node = self.root.clone(); let mut node = self.root.clone();
for name in path { for name in path {
if let Some(child) = node.clone().borrow().child(name) { if let Some(child) = node.clone().borrow().child(name) {
node = child node = child;
} else { } else {
return None; return None;
} }
@ -228,7 +228,7 @@ impl<S> CommandDispatcher<S> {
let mut next: Vec<CommandContext<S>> = vec![]; let mut next: Vec<CommandContext<S>> = vec![];
while !contexts.is_empty() { while !contexts.is_empty() {
for context in contexts.iter() { for context in &contexts {
let child = &context.child; let child = &context.child;
if let Some(child) = child { if let Some(child) = child {
forked |= child.forks; forked |= child.forks;

View file

@ -88,7 +88,7 @@ impl fmt::Debug for BuiltInExceptions {
BuiltInExceptions::ReaderInvalidBool { value } => { BuiltInExceptions::ReaderInvalidBool { value } => {
write!( write!(
f, f,
"Invalid bool, expected true or false but found '{value}'", "Invalid bool, expected true or false but found '{value}'"
) )
} }
BuiltInExceptions::ReaderInvalidInt { value } => { BuiltInExceptions::ReaderInvalidInt { value } => {

View file

@ -4,7 +4,7 @@ use crate::context::StringRange;
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
use azalea_buf::McBufWritable; use azalea_buf::McBufWritable;
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
use azalea_chat::Component; use azalea_chat::FormattedText;
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
use std::io::Write; use std::io::Write;
pub use suggestions::*; pub use suggestions::*;
@ -31,7 +31,7 @@ impl<M: Clone> Suggestion<M> {
} }
result.push_str(&self.text); result.push_str(&self.text);
if self.range.end() < input.len() { if self.range.end() < input.len() {
result.push_str(&input[self.range.end()..]) result.push_str(&input[self.range.end()..]);
} }
result result
@ -58,7 +58,7 @@ impl<M: Clone> Suggestion<M> {
} }
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
impl McBufWritable for Suggestion<Component> { impl McBufWritable for Suggestion<FormattedText> {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.text.write_into(buf)?; self.text.write_into(buf)?;
self.tooltip.write_into(buf)?; self.tooltip.write_into(buf)?;

View file

@ -5,7 +5,7 @@ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable, BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
}; };
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
use azalea_chat::Component; use azalea_chat::FormattedText;
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use std::{collections::HashSet, hash::Hash}; use std::{collections::HashSet, hash::Hash};
@ -68,12 +68,12 @@ impl<M> Default for Suggestions<M> {
} }
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
impl McBufReadable for Suggestions<Component> { impl McBufReadable for Suggestions<FormattedText> {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> { fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
#[derive(McBuf)] #[derive(McBuf)]
struct StandaloneSuggestion { struct StandaloneSuggestion {
pub text: String, pub text: String,
pub tooltip: Option<Component>, pub tooltip: Option<FormattedText>,
} }
let start = u32::var_read_from(buf)? as usize; let start = u32::var_read_from(buf)? as usize;
@ -97,7 +97,7 @@ impl McBufReadable for Suggestions<Component> {
} }
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
impl McBufWritable for Suggestions<Component> { impl McBufWritable for Suggestions<FormattedText> {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
(self.range.start() as u32).var_write_into(buf)?; (self.range.start() as u32).var_write_into(buf)?;
(self.range.length() as u32).var_write_into(buf)?; (self.range.length() as u32).var_write_into(buf)?;

View file

@ -65,7 +65,9 @@ impl<S> CommandNode<S> {
pub fn get_relevant_nodes(&self, input: &mut StringReader) -> Vec<Rc<RefCell<CommandNode<S>>>> { pub fn get_relevant_nodes(&self, input: &mut StringReader) -> Vec<Rc<RefCell<CommandNode<S>>>> {
let literals = &self.literals; let literals = &self.literals;
if !literals.is_empty() { if literals.is_empty() {
self.arguments.values().cloned().collect()
} else {
let cursor = input.cursor(); let cursor = input.cursor();
while input.can_read() && input.peek() != ' ' { while input.can_read() && input.peek() != ' ' {
input.skip(); input.skip();
@ -83,8 +85,6 @@ impl<S> CommandNode<S> {
} else { } else {
self.arguments.values().cloned().collect() self.arguments.values().cloned().collect()
} }
} else {
self.arguments.values().cloned().collect()
} }
} }

View file

@ -39,9 +39,8 @@ fn read_named_fields(
pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream { pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data { match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => { syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let FieldsNamed { named, .. } = match fields { let syn::Fields::Named(FieldsNamed { named, .. }) = fields else {
syn::Fields::Named(f) => f, panic!("#[derive(McBuf)] can only be used on structs with named fields")
_ => panic!("#[derive(McBuf)] can only be used on structs with named fields"),
}; };
let (read_fields, read_field_names) = read_named_fields(named); let (read_fields, read_field_names) = read_named_fields(named);
@ -69,7 +68,7 @@ pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::Tok
variant_discrim = match &d.1 { variant_discrim = match &d.1 {
syn::Expr::Lit(e) => match &e.lit { syn::Expr::Lit(e) => match &e.lit {
syn::Lit::Int(i) => i.base10_parse().unwrap(), syn::Lit::Int(i) => i.base10_parse().unwrap(),
_ => panic!("Error parsing enum discriminant as int (is {e:?})",), _ => panic!("Error parsing enum discriminant as int (is {e:?})"),
}, },
syn::Expr::Unary(_) => { syn::Expr::Unary(_) => {
panic!("Negative enum discriminants are not supported") panic!("Negative enum discriminants are not supported")
@ -102,11 +101,11 @@ pub fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::Tok
if f.attrs.iter().any(|attr| attr.path.is_ident("var")) { if f.attrs.iter().any(|attr| attr.path.is_ident("var")) {
reader_code.extend(quote! { reader_code.extend(quote! {
Self::#variant_name(azalea_buf::McBufVarReadable::var_read_from(buf)?), Self::#variant_name(azalea_buf::McBufVarReadable::var_read_from(buf)?),
}) });
} else { } else {
reader_code.extend(quote! { reader_code.extend(quote! {
Self::#variant_name(azalea_buf::McBufReadable::read_from(buf)?), Self::#variant_name(azalea_buf::McBufReadable::read_from(buf)?),
}) });
} }
} }
quote! { Ok(#reader_code) } quote! { Ok(#reader_code) }

View file

@ -40,9 +40,8 @@ fn write_named_fields(
pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream { pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data { match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => { syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let FieldsNamed { named, .. } = match fields { let syn::Fields::Named(FieldsNamed { named, .. }) = fields else {
syn::Fields::Named(f) => f, panic!("#[derive(McBuf)] can only be used on structs with named fields")
_ => panic!("#[derive(McBuf)] can only be used on structs with named fields"),
}; };
let write_fields = let write_fields =
@ -137,11 +136,11 @@ pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::Tok
if f.attrs.iter().any(|attr| attr.path.is_ident("var")) { if f.attrs.iter().any(|attr| attr.path.is_ident("var")) {
writers_code.extend(quote! { writers_code.extend(quote! {
azalea_buf::McBufVarWritable::var_write_into(#param_ident, buf)?; azalea_buf::McBufVarWritable::var_write_into(#param_ident, buf)?;
}) });
} else { } else {
writers_code.extend(quote! { writers_code.extend(quote! {
azalea_buf::McBufWritable::write_into(#param_ident, buf)?; azalea_buf::McBufWritable::write_into(#param_ident, buf)?;
}) });
} }
} }
match_arms.extend(quote! { match_arms.extend(quote! {

View file

@ -5,9 +5,9 @@ Things for working with Minecraft formatted text components.
# Examples # Examples
``` ```
// convert a Minecraft component JSON into colored text that can be printed to the terminal. // convert a Minecraft formatted text JSON into colored text that can be printed to the terminal.
use azalea_chat::Component; use azalea_chat::FormattedText;
use serde_json::Value; use serde_json::Value;
use serde::Deserialize; use serde::Deserialize;
@ -15,9 +15,9 @@ let j: Value = serde_json::from_str(
r#"{"text": "hello","color": "red","bold": true}"# r#"{"text": "hello","color": "red","bold": true}"#
) )
.unwrap(); .unwrap();
let component = Component::deserialize(&j).unwrap(); let text = FormattedText::deserialize(&j).unwrap();
assert_eq!( assert_eq!(
component.to_ansi(), text.to_ansi(),
"\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m"
); );
``` ```

View file

@ -1,11 +1,11 @@
use crate::{style::Style, Component}; use crate::{style::Style, FormattedText};
use serde::Serialize; use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize)]
pub struct BaseComponent { pub struct BaseComponent {
// implements mutablecomponent // implements mutablecomponent
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub siblings: Vec<Component>, pub siblings: Vec<FormattedText>,
#[serde(flatten)] #[serde(flatten)]
pub style: Style, pub style: Style,
} }

View file

@ -17,7 +17,7 @@ use std::{
/// A chat component, basically anything you can see in chat. /// A chat component, basically anything you can see in chat.
#[derive(Clone, Debug, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum Component { pub enum FormattedText {
Text(TextComponent), Text(TextComponent),
Translatable(TranslatableComponent), Translatable(TranslatableComponent),
} }
@ -28,7 +28,7 @@ pub static DEFAULT_STYLE: Lazy<Style> = Lazy::new(|| Style {
}); });
/// A chat component /// A chat component
impl Component { impl FormattedText {
pub fn get_base_mut(&mut self) -> &mut BaseComponent { pub fn get_base_mut(&mut self) -> &mut BaseComponent {
match self { match self {
Self::Text(c) => &mut c.base, Self::Text(c) => &mut c.base,
@ -44,14 +44,16 @@ impl Component {
} }
/// Add a component as a sibling of this one /// Add a component as a sibling of this one
fn append(&mut self, sibling: Component) { fn append(&mut self, sibling: FormattedText) {
self.get_base_mut().siblings.push(sibling); self.get_base_mut().siblings.push(sibling);
} }
/// Get the "separator" component from the json /// Get the "separator" component from the json
fn parse_separator(json: &serde_json::Value) -> Result<Option<Component>, serde_json::Error> { fn parse_separator(
json: &serde_json::Value,
) -> Result<Option<FormattedText>, serde_json::Error> {
if json.get("separator").is_some() { if json.get("separator").is_some() {
return Ok(Some(Component::deserialize( return Ok(Some(FormattedText::deserialize(
json.get("separator").unwrap(), json.get("separator").unwrap(),
)?)); )?));
} }
@ -62,16 +64,17 @@ impl Component {
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code), so you
/// can print it to your terminal and get styling. /// can print it to your terminal and get styling.
/// ///
/// This is technically a shortcut for [`Component::to_ansi_custom_style`] /// This is technically a shortcut for
/// with a default [`Style`] colored white. /// [`FormattedText::to_ansi_custom_style`] with a default [`Style`]
/// colored white.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// use azalea_chat::Component; /// use azalea_chat::FormattedText;
/// use serde::de::Deserialize; /// use serde::de::Deserialize;
/// ///
/// let component = Component::deserialize(&serde_json::json!({ /// let component = FormattedText::deserialize(&serde_json::json!({
/// "text": "Hello, world!", /// "text": "Hello, world!",
/// "color": "red", /// "color": "red",
/// })).unwrap(); /// })).unwrap();
@ -86,7 +89,7 @@ impl Component {
/// Convert this component into an /// Convert this component into an
/// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code). /// [ANSI string](https://en.wikipedia.org/wiki/ANSI_escape_code).
/// ///
/// This is the same as [`Component::to_ansi`], but you can specify a /// This is the same as [`FormattedText::to_ansi`], but you can specify a
/// default [`Style`] to use. /// default [`Style`] to use.
pub fn to_ansi_custom_style(&self, default_style: &Style) -> String { pub fn to_ansi_custom_style(&self, default_style: &Style) -> String {
// this contains the final string will all the ansi escape codes // this contains the final string will all the ansi escape codes
@ -117,12 +120,12 @@ impl Component {
} }
} }
impl IntoIterator for Component { impl IntoIterator for FormattedText {
/// Recursively call the function for every component in this component /// Recursively call the function for every component in this component
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
let base = self.get_base(); let base = self.get_base();
let siblings = base.siblings.clone(); let siblings = base.siblings.clone();
let mut v: Vec<Component> = Vec::with_capacity(siblings.len() + 1); let mut v: Vec<FormattedText> = Vec::with_capacity(siblings.len() + 1);
v.push(self); v.push(self);
for sibling in siblings { for sibling in siblings {
v.extend(sibling.into_iter()); v.extend(sibling.into_iter());
@ -131,11 +134,11 @@ impl IntoIterator for Component {
v.into_iter() v.into_iter()
} }
type Item = Component; type Item = FormattedText;
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;
} }
impl<'de> Deserialize<'de> for Component { impl<'de> Deserialize<'de> for FormattedText {
fn deserialize<D>(de: D) -> Result<Self, D::Error> fn deserialize<D>(de: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@ -143,11 +146,11 @@ impl<'de> Deserialize<'de> for Component {
let json: serde_json::Value = serde::Deserialize::deserialize(de)?; let json: serde_json::Value = serde::Deserialize::deserialize(de)?;
// we create a component that we might add siblings to // we create a component that we might add siblings to
let mut component: Component; let mut component: FormattedText;
// if it's primitive, make it a text component // if it's primitive, make it a text component
if !json.is_array() && !json.is_object() { if !json.is_array() && !json.is_object() {
return Ok(Component::Text(TextComponent::new( return Ok(FormattedText::Text(TextComponent::new(
json.as_str().unwrap_or("").to_string(), json.as_str().unwrap_or("").to_string(),
))); )));
} }
@ -155,7 +158,7 @@ impl<'de> Deserialize<'de> for Component {
else if json.is_object() { else if json.is_object() {
if let Some(text) = json.get("text") { if let Some(text) = json.get("text") {
let text = text.as_str().unwrap_or("").to_string(); let text = text.as_str().unwrap_or("").to_string();
component = Component::Text(TextComponent::new(text)); component = FormattedText::Text(TextComponent::new(text));
} else if let Some(translate) = json.get("translate") { } else if let Some(translate) = json.get("translate") {
let translate = translate let translate = translate
.as_str() .as_str()
@ -170,8 +173,8 @@ impl<'de> Deserialize<'de> for Component {
// if it's a string component with no styling and no siblings, just add a // if it's a string component with no styling and no siblings, just add a
// string to with_array otherwise add the component // string to with_array otherwise add the component
// to the array // to the array
let c = Component::deserialize(item).map_err(de::Error::custom)?; let c = FormattedText::deserialize(item).map_err(de::Error::custom)?;
if let Component::Text(text_component) = c { if let FormattedText::Text(text_component) = c {
if text_component.base.siblings.is_empty() if text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty() && text_component.base.style.is_empty()
{ {
@ -179,16 +182,19 @@ impl<'de> Deserialize<'de> for Component {
continue; continue;
} }
} }
with_array.push(StringOrComponent::Component( with_array.push(StringOrComponent::FormattedText(
Component::deserialize(item).map_err(de::Error::custom)?, FormattedText::deserialize(item).map_err(de::Error::custom)?,
)); ));
} }
component = component = FormattedText::Translatable(TranslatableComponent::new(
Component::Translatable(TranslatableComponent::new(translate, with_array)); translate, with_array,
));
} else { } else {
// if it doesn't have a "with", just have the with_array be empty // if it doesn't have a "with", just have the with_array be empty
component = component = FormattedText::Translatable(TranslatableComponent::new(
Component::Translatable(TranslatableComponent::new(translate, Vec::new())); translate,
Vec::new(),
));
} }
} else if let Some(score) = json.get("score") { } else if let Some(score) = json.get("score") {
// object = GsonHelper.getAsJsonObject(jsonObject, "score"); // object = GsonHelper.getAsJsonObject(jsonObject, "score");
@ -210,14 +216,13 @@ impl<'de> Deserialize<'de> for Component {
"keybind text components aren't yet supported", "keybind text components aren't yet supported",
)); ));
} else { } else {
let _nbt = if let Some(nbt) = json.get("nbt") { let Some(_nbt) = json.get("nbt") else {
nbt
} else {
return Err(de::Error::custom( return Err(de::Error::custom(
format!("Don't know how to turn {json} into a Component").as_str(), format!("Don't know how to turn {json} into a FormattedText").as_str(),
)); ));
}; };
let _separator = Component::parse_separator(&json).map_err(de::Error::custom)?; let _separator =
FormattedText::parse_separator(&json).map_err(de::Error::custom)?;
let _interpret = match json.get("interpret") { let _interpret = match json.get("interpret") {
Some(v) => v.as_bool().ok_or(Some(false)).unwrap(), Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
@ -229,16 +234,15 @@ impl<'de> Deserialize<'de> for Component {
)); ));
} }
if let Some(extra) = json.get("extra") { if let Some(extra) = json.get("extra") {
let extra = match extra.as_array() { let Some(extra) = extra.as_array() else {
Some(r) => r, return Err(de::Error::custom("Extra isn't an array"));
None => return Err(de::Error::custom("Extra isn't an array")),
}; };
if extra.is_empty() { if extra.is_empty() {
return Err(de::Error::custom("Unexpected empty array of components")); return Err(de::Error::custom("Unexpected empty array of components"));
} }
for extra_component in extra { for extra_component in extra {
let sibling = let sibling =
Component::deserialize(extra_component).map_err(de::Error::custom)?; FormattedText::deserialize(extra_component).map_err(de::Error::custom)?;
component.append(sibling); component.append(sibling);
} }
} }
@ -251,16 +255,18 @@ impl<'de> Deserialize<'de> for Component {
// ok so it's not an object, if it's an array deserialize every item // ok so it's not an object, if it's an array deserialize every item
else if !json.is_array() { else if !json.is_array() {
return Err(de::Error::custom( return Err(de::Error::custom(
format!("Don't know how to turn {json} into a Component").as_str(), format!("Don't know how to turn {json} into a FormattedText").as_str(),
)); ));
} }
let json_array = json.as_array().unwrap(); let json_array = json.as_array().unwrap();
// the first item in the array is the one that we're gonna return, the others // the first item in the array is the one that we're gonna return, the others
// are siblings // are siblings
let mut component = Component::deserialize(&json_array[0]).map_err(de::Error::custom)?; let mut component =
FormattedText::deserialize(&json_array[0]).map_err(de::Error::custom)?;
for i in 1..json_array.len() { for i in 1..json_array.len() {
component.append( component.append(
Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?, FormattedText::deserialize(json_array.get(i).unwrap())
.map_err(de::Error::custom)?,
); );
} }
Ok(component) Ok(component)
@ -268,18 +274,18 @@ impl<'de> Deserialize<'de> for Component {
} }
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
impl McBufReadable for Component { impl McBufReadable for FormattedText {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> { fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let string = String::read_from(buf)?; let string = String::read_from(buf)?;
debug!("Component string: {}", string); debug!("FormattedText string: {}", string);
let json: serde_json::Value = serde_json::from_str(string.as_str())?; let json: serde_json::Value = serde_json::from_str(string.as_str())?;
let component = Component::deserialize(json)?; let component = FormattedText::deserialize(json)?;
Ok(component) Ok(component)
} }
} }
#[cfg(feature = "azalea-buf")] #[cfg(feature = "azalea-buf")]
impl McBufWritable for Component { impl McBufWritable for FormattedText {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
let json = serde_json::to_string(self).unwrap(); let json = serde_json::to_string(self).unwrap();
json.write_into(buf)?; json.write_into(buf)?;
@ -287,31 +293,31 @@ impl McBufWritable for Component {
} }
} }
impl From<String> for Component { impl From<String> for FormattedText {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Component::Text(TextComponent { FormattedText::Text(TextComponent {
text: s, text: s,
base: BaseComponent::default(), base: BaseComponent::default(),
}) })
} }
} }
impl From<&str> for Component { impl From<&str> for FormattedText {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Self::from(s.to_string()) Self::from(s.to_string())
} }
} }
impl Display for Component { impl Display for FormattedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Component::Text(c) => c.fmt(f), FormattedText::Text(c) => c.fmt(f),
Component::Translatable(c) => c.fmt(f), FormattedText::Translatable(c) => c.fmt(f),
} }
} }
} }
impl Default for Component { impl Default for FormattedText {
fn default() -> Self { fn default() -> Self {
Component::Text(TextComponent::default()) FormattedText::Text(TextComponent::default())
} }
} }

View file

@ -6,4 +6,4 @@ pub mod style;
pub mod text_component; pub mod text_component;
pub mod translatable_component; pub mod translatable_component;
pub use component::Component; pub use component::FormattedText;

View file

@ -1,4 +1,4 @@
use crate::{base_component::BaseComponent, style::ChatFormatting, Component}; use crate::{base_component::BaseComponent, style::ChatFormatting, FormattedText};
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer}; use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
use std::fmt::Display; use std::fmt::Display;
@ -26,7 +26,7 @@ impl Serialize for TextComponent {
const LEGACY_FORMATTING_CODE_SYMBOL: char = '§'; const LEGACY_FORMATTING_CODE_SYMBOL: char = '§';
/// Convert a legacy color code string into a Component /// Convert a legacy color code string into a FormattedText
/// Technically in Minecraft this is done when displaying the text, but AFAIK /// Technically in Minecraft this is done when displaying the text, but AFAIK
/// it's the same as just doing it in TextComponent /// it's the same as just doing it in TextComponent
pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent { pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent {
@ -41,12 +41,9 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo
while i < legacy_color_code.chars().count() { while i < legacy_color_code.chars().count() {
if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL { if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL {
let formatting_code = legacy_color_code.chars().nth(i + 1); let formatting_code = legacy_color_code.chars().nth(i + 1);
let formatting_code = match formatting_code { let Some(formatting_code) = formatting_code else {
Some(formatting_code) => formatting_code,
None => {
i += 1; i += 1;
continue; continue;
}
}; };
if let Some(formatter) = ChatFormatting::from_code(formatting_code) { if let Some(formatter) = ChatFormatting::from_code(formatting_code) {
if components.is_empty() || !components.last().unwrap().text.is_empty() { if components.is_empty() || !components.last().unwrap().text.is_empty() {
@ -98,18 +95,18 @@ impl TextComponent {
} }
} }
fn get(self) -> Component { fn get(self) -> FormattedText {
Component::Text(self) FormattedText::Text(self)
} }
} }
impl Display for TextComponent { impl Display for TextComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// this contains the final string will all the ansi escape codes // this contains the final string will all the ansi escape codes
for component in Component::Text(self.clone()).into_iter() { for component in FormattedText::Text(self.clone()).into_iter() {
let component_text = match &component { let component_text = match &component {
Component::Text(c) => c.text.to_string(), FormattedText::Text(c) => c.text.to_string(),
Component::Translatable(c) => c.read()?.to_string(), FormattedText::Translatable(c) => c.read()?.to_string(),
}; };
f.write_str(&component_text)?; f.write_str(&component_text)?;

View file

@ -1,7 +1,7 @@
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use crate::{ use crate::{
base_component::BaseComponent, style::Style, text_component::TextComponent, Component, base_component::BaseComponent, style::Style, text_component::TextComponent, FormattedText,
}; };
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer}; use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
@ -9,7 +9,7 @@ use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSer
#[serde(untagged)] #[serde(untagged)]
pub enum StringOrComponent { pub enum StringOrComponent {
String(String), String(String),
Component(Component), FormattedText(FormattedText),
} }
/// A message whose content depends on the client's language. /// A message whose content depends on the client's language.
@ -42,7 +42,7 @@ impl TranslatableComponent {
} }
} }
/// Convert the key and args to a Component. /// Convert the key and args to a FormattedText.
pub fn read(&self) -> Result<TextComponent, fmt::Error> { pub fn read(&self) -> Result<TextComponent, fmt::Error> {
let template = azalea_language::get(&self.key).unwrap_or(&self.key); let template = azalea_language::get(&self.key).unwrap_or(&self.key);
// decode the % things // decode the % things
@ -57,12 +57,9 @@ impl TranslatableComponent {
while i < template.len() { while i < template.len() {
if template.chars().nth(i).unwrap() == '%' { if template.chars().nth(i).unwrap() == '%' {
let char_after = match template.chars().nth(i + 1) { let Some(char_after) = template.chars().nth(i + 1) else {
Some(c) => c,
None => {
built_text.push(template.chars().nth(i).unwrap()); built_text.push(template.chars().nth(i).unwrap());
break; break;
}
}; };
i += 1; i += 1;
match char_after { match char_after {
@ -111,7 +108,7 @@ impl TranslatableComponent {
built_text.push(template.chars().nth(i).unwrap()); built_text.push(template.chars().nth(i).unwrap());
} }
i += 1 i += 1;
} }
if components.is_empty() { if components.is_empty() {
@ -122,7 +119,7 @@ impl TranslatableComponent {
Ok(TextComponent { Ok(TextComponent {
base: BaseComponent { base: BaseComponent {
siblings: components.into_iter().map(Component::Text).collect(), siblings: components.into_iter().map(FormattedText::Text).collect(),
style: Style::default(), style: Style::default(),
}, },
text: "".to_string(), text: "".to_string(),
@ -133,10 +130,10 @@ impl TranslatableComponent {
impl Display for TranslatableComponent { impl Display for TranslatableComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// this contains the final string will all the ansi escape codes // this contains the final string will all the ansi escape codes
for component in Component::Translatable(self.clone()).into_iter() { for component in FormattedText::Translatable(self.clone()).into_iter() {
let component_text = match &component { let component_text = match &component {
Component::Text(c) => c.text.to_string(), FormattedText::Text(c) => c.text.to_string(),
Component::Translatable(c) => c.read()?.to_string(), FormattedText::Translatable(c) => c.read()?.to_string(),
}; };
f.write_str(&component_text)?; f.write_str(&component_text)?;
@ -150,7 +147,7 @@ impl Display for StringOrComponent {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match self { match self {
StringOrComponent::String(s) => write!(f, "{s}"), StringOrComponent::String(s) => write!(f, "{s}"),
StringOrComponent::Component(c) => write!(f, "{c}"), StringOrComponent::FormattedText(c) => write!(f, "{c}"),
} }
} }
} }
@ -159,7 +156,7 @@ impl From<StringOrComponent> for TextComponent {
fn from(soc: StringOrComponent) -> Self { fn from(soc: StringOrComponent) -> Self {
match soc { match soc {
StringOrComponent::String(s) => TextComponent::new(s), StringOrComponent::String(s) => TextComponent::new(s),
StringOrComponent::Component(c) => TextComponent::new(c.to_string()), StringOrComponent::FormattedText(c) => TextComponent::new(c.to_string()),
} }
} }
} }

View file

@ -1,6 +1,6 @@
use azalea_chat::{ use azalea_chat::{
style::{Ansi, ChatFormatting, TextColor}, style::{Ansi, ChatFormatting, TextColor},
Component, FormattedText,
}; };
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
@ -15,7 +15,7 @@ fn basic_ansi_test() {
}"#, }"#,
) )
.unwrap(); .unwrap();
let component = Component::deserialize(&j).unwrap(); let component = FormattedText::deserialize(&j).unwrap();
assert_eq!( assert_eq!(
component.to_ansi(), component.to_ansi(),
"\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m"
@ -51,7 +51,7 @@ fn complex_ansi_test() {
]"##, ]"##,
) )
.unwrap(); .unwrap();
let component = Component::deserialize(&j).unwrap(); let component = FormattedText::deserialize(&j).unwrap();
assert_eq!( assert_eq!(
component.to_ansi(), component.to_ansi(),
format!( format!(
@ -70,6 +70,6 @@ fn complex_ansi_test() {
#[test] #[test]
fn component_from_string() { fn component_from_string() {
let j: Value = serde_json::from_str("\"foo\"").unwrap(); let j: Value = serde_json::from_str("\"foo\"").unwrap();
let component = Component::deserialize(&j).unwrap(); let component = FormattedText::deserialize(&j).unwrap();
assert_eq!(component.to_ansi(), "foo"); assert_eq!(component.to_ansi(), "foo");
} }

View file

@ -16,9 +16,16 @@ azalea-block = {path = "../azalea-block", version = "0.5.0" }
azalea-chat = {path = "../azalea-chat", version = "0.5.0"} azalea-chat = {path = "../azalea-chat", version = "0.5.0"}
azalea-core = {path = "../azalea-core", version = "0.5.0"} azalea-core = {path = "../azalea-core", version = "0.5.0"}
azalea-crypto = {path = "../azalea-crypto", version = "0.5.0"} azalea-crypto = {path = "../azalea-crypto", version = "0.5.0"}
azalea-ecs = {path = "../azalea-ecs", version = "0.5.0"}
azalea-physics = {path = "../azalea-physics", version = "0.5.0"} azalea-physics = {path = "../azalea-physics", version = "0.5.0"}
azalea-protocol = {path = "../azalea-protocol", version = "0.5.0"} azalea-protocol = {path = "../azalea-protocol", version = "0.5.0"}
azalea-registry = {path = "../azalea-registry", version = "0.5.0"}
azalea-world = {path = "../azalea-world", version = "0.5.0"} azalea-world = {path = "../azalea-world", version = "0.5.0"}
bevy_tasks = "0.9.1"
bevy_time = "0.9.1"
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
futures = "0.3.25"
iyes_loopless = "0.9.1"
log = "0.4.17" log = "0.4.17"
nohash-hasher = "0.2.0" nohash-hasher = "0.2.0"
once_cell = "1.16.0" once_cell = "1.16.0"
@ -28,3 +35,6 @@ thiserror = "^1.0.34"
tokio = {version = "^1.24.2", features = ["sync"]} tokio = {version = "^1.24.2", features = ["sync"]}
typemap_rev = "0.3.0" typemap_rev = "0.3.0"
uuid = "^1.1.2" uuid = "^1.1.2"
[dev-dependencies]
env_logger = "0.9.1"

View file

@ -0,0 +1,49 @@
//! A simple bot that repeats chat messages sent by other players.
use azalea_client::{Account, Client, Event};
#[tokio::main]
async fn main() {
env_logger::init();
// deadlock detection, you can safely delete this block if you're not trying to
// debug deadlocks in azalea
{
use parking_lot::deadlock;
use std::thread;
use std::time::Duration;
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
println!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
println!("Deadlock #{i}");
for t in threads {
println!("Thread Id {:#?}", t.thread_id());
println!("{:#?}", t.backtrace());
}
}
});
}
let account = Account::offline("bot");
// or let account = Account::microsoft("email").await;
let (client, mut rx) = Client::join(&account, "localhost").await.unwrap();
while let Some(event) = rx.recv().await {
match &event {
Event::Chat(m) => {
if let (Some(sender), content) = m.split_sender_and_content() {
if sender == client.profile.name {
continue; // ignore our own messages
}
client.chat(&content);
};
}
_ => {}
}
}
}

View file

@ -28,7 +28,7 @@ pub struct Account {
/// The access token for authentication. You can obtain one of these /// The access token for authentication. You can obtain one of these
/// manually from azalea-auth. /// manually from azalea-auth.
/// ///
/// This is an Arc<Mutex> so it can be modified by [`Self::refresh`]. /// This is an `Arc<Mutex>` so it can be modified by [`Self::refresh`].
pub access_token: Option<Arc<Mutex<String>>>, pub access_token: Option<Arc<Mutex<String>>>,
/// Only required for online-mode accounts. /// Only required for online-mode accounts.
pub uuid: Option<Uuid>, pub uuid: Option<Uuid>,

View file

@ -1,7 +1,6 @@
//! Implementations of chat-related features. //! Implementations of chat-related features.
use crate::Client; use azalea_chat::FormattedText;
use azalea_chat::Component;
use azalea_protocol::packets::game::{ use azalea_protocol::packets::game::{
clientbound_player_chat_packet::ClientboundPlayerChatPacket, clientbound_player_chat_packet::ClientboundPlayerChatPacket,
clientbound_system_chat_packet::ClientboundSystemChatPacket, clientbound_system_chat_packet::ClientboundSystemChatPacket,
@ -14,6 +13,8 @@ use std::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::client::Client;
/// A chat packet, either a system message or a chat message. /// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum ChatPacket { pub enum ChatPacket {
@ -30,7 +31,7 @@ macro_rules! regex {
impl ChatPacket { impl ChatPacket {
/// Get the message shown in chat for this packet. /// Get the message shown in chat for this packet.
pub fn message(&self) -> Component { pub fn message(&self) -> FormattedText {
match self { match self {
ChatPacket::System(p) => p.content.clone(), ChatPacket::System(p) => p.content.clone(),
ChatPacket::Player(p) => p.message(), ChatPacket::Player(p) => p.message(),
@ -94,7 +95,7 @@ impl ChatPacket {
/// convenience function for testing. /// convenience function for testing.
pub fn new(message: &str) -> Self { pub fn new(message: &str) -> Self {
ChatPacket::System(Arc::new(ClientboundSystemChatPacket { ChatPacket::System(Arc::new(ClientboundSystemChatPacket {
content: Component::from(message), content: FormattedText::from(message),
overlay: false, overlay: false,
})) }))
} }
@ -105,7 +106,7 @@ impl Client {
/// not the command packet. The [`Client::chat`] function handles checking /// not the command packet. The [`Client::chat`] function handles checking
/// whether the message is a command and using the proper packet for you, /// whether the message is a command and using the proper packet for you,
/// so you should use that instead. /// so you should use that instead.
pub async fn send_chat_packet(&self, message: &str) -> Result<(), std::io::Error> { pub fn send_chat_packet(&self, message: &str) {
// TODO: chat signing // TODO: chat signing
// let signature = sign_message(); // let signature = sign_message();
let packet = ServerboundChatPacket { let packet = ServerboundChatPacket {
@ -121,12 +122,12 @@ impl Client {
last_seen_messages: LastSeenMessagesUpdate::default(), last_seen_messages: LastSeenMessagesUpdate::default(),
} }
.get(); .get();
self.write_packet(packet).await self.write_packet(packet);
} }
/// Send a command packet to the server. The `command` argument should not /// Send a command packet to the server. The `command` argument should not
/// include the slash at the front. /// include the slash at the front.
pub async fn send_command_packet(&self, command: &str) -> Result<(), std::io::Error> { pub fn send_command_packet(&self, command: &str) {
// TODO: chat signing // TODO: chat signing
let packet = ServerboundChatCommandPacket { let packet = ServerboundChatCommandPacket {
command: command.to_string(), command: command.to_string(),
@ -141,7 +142,7 @@ impl Client {
last_seen_messages: LastSeenMessagesUpdate::default(), last_seen_messages: LastSeenMessagesUpdate::default(),
} }
.get(); .get();
self.write_packet(packet).await self.write_packet(packet);
} }
/// Send a message in chat. /// Send a message in chat.
@ -149,15 +150,15 @@ impl Client {
/// ```rust,no_run /// ```rust,no_run
/// # use azalea_client::{Client, Event}; /// # use azalea_client::{Client, Event};
/// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> { /// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> {
/// bot.chat("Hello, world!").await.unwrap(); /// bot.chat("Hello, world!");
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub async fn chat(&self, message: &str) -> Result<(), std::io::Error> { pub fn chat(&self, message: &str) {
if let Some(command) = message.strip_prefix('/') { if let Some(command) = message.strip_prefix('/') {
self.send_command_packet(command).await self.send_command_packet(command);
} else { } else {
self.send_chat_packet(message).await self.send_chat_packet(message);
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,100 @@
use std::sync::Arc;
use azalea_ecs::{
component::Component,
ecs::Ecs,
entity::Entity,
query::{ROQueryItem, ReadOnlyWorldQuery, WorldQuery},
};
use parking_lot::Mutex;
use crate::Client;
impl Client {
/// A convenience function for getting components of our player's entity.
pub fn query<'w, Q: WorldQuery>(&self, ecs: &'w mut Ecs) -> <Q as WorldQuery>::Item<'w> {
ecs.query::<Q>()
.get_mut(ecs, self.entity)
.expect("Our client is missing a required component.")
}
/// Return a lightweight [`Entity`] for the entity that matches the given
/// predicate function.
///
/// You can then use [`Self::entity_component`] to get components from this
/// entity.
///
/// # Example
/// Note that this will very likely change in the future.
/// ```
/// use azalea_client::{Client, GameProfileComponent};
/// use azalea_ecs::query::With;
/// use azalea_world::entity::{Position, metadata::Player};
///
/// # fn example(mut bot: Client, sender_name: String) {
/// let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
/// |profile: &&GameProfileComponent| profile.name == sender_name,
/// );
/// if let Some(entity) = entity {
/// let position = bot.entity_component::<Position>(entity);
/// // ...
/// }
/// # }
/// ```
pub fn entity_by<F: ReadOnlyWorldQuery, Q: ReadOnlyWorldQuery>(
&mut self,
predicate: impl EntityPredicate<Q, F>,
) -> Option<Entity> {
predicate.find(self.ecs.clone())
}
/// Get a component from an entity. Note that this will return an owned type
/// (i.e. not a reference) so it may be expensive for larger types.
///
/// If you're trying to get a component for this client, use
/// [`Self::component`].
pub fn entity_component<Q: Component + Clone>(&mut self, entity: Entity) -> Q {
let mut ecs = self.ecs.lock();
let mut q = ecs.query::<&Q>();
let components = q
.get(&ecs, entity)
.expect("Entity components must be present in Client::entity)components.");
components.clone()
}
}
pub trait EntityPredicate<Q: ReadOnlyWorldQuery, Filter: ReadOnlyWorldQuery> {
fn find(&self, ecs_lock: Arc<Mutex<Ecs>>) -> Option<Entity>;
}
impl<F, Q, Filter> EntityPredicate<(Q,), Filter> for F
where
F: Fn(&ROQueryItem<Q>) -> bool,
Q: ReadOnlyWorldQuery,
Filter: ReadOnlyWorldQuery,
{
fn find(&self, ecs_lock: Arc<Mutex<Ecs>>) -> Option<Entity> {
let mut ecs = ecs_lock.lock();
let mut query = ecs.query_filtered::<(Entity, Q), Filter>();
let entity = query.iter(&ecs).find(|(_, q)| (self)(q)).map(|(e, _)| e);
entity
}
}
// impl<'a, F, Q1, Q2> EntityPredicate<'a, (Q1, Q2)> for F
// where
// F: Fn(&<Q1 as WorldQuery>::Item<'_>, &<Q2 as WorldQuery>::Item<'_>) ->
// bool, Q1: ReadOnlyWorldQuery,
// Q2: ReadOnlyWorldQuery,
// {
// fn find(&self, ecs: &mut Ecs) -> Option<Entity> {
// // (self)(query)
// let mut query = ecs.query_filtered::<(Entity, Q1, Q2), ()>();
// let entity = query
// .iter(ecs)
// .find(|(_, q1, q2)| (self)(q1, q2))
// .map(|(e, _, _)| e);
// entity
// }
// }

189
azalea-client/src/events.rs Normal file
View file

@ -0,0 +1,189 @@
//! Defines the [`Event`] enum and makes those events trigger when they're sent
//! in the ECS.
use std::sync::Arc;
use azalea_ecs::{
app::{App, Plugin},
component::Component,
event::EventReader,
query::{Added, Changed},
system::Query,
AppTickExt,
};
use azalea_protocol::packets::game::{
clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, ClientboundGamePacket,
};
use azalea_world::entity::MinecraftEntityId;
use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc;
use crate::{
packet_handling::{
AddPlayerEvent, ChatReceivedEvent, DeathEvent, PacketReceiver, RemovePlayerEvent,
UpdatePlayerEvent,
},
ChatPacket, PlayerInfo,
};
// (for contributors):
// HOW TO ADD A NEW (packet based) EVENT:
// - make a struct that contains an entity field and a data field (look in
// packet_handling.rs for examples, also you should end the struct name with
// "Event")
// - the entity field is the local player entity that's receiving the event
// - in packet_handling, you always have a variable called player_entity that
// you can use
// - add the event struct in the `impl Plugin for PacketHandlerPlugin`
// - to get the event writer, you have to get an
// EventWriter<SomethingHappenedEvent> from the SystemState (the convention is
// to end your variable with the word "events", like "something_events")
//
// - then here in this file, add it to the Event enum
// - and make an event listener system/function like the other ones and put the
// function in the `impl Plugin for EventPlugin`
/// Something that happened in-game, such as a tick passing or chat message
/// being sent.
///
/// Note: Events are sent before they're processed, so for example game ticks
/// happen at the beginning of a tick before anything has happened.
#[derive(Debug, Clone)]
pub enum Event {
/// Happens right after the bot switches into the Game state, but before
/// it's actually spawned. This can be useful for setting the client
/// information with `Client::set_client_information`, so the packet
/// doesn't have to be sent twice.
Init,
/// The client is now in the world. Fired when we receive a login packet.
Login,
/// A chat message was sent in the game chat.
Chat(ChatPacket),
/// Happens 20 times per second, but only when the world is loaded.
Tick,
Packet(Arc<ClientboundGamePacket>),
/// A player joined the game (or more specifically, was added to the tab
/// list).
AddPlayer(PlayerInfo),
/// A player left the game (or maybe is still in the game and was just
/// removed from the tab list).
RemovePlayer(PlayerInfo),
/// A player was updated in the tab list (gamemode, display
/// name, or latency changed).
UpdatePlayer(PlayerInfo),
/// The client player died in-game.
Death(Option<Arc<ClientboundPlayerCombatKillPacket>>),
}
/// A component that contains an event sender for events that are only
/// received by local players. The receiver for this is returned by
/// [`Client::start_client`].
///
/// [`Client::start_client`]: crate::Client::start_client
#[derive(Component, Deref, DerefMut)]
pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
pub struct EventPlugin;
impl Plugin for EventPlugin {
fn build(&self, app: &mut App) {
app.add_system(chat_listener)
.add_system(login_listener)
.add_system(init_listener)
.add_system(packet_listener)
.add_system(add_player_listener)
.add_system(update_player_listener)
.add_system(remove_player_listener)
.add_system(death_listener)
.add_tick_system(tick_listener);
}
}
// when LocalPlayerEvents is added, it means the client just started
fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) {
for local_player_events in &query {
local_player_events.send(Event::Init).unwrap();
}
}
// when MinecraftEntityId is added, it means the player is now in the world
fn login_listener(query: Query<&LocalPlayerEvents, Added<MinecraftEntityId>>) {
for local_player_events in &query {
local_player_events.send(Event::Login).unwrap();
}
}
fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<ChatReceivedEvent>) {
for event in events.iter() {
let local_player_events = query
.get(event.entity)
.expect("Non-localplayer entities shouldn't be able to receive chat events");
local_player_events
.send(Event::Chat(event.packet.clone()))
.unwrap();
}
}
fn tick_listener(query: Query<&LocalPlayerEvents>) {
for local_player_events in &query {
local_player_events.send(Event::Tick).unwrap();
}
}
fn packet_listener(query: Query<(&LocalPlayerEvents, &PacketReceiver), Changed<PacketReceiver>>) {
for (local_player_events, packet_receiver) in &query {
for packet in packet_receiver.packets.lock().iter() {
local_player_events
.send(Event::Packet(packet.clone().into()))
.unwrap();
}
}
}
fn add_player_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<AddPlayerEvent>) {
for event in events.iter() {
let local_player_events = query
.get(event.entity)
.expect("Non-localplayer entities shouldn't be able to receive add player events");
local_player_events
.send(Event::AddPlayer(event.info.clone()))
.unwrap();
}
}
fn update_player_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<UpdatePlayerEvent>,
) {
for event in events.iter() {
let local_player_events = query
.get(event.entity)
.expect("Non-localplayer entities shouldn't be able to receive add player events");
local_player_events
.send(Event::UpdatePlayer(event.info.clone()))
.unwrap();
}
}
fn remove_player_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<RemovePlayerEvent>,
) {
for event in events.iter() {
let local_player_events = query
.get(event.entity)
.expect("Non-localplayer entities shouldn't be able to receive add player events");
local_player_events
.send(Event::RemovePlayer(event.info.clone()))
.unwrap();
}
}
fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<DeathEvent>) {
for event in events.iter() {
if let Ok(local_player_events) = query.get(event.entity) {
local_player_events
.send(Event::Death(event.packet.clone().map(|p| p.into())))
.unwrap();
}
}
}

View file

@ -9,27 +9,25 @@
#![allow(incomplete_features)] #![allow(incomplete_features)]
#![feature(trait_upcasting)] #![feature(trait_upcasting)]
#![feature(error_generic_member_access)] #![feature(error_generic_member_access)]
#![feature(type_alias_impl_trait)]
mod account; mod account;
mod chat; mod chat;
mod client; mod client;
mod entity_query;
mod events;
mod get_mc_dir; mod get_mc_dir;
mod local_player;
mod movement; mod movement;
pub mod packet_handling;
pub mod ping; pub mod ping;
mod player; mod player;
mod plugins; mod task_pool;
pub use account::Account; pub use account::Account;
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError, PhysicsState}; pub use azalea_ecs as ecs;
pub use movement::{SprintDirection, WalkDirection}; pub use client::{init_ecs_app, start_ecs, ChatPacket, Client, ClientInformation, JoinError};
pub use events::Event;
pub use local_player::{GameProfileComponent, LocalPlayer};
pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
pub use player::PlayerInfo; pub use player::PlayerInfo;
pub use plugins::{Plugin, PluginState, PluginStates, Plugins};
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}

View file

@ -0,0 +1,164 @@
use std::{collections::HashMap, io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::{ChunkPos, ResourceLocation};
use azalea_ecs::component::Component;
use azalea_ecs::entity::Entity;
use azalea_ecs::{query::Added, system::Query};
use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{
entity::{self, Dead},
PartialWorld, World,
};
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
use thiserror::Error;
use tokio::{sync::mpsc, task::JoinHandle};
use uuid::Uuid;
use crate::{
events::{Event, LocalPlayerEvents},
ClientInformation, PlayerInfo, WalkDirection,
};
/// A player that you control that is currently in a Minecraft server.
#[derive(Component)]
pub struct LocalPlayer {
pub packet_writer: mpsc::UnboundedSender<ServerboundGamePacket>,
pub client_information: ClientInformation,
/// A map of player uuids to their information in the tab list
pub players: HashMap<Uuid, PlayerInfo>,
/// The partial world is the world this client currently has loaded. It has
/// a limited render distance.
pub partial_world: Arc<RwLock<PartialWorld>>,
/// The world is the combined [`PartialWorld`]s of all clients in the same
/// world. (Only relevant if you're using a shared world, i.e. a swarm)
pub world: Arc<RwLock<World>>,
pub world_name: Option<ResourceLocation>,
/// A list of async tasks that are running and will stop running when this
/// LocalPlayer is dropped or disconnected with [`Self::disconnect`]
pub(crate) tasks: Vec<JoinHandle<()>>,
}
/// Component for entities that can move and sprint. Usually only in
/// [`LocalPlayer`] entities.
#[derive(Default, Component)]
pub struct PhysicsState {
/// Minecraft only sends a movement packet either after 20 ticks or if the
/// player moved enough. This is that tick counter.
pub position_remainder: u32,
pub was_sprinting: bool,
// Whether we're going to try to start sprinting this tick. Equivalent to
// holding down ctrl for a tick.
pub trying_to_sprint: bool,
pub move_direction: WalkDirection,
pub forward_impulse: f32,
pub left_impulse: f32,
}
/// A component only present in players that contains the [`GameProfile`] (which
/// you can use to get a player's name).
///
/// Note that it's possible for this to be missing in a player if the server
/// never sent the player info for them (though this is uncommon).
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct GameProfileComponent(pub GameProfile);
/// Marks a [`LocalPlayer`] that's in a loaded chunk. This is updated at the
/// beginning of every tick.
#[derive(Component)]
pub struct LocalPlayerInLoadedChunk;
impl LocalPlayer {
/// Create a new `LocalPlayer`.
pub fn new(
entity: Entity,
packet_writer: mpsc::UnboundedSender<ServerboundGamePacket>,
world: Arc<RwLock<World>>,
) -> Self {
let client_information = ClientInformation::default();
LocalPlayer {
packet_writer,
client_information: ClientInformation::default(),
players: HashMap::new(),
world,
partial_world: Arc::new(RwLock::new(PartialWorld::new(
client_information.view_distance.into(),
Some(entity),
))),
world_name: None,
tasks: Vec::new(),
}
}
/// Spawn a task to write a packet directly to the server.
pub fn write_packet(&mut self, packet: ServerboundGamePacket) {
self.packet_writer
.send(packet)
.expect("write_packet shouldn't be able to be called if the connection is closed");
}
/// Disconnect this client from the server by ending all tasks.
///
/// The OwnedReadHalf for the TCP connection is in one of the tasks, so it
/// automatically closes the connection when that's dropped.
pub fn disconnect(&self) {
for task in &self.tasks {
task.abort();
}
}
}
/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s.
pub fn update_in_loaded_chunk(
mut commands: azalea_ecs::system::Commands,
query: Query<(Entity, &LocalPlayer, &entity::Position)>,
) {
for (entity, local_player, position) in &query {
let player_chunk_pos = ChunkPos::from(position);
let in_loaded_chunk = local_player
.world
.read()
.chunks
.get(&player_chunk_pos)
.is_some();
if in_loaded_chunk {
commands.entity(entity).insert(LocalPlayerInLoadedChunk);
} else {
commands.entity(entity).remove::<LocalPlayerInLoadedChunk>();
}
}
}
/// Send the "Death" event for [`LocalPlayer`]s that died with no reason.
pub fn death_event(query: Query<&LocalPlayerEvents, Added<Dead>>) {
for local_player_events in &query {
local_player_events.send(Event::Death(None)).unwrap();
}
}
#[derive(Error, Debug)]
pub enum HandlePacketError {
#[error("{0}")]
Poison(String),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
#[error("{0}")]
Send(#[from] mpsc::error::SendError<Event>),
}
impl<T> From<std::sync::PoisonError<T>> for HandlePacketError {
fn from(e: std::sync::PoisonError<T>) -> Self {
HandlePacketError::Poison(e.to_string())
}
}

View file

@ -1,9 +1,7 @@
use std::backtrace::Backtrace; use crate::client::Client;
use crate::local_player::{LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState};
use crate::Client; use azalea_ecs::entity::Entity;
use azalea_core::Vec3; use azalea_ecs::{event::EventReader, query::With, system::Query};
use azalea_physics::collision::{MovableEntity, MoverType};
use azalea_physics::HasPhysics;
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket; use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
use azalea_protocol::packets::game::{ use azalea_protocol::packets::game::{
serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket, serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket,
@ -11,7 +9,11 @@ use azalea_protocol::packets::game::{
serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket, serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket,
serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket, serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket,
}; };
use azalea_world::MoveEntityError; use azalea_world::{
entity::{self, metadata::Sprinting, Attributes, Jumping, MinecraftEntityId},
MoveEntityError,
};
use std::backtrace::Backtrace;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -33,24 +35,72 @@ impl From<MoveEntityError> for MovePlayerError {
} }
impl Client { impl Client {
/// This gets called automatically every tick. /// Set whether we're jumping. This acts as if you held space in
pub(crate) async fn send_position(&mut self) -> Result<(), MovePlayerError> { /// vanilla. If you want to jump once, use the `jump` function.
///
/// If you're making a realistic client, calling this function every tick is
/// recommended.
pub fn set_jumping(&mut self, jumping: bool) {
let mut ecs = self.ecs.lock();
let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs);
**jumping_mut = jumping;
}
/// Returns whether the player will try to jump next tick.
pub fn jumping(&self) -> bool {
let mut ecs = self.ecs.lock();
let jumping_ref = self.query::<&Jumping>(&mut ecs);
**jumping_ref
}
/// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
/// pitch (looking up and down). You can get these numbers from the vanilla
/// f3 screen.
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
let mut ecs = self.ecs.lock();
let mut physics = self.query::<&mut entity::Physics>(&mut ecs);
entity::set_rotation(&mut physics, y_rot, x_rot);
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn send_position(
mut query: Query<
(
&MinecraftEntityId,
&mut LocalPlayer,
&mut PhysicsState,
&entity::Position,
&mut entity::LastSentPosition,
&mut entity::Physics,
&entity::metadata::Sprinting,
),
&LocalPlayerInLoadedChunk,
>,
) {
for (
id,
mut local_player,
mut physics_state,
position,
mut last_sent_position,
mut physics,
sprinting,
) in query.iter_mut()
{
local_player.send_sprinting_if_needed(id, sprinting, &mut physics_state);
let packet = { let packet = {
self.send_sprinting_if_needed().await?;
// TODO: the camera being able to be controlled by other entities isn't // TODO: the camera being able to be controlled by other entities isn't
// implemented yet if !self.is_controlled_camera() { return }; // implemented yet if !self.is_controlled_camera() { return };
let mut physics_state = self.physics_state.lock(); let x_delta = position.x - last_sent_position.x;
let y_delta = position.y - last_sent_position.y;
let player_entity = self.entity(); let z_delta = position.z - last_sent_position.z;
let player_pos = player_entity.pos(); let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
let player_old_pos = player_entity.last_pos; let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
let x_delta = player_pos.x - player_old_pos.x;
let y_delta = player_pos.y - player_old_pos.y;
let z_delta = player_pos.z - player_old_pos.z;
let y_rot_delta = (player_entity.y_rot - player_entity.y_rot_last) as f64;
let x_rot_delta = (player_entity.x_rot - player_entity.x_rot_last) as f64;
physics_state.position_remainder += 1; physics_state.position_remainder += 1;
@ -67,38 +117,38 @@ impl Client {
let packet = if sending_position && sending_rotation { let packet = if sending_position && sending_rotation {
Some( Some(
ServerboundMovePlayerPosRotPacket { ServerboundMovePlayerPosRotPacket {
x: player_pos.x, x: position.x,
y: player_pos.y, y: position.y,
z: player_pos.z, z: position.z,
x_rot: player_entity.x_rot, x_rot: physics.x_rot,
y_rot: player_entity.y_rot, y_rot: physics.y_rot,
on_ground: player_entity.on_ground, on_ground: physics.on_ground,
} }
.get(), .get(),
) )
} else if sending_position { } else if sending_position {
Some( Some(
ServerboundMovePlayerPosPacket { ServerboundMovePlayerPosPacket {
x: player_pos.x, x: position.x,
y: player_pos.y, y: position.y,
z: player_pos.z, z: position.z,
on_ground: player_entity.on_ground, on_ground: physics.on_ground,
} }
.get(), .get(),
) )
} else if sending_rotation { } else if sending_rotation {
Some( Some(
ServerboundMovePlayerRotPacket { ServerboundMovePlayerRotPacket {
x_rot: player_entity.x_rot, x_rot: physics.x_rot,
y_rot: player_entity.y_rot, y_rot: physics.y_rot,
on_ground: player_entity.on_ground, on_ground: physics.on_ground,
} }
.get(), .get(),
) )
} else if player_entity.last_on_ground != player_entity.on_ground { } else if physics.last_on_ground != physics.on_ground {
Some( Some(
ServerboundMovePlayerStatusOnlyPacket { ServerboundMovePlayerStatusOnlyPacket {
on_ground: player_entity.on_ground, on_ground: physics.on_ground,
} }
.get(), .get(),
) )
@ -106,131 +156,56 @@ impl Client {
None None
}; };
drop(player_entity);
let mut player_entity = self.entity();
if sending_position { if sending_position {
player_entity.last_pos = *player_entity.pos(); **last_sent_position = **position;
physics_state.position_remainder = 0; physics_state.position_remainder = 0;
} }
if sending_rotation { if sending_rotation {
player_entity.y_rot_last = player_entity.y_rot; physics.y_rot_last = physics.y_rot;
player_entity.x_rot_last = player_entity.x_rot; physics.x_rot_last = physics.x_rot;
} }
player_entity.last_on_ground = player_entity.on_ground; physics.last_on_ground = physics.on_ground;
// minecraft checks for autojump here, but also autojump is bad so // minecraft checks for autojump here, but also autojump is bad so
packet packet
}; };
if let Some(packet) = packet { if let Some(packet) = packet {
self.write_packet(packet).await?; local_player.write_packet(packet);
}
}
} }
Ok(()) impl LocalPlayer {
} fn send_sprinting_if_needed(
&mut self,
async fn send_sprinting_if_needed(&mut self) -> Result<(), MovePlayerError> { id: &MinecraftEntityId,
let is_sprinting = self.entity().metadata.sprinting; sprinting: &entity::metadata::Sprinting,
let was_sprinting = self.physics_state.lock().was_sprinting; physics_state: &mut PhysicsState,
if is_sprinting != was_sprinting { ) {
let sprinting_action = if is_sprinting { let was_sprinting = physics_state.was_sprinting;
if **sprinting != was_sprinting {
let sprinting_action = if **sprinting {
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StartSprinting azalea_protocol::packets::game::serverbound_player_command_packet::Action::StartSprinting
} else { } else {
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting
}; };
let player_entity_id = self.entity().id;
self.write_packet( self.write_packet(
ServerboundPlayerCommandPacket { ServerboundPlayerCommandPacket {
id: player_entity_id, id: **id,
action: sprinting_action, action: sprinting_action,
data: 0, data: 0,
} }
.get(), .get(),
)
.await?;
self.physics_state.lock().was_sprinting = is_sprinting;
}
Ok(())
}
// Set our current position to the provided Vec3, potentially clipping through
// blocks.
pub async fn set_position(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> {
let player_entity_id = *self.entity_id.read();
let mut world_lock = self.world.write();
world_lock.set_entity_pos(player_entity_id, new_pos)?;
Ok(())
}
pub async fn move_entity(&mut self, movement: &Vec3) -> Result<(), MovePlayerError> {
let mut world_lock = self.world.write();
let player_entity_id = *self.entity_id.read();
let mut entity = world_lock
.entity_mut(player_entity_id)
.ok_or(MovePlayerError::PlayerNotInWorld(Backtrace::capture()))?;
log::trace!(
"move entity bounding box: {} {:?}",
entity.id,
entity.bounding_box
); );
physics_state.was_sprinting = **sprinting;
entity.move_colliding(&MoverType::Own, movement)?;
Ok(())
} }
/// Makes the bot do one physics tick. Note that this is already handled
/// automatically by the client.
pub fn ai_step(&mut self) {
self.tick_controls(None);
// server ai step
{
let mut player_entity = self.entity();
let physics_state = self.physics_state.lock();
player_entity.xxa = physics_state.left_impulse;
player_entity.zza = physics_state.forward_impulse;
}
// TODO: food data and abilities
// let has_enough_food_to_sprint = self.food_data().food_level ||
// self.abilities().may_fly;
let has_enough_food_to_sprint = true;
// TODO: double tapping w to sprint i think
let trying_to_sprint = self.physics_state.lock().trying_to_sprint;
if !self.sprinting()
&& (
// !self.is_in_water()
// || self.is_underwater() &&
self.has_enough_impulse_to_start_sprinting()
&& has_enough_food_to_sprint
// && !self.using_item()
// && !self.has_effect(MobEffects.BLINDNESS)
&& trying_to_sprint
)
{
self.set_sprinting(true);
}
let mut player_entity = self.entity();
player_entity.ai_step();
} }
/// Update the impulse from self.move_direction. The multipler is used for /// Update the impulse from self.move_direction. The multipler is used for
/// sneaking. /// sneaking.
pub(crate) fn tick_controls(&mut self, multiplier: Option<f32>) { pub(crate) fn tick_controls(multiplier: Option<f32>, physics_state: &mut PhysicsState) {
let mut physics_state = self.physics_state.lock();
let mut forward_impulse: f32 = 0.; let mut forward_impulse: f32 = 0.;
let mut left_impulse: f32 = 0.; let mut left_impulse: f32 = 0.;
let move_direction = physics_state.move_direction; let move_direction = physics_state.move_direction;
@ -262,7 +237,54 @@ impl Client {
physics_state.left_impulse *= multiplier; physics_state.left_impulse *= multiplier;
} }
} }
}
/// Makes the bot do one physics tick. Note that this is already handled
/// automatically by the client.
pub fn local_player_ai_step(
mut query: Query<
(
&mut PhysicsState,
&mut entity::Physics,
&mut entity::metadata::Sprinting,
&mut entity::Attributes,
),
With<LocalPlayerInLoadedChunk>,
>,
) {
for (mut physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
LocalPlayer::tick_controls(None, &mut physics_state);
// server ai step
physics.xxa = physics_state.left_impulse;
physics.zza = physics_state.forward_impulse;
// TODO: food data and abilities
// let has_enough_food_to_sprint = self.food_data().food_level ||
// self.abilities().may_fly;
let has_enough_food_to_sprint = true;
// TODO: double tapping w to sprint i think
let trying_to_sprint = physics_state.trying_to_sprint;
if !**sprinting
&& (
// !self.is_in_water()
// || self.is_underwater() &&
has_enough_impulse_to_start_sprinting(&physics_state)
&& has_enough_food_to_sprint
// && !self.using_item()
// && !self.has_effect(MobEffects.BLINDNESS)
&& trying_to_sprint
)
{
set_sprinting(true, &mut sprinting, &mut attributes);
}
}
}
impl Client {
/// Start walking in the given direction. To sprint, use /// Start walking in the given direction. To sprint, use
/// [`Client::sprint`]. To stop walking, call walk with /// [`Client::sprint`]. To stop walking, call walk with
/// `WalkDirection::None`. /// `WalkDirection::None`.
@ -280,12 +302,11 @@ impl Client {
/// # } /// # }
/// ``` /// ```
pub fn walk(&mut self, direction: WalkDirection) { pub fn walk(&mut self, direction: WalkDirection) {
{ let mut ecs = self.ecs.lock();
let mut physics_state = self.physics_state.lock(); ecs.send_event(StartWalkEvent {
physics_state.move_direction = direction; entity: self.entity,
} direction,
});
self.set_sprinting(false);
} }
/// Start sprinting in the given direction. To stop moving, call /// Start sprinting in the given direction. To stop moving, call
@ -304,72 +325,82 @@ impl Client {
/// # } /// # }
/// ``` /// ```
pub fn sprint(&mut self, direction: SprintDirection) { pub fn sprint(&mut self, direction: SprintDirection) {
let mut physics_state = self.physics_state.lock(); let mut ecs = self.ecs.lock();
physics_state.move_direction = WalkDirection::from(direction); ecs.send_event(StartSprintEvent {
physics_state.trying_to_sprint = true; entity: self.entity,
direction,
});
}
} }
// Whether we're currently sprinting. pub struct StartWalkEvent {
pub fn sprinting(&self) -> bool { pub entity: Entity,
self.entity().metadata.sprinting pub direction: WalkDirection,
}
/// Start walking in the given direction. To sprint, use
/// [`Client::sprint`]. To stop walking, call walk with
/// `WalkDirection::None`.
pub fn walk_listener(
mut events: EventReader<StartWalkEvent>,
mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
) {
for event in events.iter() {
if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
{
physics_state.move_direction = event.direction;
set_sprinting(false, &mut sprinting, &mut attributes);
}
}
}
pub struct StartSprintEvent {
pub entity: Entity,
pub direction: SprintDirection,
}
/// Start sprinting in the given direction.
pub fn sprint_listener(
mut query: Query<&mut PhysicsState>,
mut events: EventReader<StartSprintEvent>,
) {
for event in events.iter() {
if let Ok(mut physics_state) = query.get_mut(event.entity) {
physics_state.move_direction = WalkDirection::from(event.direction);
physics_state.trying_to_sprint = true;
}
}
} }
/// Change whether we're sprinting by adding an attribute modifier to the /// Change whether we're sprinting by adding an attribute modifier to the
/// player. You should use the [`walk`] and [`sprint`] methods instead. /// player. You should use the [`walk`] and [`sprint`] methods instead.
/// Returns if the operation was successful. /// Returns if the operation was successful.
fn set_sprinting(&mut self, sprinting: bool) -> bool { fn set_sprinting(
let mut player_entity = self.entity(); sprinting: bool,
player_entity.metadata.sprinting = sprinting; currently_sprinting: &mut Sprinting,
attributes: &mut Attributes,
) -> bool {
**currently_sprinting = sprinting;
if sprinting { if sprinting {
player_entity attributes
.attributes
.speed .speed
.insert(azalea_world::entity::attributes::sprinting_modifier()) .insert(entity::attributes::sprinting_modifier())
.is_ok() .is_ok()
} else { } else {
player_entity attributes
.attributes
.speed .speed
.remove(&azalea_world::entity::attributes::sprinting_modifier().uuid) .remove(&entity::attributes::sprinting_modifier().uuid)
.is_none() .is_none()
} }
} }
/// Set whether we're jumping. This acts as if you held space in
/// vanilla. If you want to jump once, use the `jump` function.
///
/// If you're making a realistic client, calling this function every tick is
/// recommended.
pub fn set_jumping(&mut self, jumping: bool) {
let mut player_entity = self.entity();
player_entity.jumping = jumping;
}
/// Returns whether the player will try to jump next tick.
pub fn jumping(&self) -> bool {
let player_entity = self.entity();
player_entity.jumping
}
/// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
/// pitch (looking up and down). You can get these numbers from the vanilla
/// f3 screen.
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
let mut player_entity = self.entity();
player_entity.set_rotation(y_rot, x_rot);
}
// Whether the player is moving fast enough to be able to start sprinting. // Whether the player is moving fast enough to be able to start sprinting.
fn has_enough_impulse_to_start_sprinting(&self) -> bool { fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
// if self.underwater() { // if self.underwater() {
// self.has_forward_impulse() // self.has_forward_impulse()
// } else { // } else {
let physics_state = self.physics_state.lock();
physics_state.forward_impulse > 0.8 physics_state.forward_impulse > 0.8
// } // }
} }
}
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub enum WalkDirection { pub enum WalkDirection {

View file

@ -0,0 +1,935 @@
use std::{collections::HashSet, io::Cursor, sync::Arc};
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
use azalea_ecs::{
app::{App, Plugin},
component::Component,
ecs::Ecs,
entity::Entity,
event::EventWriter,
query::Changed,
schedule::{IntoSystemDescriptor, SystemSet},
system::{Commands, Query, ResMut, SystemState},
};
use azalea_protocol::{
connect::{ReadConnection, WriteConnection},
packets::game::{
clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket,
serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket,
serverbound_custom_payload_packet::ServerboundCustomPayloadPacket,
serverbound_keep_alive_packet::ServerboundKeepAlivePacket,
serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket,
ClientboundGamePacket, ServerboundGamePacket,
},
};
use azalea_world::{
entity::{
metadata::{apply_metadata, Health, PlayerMetadataBundle},
set_rotation, Dead, EntityBundle, EntityKind, LastSentPosition, MinecraftEntityId, Physics,
PlayerBundle, Position,
},
LoadedBy, PartialWorld, RelativeEntityUpdate, WorldContainer,
};
use log::{debug, error, trace, warn};
use parking_lot::Mutex;
use tokio::sync::mpsc;
use crate::{
local_player::{GameProfileComponent, LocalPlayer},
ChatPacket, ClientInformation, PlayerInfo,
};
pub struct PacketHandlerPlugin;
impl Plugin for PacketHandlerPlugin {
fn build(&self, app: &mut App) {
app.add_system_set(
SystemSet::new().with_system(handle_packets.label("packet").before("tick")),
)
.add_event::<AddPlayerEvent>()
.add_event::<RemovePlayerEvent>()
.add_event::<UpdatePlayerEvent>()
.add_event::<ChatReceivedEvent>()
.add_event::<DeathEvent>();
}
}
/// A player joined the game (or more specifically, was added to the tab
/// list of a local player).
#[derive(Debug)]
pub struct AddPlayerEvent {
/// The local player entity that received this event.
pub entity: Entity,
pub info: PlayerInfo,
}
/// A player left the game (or maybe is still in the game and was just
/// removed from the tab list of a local player).
#[derive(Debug)]
pub struct RemovePlayerEvent {
/// The local player entity that received this event.
pub entity: Entity,
pub info: PlayerInfo,
}
/// A player was updated in the tab list of a local player (gamemode, display
/// name, or latency changed).
#[derive(Debug)]
pub struct UpdatePlayerEvent {
/// The local player entity that received this event.
pub entity: Entity,
pub info: PlayerInfo,
}
/// A client received a chat message packet.
#[derive(Debug)]
pub struct ChatReceivedEvent {
pub entity: Entity,
pub packet: ChatPacket,
}
/// Event for when an entity dies. dies. If it's a local player and there's a
/// reason in the death screen, the [`ClientboundPlayerCombatKillPacket`] will
/// be included.
pub struct DeathEvent {
pub entity: Entity,
pub packet: Option<ClientboundPlayerCombatKillPacket>,
}
/// Something that receives packets from the server.
#[derive(Component, Clone)]
pub struct PacketReceiver {
pub packets: Arc<Mutex<Vec<ClientboundGamePacket>>>,
pub run_schedule_sender: mpsc::Sender<()>,
}
fn handle_packets(ecs: &mut Ecs) {
let mut events_owned = Vec::new();
{
let mut system_state: SystemState<
Query<(Entity, &PacketReceiver), Changed<PacketReceiver>>,
> = SystemState::new(ecs);
let query = system_state.get(ecs);
for (player_entity, packet_events) in &query {
let mut packets = packet_events.packets.lock();
if !packets.is_empty() {
events_owned.push((player_entity, packets.clone()));
// clear the packets right after we read them
packets.clear();
}
}
}
for (player_entity, packets) in events_owned {
for packet in &packets {
match packet {
ClientboundGamePacket::Login(p) => {
debug!("Got login packet");
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
Query<(&mut LocalPlayer, &GameProfileComponent)>,
ResMut<WorldContainer>,
)> = SystemState::new(ecs);
let (mut commands, mut query, mut world_container) = system_state.get_mut(ecs);
let (mut local_player, game_profile) = query.get_mut(player_entity).unwrap();
{
// TODO: have registry_holder be a struct because this sucks rn
// best way would be to add serde support to azalea-nbt
let registry_holder = p
.registry_holder
.as_compound()
.expect("Registry holder is not a compound")
.get("")
.expect("No \"\" tag")
.as_compound()
.expect("\"\" tag is not a compound");
let dimension_types = registry_holder
.get("minecraft:dimension_type")
.expect("No dimension_type tag")
.as_compound()
.expect("dimension_type is not a compound")
.get("value")
.expect("No dimension_type value")
.as_list()
.expect("dimension_type value is not a list");
let dimension_type = dimension_types
.iter()
.find(|t| {
t.as_compound()
.expect("dimension_type value is not a compound")
.get("name")
.expect("No name tag")
.as_string()
.expect("name is not a string")
== p.dimension_type.to_string()
})
.unwrap_or_else(|| {
panic!("No dimension_type with name {}", p.dimension_type)
})
.as_compound()
.unwrap()
.get("element")
.expect("No element tag")
.as_compound()
.expect("element is not a compound");
let height = (*dimension_type
.get("height")
.expect("No height tag")
.as_int()
.expect("height tag is not an int"))
.try_into()
.expect("height is not a u32");
let min_y = *dimension_type
.get("min_y")
.expect("No min_y tag")
.as_int()
.expect("min_y tag is not an int");
let world_name = p.dimension.clone();
local_player.world_name = Some(world_name.clone());
// add this world to the world_container (or don't if it's already
// there)
let weak_world = world_container.insert(world_name.clone(), height, min_y);
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
// world_container)
*local_player.partial_world.write() = PartialWorld::new(
local_player.client_information.view_distance.into(),
// this argument makes it so other clients don't update this
// player entity
// in a shared world
Some(player_entity),
);
local_player.world = weak_world;
let player_bundle = PlayerBundle {
entity: EntityBundle::new(
game_profile.uuid,
Vec3::default(),
azalea_registry::EntityKind::Player,
world_name,
),
metadata: PlayerMetadataBundle::default(),
};
// insert our components into the ecs :)
commands
.entity(player_entity)
.insert((MinecraftEntityId(p.player_id), player_bundle));
}
// send the client information that we have set
let client_information_packet: ClientInformation =
local_player.client_information.clone();
log::debug!(
"Sending client information because login: {:?}",
client_information_packet
);
local_player.write_packet(client_information_packet.get());
// brand
local_player.write_packet(
ServerboundCustomPayloadPacket {
identifier: ResourceLocation::new("brand").unwrap(),
// they don't have to know :)
data: "vanilla".into(),
}
.get(),
);
system_state.apply(ecs);
}
ClientboundGamePacket::SetChunkCacheRadius(p) => {
debug!("Got set chunk cache radius packet {:?}", p);
}
ClientboundGamePacket::CustomPayload(p) => {
debug!("Got custom payload packet {:?}", p);
}
ClientboundGamePacket::ChangeDifficulty(p) => {
debug!("Got difficulty packet {:?}", p);
}
ClientboundGamePacket::Commands(_p) => {
debug!("Got declare commands packet");
}
ClientboundGamePacket::PlayerAbilities(p) => {
debug!("Got player abilities packet {:?}", p);
}
ClientboundGamePacket::SetCarriedItem(p) => {
debug!("Got set carried item packet {:?}", p);
}
ClientboundGamePacket::UpdateTags(_p) => {
debug!("Got update tags packet");
}
ClientboundGamePacket::Disconnect(p) => {
debug!("Got disconnect packet {:?}", p);
let mut system_state: SystemState<Query<&LocalPlayer>> = SystemState::new(ecs);
let query = system_state.get(ecs);
let local_player = query.get(player_entity).unwrap();
local_player.disconnect();
}
ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet");
}
ClientboundGamePacket::EntityEvent(_p) => {
// debug!("Got entity event packet {:?}", p);
}
ClientboundGamePacket::Recipe(_p) => {
debug!("Got recipe packet");
}
ClientboundGamePacket::PlayerPosition(p) => {
// TODO: reply with teleport confirm
debug!("Got player position packet {:?}", p);
let mut system_state: SystemState<
Query<(
&mut LocalPlayer,
&mut Physics,
&mut Position,
&mut LastSentPosition,
)>,
> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let Ok((mut local_player, mut physics, mut position, mut last_sent_position)) =
query.get_mut(player_entity) else {
continue;
};
let delta_movement = physics.delta;
let is_x_relative = p.relative_arguments.x;
let is_y_relative = p.relative_arguments.y;
let is_z_relative = p.relative_arguments.z;
let (delta_x, new_pos_x) = if is_x_relative {
last_sent_position.x += p.x;
(delta_movement.x, position.x + p.x)
} else {
last_sent_position.x = p.x;
(0.0, p.x)
};
let (delta_y, new_pos_y) = if is_y_relative {
last_sent_position.y += p.y;
(delta_movement.y, position.y + p.y)
} else {
last_sent_position.y = p.y;
(0.0, p.y)
};
let (delta_z, new_pos_z) = if is_z_relative {
last_sent_position.z += p.z;
(delta_movement.z, position.z + p.z)
} else {
last_sent_position.z = p.z;
(0.0, p.z)
};
let mut y_rot = p.y_rot;
let mut x_rot = p.x_rot;
if p.relative_arguments.x_rot {
x_rot += physics.x_rot;
}
if p.relative_arguments.y_rot {
y_rot += physics.y_rot;
}
physics.delta = Vec3 {
x: delta_x,
y: delta_y,
z: delta_z,
};
// we call a function instead of setting the fields ourself since the
// function makes sure the rotations stay in their
// ranges
set_rotation(&mut physics, y_rot, x_rot);
// TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means
// so investigate that ig
let new_pos = Vec3 {
x: new_pos_x,
y: new_pos_y,
z: new_pos_z,
};
**position = new_pos;
local_player
.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get());
local_player.write_packet(
ServerboundMovePlayerPosRotPacket {
x: new_pos.x,
y: new_pos.y,
z: new_pos.z,
y_rot,
x_rot,
// this is always false
on_ground: false,
}
.get(),
);
}
ClientboundGamePacket::PlayerInfoUpdate(p) => {
debug!("Got player info packet {:?}", p);
let mut system_state: SystemState<(
Query<&mut LocalPlayer>,
EventWriter<AddPlayerEvent>,
EventWriter<UpdatePlayerEvent>,
)> = SystemState::new(ecs);
let (mut query, mut add_player_events, mut update_player_events) =
system_state.get_mut(ecs);
let mut local_player = query.get_mut(player_entity).unwrap();
for updated_info in &p.entries {
// add the new player maybe
if p.actions.add_player {
let info = PlayerInfo {
profile: updated_info.profile.clone(),
uuid: updated_info.profile.uuid,
gamemode: updated_info.game_mode,
latency: updated_info.latency,
display_name: updated_info.display_name.clone(),
};
local_player
.players
.insert(updated_info.profile.uuid, info.clone());
add_player_events.send(AddPlayerEvent {
entity: player_entity,
info: info.clone(),
});
} else if let Some(info) =
local_player.players.get_mut(&updated_info.profile.uuid)
{
// `else if` because the block for add_player above
// already sets all the fields
if p.actions.update_game_mode {
info.gamemode = updated_info.game_mode;
}
if p.actions.update_latency {
info.latency = updated_info.latency;
}
if p.actions.update_display_name {
info.display_name = updated_info.display_name.clone();
}
update_player_events.send(UpdatePlayerEvent {
entity: player_entity,
info: info.clone(),
});
} else {
warn!(
"Ignoring PlayerInfoUpdate for unknown player {}",
updated_info.profile.uuid
);
}
}
}
ClientboundGamePacket::PlayerInfoRemove(p) => {
let mut system_state: SystemState<(
Query<&mut LocalPlayer>,
EventWriter<RemovePlayerEvent>,
)> = SystemState::new(ecs);
let (mut query, mut remove_player_events) = system_state.get_mut(ecs);
let mut local_player = query.get_mut(player_entity).unwrap();
for uuid in &p.profile_ids {
if let Some(info) = local_player.players.remove(uuid) {
remove_player_events.send(RemovePlayerEvent {
entity: player_entity,
info,
});
}
}
}
ClientboundGamePacket::SetChunkCacheCenter(p) => {
debug!("Got chunk cache center packet {:?}", p);
let mut system_state: SystemState<Query<&mut LocalPlayer>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let mut partial_world = local_player.partial_world.write();
partial_world.chunks.view_center = ChunkPos::new(p.x, p.z);
}
ClientboundGamePacket::LevelChunkWithLight(p) => {
debug!("Got chunk with light packet {} {}", p.x, p.z);
let pos = ChunkPos::new(p.x, p.z);
let mut system_state: SystemState<Query<&mut LocalPlayer>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
// OPTIMIZATION: if we already know about the chunk from the
// shared world (and not ourselves), then we don't need to
// parse it again. This is only used when we have a shared
// world, since we check that the chunk isn't currently owned
// by this client.
let shared_chunk = local_player.world.read().chunks.get(&pos);
let this_client_has_chunk = local_player
.partial_world
.read()
.chunks
.limited_get(&pos)
.is_some();
let mut world = local_player.world.write();
let mut partial_world = local_player.partial_world.write();
if !this_client_has_chunk {
if let Some(shared_chunk) = shared_chunk {
trace!(
"Skipping parsing chunk {:?} because we already know about it",
pos
);
partial_world.chunks.set_with_shared_reference(
&pos,
Some(shared_chunk.clone()),
&mut world.chunks,
);
continue;
}
}
if let Err(e) = partial_world.chunks.replace_with_packet_data(
&pos,
&mut Cursor::new(&p.chunk_data.data),
&mut world.chunks,
) {
error!("Couldn't set chunk data: {}", e);
}
}
ClientboundGamePacket::LightUpdate(_p) => {
// debug!("Got light update packet {:?}", p);
}
ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p);
let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
if let Some(world_name) = &local_player.world_name {
let bundle = p.as_entity_bundle(world_name.clone());
let mut entity_commands = commands.spawn((
MinecraftEntityId(p.id),
LoadedBy(HashSet::from([player_entity])),
bundle,
));
// the bundle doesn't include the default entity metadata so we add that
// separately
p.apply_metadata(&mut entity_commands);
} else {
warn!("got add player packet but we haven't gotten a login packet yet");
}
system_state.apply(ecs);
}
ClientboundGamePacket::SetEntityData(p) => {
debug!("Got set entity data packet {:?}", p);
let mut system_state: SystemState<(
Commands,
Query<&mut LocalPlayer>,
Query<&EntityKind>,
)> = SystemState::new(ecs);
let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let world = local_player.world.read();
let entity = world.entity_by_id(&MinecraftEntityId(p.id));
drop(world);
if let Some(entity) = entity {
let entity_kind = entity_kind_query.get(entity).unwrap();
let mut entity_commands = commands.entity(entity);
if let Err(e) = apply_metadata(
&mut entity_commands,
**entity_kind,
(*p.packed_items).clone(),
) {
warn!("{e}");
}
} else {
warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id);
}
system_state.apply(ecs);
}
ClientboundGamePacket::UpdateAttributes(_p) => {
// debug!("Got update attributes packet {:?}", p);
}
ClientboundGamePacket::SetEntityMotion(_p) => {
// debug!("Got entity velocity packet {:?}", p);
}
ClientboundGamePacket::SetEntityLink(p) => {
debug!("Got set entity link packet {:?}", p);
}
ClientboundGamePacket::AddPlayer(p) => {
debug!("Got add player packet {:?}", p);
let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
if let Some(world_name) = &local_player.world_name {
let bundle = p.as_player_bundle(world_name.clone());
let mut spawned = commands.spawn((
MinecraftEntityId(p.id),
LoadedBy(HashSet::from([player_entity])),
bundle,
));
if let Some(player_info) = local_player.players.get(&p.uuid) {
spawned.insert(GameProfileComponent(player_info.profile.clone()));
}
} else {
warn!("got add player packet but we haven't gotten a login packet yet");
}
system_state.apply(ecs);
}
ClientboundGamePacket::InitializeBorder(p) => {
debug!("Got initialize border packet {:?}", p);
}
ClientboundGamePacket::SetTime(_p) => {
// debug!("Got set time packet {:?}", p);
}
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
debug!("Got set default spawn position packet {:?}", p);
}
ClientboundGamePacket::ContainerSetContent(p) => {
debug!("Got container set content packet {:?}", p);
}
ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p);
let mut system_state: SystemState<(
Query<&mut Health>,
EventWriter<DeathEvent>,
)> = SystemState::new(ecs);
let (mut query, mut death_events) = system_state.get_mut(ecs);
let mut health = query.get_mut(player_entity).unwrap();
if p.health == 0. && **health != 0. {
death_events.send(DeathEvent {
entity: player_entity,
packet: None,
});
}
**health = p.health;
// the `Dead` component is added by the `update_dead` system
// in azalea-world and then the `dead_event` system fires
// the Death event.
}
ClientboundGamePacket::SetExperience(p) => {
debug!("Got set experience packet {:?}", p);
}
ClientboundGamePacket::TeleportEntity(p) => {
let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let world = local_player.world.read();
let entity = world.entity_by_id(&MinecraftEntityId(p.id));
drop(world);
if let Some(entity) = entity {
let new_position = p.position;
commands.add(RelativeEntityUpdate {
entity,
partial_world: local_player.partial_world.clone(),
update: Box::new(move |entity| {
let mut position = entity.get_mut::<Position>().unwrap();
**position = new_position;
}),
});
} else {
warn!("Got teleport entity packet for unknown entity id {}", p.id);
}
system_state.apply(ecs);
}
ClientboundGamePacket::UpdateAdvancements(p) => {
debug!("Got update advancements packet {:?}", p);
}
ClientboundGamePacket::RotateHead(_p) => {
// debug!("Got rotate head packet {:?}", p);
}
ClientboundGamePacket::MoveEntityPos(p) => {
let mut system_state: SystemState<(Commands, Query<&LocalPlayer>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let world = local_player.world.read();
let entity = world.entity_by_id(&MinecraftEntityId(p.entity_id));
drop(world);
if let Some(entity) = entity {
let delta = p.delta.clone();
commands.add(RelativeEntityUpdate {
entity,
partial_world: local_player.partial_world.clone(),
update: Box::new(move |entity_mut| {
let mut position = entity_mut.get_mut::<Position>().unwrap();
**position = position.with_delta(&delta);
}),
});
} else {
warn!(
"Got move entity pos packet for unknown entity id {}",
p.entity_id
);
}
system_state.apply(ecs);
}
ClientboundGamePacket::MoveEntityPosRot(p) => {
let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let world = local_player.world.read();
let entity = world.entity_by_id(&MinecraftEntityId(p.entity_id));
drop(world);
if let Some(entity) = entity {
let delta = p.delta.clone();
commands.add(RelativeEntityUpdate {
entity,
partial_world: local_player.partial_world.clone(),
update: Box::new(move |entity_mut| {
let mut position = entity_mut.get_mut::<Position>().unwrap();
**position = position.with_delta(&delta);
}),
});
} else {
warn!(
"Got move entity pos rot packet for unknown entity id {}",
p.entity_id
);
}
system_state.apply(ecs);
}
ClientboundGamePacket::MoveEntityRot(_p) => {
// debug!("Got move entity rot packet {:?}", p);
}
ClientboundGamePacket::KeepAlive(p) => {
debug!("Got keep alive packet {p:?} for {player_entity:?}");
let mut system_state: SystemState<Query<&mut LocalPlayer>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let mut local_player = query.get_mut(player_entity).unwrap();
local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get());
debug!("Sent keep alive packet {p:?} for {player_entity:?}");
}
ClientboundGamePacket::RemoveEntities(p) => {
debug!("Got remove entities packet {:?}", p);
}
ClientboundGamePacket::PlayerChat(p) => {
debug!("Got player chat packet {:?}", p);
let mut system_state: SystemState<EventWriter<ChatReceivedEvent>> =
SystemState::new(ecs);
let mut chat_events = system_state.get_mut(ecs);
chat_events.send(ChatReceivedEvent {
entity: player_entity,
packet: ChatPacket::Player(Arc::new(p.clone())),
});
}
ClientboundGamePacket::SystemChat(p) => {
debug!("Got system chat packet {:?}", p);
let mut system_state: SystemState<EventWriter<ChatReceivedEvent>> =
SystemState::new(ecs);
let mut chat_events = system_state.get_mut(ecs);
chat_events.send(ChatReceivedEvent {
entity: player_entity,
packet: ChatPacket::System(Arc::new(p.clone())),
});
}
ClientboundGamePacket::Sound(_p) => {
// debug!("Got sound packet {:?}", p);
}
ClientboundGamePacket::LevelEvent(p) => {
debug!("Got level event packet {:?}", p);
}
ClientboundGamePacket::BlockUpdate(p) => {
debug!("Got block update packet {:?}", p);
let mut system_state: SystemState<Query<&mut LocalPlayer>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let world = local_player.world.write();
world.chunks.set_block_state(&p.pos, p.block_state);
}
ClientboundGamePacket::Animate(p) => {
debug!("Got animate packet {:?}", p);
}
ClientboundGamePacket::SectionBlocksUpdate(p) => {
debug!("Got section blocks update packet {:?}", p);
let mut system_state: SystemState<Query<&mut LocalPlayer>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let world = local_player.world.write();
for state in &p.states {
world
.chunks
.set_block_state(&(p.section_pos + state.pos.clone()), state.state);
}
}
ClientboundGamePacket::GameEvent(p) => {
debug!("Got game event packet {:?}", p);
}
ClientboundGamePacket::LevelParticles(p) => {
debug!("Got level particles packet {:?}", p);
}
ClientboundGamePacket::ServerData(p) => {
debug!("Got server data packet {:?}", p);
}
ClientboundGamePacket::SetEquipment(p) => {
debug!("Got set equipment packet {:?}", p);
}
ClientboundGamePacket::UpdateMobEffect(p) => {
debug!("Got update mob effect packet {:?}", p);
}
ClientboundGamePacket::AddExperienceOrb(_) => {}
ClientboundGamePacket::AwardStats(_) => {}
ClientboundGamePacket::BlockChangedAck(_) => {}
ClientboundGamePacket::BlockDestruction(_) => {}
ClientboundGamePacket::BlockEntityData(_) => {}
ClientboundGamePacket::BlockEvent(_) => {}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
ClientboundGamePacket::ContainerSetData(_) => {}
ClientboundGamePacket::ContainerSetSlot(_) => {}
ClientboundGamePacket::Cooldown(_) => {}
ClientboundGamePacket::CustomChatCompletions(_) => {}
ClientboundGamePacket::DeleteChat(_) => {}
ClientboundGamePacket::Explode(_) => {}
ClientboundGamePacket::ForgetLevelChunk(_) => {}
ClientboundGamePacket::HorseScreenOpen(_) => {}
ClientboundGamePacket::MapItemData(_) => {}
ClientboundGamePacket::MerchantOffers(_) => {}
ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {}
ClientboundGamePacket::OpenScreen(_) => {}
ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(_) => {}
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
ClientboundGamePacket::PlayerCombatEnd(_) => {}
ClientboundGamePacket::PlayerCombatEnter(_) => {}
ClientboundGamePacket::PlayerCombatKill(p) => {
debug!("Got player kill packet {:?}", p);
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
Query<(&MinecraftEntityId, Option<&Dead>)>,
EventWriter<DeathEvent>,
)> = SystemState::new(ecs);
let (mut commands, mut query, mut death_events) = system_state.get_mut(ecs);
let (entity_id, dead) = query.get_mut(player_entity).unwrap();
if **entity_id == p.player_id && dead.is_none() {
commands.entity(player_entity).insert(Dead);
death_events.send(DeathEvent {
entity: player_entity,
packet: Some(p.clone()),
});
}
system_state.apply(ecs);
}
ClientboundGamePacket::PlayerLookAt(_) => {}
ClientboundGamePacket::RemoveMobEffect(_) => {}
ClientboundGamePacket::ResourcePack(_) => {}
ClientboundGamePacket::Respawn(p) => {
debug!("Got respawn packet {:?}", p);
let mut system_state: SystemState<Commands> = SystemState::new(ecs);
let mut commands = system_state.get(ecs);
// Remove the Dead marker component from the player.
commands.entity(player_entity).remove::<Dead>();
system_state.apply(ecs);
}
ClientboundGamePacket::SelectAdvancementsTab(_) => {}
ClientboundGamePacket::SetActionBarText(_) => {}
ClientboundGamePacket::SetBorderCenter(_) => {}
ClientboundGamePacket::SetBorderLerpSize(_) => {}
ClientboundGamePacket::SetBorderSize(_) => {}
ClientboundGamePacket::SetBorderWarningDelay(_) => {}
ClientboundGamePacket::SetBorderWarningDistance(_) => {}
ClientboundGamePacket::SetCamera(_) => {}
ClientboundGamePacket::SetDisplayObjective(_) => {}
ClientboundGamePacket::SetObjective(_) => {}
ClientboundGamePacket::SetPassengers(_) => {}
ClientboundGamePacket::SetPlayerTeam(_) => {}
ClientboundGamePacket::SetScore(_) => {}
ClientboundGamePacket::SetSimulationDistance(_) => {}
ClientboundGamePacket::SetSubtitleText(_) => {}
ClientboundGamePacket::SetTitleText(_) => {}
ClientboundGamePacket::SetTitlesAnimation(_) => {}
ClientboundGamePacket::SoundEntity(_) => {}
ClientboundGamePacket::StopSound(_) => {}
ClientboundGamePacket::TabList(_) => {}
ClientboundGamePacket::TagQuery(_) => {}
ClientboundGamePacket::TakeItemEntity(_) => {}
ClientboundGamePacket::DisguisedChat(_) => {}
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
ClientboundGamePacket::ContainerClose(_) => {}
}
}
}
}
impl PacketReceiver {
/// Loop that reads from the connection and adds the packets to the queue +
/// runs the schedule.
pub async fn read_task(self, mut read_conn: ReadConnection<ClientboundGamePacket>) {
while let Ok(packet) = read_conn.read().await {
self.packets.lock().push(packet);
// tell the client to run all the systems
self.run_schedule_sender.send(()).await.unwrap();
}
}
/// Consume the [`ServerboundGamePacket`] queue and actually write the
/// packets to the server. It's like this so writing packets doesn't need to
/// be awaited.
pub async fn write_task(
self,
mut write_conn: WriteConnection<ServerboundGamePacket>,
mut write_receiver: mpsc::UnboundedReceiver<ServerboundGamePacket>,
) {
while let Some(packet) = write_receiver.recv().await {
if let Err(err) = write_conn.write(packet).await {
error!("Disconnecting because we couldn't write a packet: {err}.");
break;
};
}
// receiver is automatically closed when it's dropped
}
}

View file

@ -1,13 +1,14 @@
use azalea_auth::game_profile::GameProfile; use azalea_auth::game_profile::GameProfile;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_core::GameType; use azalea_core::GameType;
use azalea_world::PartialWorld; use azalea_ecs::{
event::EventReader,
system::{Commands, Res},
};
use azalea_world::EntityInfos;
use uuid::Uuid; use uuid::Uuid;
/// Something that has a world associated to it. this is usually a `Client`. use crate::{packet_handling::AddPlayerEvent, GameProfileComponent};
pub trait WorldHaver {
fn world(&self) -> &PartialWorld;
}
/// A player in the tab list. /// A player in the tab list.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -18,5 +19,22 @@ pub struct PlayerInfo {
pub gamemode: GameType, pub gamemode: GameType,
pub latency: i32, pub latency: i32,
/// The player's display name in the tab list. /// The player's display name in the tab list.
pub display_name: Option<Component>, pub display_name: Option<FormattedText>,
}
/// Add a [`GameProfileComponent`] when an [`AddPlayerEvent`] is received.
/// Usually the `GameProfileComponent` will be added from the
/// `ClientboundGamePacket::AddPlayer` handler though.
pub fn retroactively_add_game_profile_component(
mut commands: Commands,
mut events: EventReader<AddPlayerEvent>,
entity_infos: Res<EntityInfos>,
) {
for event in events.iter() {
if let Some(entity) = entity_infos.get_entity_by_uuid(&event.info.uuid) {
commands
.entity(entity)
.insert(GameProfileComponent(event.info.profile.clone()));
}
}
} }

View file

@ -1,144 +0,0 @@
use crate::{Client, Event};
use async_trait::async_trait;
use nohash_hasher::NoHashHasher;
use std::{
any::{Any, TypeId},
collections::HashMap,
hash::BuildHasherDefault,
};
type U64Hasher = BuildHasherDefault<NoHashHasher<u64>>;
// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html
#[derive(Clone, Default)]
pub struct PluginStates {
map: Option<HashMap<TypeId, Box<dyn PluginState>, U64Hasher>>,
}
/// A map of PluginState TypeIds to AnyPlugin objects. This can then be built
/// into a [`PluginStates`] object to get a fresh new state based on this
/// plugin.
///
/// If you're using the azalea crate, you should generate this from the
/// `plugins!` macro.
#[derive(Clone, Default)]
pub struct Plugins {
map: Option<HashMap<TypeId, Box<dyn AnyPlugin>, U64Hasher>>,
}
impl PluginStates {
pub fn get<T: PluginState>(&self) -> Option<&T> {
self.map
.as_ref()
.and_then(|map| map.get(&TypeId::of::<T>()))
.and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>())
}
}
impl Plugins {
/// Create a new empty set of plugins.
pub fn new() -> Self {
Self::default()
}
/// Add a new plugin to this set.
pub fn add<T: Plugin + Clone>(&mut self, plugin: T) {
if self.map.is_none() {
self.map = Some(HashMap::with_hasher(BuildHasherDefault::default()));
}
self.map
.as_mut()
.unwrap()
.insert(TypeId::of::<T::State>(), Box::new(plugin));
}
/// Build our plugin states from this set of plugins. Note that if you're
/// using `azalea` you'll probably never need to use this as it's called
/// for you.
pub fn build(self) -> PluginStates {
let mut map = HashMap::with_hasher(BuildHasherDefault::default());
for (id, plugin) in self.map.unwrap().into_iter() {
map.insert(id, plugin.build());
}
PluginStates { map: Some(map) }
}
}
impl IntoIterator for PluginStates {
type Item = Box<dyn PluginState>;
type IntoIter = std::vec::IntoIter<Self::Item>;
/// Iterate over the plugin states.
fn into_iter(self) -> Self::IntoIter {
self.map
.map(|map| map.into_values().collect::<Vec<_>>())
.unwrap_or_default()
.into_iter()
}
}
/// A `PluginState` keeps the current state of a plugin for a client. All the
/// fields must be atomic. Unique `PluginState`s are built from [`Plugin`]s.
#[async_trait]
pub trait PluginState: Send + Sync + PluginStateClone + Any + 'static {
async fn handle(self: Box<Self>, event: Event, bot: Client);
}
/// Plugins can keep their own personal state, listen to [`Event`]s, and add
/// new functions to [`Client`].
pub trait Plugin: Send + Sync + Any + 'static {
type State: PluginState;
fn build(&self) -> Self::State;
}
/// AnyPlugin is basically a Plugin but without the State associated type
/// it has to exist so we can do a hashmap with Box<dyn AnyPlugin>
#[doc(hidden)]
pub trait AnyPlugin: Send + Sync + Any + AnyPluginClone + 'static {
fn build(&self) -> Box<dyn PluginState>;
}
impl<S: PluginState, B: Plugin<State = S> + Clone> AnyPlugin for B {
fn build(&self) -> Box<dyn PluginState> {
Box::new(self.build())
}
}
/// An internal trait that allows PluginState to be cloned.
#[doc(hidden)]
pub trait PluginStateClone {
fn clone_box(&self) -> Box<dyn PluginState>;
}
impl<T> PluginStateClone for T
where
T: 'static + PluginState + Clone,
{
fn clone_box(&self) -> Box<dyn PluginState> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn PluginState> {
fn clone(&self) -> Self {
self.clone_box()
}
}
/// An internal trait that allows AnyPlugin to be cloned.
#[doc(hidden)]
pub trait AnyPluginClone {
fn clone_box(&self) -> Box<dyn AnyPlugin>;
}
impl<T> AnyPluginClone for T
where
T: 'static + Plugin + Clone,
{
fn clone_box(&self) -> Box<dyn AnyPlugin> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn AnyPlugin> {
fn clone(&self) -> Self {
self.clone_box()
}
}

View file

@ -0,0 +1,177 @@
//! Borrowed from `bevy_core`.
use azalea_ecs::{
app::{App, Plugin},
schedule::IntoSystemDescriptor,
system::Resource,
};
use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder};
/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`,
/// `IoTaskPool`.
#[derive(Default)]
pub struct TaskPoolPlugin {
/// Options for the [`TaskPool`](bevy_tasks::TaskPool) created at
/// application start.
pub task_pool_options: TaskPoolOptions,
}
impl Plugin for TaskPoolPlugin {
fn build(&self, app: &mut App) {
// Setup the default bevy task pools
self.task_pool_options.create_default_pools();
#[cfg(not(target_arch = "wasm32"))]
app.add_system_to_stage(
azalea_ecs::app::CoreStage::Last,
bevy_tasks::tick_global_task_pools_on_main_thread.at_end(),
);
}
}
/// Helper for configuring and creating the default task pools. For end-users
/// who want full control, set up [`TaskPoolPlugin`](super::TaskPoolPlugin)
#[derive(Clone, Resource)]
pub struct TaskPoolOptions {
/// If the number of physical cores is less than min_total_threads, force
/// using min_total_threads
pub min_total_threads: usize,
/// If the number of physical cores is greater than max_total_threads, force
/// using max_total_threads
pub max_total_threads: usize,
/// Used to determine number of IO threads to allocate
pub io: TaskPoolThreadAssignmentPolicy,
/// Used to determine number of async compute threads to allocate
pub async_compute: TaskPoolThreadAssignmentPolicy,
/// Used to determine number of compute threads to allocate
pub compute: TaskPoolThreadAssignmentPolicy,
}
impl Default for TaskPoolOptions {
fn default() -> Self {
TaskPoolOptions {
// By default, use however many cores are available on the system
min_total_threads: 1,
max_total_threads: std::usize::MAX,
// Use 25% of cores for IO, at least 1, no more than 4
io: TaskPoolThreadAssignmentPolicy {
min_threads: 1,
max_threads: 4,
percent: 0.25,
},
// Use 25% of cores for async compute, at least 1, no more than 4
async_compute: TaskPoolThreadAssignmentPolicy {
min_threads: 1,
max_threads: 4,
percent: 0.25,
},
// Use all remaining cores for compute (at least 1)
compute: TaskPoolThreadAssignmentPolicy {
min_threads: 1,
max_threads: std::usize::MAX,
percent: 1.0, // This 1.0 here means "whatever is left over"
},
}
}
}
impl TaskPoolOptions {
// /// Create a configuration that forces using the given number of threads.
// pub fn with_num_threads(thread_count: usize) -> Self {
// TaskPoolOptions {
// min_total_threads: thread_count,
// max_total_threads: thread_count,
// ..Default::default()
// }
// }
/// Inserts the default thread pools into the given resource map based on
/// the configured values
pub fn create_default_pools(&self) {
let total_threads = bevy_tasks::available_parallelism()
.clamp(self.min_total_threads, self.max_total_threads);
let mut remaining_threads = total_threads;
{
// Determine the number of IO threads we will use
let io_threads = self
.io
.get_number_of_threads(remaining_threads, total_threads);
remaining_threads = remaining_threads.saturating_sub(io_threads);
IoTaskPool::init(|| {
TaskPoolBuilder::default()
.num_threads(io_threads)
.thread_name("IO Task Pool".to_string())
.build()
});
}
{
// Determine the number of async compute threads we will use
let async_compute_threads = self
.async_compute
.get_number_of_threads(remaining_threads, total_threads);
remaining_threads = remaining_threads.saturating_sub(async_compute_threads);
AsyncComputeTaskPool::init(|| {
TaskPoolBuilder::default()
.num_threads(async_compute_threads)
.thread_name("Async Compute Task Pool".to_string())
.build()
});
}
{
// Determine the number of compute threads we will use
// This is intentionally last so that an end user can specify 1.0 as the percent
let compute_threads = self
.compute
.get_number_of_threads(remaining_threads, total_threads);
ComputeTaskPool::init(|| {
TaskPoolBuilder::default()
.num_threads(compute_threads)
.thread_name("Compute Task Pool".to_string())
.build()
});
}
}
}
/// Defines a simple way to determine how many threads to use given the number
/// of remaining cores and number of total cores
#[derive(Clone)]
pub struct TaskPoolThreadAssignmentPolicy {
/// Force using at least this many threads
pub min_threads: usize,
/// Under no circumstance use more than this many threads for this pool
pub max_threads: usize,
/// Target using this percentage of total cores, clamped by min_threads and
/// max_threads. It is permitted to use 1.0 to try to use all remaining
/// threads
pub percent: f32,
}
impl TaskPoolThreadAssignmentPolicy {
/// Determine the number of threads to use for this task pool
fn get_number_of_threads(&self, remaining_threads: usize, total_threads: usize) -> usize {
assert!(self.percent >= 0.0);
let mut desired = (total_threads as f32 * self.percent).round() as usize;
// Limit ourselves to the number of cores available
desired = desired.min(remaining_threads);
// Clamp by min_threads, max_threads. (This may result in us using more threads
// than are available, this is intended. An example case where this
// might happen is a device with <= 2 threads.
desired.clamp(self.min_threads, self.max_threads)
}
}

View file

@ -3,8 +3,8 @@ description = "Miscellaneous things in Azalea."
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "azalea-core" name = "azalea-core"
version = "0.5.0"
repository = "https://github.com/mat-1/azalea/tree/main/azalea-core" repository = "https://github.com/mat-1/azalea/tree/main/azalea-core"
version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -12,4 +12,8 @@ repository = "https://github.com/mat-1/azalea/tree/main/azalea-core"
azalea-buf = {path = "../azalea-buf", version = "^0.5.0"} azalea-buf = {path = "../azalea-buf", version = "^0.5.0"}
azalea-chat = {path = "../azalea-chat", version = "^0.5.0"} azalea-chat = {path = "../azalea-chat", version = "^0.5.0"}
azalea-nbt = {path = "../azalea-nbt", version = "^0.5.0"} azalea-nbt = {path = "../azalea-nbt", version = "^0.5.0"}
bevy_ecs = {version = "0.9.1", default-features = false, optional = true}
uuid = "^1.1.2" uuid = "^1.1.2"
[features]
bevy_ecs = ["dep:bevy_ecs"]

View file

@ -39,6 +39,7 @@ impl PositionDeltaTrait for PositionDelta8 {
} }
impl Vec3 { impl Vec3 {
#[must_use]
pub fn with_delta(&self, delta: &dyn PositionDeltaTrait) -> Vec3 { pub fn with_delta(&self, delta: &dyn PositionDeltaTrait) -> Vec3 {
Vec3 { Vec3 {
x: self.x + delta.x(), x: self.x + delta.x(),

View file

@ -1,6 +1,7 @@
use crate::{BlockPos, Slot}; use crate::{BlockPos, Slot};
use azalea_buf::McBuf; use azalea_buf::McBuf;
#[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))]
#[derive(Debug, Clone, McBuf, Default)] #[derive(Debug, Clone, McBuf, Default)]
pub struct Particle { pub struct Particle {
#[var] #[var]

View file

@ -1,5 +1,5 @@
use crate::ResourceLocation; use crate::ResourceLocation;
use azalea_buf::{BufReadError, McBufReadable, McBufWritable}; use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
ops::{Add, AddAssign, Mul, Rem, Sub}, ops::{Add, AddAssign, Mul, Rem, Sub},
@ -109,7 +109,7 @@ macro_rules! vec3_impl {
}; };
} }
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq, McBuf)]
pub struct Vec3 { pub struct Vec3 {
pub x: f64, pub x: f64,
pub y: f64, pub y: f64,
@ -270,12 +270,22 @@ impl From<&Vec3> for BlockPos {
} }
} }
} }
impl From<Vec3> for BlockPos {
fn from(pos: Vec3) -> Self {
BlockPos::from(&pos)
}
}
impl From<&Vec3> for ChunkPos { impl From<&Vec3> for ChunkPos {
fn from(pos: &Vec3) -> Self { fn from(pos: &Vec3) -> Self {
ChunkPos::from(&BlockPos::from(pos)) ChunkPos::from(&BlockPos::from(pos))
} }
} }
impl From<Vec3> for ChunkPos {
fn from(pos: Vec3) -> Self {
ChunkPos::from(&pos)
}
}
const PACKED_X_LENGTH: u64 = 1 + 25; // minecraft does something a bit more complicated to get this 25 const PACKED_X_LENGTH: u64 = 1 + 25; // minecraft does something a bit more complicated to get this 25
const PACKED_Z_LENGTH: u64 = PACKED_X_LENGTH; const PACKED_Z_LENGTH: u64 = PACKED_X_LENGTH;

View file

@ -3,6 +3,9 @@
use azalea_buf::{BufReadError, McBufReadable, McBufWritable}; use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
// TODO: make a `resourcelocation!("minecraft:overwolrd")` macro that checks if
// it's correct at compile-time.
#[derive(Hash, Clone, PartialEq, Eq)] #[derive(Hash, Clone, PartialEq, Eq)]
pub struct ResourceLocation { pub struct ResourceLocation {
pub namespace: String, pub namespace: String,

13
azalea-ecs/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
edition = "2021"
name = "azalea-ecs"
version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-ecs-macros = {path = "./azalea-ecs-macros", version = "^0.5.0"}
bevy_app = "0.9.1"
bevy_ecs = {version = "0.9.1", default-features = false}
iyes_loopless = "0.9.1"
tokio = {version = "1.25.0", features = ["time"]}

View file

@ -0,0 +1,15 @@
[package]
description = "Azalea ECS Macros"
edition = "2021"
license = "MIT OR Apache-2.0"
name = "azalea-ecs-macros"
version = "0.5.0"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "1.0"
toml = "0.7.0"

View file

@ -0,0 +1,125 @@
use crate::utils::{get_lit_str, Symbol};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::{parse_macro_input, parse_quote, DeriveInput, Error, Ident, Path, Result};
use crate::utils;
pub fn derive_resource(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
let azalea_ecs_path: Path = crate::azalea_ecs_path();
ast.generics
.make_where_clause()
.predicates
.push(parse_quote! { Self: Send + Sync + 'static });
let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics #azalea_ecs_path::system::BevyResource for #struct_name #type_generics #where_clause {
}
})
}
pub fn derive_component(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
let azalea_ecs_path: Path = crate::azalea_ecs_path();
let attrs = match parse_component_attr(&ast) {
Ok(attrs) => attrs,
Err(e) => return e.into_compile_error().into(),
};
let storage = storage_path(&azalea_ecs_path, attrs.storage);
ast.generics
.make_where_clause()
.predicates
.push(parse_quote! { Self: Send + Sync + 'static });
let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics #azalea_ecs_path::component::BevyComponent for #struct_name #type_generics #where_clause {
type Storage = #storage;
}
})
}
pub const COMPONENT: Symbol = Symbol("component");
pub const STORAGE: Symbol = Symbol("storage");
struct Attrs {
storage: StorageTy,
}
#[derive(Clone, Copy)]
enum StorageTy {
Table,
SparseSet,
}
// values for `storage` attribute
const TABLE: &str = "Table";
const SPARSE_SET: &str = "SparseSet";
fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
let meta_items = utils::parse_attrs(ast, COMPONENT)?;
let mut attrs = Attrs {
storage: StorageTy::Table,
};
for meta in meta_items {
use syn::{
Meta::NameValue,
NestedMeta::{Lit, Meta},
};
match meta {
Meta(NameValue(m)) if m.path == STORAGE => {
attrs.storage = match get_lit_str(STORAGE, &m.lit)?.value().as_str() {
TABLE => StorageTy::Table,
SPARSE_SET => StorageTy::SparseSet,
s => {
return Err(Error::new_spanned(
m.lit,
format!(
"Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'."
),
))
}
};
}
Meta(meta_item) => {
return Err(Error::new_spanned(
meta_item.path(),
format!(
"unknown component attribute `{}`",
meta_item.path().into_token_stream()
),
));
}
Lit(lit) => {
return Err(Error::new_spanned(
lit,
"unexpected literal in component attribute",
))
}
}
}
Ok(attrs)
}
fn storage_path(azalea_ecs_path: &Path, ty: StorageTy) -> TokenStream2 {
let typename = match ty {
StorageTy::Table => Ident::new("TableStorage", Span::call_site()),
StorageTy::SparseSet => Ident::new("SparseStorage", Span::call_site()),
};
quote! { #azalea_ecs_path::component::#typename }
}

View file

@ -0,0 +1,466 @@
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
Attribute, Data, DataStruct, DeriveInput, Field, Fields,
};
use crate::azalea_ecs_path;
#[derive(Default)]
struct FetchStructAttributes {
pub is_mutable: bool,
pub derive_args: Punctuated<syn::NestedMeta, syn::token::Comma>,
}
static MUTABLE_ATTRIBUTE_NAME: &str = "mutable";
static DERIVE_ATTRIBUTE_NAME: &str = "derive";
mod field_attr_keywords {
syn::custom_keyword!(ignore);
}
pub static WORLD_QUERY_ATTRIBUTE_NAME: &str = "world_query";
pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream {
let visibility = ast.vis;
let mut fetch_struct_attributes = FetchStructAttributes::default();
for attr in &ast.attrs {
if !attr
.path
.get_ident()
.map_or(false, |ident| ident == WORLD_QUERY_ATTRIBUTE_NAME)
{
continue;
}
attr.parse_args_with(|input: ParseStream| {
let meta = input.parse_terminated::<syn::Meta, syn::token::Comma>(syn::Meta::parse)?;
for meta in meta {
let ident = meta.path().get_ident().unwrap_or_else(|| {
panic!(
"Unrecognized attribute: `{}`",
meta.path().to_token_stream()
)
});
if ident == MUTABLE_ATTRIBUTE_NAME {
if let syn::Meta::Path(_) = meta {
fetch_struct_attributes.is_mutable = true;
} else {
panic!(
"The `{MUTABLE_ATTRIBUTE_NAME}` attribute is expected to have no value or arguments"
);
}
} else if ident == DERIVE_ATTRIBUTE_NAME {
if let syn::Meta::List(meta_list) = meta {
fetch_struct_attributes
.derive_args
.extend(meta_list.nested.iter().cloned());
} else {
panic!(
"Expected a structured list within the `{DERIVE_ATTRIBUTE_NAME}` attribute"
);
}
} else {
panic!(
"Unrecognized attribute: `{}`",
meta.path().to_token_stream()
);
}
}
Ok(())
})
.unwrap_or_else(|_| panic!("Invalid `{WORLD_QUERY_ATTRIBUTE_NAME}` attribute format"));
}
let path = azalea_ecs_path();
let user_generics = ast.generics.clone();
let (user_impl_generics, user_ty_generics, user_where_clauses) = user_generics.split_for_impl();
let user_generics_with_world = {
let mut generics = ast.generics.clone();
generics.params.insert(0, parse_quote!('__w));
generics
};
let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) =
user_generics_with_world.split_for_impl();
let struct_name = ast.ident.clone();
let read_only_struct_name = if fetch_struct_attributes.is_mutable {
Ident::new(&format!("{struct_name}ReadOnly"), Span::call_site())
} else {
struct_name.clone()
};
let item_struct_name = Ident::new(&format!("{struct_name}Item"), Span::call_site());
let read_only_item_struct_name = if fetch_struct_attributes.is_mutable {
Ident::new(&format!("{struct_name}ReadOnlyItem"), Span::call_site())
} else {
item_struct_name.clone()
};
let fetch_struct_name = Ident::new(&format!("{struct_name}Fetch"), Span::call_site());
let read_only_fetch_struct_name = if fetch_struct_attributes.is_mutable {
Ident::new(&format!("{struct_name}ReadOnlyFetch"), Span::call_site())
} else {
fetch_struct_name.clone()
};
let state_struct_name = Ident::new(&format!("{struct_name}State"), Span::call_site());
let fields = match &ast.data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => &fields.named,
_ => panic!("Expected a struct with named fields"),
};
let mut ignored_field_attrs = Vec::new();
let mut ignored_field_visibilities = Vec::new();
let mut ignored_field_idents = Vec::new();
let mut ignored_field_types = Vec::new();
let mut field_attrs = Vec::new();
let mut field_visibilities = Vec::new();
let mut field_idents = Vec::new();
let mut field_types = Vec::new();
let mut read_only_field_types = Vec::new();
for field in fields {
let WorldQueryFieldInfo { is_ignored, attrs } = read_world_query_field_info(field);
let field_ident = field.ident.as_ref().unwrap().clone();
if is_ignored {
ignored_field_attrs.push(attrs);
ignored_field_visibilities.push(field.vis.clone());
ignored_field_idents.push(field_ident.clone());
ignored_field_types.push(field.ty.clone());
} else {
field_attrs.push(attrs);
field_visibilities.push(field.vis.clone());
field_idents.push(field_ident.clone());
let field_ty = field.ty.clone();
field_types.push(quote!(#field_ty));
read_only_field_types.push(quote!(<#field_ty as #path::query::WorldQuery>::ReadOnly));
}
}
let derive_args = &fetch_struct_attributes.derive_args;
// `#[derive()]` is valid syntax
let derive_macro_call = quote! { #[derive(#derive_args)] };
let impl_fetch = |is_readonly: bool| {
let struct_name = if is_readonly {
&read_only_struct_name
} else {
&struct_name
};
let item_struct_name = if is_readonly {
&read_only_item_struct_name
} else {
&item_struct_name
};
let fetch_struct_name = if is_readonly {
&read_only_fetch_struct_name
} else {
&fetch_struct_name
};
let field_types = if is_readonly {
&read_only_field_types
} else {
&field_types
};
quote! {
#derive_macro_call
#[doc = "Automatically generated [`WorldQuery`] item type for [`"]
#[doc = stringify!(#struct_name)]
#[doc = "`], returned when iterating over query results."]
#[automatically_derived]
#visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world {
#(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::WorldQuery>::Item<'__w>,)*
#(#(#ignored_field_attrs)* #ignored_field_visibilities #ignored_field_idents: #ignored_field_types,)*
}
#[doc(hidden)]
#[doc = "Automatically generated internal [`WorldQuery`] fetch type for [`"]
#[doc = stringify!(#struct_name)]
#[doc = "`], used to define the world data accessed by this query."]
#[automatically_derived]
#visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world {
#(#field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)*
#(#ignored_field_idents: #ignored_field_types,)*
}
// SAFETY: `update_component_access` and `update_archetype_component_access` are called on every field
unsafe impl #user_impl_generics #path::query::WorldQuery
for #struct_name #user_ty_generics #user_where_clauses {
type Item<'__w> = #item_struct_name #user_ty_generics_with_world;
type Fetch<'__w> = #fetch_struct_name #user_ty_generics_with_world;
type ReadOnly = #read_only_struct_name #user_ty_generics;
type State = #state_struct_name #user_ty_generics;
fn shrink<'__wlong: '__wshort, '__wshort>(
item: <#struct_name #user_ty_generics as #path::query::WorldQuery>::Item<'__wlong>
) -> <#struct_name #user_ty_generics as #path::query::WorldQuery>::Item<'__wshort> {
#item_struct_name {
#(
#field_idents: <#field_types>::shrink(item.#field_idents),
)*
#(
#ignored_field_idents: item.#ignored_field_idents,
)*
}
}
unsafe fn init_fetch<'__w>(
_world: &'__w #path::world::World,
state: &Self::State,
_last_change_tick: u32,
_change_tick: u32
) -> <Self as #path::query::WorldQuery>::Fetch<'__w> {
#fetch_struct_name {
#(#field_idents:
<#field_types>::init_fetch(
_world,
&state.#field_idents,
_last_change_tick,
_change_tick
),
)*
#(#ignored_field_idents: Default::default(),)*
}
}
unsafe fn clone_fetch<'__w>(
_fetch: &<Self as #path::query::WorldQuery>::Fetch<'__w>
) -> <Self as #path::query::WorldQuery>::Fetch<'__w> {
#fetch_struct_name {
#(
#field_idents: <#field_types>::clone_fetch(& _fetch. #field_idents),
)*
#(
#ignored_field_idents: Default::default(),
)*
}
}
const IS_DENSE: bool = true #(&& <#field_types>::IS_DENSE)*;
const IS_ARCHETYPAL: bool = true #(&& <#field_types>::IS_ARCHETYPAL)*;
/// SAFETY: we call `set_archetype` for each member that implements `Fetch`
#[inline]
unsafe fn set_archetype<'__w>(
_fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
_state: &Self::State,
_archetype: &'__w #path::archetype::Archetype,
_table: &'__w #path::storage::Table
) {
#(<#field_types>::set_archetype(&mut _fetch.#field_idents, &_state.#field_idents, _archetype, _table);)*
}
/// SAFETY: we call `set_table` for each member that implements `Fetch`
#[inline]
unsafe fn set_table<'__w>(
_fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
_state: &Self::State,
_table: &'__w #path::storage::Table
) {
#(<#field_types>::set_table(&mut _fetch.#field_idents, &_state.#field_idents, _table);)*
}
/// SAFETY: we call `fetch` for each member that implements `Fetch`.
#[inline(always)]
unsafe fn fetch<'__w>(
_fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
_entity: Entity,
_table_row: usize
) -> <Self as #path::query::WorldQuery>::Item<'__w> {
Self::Item {
#(#field_idents: <#field_types>::fetch(&mut _fetch.#field_idents, _entity, _table_row),)*
#(#ignored_field_idents: Default::default(),)*
}
}
#[allow(unused_variables)]
#[inline(always)]
unsafe fn filter_fetch<'__w>(
_fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
_entity: Entity,
_table_row: usize
) -> bool {
true #(&& <#field_types>::filter_fetch(&mut _fetch.#field_idents, _entity, _table_row))*
}
fn update_component_access(state: &Self::State, _access: &mut #path::query::FilteredAccess<#path::component::ComponentId>) {
#( <#field_types>::update_component_access(&state.#field_idents, _access); )*
}
fn update_archetype_component_access(
state: &Self::State,
_archetype: &#path::archetype::Archetype,
_access: &mut #path::query::Access<#path::archetype::ArchetypeComponentId>
) {
#(
<#field_types>::update_archetype_component_access(&state.#field_idents, _archetype, _access);
)*
}
fn init_state(world: &mut #path::world::World) -> #state_struct_name #user_ty_generics {
#state_struct_name {
#(#field_idents: <#field_types>::init_state(world),)*
#(#ignored_field_idents: Default::default(),)*
}
}
fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(#path::component::ComponentId) -> bool) -> bool {
true #(&& <#field_types>::matches_component_set(&state.#field_idents, _set_contains_id))*
}
}
}
};
let mutable_impl = impl_fetch(false);
let readonly_impl = if fetch_struct_attributes.is_mutable {
let world_query_impl = impl_fetch(true);
quote! {
#[doc(hidden)]
#[doc = "Automatically generated internal [`WorldQuery`] type for [`"]
#[doc = stringify!(#struct_name)]
#[doc = "`], used for read-only access."]
#[automatically_derived]
#visibility struct #read_only_struct_name #user_impl_generics #user_where_clauses {
#( #field_idents: #read_only_field_types, )*
#(#(#ignored_field_attrs)* #ignored_field_visibilities #ignored_field_idents: #ignored_field_types,)*
}
#world_query_impl
}
} else {
quote! {}
};
let read_only_asserts = if fetch_struct_attributes.is_mutable {
quote! {
// Double-check that the data fetched by `<_ as WorldQuery>::ReadOnly` is read-only.
// This is technically unnecessary as `<_ as WorldQuery>::ReadOnly: ReadOnlyWorldQuery`
// but to protect against future mistakes we assert the assoc type implements `ReadOnlyWorldQuery` anyway
#( assert_readonly::<#read_only_field_types>(); )*
}
} else {
quote! {
// Statically checks that the safety guarantee of `ReadOnlyWorldQuery` for `$fetch_struct_name` actually holds true.
// We need this to make sure that we don't compile `ReadOnlyWorldQuery` if our struct contains nested `WorldQuery`
// members that don't implement it. I.e.:
// ```
// #[derive(WorldQuery)]
// pub struct Foo { a: &'static mut MyComponent }
// ```
#( assert_readonly::<#field_types>(); )*
}
};
TokenStream::from(quote! {
#mutable_impl
#readonly_impl
#[doc(hidden)]
#[doc = "Automatically generated internal [`WorldQuery`] state type for [`"]
#[doc = stringify!(#struct_name)]
#[doc = "`], used for caching."]
#[automatically_derived]
#visibility struct #state_struct_name #user_impl_generics #user_where_clauses {
#(#field_idents: <#field_types as #path::query::WorldQuery>::State,)*
#(#ignored_field_idents: #ignored_field_types,)*
}
/// SAFETY: we assert fields are readonly below
unsafe impl #user_impl_generics #path::query::ReadOnlyWorldQuery
for #read_only_struct_name #user_ty_generics #user_where_clauses {}
#[allow(dead_code)]
const _: () = {
fn assert_readonly<T>()
where
T: #path::query::ReadOnlyWorldQuery,
{
}
// We generate a readonly assertion for every struct member.
fn assert_all #user_impl_generics_with_world () #user_where_clauses_with_world {
#read_only_asserts
}
};
// The original struct will most likely be left unused. As we don't want our users having
// to specify `#[allow(dead_code)]` for their custom queries, we are using this cursed
// workaround.
#[allow(dead_code)]
const _: () = {
fn dead_code_workaround #user_impl_generics (
q: #struct_name #user_ty_generics,
q2: #read_only_struct_name #user_ty_generics
) #user_where_clauses {
#(q.#field_idents;)*
#(q.#ignored_field_idents;)*
#(q2.#field_idents;)*
#(q2.#ignored_field_idents;)*
}
};
})
}
struct WorldQueryFieldInfo {
/// Has `#[fetch(ignore)]` or `#[filter_fetch(ignore)]` attribute.
is_ignored: bool,
/// All field attributes except for `world_query` ones.
attrs: Vec<Attribute>,
}
fn read_world_query_field_info(field: &Field) -> WorldQueryFieldInfo {
let is_ignored = field
.attrs
.iter()
.find(|attr| {
attr.path
.get_ident()
.map_or(false, |ident| ident == WORLD_QUERY_ATTRIBUTE_NAME)
})
.map_or(false, |attr| {
let mut is_ignored = false;
attr.parse_args_with(|input: ParseStream| {
if input
.parse::<Option<field_attr_keywords::ignore>>()?
.is_some()
{
is_ignored = true;
}
Ok(())
})
.unwrap_or_else(|_| panic!("Invalid `{WORLD_QUERY_ATTRIBUTE_NAME}` attribute format"));
is_ignored
});
let attrs = field
.attrs
.iter()
.filter(|attr| {
attr.path
.get_ident()
.map_or(true, |ident| ident != WORLD_QUERY_ATTRIBUTE_NAME)
})
.cloned()
.collect();
WorldQueryFieldInfo { is_ignored, attrs }
}

View file

@ -0,0 +1,523 @@
//! A fork of bevy_ecs_macros that uses azalea_ecs instead of bevy_ecs.
extern crate proc_macro;
mod component;
mod fetch;
pub(crate) mod utils;
use crate::fetch::derive_world_query_impl;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
spanned::Spanned,
token::Comma,
DeriveInput, Field, GenericParam, Ident, Index, LitInt, Meta, MetaList, NestedMeta, Result,
Token, TypeParam,
};
use utils::{derive_label, get_named_struct_fields, BevyManifest};
struct AllTuples {
macro_ident: Ident,
start: usize,
end: usize,
idents: Vec<Ident>,
}
impl Parse for AllTuples {
fn parse(input: ParseStream) -> Result<Self> {
let macro_ident = input.parse::<Ident>()?;
input.parse::<Comma>()?;
let start = input.parse::<LitInt>()?.base10_parse()?;
input.parse::<Comma>()?;
let end = input.parse::<LitInt>()?.base10_parse()?;
input.parse::<Comma>()?;
let mut idents = vec![input.parse::<Ident>()?];
while input.parse::<Comma>().is_ok() {
idents.push(input.parse::<Ident>()?);
}
Ok(AllTuples {
macro_ident,
start,
end,
idents,
})
}
}
#[proc_macro]
pub fn all_tuples(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as AllTuples);
let len = input.end - input.start;
let mut ident_tuples = Vec::with_capacity(len);
for i in input.start..=input.end {
let idents = input
.idents
.iter()
.map(|ident| format_ident!("{}{}", ident, i));
if input.idents.len() < 2 {
ident_tuples.push(quote! {
#(#idents)*
});
} else {
ident_tuples.push(quote! {
(#(#idents),*)
});
}
}
let macro_ident = &input.macro_ident;
let invocations = (input.start..=input.end).map(|i| {
let ident_tuples = &ident_tuples[..i];
quote! {
#macro_ident!(#(#ident_tuples),*);
}
});
TokenStream::from(quote! {
#(
#invocations
)*
})
}
enum BundleFieldKind {
Component,
Ignore,
}
const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
#[proc_macro_derive(Bundle, attributes(bundle))]
pub fn derive_bundle(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ecs_path = azalea_ecs_path();
let named_fields = match get_named_struct_fields(&ast.data) {
Ok(fields) => &fields.named,
Err(e) => return e.into_compile_error().into(),
};
let mut field_kind = Vec::with_capacity(named_fields.len());
'field_loop: for field in named_fields.iter() {
for attr in &field.attrs {
if attr.path.is_ident(BUNDLE_ATTRIBUTE_NAME) {
if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
if let Some(&NestedMeta::Meta(Meta::Path(ref path))) = nested.first() {
if path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) {
field_kind.push(BundleFieldKind::Ignore);
continue 'field_loop;
}
return syn::Error::new(
path.span(),
format!(
"Invalid bundle attribute. Use `{BUNDLE_ATTRIBUTE_IGNORE_NAME}`"
),
)
.into_compile_error()
.into();
}
return syn::Error::new(attr.span(), format!("Invalid bundle attribute. Use `#[{BUNDLE_ATTRIBUTE_NAME}({BUNDLE_ATTRIBUTE_IGNORE_NAME})]`")).into_compile_error().into();
}
}
}
field_kind.push(BundleFieldKind::Component);
}
let field = named_fields
.iter()
.map(|field| field.ident.as_ref().unwrap())
.collect::<Vec<_>>();
let field_type = named_fields
.iter()
.map(|field| &field.ty)
.collect::<Vec<_>>();
let mut field_component_ids = Vec::new();
let mut field_get_components = Vec::new();
let mut field_from_components = Vec::new();
for ((field_type, field_kind), field) in
field_type.iter().zip(field_kind.iter()).zip(field.iter())
{
match field_kind {
BundleFieldKind::Component => {
field_component_ids.push(quote! {
<#field_type as #ecs_path::bundle::BevyBundle>::component_ids(components, storages, &mut *ids);
});
field_get_components.push(quote! {
self.#field.get_components(&mut *func);
});
field_from_components.push(quote! {
#field: <#field_type as #ecs_path::bundle::BevyBundle>::from_components(ctx, &mut *func),
});
}
BundleFieldKind::Ignore => {
field_from_components.push(quote! {
#field: ::std::default::Default::default(),
});
}
}
}
let generics = ast.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let struct_name = &ast.ident;
TokenStream::from(quote! {
/// SAFETY: ComponentId is returned in field-definition-order. [from_components] and [get_components] use field-definition-order
unsafe impl #impl_generics #ecs_path::bundle::BevyBundle for #struct_name #ty_generics #where_clause {
fn component_ids(
components: &mut #ecs_path::component::Components,
storages: &mut #ecs_path::storage::Storages,
ids: &mut impl FnMut(#ecs_path::component::ComponentId)
){
#(#field_component_ids)*
}
#[allow(unused_variables, non_snake_case)]
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
where
__F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_>
{
Self {
#(#field_from_components)*
}
}
#[allow(unused_variables)]
fn get_components(self, func: &mut impl FnMut(#ecs_path::ptr::OwningPtr<'_>)) {
#(#field_get_components)*
}
}
})
}
fn get_idents(fmt_string: fn(usize) -> String, count: usize) -> Vec<Ident> {
(0..count)
.map(|i| Ident::new(&fmt_string(i), Span::call_site()))
.collect::<Vec<Ident>>()
}
#[proc_macro]
pub fn impl_param_set(_input: TokenStream) -> TokenStream {
let mut tokens = TokenStream::new();
let max_params = 8;
let params = get_idents(|i| format!("P{i}"), max_params);
let params_fetch = get_idents(|i| format!("PF{i}"), max_params);
let metas = get_idents(|i| format!("m{i}"), max_params);
let mut param_fn_muts = Vec::new();
for (i, param) in params.iter().enumerate() {
let fn_name = Ident::new(&format!("p{i}"), Span::call_site());
let index = Index::from(i);
param_fn_muts.push(quote! {
pub fn #fn_name<'a>(&'a mut self) -> <#param::Fetch as SystemParamFetch<'a, 'a>>::Item {
// SAFETY: systems run without conflicts with other systems.
// Conflicting params in ParamSet are not accessible at the same time
// ParamSets are guaranteed to not conflict with other SystemParams
unsafe {
<#param::Fetch as SystemParamFetch<'a, 'a>>::get_param(&mut self.param_states.#index, &self.system_meta, self.world, self.change_tick)
}
}
});
}
for param_count in 1..=max_params {
let param = &params[0..param_count];
let param_fetch = &params_fetch[0..param_count];
let meta = &metas[0..param_count];
let param_fn_mut = &param_fn_muts[0..param_count];
tokens.extend(TokenStream::from(quote! {
impl<'w, 's, #(#param: SystemParam,)*> SystemParam for ParamSet<'w, 's, (#(#param,)*)>
{
type Fetch = ParamSetState<(#(#param::Fetch,)*)>;
}
// SAFETY: All parameters are constrained to ReadOnlyFetch, so World is only read
unsafe impl<#(#param_fetch: for<'w1, 's1> SystemParamFetch<'w1, 's1>,)*> ReadOnlySystemParamFetch for ParamSetState<(#(#param_fetch,)*)>
where #(#param_fetch: ReadOnlySystemParamFetch,)*
{ }
// SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts
// with any prior access, a panic will occur.
unsafe impl<#(#param_fetch: for<'w1, 's1> SystemParamFetch<'w1, 's1>,)*> SystemParamState for ParamSetState<(#(#param_fetch,)*)>
{
fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self {
#(
// Pretend to add each param to the system alone, see if it conflicts
let mut #meta = system_meta.clone();
#meta.component_access_set.clear();
#meta.archetype_component_access.clear();
#param_fetch::init(world, &mut #meta);
let #param = #param_fetch::init(world, &mut system_meta.clone());
)*
#(
system_meta
.component_access_set
.extend(#meta.component_access_set);
system_meta
.archetype_component_access
.extend(&#meta.archetype_component_access);
)*
ParamSetState((#(#param,)*))
}
fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) {
let (#(#param,)*) = &mut self.0;
#(
#param.new_archetype(archetype, system_meta);
)*
}
fn apply(&mut self, world: &mut World) {
self.0.apply(world)
}
}
impl<'w, 's, #(#param_fetch: for<'w1, 's1> SystemParamFetch<'w1, 's1>,)*> SystemParamFetch<'w, 's> for ParamSetState<(#(#param_fetch,)*)>
{
type Item = ParamSet<'w, 's, (#(<#param_fetch as SystemParamFetch<'w, 's>>::Item,)*)>;
#[inline]
unsafe fn get_param(
state: &'s mut Self,
system_meta: &SystemMeta,
world: &'w World,
change_tick: u32,
) -> Self::Item {
ParamSet {
param_states: &mut state.0,
system_meta: system_meta.clone(),
world,
change_tick,
}
}
}
impl<'w, 's, #(#param: SystemParam,)*> ParamSet<'w, 's, (#(#param,)*)>
{
#(#param_fn_mut)*
}
}));
}
tokens
}
#[derive(Default)]
struct SystemParamFieldAttributes {
pub ignore: bool,
}
static SYSTEM_PARAM_ATTRIBUTE_NAME: &str = "system_param";
/// Implement `SystemParam` to use a struct as a parameter in a system
#[proc_macro_derive(SystemParam, attributes(system_param))]
pub fn derive_system_param(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let fields = match get_named_struct_fields(&ast.data) {
Ok(fields) => &fields.named,
Err(e) => return e.into_compile_error().into(),
};
let path = azalea_ecs_path();
let field_attributes = fields
.iter()
.map(|field| {
(
field,
field
.attrs
.iter()
.find(|a| *a.path.get_ident().as_ref().unwrap() == SYSTEM_PARAM_ATTRIBUTE_NAME)
.map_or_else(SystemParamFieldAttributes::default, |a| {
syn::custom_keyword!(ignore);
let mut attributes = SystemParamFieldAttributes::default();
a.parse_args_with(|input: ParseStream| {
if input.parse::<Option<ignore>>()?.is_some() {
attributes.ignore = true;
}
Ok(())
})
.expect("Invalid 'system_param' attribute format.");
attributes
}),
)
})
.collect::<Vec<(&Field, SystemParamFieldAttributes)>>();
let mut fields = Vec::new();
let mut field_indices = Vec::new();
let mut field_types = Vec::new();
let mut ignored_fields = Vec::new();
let mut ignored_field_types = Vec::new();
for (i, (field, attrs)) in field_attributes.iter().enumerate() {
if attrs.ignore {
ignored_fields.push(field.ident.as_ref().unwrap());
ignored_field_types.push(&field.ty);
} else {
fields.push(field.ident.as_ref().unwrap());
field_types.push(&field.ty);
field_indices.push(Index::from(i));
}
}
let generics = ast.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let lifetimeless_generics: Vec<_> = generics
.params
.iter()
.filter(|g| matches!(g, GenericParam::Type(_)))
.collect();
let mut punctuated_generics = Punctuated::<_, Token![,]>::new();
punctuated_generics.extend(lifetimeless_generics.iter().map(|g| match g {
GenericParam::Type(g) => GenericParam::Type(TypeParam {
default: None,
..g.clone()
}),
_ => unreachable!(),
}));
let mut punctuated_generic_idents = Punctuated::<_, Token![,]>::new();
punctuated_generic_idents.extend(lifetimeless_generics.iter().map(|g| match g {
GenericParam::Type(g) => &g.ident,
_ => unreachable!(),
}));
let struct_name = &ast.ident;
let fetch_struct_visibility = &ast.vis;
TokenStream::from(quote! {
// We define the FetchState struct in an anonymous scope to avoid polluting the user namespace.
// The struct can still be accessed via SystemParam::Fetch, e.g. EventReaderState can be accessed via
// <EventReader<'static, 'static, T> as SystemParam>::Fetch
const _: () = {
impl #impl_generics #path::system::SystemParam for #struct_name #ty_generics #where_clause {
type Fetch = FetchState <(#(<#field_types as #path::system::SystemParam>::Fetch,)*), #punctuated_generic_idents>;
}
#[doc(hidden)]
#fetch_struct_visibility struct FetchState <TSystemParamState, #punctuated_generic_idents> {
state: TSystemParamState,
marker: std::marker::PhantomData<fn()->(#punctuated_generic_idents)>
}
unsafe impl<TSystemParamState: #path::system::SystemParamState, #punctuated_generics> #path::system::SystemParamState for FetchState <TSystemParamState, #punctuated_generic_idents> #where_clause {
fn init(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self {
Self {
state: TSystemParamState::init(world, system_meta),
marker: std::marker::PhantomData,
}
}
fn new_archetype(&mut self, archetype: &#path::archetype::Archetype, system_meta: &mut #path::system::SystemMeta) {
self.state.new_archetype(archetype, system_meta)
}
fn apply(&mut self, world: &mut #path::world::World) {
self.state.apply(world)
}
}
impl #impl_generics #path::system::SystemParamFetch<'w, 's> for FetchState <(#(<#field_types as #path::system::SystemParam>::Fetch,)*), #punctuated_generic_idents> #where_clause {
type Item = #struct_name #ty_generics;
unsafe fn get_param(
state: &'s mut Self,
system_meta: &#path::system::SystemMeta,
world: &'w #path::world::World,
change_tick: u32,
) -> Self::Item {
#struct_name {
#(#fields: <<#field_types as #path::system::SystemParam>::Fetch as #path::system::SystemParamFetch>::get_param(&mut state.state.#field_indices, system_meta, world, change_tick),)*
#(#ignored_fields: <#ignored_field_types>::default(),)*
}
}
}
// Safety: The `ParamState` is `ReadOnlySystemParamFetch`, so this can only read from the `World`
unsafe impl<TSystemParamState: #path::system::SystemParamState + #path::system::ReadOnlySystemParamFetch, #punctuated_generics> #path::system::ReadOnlySystemParamFetch for FetchState <TSystemParamState, #punctuated_generic_idents> #where_clause {}
};
})
}
/// Implement `WorldQuery` to use a struct as a parameter in a query
#[proc_macro_derive(WorldQuery, attributes(world_query))]
pub fn derive_world_query(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
derive_world_query_impl(ast)
}
/// Generates an impl of the `SystemLabel` trait.
///
/// This works only for unit structs, or enums with only unit variants.
/// You may force a struct or variant to behave as if it were fieldless with
/// `#[system_label(ignore_fields)]`.
#[proc_macro_derive(SystemLabel, attributes(system_label))]
pub fn derive_system_label(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let mut trait_path = azalea_ecs_path();
trait_path.segments.push(format_ident!("schedule").into());
trait_path
.segments
.push(format_ident!("SystemLabel").into());
derive_label(input, &trait_path, "system_label")
}
/// Generates an impl of the `StageLabel` trait.
///
/// This works only for unit structs, or enums with only unit variants.
/// You may force a struct or variant to behave as if it were fieldless with
/// `#[stage_label(ignore_fields)]`.
#[proc_macro_derive(StageLabel, attributes(stage_label))]
pub fn derive_stage_label(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let mut trait_path = azalea_ecs_path();
trait_path.segments.push(format_ident!("schedule").into());
trait_path.segments.push(format_ident!("StageLabel").into());
derive_label(input, &trait_path, "stage_label")
}
/// Generates an impl of the `RunCriteriaLabel` trait.
///
/// This works only for unit structs, or enums with only unit variants.
/// You may force a struct or variant to behave as if it were fieldless with
/// `#[run_criteria_label(ignore_fields)]`.
#[proc_macro_derive(RunCriteriaLabel, attributes(run_criteria_label))]
pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let mut trait_path = azalea_ecs_path();
trait_path.segments.push(format_ident!("schedule").into());
trait_path
.segments
.push(format_ident!("RunCriteriaLabel").into());
derive_label(input, &trait_path, "run_criteria_label")
}
pub(crate) fn azalea_ecs_path() -> syn::Path {
BevyManifest::default().get_path("azalea_ecs")
}
#[proc_macro_derive(Resource)]
pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
}
#[proc_macro_derive(Component, attributes(component))]
pub fn derive_component(input: TokenStream) -> TokenStream {
component::derive_component(input)
}

View file

@ -0,0 +1,45 @@
#![allow(dead_code)]
use syn::DeriveInput;
use super::symbol::Symbol;
pub fn parse_attrs(ast: &DeriveInput, attr_name: Symbol) -> syn::Result<Vec<syn::NestedMeta>> {
let mut list = Vec::new();
for attr in ast.attrs.iter().filter(|a| a.path == attr_name) {
match attr.parse_meta()? {
syn::Meta::List(meta) => list.extend(meta.nested.into_iter()),
other => {
return Err(syn::Error::new_spanned(
other,
format!("expected #[{attr_name}(...)]"),
))
}
}
}
Ok(list)
}
pub fn get_lit_str(attr_name: Symbol, lit: &syn::Lit) -> syn::Result<&syn::LitStr> {
if let syn::Lit::Str(lit) = lit {
Ok(lit)
} else {
Err(syn::Error::new_spanned(
lit,
format!("expected {attr_name} attribute to be a string: `{attr_name} = \"...\"`"),
))
}
}
pub fn get_lit_bool(attr_name: Symbol, lit: &syn::Lit) -> syn::Result<bool> {
if let syn::Lit::Bool(lit) = lit {
Ok(lit.value())
} else {
Err(syn::Error::new_spanned(
lit,
format!(
"expected {attr_name} attribute to be a bool value, `true` or `false`: `{attr_name} = ...`"
),
))
}
}

View file

@ -0,0 +1,224 @@
#![allow(dead_code)]
extern crate proc_macro;
mod attrs;
mod shape;
mod symbol;
pub use attrs::*;
pub use shape::*;
pub use symbol::*;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use std::{env, path::PathBuf};
use syn::spanned::Spanned;
use toml::{map::Map, Value};
pub struct BevyManifest {
manifest: Map<String, Value>,
}
impl Default for BevyManifest {
fn default() -> Self {
Self {
manifest: env::var_os("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.map(|mut path| {
path.push("Cargo.toml");
let manifest = std::fs::read_to_string(path).unwrap();
toml::from_str(&manifest).unwrap()
})
.unwrap(),
}
}
}
impl BevyManifest {
pub fn maybe_get_path(&self, name: &str) -> Option<syn::Path> {
const AZALEA: &str = "azalea";
const BEVY_ECS: &str = "bevy_ecs";
const BEVY: &str = "bevy";
fn dep_package(dep: &Value) -> Option<&str> {
if dep.as_str().is_some() {
None
} else {
dep.as_table()
.unwrap()
.get("package")
.map(|name| name.as_str().unwrap())
}
}
let find_in_deps = |deps: &Map<String, Value>| -> Option<syn::Path> {
let package = if let Some(dep) = deps.get(name) {
return Some(Self::parse_str(dep_package(dep).unwrap_or(name)));
} else if let Some(dep) = deps.get(AZALEA) {
dep_package(dep).unwrap_or(AZALEA)
} else if let Some(dep) = deps.get(BEVY_ECS) {
dep_package(dep).unwrap_or(BEVY_ECS)
} else if let Some(dep) = deps.get(BEVY) {
dep_package(dep).unwrap_or(BEVY)
} else {
return None;
};
let mut path = Self::parse_str::<syn::Path>(package);
if let Some(module) = name.strip_prefix("azalea_") {
path.segments.push(Self::parse_str(module));
}
Some(path)
};
let deps = self
.manifest
.get("dependencies")
.map(|deps| deps.as_table().unwrap());
let deps_dev = self
.manifest
.get("dev-dependencies")
.map(|deps| deps.as_table().unwrap());
deps.and_then(find_in_deps)
.or_else(|| deps_dev.and_then(find_in_deps))
}
/// Returns the path for the crate with the given name.
///
/// This is a convenience method for constructing a [manifest] and
/// calling the [`get_path`] method.
///
/// This method should only be used where you just need the path and can't
/// cache the [manifest]. If caching is possible, it's recommended to create
/// the [manifest] yourself and use the [`get_path`] method.
///
/// [`get_path`]: Self::get_path
/// [manifest]: Self
pub fn get_path_direct(name: &str) -> syn::Path {
Self::default().get_path(name)
}
pub fn get_path(&self, name: &str) -> syn::Path {
self.maybe_get_path(name)
.unwrap_or_else(|| Self::parse_str(name))
}
pub fn parse_str<T: syn::parse::Parse>(path: &str) -> T {
syn::parse(path.parse::<TokenStream>().unwrap()).unwrap()
}
}
/// Derive a label trait
///
/// # Args
///
/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label
/// trait
/// - `trait_path`: The path [`syn::Path`] to the label trait
pub fn derive_label(
input: syn::DeriveInput,
trait_path: &syn::Path,
attr_name: &str,
) -> TokenStream {
// return true if the variant specified is an `ignore_fields` attribute
fn is_ignore(attr: &syn::Attribute, attr_name: &str) -> bool {
if attr.path.get_ident().as_ref().unwrap() != &attr_name {
return false;
}
syn::custom_keyword!(ignore_fields);
attr.parse_args_with(|input: syn::parse::ParseStream| {
let ignore = input.parse::<Option<ignore_fields>>()?.is_some();
Ok(ignore)
})
.unwrap()
}
let ident = input.ident.clone();
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
where_token: Default::default(),
predicates: Default::default(),
});
where_clause
.predicates
.push(syn::parse2(quote! { Self: 'static }).unwrap());
let as_str = match input.data {
syn::Data::Struct(d) => {
// see if the user tried to ignore fields incorrectly
if let Some(attr) = d
.fields
.iter()
.flat_map(|f| &f.attrs)
.find(|a| is_ignore(a, attr_name))
{
let err_msg = format!("`#[{attr_name}(ignore_fields)]` cannot be applied to fields individually: add it to the struct declaration");
return quote_spanned! {
attr.span() => compile_error!(#err_msg);
}
.into();
}
// Structs must either be fieldless, or explicitly ignore the fields.
let ignore_fields = input.attrs.iter().any(|a| is_ignore(a, attr_name));
if matches!(d.fields, syn::Fields::Unit) || ignore_fields {
let lit = ident.to_string();
quote! { #lit }
} else {
let err_msg = format!("Labels cannot contain data, unless explicitly ignored with `#[{attr_name}(ignore_fields)]`");
return quote_spanned! {
d.fields.span() => compile_error!(#err_msg);
}
.into();
}
}
syn::Data::Enum(d) => {
// check if the user put #[label(ignore_fields)] in the wrong place
if let Some(attr) = input.attrs.iter().find(|a| is_ignore(a, attr_name)) {
let err_msg = format!("`#[{attr_name}(ignore_fields)]` can only be applied to enum variants or struct declarations");
return quote_spanned! {
attr.span() => compile_error!(#err_msg);
}
.into();
}
let arms = d.variants.iter().map(|v| {
// Variants must either be fieldless, or explicitly ignore the fields.
let ignore_fields = v.attrs.iter().any(|a| is_ignore(a, attr_name));
if matches!(v.fields, syn::Fields::Unit) | ignore_fields {
let mut path = syn::Path::from(ident.clone());
path.segments.push(v.ident.clone().into());
let lit = format!("{ident}::{}", v.ident.clone());
quote! { #path { .. } => #lit }
} else {
let err_msg = format!("Label variants cannot contain data, unless explicitly ignored with `#[{attr_name}(ignore_fields)]`");
quote_spanned! {
v.fields.span() => _ => { compile_error!(#err_msg); }
}
}
});
quote! {
match self {
#(#arms),*
}
}
}
syn::Data::Union(_) => {
return quote_spanned! {
input.span() => compile_error!("Unions cannot be used as labels.");
}
.into();
}
};
(quote! {
impl #impl_generics #trait_path for #ident #ty_generics #where_clause {
fn as_str(&self) -> &'static str {
#as_str
}
}
})
.into()
}

View file

@ -0,0 +1,21 @@
use proc_macro::Span;
use syn::{Data, DataStruct, Error, Fields, FieldsNamed};
/// Get the fields of a data structure if that structure is a struct with named
/// fields; otherwise, return a compile error that points to the site of the
/// macro invocation.
pub fn get_named_struct_fields(data: &syn::Data) -> syn::Result<&FieldsNamed> {
match data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => Ok(fields),
_ => Err(Error::new(
// This deliberately points to the call site rather than the structure
// body; marking the entire body as the source of the error makes it
// impossible to figure out which `derive` has a problem.
Span::call_site().into(),
"Only structs with named fields are supported",
)),
}
}

View file

@ -0,0 +1,35 @@
use std::fmt::{self, Display};
use syn::{Ident, Path};
#[derive(Copy, Clone)]
pub struct Symbol(pub &'static str);
impl PartialEq<Symbol> for Ident {
fn eq(&self, word: &Symbol) -> bool {
self == word.0
}
}
impl<'a> PartialEq<Symbol> for &'a Ident {
fn eq(&self, word: &Symbol) -> bool {
*self == word.0
}
}
impl PartialEq<Symbol> for Path {
fn eq(&self, word: &Symbol) -> bool {
self.is_ident(word.0)
}
}
impl<'a> PartialEq<Symbol> for &'a Path {
fn eq(&self, word: &Symbol) -> bool {
self.is_ident(word.0)
}
}
impl Display for Symbol {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(self.0)
}
}

144
azalea-ecs/src/lib.rs Normal file
View file

@ -0,0 +1,144 @@
#![feature(trait_alias)]
//! Re-export important parts of `bevy_ecs` and `bevy_app` and make them more
//! compatible with Azalea.
//!
//! This is completely compatible with `bevy_ecs`, so it won't cause issues if
//! you use plugins meant for Bevy.
//!
//! Changes:
//! - Add [`TickPlugin`], [`TickStage`] and [`AppTickExt`]
//! - Change the macros to use azalea/azalea_ecs instead of bevy/bevy_ecs
//! - Rename bevy_ecs::world::World to azalea_ecs::ecs::Ecs
//! - Re-export `bevy_app` in the `app` module.
use std::time::{Duration, Instant};
pub mod ecs {
pub use bevy_ecs::world::World as Ecs;
pub use bevy_ecs::world::{EntityMut, EntityRef, Mut};
}
pub mod component {
pub use azalea_ecs_macros::Component;
pub use bevy_ecs::component::{ComponentId, ComponentStorage, Components, TableStorage};
// we do this because re-exporting Component would re-export the macro as well,
// which is bad (since we have our own Component macro)
// instead, we have to do this so Component is a trait alias and the original
// impl-able trait is still available as BevyComponent
pub trait Component = bevy_ecs::component::Component;
pub use bevy_ecs::component::Component as BevyComponent;
}
pub mod bundle {
pub use azalea_ecs_macros::Bundle;
pub trait Bundle = bevy_ecs::bundle::Bundle;
pub use bevy_ecs::bundle::Bundle as BevyBundle;
}
pub mod system {
pub use azalea_ecs_macros::Resource;
pub use bevy_ecs::system::{
Command, Commands, EntityCommands, Query, Res, ResMut, SystemState,
};
pub trait Resource = bevy_ecs::system::Resource;
pub use bevy_ecs::system::Resource as BevyResource;
}
pub use bevy_app as app;
pub use bevy_ecs::{entity, event, ptr, query, schedule, storage};
use app::{App, CoreStage, Plugin};
use bevy_ecs::schedule::*;
use ecs::Ecs;
pub struct TickPlugin {
/// How often a tick should happen. 50 milliseconds by default. Set to 0 to
/// tick every update.
pub tick_interval: Duration,
}
impl Plugin for TickPlugin {
fn build(&self, app: &mut App) {
app.add_stage_before(
CoreStage::Update,
TickLabel,
TickStage {
interval: self.tick_interval,
next_tick: Instant::now(),
stage: Box::new(SystemStage::parallel()),
},
);
}
}
impl Default for TickPlugin {
fn default() -> Self {
Self {
tick_interval: Duration::from_millis(50),
}
}
}
#[derive(StageLabel)]
struct TickLabel;
/// A [`Stage`] that runs every 50 milliseconds.
pub struct TickStage {
pub interval: Duration,
pub next_tick: Instant,
stage: Box<dyn Stage>,
}
impl Stage for TickStage {
fn run(&mut self, ecs: &mut Ecs) {
// if the interval is 0, that means it runs every tick
if self.interval.is_zero() {
self.stage.run(ecs);
return;
}
// keep calling run until it's caught up
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too (but
// check the source so we do it right)
while Instant::now() > self.next_tick {
self.next_tick += self.interval;
self.stage.run(ecs);
}
}
}
pub trait AppTickExt {
fn add_tick_system_set(&mut self, system_set: SystemSet) -> &mut App;
fn add_tick_system<Params>(&mut self, system: impl IntoSystemDescriptor<Params>) -> &mut App;
}
impl AppTickExt for App {
/// Adds a set of ECS systems that will run every 50 milliseconds.
///
/// Note that you should NOT have `EventReader`s in tick systems, as this
/// will make them sometimes be missed.
fn add_tick_system_set(&mut self, system_set: SystemSet) -> &mut App {
let tick_stage = self
.schedule
.get_stage_mut::<TickStage>(TickLabel)
.expect("Tick Stage not found");
let stage = tick_stage
.stage
.downcast_mut::<SystemStage>()
.expect("Fixed Timestep sub-stage is not a SystemStage");
stage.add_system_set(system_set);
self
}
/// Adds a new ECS system that will run every 50 milliseconds.
///
/// Note that you should NOT have `EventReader`s in tick systems, as this
/// will make them sometimes be missed.
fn add_tick_system<Params>(&mut self, system: impl IntoSystemDescriptor<Params>) -> &mut App {
let tick_stage = self
.schedule
.get_stage_mut::<TickStage>(TickLabel)
.expect("Tick Stage not found");
let stage = tick_stage
.stage
.downcast_mut::<SystemStage>()
.expect("Fixed Timestep sub-stage is not a SystemStage");
stage.add_system(system);
self
}
}

View file

@ -26,62 +26,62 @@ fn write_compound(
Tag::Byte(value) => { Tag::Byte(value) => {
writer.write_u8(1)?; writer.write_u8(1)?;
write_string(writer, key)?; write_string(writer, key)?;
writer.write_i8(*value)? writer.write_i8(*value)?;
} }
Tag::Short(value) => { Tag::Short(value) => {
writer.write_u8(2)?; writer.write_u8(2)?;
write_string(writer, key)?; write_string(writer, key)?;
writer.write_i16::<BE>(*value)? writer.write_i16::<BE>(*value)?;
} }
Tag::Int(value) => { Tag::Int(value) => {
writer.write_u8(3)?; writer.write_u8(3)?;
write_string(writer, key)?; write_string(writer, key)?;
writer.write_i32::<BE>(*value)? writer.write_i32::<BE>(*value)?;
} }
Tag::Long(value) => { Tag::Long(value) => {
writer.write_u8(4)?; writer.write_u8(4)?;
write_string(writer, key)?; write_string(writer, key)?;
writer.write_i64::<BE>(*value)? writer.write_i64::<BE>(*value)?;
} }
Tag::Float(value) => { Tag::Float(value) => {
writer.write_u8(5)?; writer.write_u8(5)?;
write_string(writer, key)?; write_string(writer, key)?;
writer.write_f32::<BE>(*value)? writer.write_f32::<BE>(*value)?;
} }
Tag::Double(value) => { Tag::Double(value) => {
writer.write_u8(6)?; writer.write_u8(6)?;
write_string(writer, key)?; write_string(writer, key)?;
writer.write_f64::<BE>(*value)? writer.write_f64::<BE>(*value)?;
} }
Tag::ByteArray(value) => { Tag::ByteArray(value) => {
writer.write_u8(7)?; writer.write_u8(7)?;
write_string(writer, key)?; write_string(writer, key)?;
write_bytearray(writer, value)? write_bytearray(writer, value)?;
} }
Tag::String(value) => { Tag::String(value) => {
writer.write_u8(8)?; writer.write_u8(8)?;
write_string(writer, key)?; write_string(writer, key)?;
write_string(writer, value)? write_string(writer, value)?;
} }
Tag::List(value) => { Tag::List(value) => {
writer.write_u8(9)?; writer.write_u8(9)?;
write_string(writer, key)?; write_string(writer, key)?;
write_list(writer, value)? write_list(writer, value)?;
} }
Tag::Compound(value) => { Tag::Compound(value) => {
writer.write_u8(10)?; writer.write_u8(10)?;
write_string(writer, key)?; write_string(writer, key)?;
write_compound(writer, value, true)? write_compound(writer, value, true)?;
} }
Tag::IntArray(value) => { Tag::IntArray(value) => {
writer.write_u8(11)?; writer.write_u8(11)?;
write_string(writer, key)?; write_string(writer, key)?;
write_intarray(writer, value)? write_intarray(writer, value)?;
} }
Tag::LongArray(value) => { Tag::LongArray(value) => {
writer.write_u8(12)?; writer.write_u8(12)?;
write_string(writer, key)?; write_string(writer, key)?;
write_longarray(writer, value)? write_longarray(writer, value)?;
} }
} }
} }

View file

@ -2,9 +2,10 @@ use ahash::AHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// An NBT value. /// An NBT value.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
#[serde(untagged)] #[serde(untagged)]
pub enum Tag { pub enum Tag {
#[default]
End, // 0 End, // 0
Byte(i8), // 1 Byte(i8), // 1
Short(i16), // 2 Short(i16), // 2
@ -20,12 +21,6 @@ pub enum Tag {
LongArray(Vec<i64>), // 12 LongArray(Vec<i64>), // 12
} }
impl Default for Tag {
fn default() -> Self {
Tag::End
}
}
impl Tag { impl Tag {
/// Get the numerical ID of the tag type. /// Get the numerical ID of the tag type.
#[inline] #[inline]

View file

@ -12,8 +12,11 @@ version = "0.5.0"
azalea-block = { path = "../azalea-block", version = "^0.5.0" } azalea-block = { path = "../azalea-block", version = "^0.5.0" }
azalea-core = { path = "../azalea-core", version = "^0.5.0" } azalea-core = { path = "../azalea-core", version = "^0.5.0" }
azalea-world = { path = "../azalea-world", version = "^0.5.0" } azalea-world = { path = "../azalea-world", version = "^0.5.0" }
azalea-registry = { path = "../azalea-registry", version = "^0.5.0" }
iyes_loopless = "0.9.1"
once_cell = "1.16.0" once_cell = "1.16.0"
parking_lot = "^0.12.1" parking_lot = "^0.12.1"
azalea-ecs = { version = "0.5.0", path = "../azalea-ecs" }
[dev-dependencies] [dev-dependencies]
uuid = "^1.1.2" uuid = "^1.1.2"

View file

@ -64,7 +64,7 @@ impl DiscreteVoxelShape {
} }
pub fn for_all_boxes(&self, consumer: impl IntLineConsumer, swap: bool) { pub fn for_all_boxes(&self, consumer: impl IntLineConsumer, swap: bool) {
BitSetDiscreteVoxelShape::for_all_boxes(self, consumer, swap) BitSetDiscreteVoxelShape::for_all_boxes(self, consumer, swap);
} }
} }

View file

@ -5,13 +5,15 @@ mod shape;
mod world_collisions; mod world_collisions;
use azalea_core::{Axis, Vec3, AABB, EPSILON}; use azalea_core::{Axis, Vec3, AABB, EPSILON};
use azalea_world::entity::{Entity, EntityData}; use azalea_world::{
use azalea_world::{MoveEntityError, WeakWorld}; entity::{self},
MoveEntityError, World,
};
pub use blocks::BlockWithShape; pub use blocks::BlockWithShape;
pub use discrete_voxel_shape::*; pub use discrete_voxel_shape::*;
pub use shape::*; pub use shape::*;
use std::ops::Deref;
use world_collisions::CollisionGetter; use self::world_collisions::get_block_collisions;
pub enum MoverType { pub enum MoverType {
Own, Own,
@ -21,19 +23,6 @@ pub enum MoverType {
Shulker, Shulker,
} }
pub trait HasCollision {
fn collide(&self, movement: &Vec3, entity: &EntityData) -> Vec3;
}
pub trait MovableEntity {
fn move_colliding(
&mut self,
mover_type: &MoverType,
movement: &Vec3,
) -> Result<(), MoveEntityError>;
}
impl<D: Deref<Target = WeakWorld>> HasCollision for D {
// private Vec3 collide(Vec3 var1) { // private Vec3 collide(Vec3 var1) {
// AABB var2 = this.getBoundingBox(); // AABB var2 = this.getBoundingBox();
// List var3 = this.level.getEntityCollisions(this, // List var3 = this.level.getEntityCollisions(this,
@ -63,8 +52,8 @@ impl<D: Deref<Target = WeakWorld>> HasCollision for D {
// return var4; // return var4;
// } // }
fn collide(&self, movement: &Vec3, entity: &EntityData) -> Vec3 { fn collide(movement: &Vec3, world: &World, physics: &entity::Physics) -> Vec3 {
let entity_bounding_box = entity.bounding_box; let entity_bounding_box = physics.bounding_box;
// TODO: get_entity_collisions // TODO: get_entity_collisions
// let entity_collisions = world.get_entity_collisions(self, // let entity_collisions = world.get_entity_collisions(self,
// entity_bounding_box.expand_towards(movement)); // entity_bounding_box.expand_towards(movement));
@ -72,27 +61,21 @@ impl<D: Deref<Target = WeakWorld>> HasCollision for D {
if movement.length_sqr() == 0.0 { if movement.length_sqr() == 0.0 {
*movement *movement
} else { } else {
collide_bounding_box( collide_bounding_box(movement, &entity_bounding_box, world, entity_collisions)
Some(entity),
movement,
&entity_bounding_box,
self,
entity_collisions,
)
} }
// TODO: stepping (for stairs and stuff) // TODO: stepping (for stairs and stuff)
// collided_movement // collided_movement
} }
}
impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
/// Move an entity by a given delta, checking for collisions. /// Move an entity by a given delta, checking for collisions.
fn move_colliding( pub fn move_colliding(
&mut self,
_mover_type: &MoverType, _mover_type: &MoverType,
movement: &Vec3, movement: &Vec3,
world: &World,
position: &mut entity::Position,
physics: &mut entity::Physics,
) -> Result<(), MoveEntityError> { ) -> Result<(), MoveEntityError> {
// TODO: do all these // TODO: do all these
@ -115,7 +98,7 @@ impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
// movement = this.maybeBackOffFromEdge(movement, moverType); // movement = this.maybeBackOffFromEdge(movement, moverType);
let collide_result = { self.world.collide(movement, self) }; let collide_result = collide(movement, world, physics);
let move_distance = collide_result.length_sqr(); let move_distance = collide_result.length_sqr();
@ -123,15 +106,14 @@ impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
// TODO: fall damage // TODO: fall damage
let new_pos = { let new_pos = {
let entity_pos = self.pos();
Vec3 { Vec3 {
x: entity_pos.x + collide_result.x, x: position.x + collide_result.x,
y: entity_pos.y + collide_result.y, y: position.y + collide_result.y,
z: entity_pos.z + collide_result.z, z: position.z + collide_result.z,
} }
}; };
self.world.set_entity_pos(self.id, new_pos)?; **position = new_pos;
} }
let x_collision = movement.x != collide_result.x; let x_collision = movement.x != collide_result.x;
@ -139,11 +121,11 @@ impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
let horizontal_collision = x_collision || z_collision; let horizontal_collision = x_collision || z_collision;
let vertical_collision = movement.y != collide_result.y; let vertical_collision = movement.y != collide_result.y;
let on_ground = vertical_collision && movement.y < 0.; let on_ground = vertical_collision && movement.y < 0.;
self.on_ground = on_ground; physics.on_ground = on_ground;
// TODO: minecraft checks for a "minor" horizontal collision here // TODO: minecraft checks for a "minor" horizontal collision here
let _block_pos_below = self.on_pos_legacy(); let _block_pos_below = entity::on_pos_legacy(&world.chunks, position);
// let _block_state_below = self // let _block_state_below = self
// .world // .world
// .get_block_state(&block_pos_below) // .get_block_state(&block_pos_below)
@ -155,8 +137,8 @@ impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
// if self.isRemoved() { return; } // if self.isRemoved() { return; }
if horizontal_collision { if horizontal_collision {
let delta_movement = &self.delta; let delta_movement = &physics.delta;
self.delta = Vec3 { physics.delta = Vec3 {
x: if x_collision { 0. } else { delta_movement.x }, x: if x_collision { 0. } else { delta_movement.x },
y: delta_movement.y, y: delta_movement.y,
z: if z_collision { 0. } else { delta_movement.z }, z: if z_collision { 0. } else { delta_movement.z },
@ -167,7 +149,7 @@ impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
// blockBelow.updateEntityAfterFallOn(this.level, this); // blockBelow.updateEntityAfterFallOn(this.level, this);
// the default implementation of updateEntityAfterFallOn sets the y movement to // the default implementation of updateEntityAfterFallOn sets the y movement to
// 0 // 0
self.delta.y = 0.; physics.delta.y = 0.;
} }
if on_ground { if on_ground {
@ -200,13 +182,11 @@ impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
Ok(()) Ok(())
} }
}
fn collide_bounding_box( fn collide_bounding_box(
entity: Option<&EntityData>,
movement: &Vec3, movement: &Vec3,
entity_bounding_box: &AABB, entity_bounding_box: &AABB,
world: &WeakWorld, world: &World,
entity_collisions: Vec<VoxelShape>, entity_collisions: Vec<VoxelShape>,
) -> Vec3 { ) -> Vec3 {
let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1); let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
@ -218,7 +198,7 @@ fn collide_bounding_box(
// TODO: world border // TODO: world border
let block_collisions = let block_collisions =
world.get_block_collisions(entity, entity_bounding_box.expand_towards(movement)); get_block_collisions(world, entity_bounding_box.expand_towards(movement));
let block_collisions = block_collisions.collect::<Vec<_>>(); let block_collisions = block_collisions.collect::<Vec<_>>();
collision_boxes.extend(block_collisions); collision_boxes.extend(block_collisions);
collide_with_shapes(movement, *entity_bounding_box, &collision_boxes) collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)

View file

@ -539,7 +539,7 @@ impl VoxelShape {
x_coords[var7 as usize], x_coords[var7 as usize],
y_coords[var8 as usize], y_coords[var8 as usize],
z_coords[var9 as usize], z_coords[var9 as usize],
) );
}, },
true, true,
); );

View file

@ -1,34 +1,17 @@
use super::Shapes;
use crate::collision::{BlockWithShape, VoxelShape, AABB}; use crate::collision::{BlockWithShape, VoxelShape, AABB};
use azalea_block::BlockState; use azalea_block::BlockState;
use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON}; use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON};
use azalea_world::entity::EntityData; use azalea_world::{Chunk, World};
use azalea_world::{Chunk, WeakWorld};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::Arc; use std::sync::Arc;
use super::Shapes; pub fn get_block_collisions(world: &World, aabb: AABB) -> BlockCollisions<'_> {
BlockCollisions::new(world, aabb)
pub trait CollisionGetter {
fn get_block_collisions<'a>(
&'a self,
entity: Option<&EntityData>,
aabb: AABB,
) -> BlockCollisions<'a>;
}
impl CollisionGetter for WeakWorld {
fn get_block_collisions<'a>(
&'a self,
entity: Option<&EntityData>,
aabb: AABB,
) -> BlockCollisions<'a> {
BlockCollisions::new(self, entity, aabb)
}
} }
pub struct BlockCollisions<'a> { pub struct BlockCollisions<'a> {
pub world: &'a WeakWorld, pub world: &'a World,
// context: CollisionContext,
pub aabb: AABB, pub aabb: AABB,
pub entity_shape: VoxelShape, pub entity_shape: VoxelShape,
pub cursor: Cursor3d, pub cursor: Cursor3d,
@ -36,8 +19,7 @@ pub struct BlockCollisions<'a> {
} }
impl<'a> BlockCollisions<'a> { impl<'a> BlockCollisions<'a> {
// TODO: the entity is stored in the context pub fn new(world: &'a World, aabb: AABB) -> Self {
pub fn new(world: &'a WeakWorld, _entity: Option<&EntityData>, aabb: AABB) -> Self {
let origin_x = (aabb.min_x - EPSILON) as i32 - 1; let origin_x = (aabb.min_x - EPSILON) as i32 - 1;
let origin_y = (aabb.min_y - EPSILON) as i32 - 1; let origin_y = (aabb.min_y - EPSILON) as i32 - 1;
let origin_z = (aabb.min_z - EPSILON) as i32 - 1; let origin_z = (aabb.min_z - EPSILON) as i32 - 1;
@ -75,7 +57,7 @@ impl<'a> BlockCollisions<'a> {
// return var7; // return var7;
// } // }
self.world.get_chunk(&chunk_pos) self.world.chunks.get(&chunk_pos)
} }
} }
@ -89,15 +71,14 @@ impl<'a> Iterator for BlockCollisions<'a> {
} }
let chunk = self.get_chunk(item.pos.x, item.pos.z); let chunk = self.get_chunk(item.pos.x, item.pos.z);
let chunk = match chunk { let Some(chunk) = chunk else {
Some(chunk) => chunk, continue
None => continue,
}; };
let pos = item.pos; let pos = item.pos;
let block_state: BlockState = chunk let block_state: BlockState = chunk
.read() .read()
.get(&(&pos).into(), self.world.min_y()) .get(&(&pos).into(), self.world.chunks.min_y)
.unwrap_or(BlockState::Air); .unwrap_or(BlockState::Air);
// TODO: continue if self.only_suffocating_blocks and the block is not // TODO: continue if self.only_suffocating_blocks and the block is not

View file

@ -5,24 +5,56 @@ pub mod collision;
use azalea_block::{Block, BlockState}; use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3}; use azalea_core::{BlockPos, Vec3};
use azalea_world::{ use azalea_ecs::{
entity::{Entity, EntityData}, app::{App, Plugin},
WeakWorld, entity::Entity,
event::{EventReader, EventWriter},
query::With,
schedule::{IntoSystemDescriptor, SystemSet},
system::{Query, Res},
AppTickExt,
}; };
use collision::{MovableEntity, MoverType}; use azalea_world::{
use std::ops::Deref; entity::{
metadata::Sprinting, move_relative, Attributes, Jumping, Physics, Position, WorldName,
},
Local, World, WorldContainer,
};
use collision::{move_colliding, MoverType};
pub trait HasPhysics { pub struct PhysicsPlugin;
fn travel(&mut self, acceleration: &Vec3); impl Plugin for PhysicsPlugin {
fn ai_step(&mut self); fn build(&self, app: &mut App) {
app.add_event::<ForceJumpEvent>()
fn jump_from_ground(&mut self); .add_system(
force_jump_listener
.label("force_jump_listener")
.after("ai_step"),
)
.add_tick_system_set(
SystemSet::new()
.with_system(ai_step.label("ai_step"))
.with_system(
travel
.label("travel")
.after("ai_step")
.after("force_jump_listener"),
),
);
}
} }
impl<D: Deref<Target = WeakWorld>> HasPhysics for Entity<'_, D> {
/// Move the entity with the given acceleration while handling friction, /// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff. /// gravity, collisions, and some other stuff.
fn travel(&mut self, acceleration: &Vec3) { fn travel(
mut query: Query<(&mut Physics, &mut Position, &Attributes, &WorldName), With<Local>>,
world_container: Res<WorldContainer>,
) {
for (mut physics, mut position, attributes, world_name) in &mut query {
let world_lock = world_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
// if !self.is_effective_ai() && !self.is_controlled_by_local_instance() { // if !self.is_effective_ai() && !self.is_controlled_by_local_instance() {
// // this.calculateEntityAnimation(this, this instanceof FlyingAnimal); // // this.calculateEntityAnimation(this, this instanceof FlyingAnimal);
// return; // return;
@ -37,24 +69,29 @@ impl<D: Deref<Target = WeakWorld>> HasPhysics for Entity<'_, D> {
// TODO: elytra // TODO: elytra
let block_pos_below = get_block_pos_below_that_affects_movement(self); let block_pos_below = get_block_pos_below_that_affects_movement(&position);
let block_state_below = self let block_state_below = world
.world .chunks
.get_block_state(&block_pos_below) .get_block_state(&block_pos_below)
.unwrap_or(BlockState::Air); .unwrap_or(BlockState::Air);
let block_below: Box<dyn Block> = block_state_below.into(); let block_below: Box<dyn Block> = block_state_below.into();
let block_friction = block_below.behavior().friction; let block_friction = block_below.behavior().friction;
let inertia = if self.on_ground { let inertia = if physics.on_ground {
block_friction * 0.91 block_friction * 0.91
} else { } else {
0.91 0.91
}; };
// this applies the current delta // this applies the current delta
let mut movement = let mut movement = handle_relative_friction_and_calculate_movement(
handle_relative_friction_and_calculate_movement(self, acceleration, block_friction); block_friction,
&world,
&mut physics,
&mut position,
attributes,
);
movement.y -= gravity; movement.y -= gravity;
@ -66,95 +103,131 @@ impl<D: Deref<Target = WeakWorld>> HasPhysics for Entity<'_, D> {
// if should_discard_friction(self) { // if should_discard_friction(self) {
if false { if false {
self.delta = movement; physics.delta = movement;
} else { } else {
self.delta = Vec3 { physics.delta = Vec3 {
x: movement.x * inertia as f64, x: movement.x * inertia as f64,
y: movement.y * 0.98f64, y: movement.y * 0.98f64,
z: movement.z * inertia as f64, z: movement.z * inertia as f64,
}; };
} }
} }
}
/// applies air resistance, calls self.travel(), and some other random /// applies air resistance, calls self.travel(), and some other random
/// stuff. /// stuff.
fn ai_step(&mut self) { pub fn ai_step(
mut query: Query<
(Entity, &mut Physics, Option<&Jumping>),
With<Local>,
// TODO: ai_step should only run for players in loaded chunks
// With<LocalPlayerInLoadedChunk> maybe there should be an InLoadedChunk/InUnloadedChunk
// component?
>,
mut force_jump_events: EventWriter<ForceJumpEvent>,
) {
for (entity, mut physics, jumping) in &mut query {
// vanilla does movement interpolation here, doesn't really matter much for a // vanilla does movement interpolation here, doesn't really matter much for a
// bot though // bot though
if self.delta.x.abs() < 0.003 { if physics.delta.x.abs() < 0.003 {
self.delta.x = 0.; physics.delta.x = 0.;
} }
if self.delta.y.abs() < 0.003 { if physics.delta.y.abs() < 0.003 {
self.delta.y = 0.; physics.delta.y = 0.;
} }
if self.delta.z.abs() < 0.003 { if physics.delta.z.abs() < 0.003 {
self.delta.z = 0.; physics.delta.z = 0.;
} }
if self.jumping { if let Some(jumping) = jumping {
if **jumping {
// TODO: jumping in liquids and jump delay // TODO: jumping in liquids and jump delay
if self.on_ground { if physics.on_ground {
self.jump_from_ground(); force_jump_events.send(ForceJumpEvent(entity));
}
} }
} }
self.xxa *= 0.98; physics.xxa *= 0.98;
self.zza *= 0.98; physics.zza *= 0.98;
self.travel(&Vec3 { // TODO: freezing, pushEntities, drowning damage (in their own systems,
x: self.xxa as f64, // after `travel`)
y: self.yya as f64, }
z: self.zza as f64,
});
// freezing
// pushEntities
// drowning damage
} }
fn jump_from_ground(&mut self) { /// Jump even if we aren't on the ground.
let jump_power: f64 = jump_power(self) as f64 + jump_boost_power(self); pub struct ForceJumpEvent(pub Entity);
let old_delta_movement = self.delta;
self.delta = Vec3 { fn force_jump_listener(
mut query: Query<(&mut Physics, &Position, &Sprinting, &WorldName)>,
world_container: Res<WorldContainer>,
mut events: EventReader<ForceJumpEvent>,
) {
for event in events.iter() {
if let Ok((mut physics, position, sprinting, world_name)) = query.get_mut(event.0) {
let world_lock = world_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power();
let old_delta_movement = physics.delta;
physics.delta = Vec3 {
x: old_delta_movement.x, x: old_delta_movement.x,
y: jump_power, y: jump_power,
z: old_delta_movement.z, z: old_delta_movement.z,
}; };
if self.metadata.sprinting { if **sprinting {
let y_rot = self.y_rot * 0.017453292; // sprint jumping gives some extra velocity
self.delta += Vec3 { let y_rot = physics.y_rot * 0.017453292;
physics.delta += Vec3 {
x: (-f32::sin(y_rot) * 0.2) as f64, x: (-f32::sin(y_rot) * 0.2) as f64,
y: 0., y: 0.,
z: (f32::cos(y_rot) * 0.2) as f64, z: (f32::cos(y_rot) * 0.2) as f64,
}; };
} }
self.has_impulse = true; physics.has_impulse = true;
}
} }
} }
fn get_block_pos_below_that_affects_movement(entity: &EntityData) -> BlockPos { fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
BlockPos::new( BlockPos::new(
entity.pos().x.floor() as i32, position.x.floor() as i32,
// TODO: this uses bounding_box.min_y instead of position.y // TODO: this uses bounding_box.min_y instead of position.y
(entity.pos().y - 0.5f64).floor() as i32, (position.y - 0.5f64).floor() as i32,
entity.pos().z.floor() as i32, position.z.floor() as i32,
) )
} }
fn handle_relative_friction_and_calculate_movement<D: Deref<Target = WeakWorld>>( fn handle_relative_friction_and_calculate_movement(
entity: &mut Entity<D>,
acceleration: &Vec3,
block_friction: f32, block_friction: f32,
world: &World,
physics: &mut Physics,
position: &mut Position,
attributes: &Attributes,
) -> Vec3 { ) -> Vec3 {
entity.move_relative( move_relative(
get_friction_influenced_speed(&*entity, block_friction), physics,
acceleration, get_friction_influenced_speed(physics, attributes, block_friction),
&Vec3 {
x: physics.xxa as f64,
y: physics.yya as f64,
z: physics.zza as f64,
},
); );
// entity.delta = entity.handle_on_climbable(entity.delta); // entity.delta = entity.handle_on_climbable(entity.delta);
entity move_colliding(
.move_colliding(&MoverType::Own, &entity.delta.clone()) &MoverType::Own,
&physics.delta.clone(),
world,
position,
physics,
)
.expect("Entity should exist."); .expect("Entity should exist.");
// let delta_movement = entity.delta; // let delta_movement = entity.delta;
// ladders // ladders
@ -164,16 +237,16 @@ fn handle_relative_friction_and_calculate_movement<D: Deref<Target = WeakWorld>>
// Vec3(var3.x, 0.2D, var3.z); } // Vec3(var3.x, 0.2D, var3.z); }
// TODO: powdered snow // TODO: powdered snow
entity.delta physics.delta
} }
// private float getFrictionInfluencedSpeed(float friction) { // private float getFrictionInfluencedSpeed(float friction) {
// return this.onGround ? this.getSpeed() * (0.21600002F / (friction * // return this.onGround ? this.getSpeed() * (0.21600002F / (friction *
// friction * friction)) : this.flyingSpeed; } // friction * friction)) : this.flyingSpeed; }
fn get_friction_influenced_speed(entity: &EntityData, friction: f32) -> f32 { fn get_friction_influenced_speed(physics: &Physics, attributes: &Attributes, friction: f32) -> f32 {
// TODO: have speed & flying_speed fields in entity // TODO: have speed & flying_speed fields in entity
if entity.on_ground { if physics.on_ground {
let speed: f32 = entity.attributes.speed.calculate() as f32; let speed: f32 = attributes.speed.calculate() as f32;
speed * (0.216f32 / (friction * friction * friction)) speed * (0.216f32 / (friction * friction * friction))
} else { } else {
// entity.flying_speed // entity.flying_speed
@ -183,11 +256,11 @@ fn get_friction_influenced_speed(entity: &EntityData, friction: f32) -> f32 {
/// Returns the what the entity's jump should be multiplied by based on the /// Returns the what the entity's jump should be multiplied by based on the
/// block they're standing on. /// block they're standing on.
fn block_jump_factor<D: Deref<Target = WeakWorld>>(entity: &Entity<D>) -> f32 { fn block_jump_factor(world: &World, position: &Position) -> f32 {
let block_at_pos = entity.world.get_block_state(&entity.pos().into()); let block_at_pos = world.chunks.get_block_state(&position.into());
let block_below = entity let block_below = world
.world .chunks
.get_block_state(&get_block_pos_below_that_affects_movement(entity)); .get_block_state(&get_block_pos_below_that_affects_movement(position));
let block_at_pos_jump_factor = if let Some(block) = block_at_pos { let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
Box::<dyn Block>::from(block).behavior().jump_factor Box::<dyn Block>::from(block).behavior().jump_factor
@ -211,11 +284,11 @@ fn block_jump_factor<D: Deref<Target = WeakWorld>>(entity: &Entity<D>) -> f32 {
// public double getJumpBoostPower() { // public double getJumpBoostPower() {
// return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F * // return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F *
// (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; } // (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; }
fn jump_power<D: Deref<Target = WeakWorld>>(entity: &Entity<D>) -> f32 { fn jump_power(world: &World, position: &Position) -> f32 {
0.42 * block_jump_factor(entity) 0.42 * block_jump_factor(world, position)
} }
fn jump_boost_power<D: Deref<Target = WeakWorld>>(_entity: &Entity<D>) -> f64 { fn jump_boost_power() -> f64 {
// TODO: potion effects // TODO: potion effects
// if let Some(effects) = entity.effects() { // if let Some(effects) = entity.effects() {
// if let Some(jump_effect) = effects.get(&Effect::Jump) { // if let Some(jump_effect) = effects.get(&Effect::Jump) {
@ -231,131 +304,218 @@ fn jump_boost_power<D: Deref<Target = WeakWorld>>(_entity: &Entity<D>) -> f64 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::time::Duration;
use super::*; use super::*;
use azalea_core::ChunkPos; use azalea_core::{ChunkPos, ResourceLocation};
use azalea_ecs::{app::App, TickPlugin};
use azalea_world::{ use azalea_world::{
entity::{metadata, EntityMetadata}, entity::{EntityBundle, MinecraftEntityId},
Chunk, PartialWorld, Chunk, EntityPlugin, PartialWorld,
}; };
use uuid::Uuid; use uuid::Uuid;
/// You need an app to spawn entities in the world and do updates.
fn make_test_app() -> App {
let mut app = App::new();
app.add_plugin(TickPlugin {
tick_interval: Duration::ZERO,
})
.add_plugin(PhysicsPlugin)
.add_plugin(EntityPlugin)
.init_resource::<WorldContainer>();
app
}
#[test] #[test]
fn test_gravity() { fn test_gravity() {
let mut world = PartialWorld::default(); let mut app = make_test_app();
let _world_lock = app.world.resource_mut::<WorldContainer>().insert(
ResourceLocation::new("minecraft:overworld").unwrap(),
384,
-64,
);
world.add_entity( let entity = app
0, .world
EntityData::new( .spawn((
EntityBundle::new(
Uuid::nil(), Uuid::nil(),
Vec3 { Vec3 {
x: 0., x: 0.,
y: 70., y: 70.,
z: 0., z: 0.,
}, },
EntityMetadata::Player(metadata::Player::default()), azalea_registry::EntityKind::Zombie,
ResourceLocation::new("minecraft:overworld").unwrap(),
), ),
); MinecraftEntityId(0),
let mut entity = world.entity_mut(0).unwrap(); Local,
))
.id();
{
let entity_pos = *app.world.get::<Position>(entity).unwrap();
// y should start at 70 // y should start at 70
assert_eq!(entity.pos().y, 70.); assert_eq!(entity_pos.y, 70.);
entity.ai_step(); }
app.update();
{
let entity_pos = *app.world.get::<Position>(entity).unwrap();
// delta is applied before gravity, so the first tick only sets the delta // delta is applied before gravity, so the first tick only sets the delta
assert_eq!(entity.pos().y, 70.); assert_eq!(entity_pos.y, 70.);
assert!(entity.delta.y < 0.); let entity_physics = app.world.get::<Physics>(entity).unwrap().clone();
entity.ai_step(); assert!(entity_physics.delta.y < 0.);
}
app.update();
{
let entity_pos = *app.world.get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, so now it should go down // the second tick applies the delta to the position, so now it should go down
assert!( assert!(
entity.pos().y < 70., entity_pos.y < 70.,
"Entity y ({}) didn't go down after physics steps", "Entity y ({}) didn't go down after physics steps",
entity.pos().y entity_pos.y
); );
} }
}
#[test] #[test]
fn test_collision() { fn test_collision() {
let mut world = PartialWorld::default(); let mut app = make_test_app();
world let world_lock = app.world.resource_mut::<WorldContainer>().insert(
.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) ResourceLocation::new("minecraft:overworld").unwrap(),
.unwrap(); 384,
world.add_entity( -64,
0, );
EntityData::new( let mut partial_world = PartialWorld::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world
.spawn((
EntityBundle::new(
Uuid::nil(), Uuid::nil(),
Vec3 { Vec3 {
x: 0.5, x: 0.5,
y: 70., y: 70.,
z: 0.5, z: 0.5,
}, },
EntityMetadata::Player(metadata::Player::default()), azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld").unwrap(),
), ),
MinecraftEntityId(0),
Local,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
BlockState::Stone,
&mut world_lock.write().chunks,
); );
let block_state = world.set_block_state(&BlockPos { x: 0, y: 69, z: 0 }, BlockState::Stone);
assert!( assert!(
block_state.is_some(), block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
); );
let mut entity = world.entity_mut(0).unwrap(); app.update();
entity.ai_step(); {
let entity_pos = *app.world.get::<Position>(entity).unwrap();
// delta will change, but it won't move until next tick // delta will change, but it won't move until next tick
assert_eq!(entity.pos().y, 70.); assert_eq!(entity_pos.y, 70.);
assert!(entity.delta.y < 0.); let entity_physics = app.world.get::<Physics>(entity).unwrap().clone();
entity.ai_step(); assert!(entity_physics.delta.y < 0.);
}
app.update();
{
let entity_pos = *app.world.get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, but it also does collision // the second tick applies the delta to the position, but it also does collision
assert_eq!(entity.pos().y, 70.); assert_eq!(entity_pos.y, 70.);
}
} }
#[test] #[test]
fn test_slab_collision() { fn test_slab_collision() {
let mut world = PartialWorld::default(); let mut app = make_test_app();
world let world_lock = app.world.resource_mut::<WorldContainer>().insert(
.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) ResourceLocation::new("minecraft:overworld").unwrap(),
.unwrap(); 384,
world.add_entity( -64,
0, );
EntityData::new( let mut partial_world = PartialWorld::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world
.spawn((
EntityBundle::new(
Uuid::nil(), Uuid::nil(),
Vec3 { Vec3 {
x: 0.5, x: 0.5,
y: 71., y: 71.,
z: 0.5, z: 0.5,
}, },
EntityMetadata::Player(metadata::Player::default()), azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld").unwrap(),
), ),
); MinecraftEntityId(0),
let block_state = world.set_block_state( Local,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 }, &BlockPos { x: 0, y: 69, z: 0 },
BlockState::StoneSlab_BottomFalse, BlockState::StoneSlab_BottomFalse,
&mut world_lock.write().chunks,
); );
assert!( assert!(
block_state.is_some(), block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
); );
let mut entity = world.entity_mut(0).unwrap();
// do a few steps so we fall on the slab // do a few steps so we fall on the slab
for _ in 0..20 { for _ in 0..20 {
entity.ai_step(); app.update();
} }
assert_eq!(entity.pos().y, 69.5); let entity_pos = app.world.get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 69.5);
} }
#[test] #[test]
fn test_top_slab_collision() { fn test_top_slab_collision() {
let mut world = PartialWorld::default(); let mut app = make_test_app();
world let world_lock = app.world.resource_mut::<WorldContainer>().insert(
.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) ResourceLocation::new("minecraft:overworld").unwrap(),
.unwrap(); 384,
world.add_entity( -64,
0, );
EntityData::new( let mut partial_world = PartialWorld::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world
.spawn((
EntityBundle::new(
Uuid::nil(), Uuid::nil(),
Vec3 { Vec3 {
x: 0.5, x: 0.5,
y: 71., y: 71.,
z: 0.5, z: 0.5,
}, },
EntityMetadata::Player(metadata::Player::default()), azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld").unwrap(),
), ),
); MinecraftEntityId(0),
let block_state = world.set_block_state( Local,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 }, &BlockPos { x: 0, y: 69, z: 0 },
BlockState::StoneSlab_TopFalse, BlockState::StoneSlab_TopFalse,
); );
@ -363,33 +523,47 @@ mod tests {
block_state.is_some(), block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
); );
let mut entity = world.entity_mut(0).unwrap();
// do a few steps so we fall on the slab // do a few steps so we fall on the slab
for _ in 0..20 { for _ in 0..20 {
entity.ai_step(); app.update();
} }
assert_eq!(entity.pos().y, 70.); let entity_pos = app.world.get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.);
} }
#[test] #[test]
fn test_weird_wall_collision() { fn test_weird_wall_collision() {
let mut world = PartialWorld::default(); let mut app = make_test_app();
world let world_lock = app.world.resource_mut::<WorldContainer>().insert(
.set_chunk(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default())) ResourceLocation::new("minecraft:overworld").unwrap(),
.unwrap(); 384,
world.add_entity( -64,
0, );
EntityData::new( let mut partial_world = PartialWorld::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world
.spawn((
EntityBundle::new(
Uuid::nil(), Uuid::nil(),
Vec3 { Vec3 {
x: 0.5, x: 0.5,
y: 73., y: 73.,
z: 0.5, z: 0.5,
}, },
EntityMetadata::Player(metadata::Player::default()), azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld").unwrap(),
), ),
); MinecraftEntityId(0),
let block_state = world.set_block_state( Local,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 }, &BlockPos { x: 0, y: 69, z: 0 },
BlockState::CobblestoneWall_LowLowLowFalseFalseLow, BlockState::CobblestoneWall_LowLowLowFalseFalseLow,
); );
@ -397,11 +571,12 @@ mod tests {
block_state.is_some(), block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed" "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
); );
let mut entity = world.entity_mut(0).unwrap();
// do a few steps so we fall on the slab // do a few steps so we fall on the slab
for _ in 0..20 { for _ in 0..20 {
entity.ai_step(); app.update();
} }
assert_eq!(entity.pos().y, 70.5);
let entity_pos = app.world.get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
} }
} }

View file

@ -22,6 +22,7 @@ azalea-nbt = {path = "../azalea-nbt", version = "^0.5.0" }
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.5.0" } azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.5.0" }
azalea-registry = {path = "../azalea-registry", version = "^0.5.0" } azalea-registry = {path = "../azalea-registry", version = "^0.5.0" }
azalea-world = {path = "../azalea-world", version = "^0.5.0" } azalea-world = {path = "../azalea-world", version = "^0.5.0" }
bevy_ecs = { version = "0.9.1", default-features = false }
byteorder = "^1.4.3" byteorder = "^1.4.3"
bytes = "^1.1.0" bytes = "^1.1.0"
flate2 = "1.0.23" flate2 = "1.0.23"

View file

@ -3,19 +3,17 @@ use quote::quote;
use syn::{ use syn::{
self, braced, self, braced,
parse::{Parse, ParseStream, Result}, parse::{Parse, ParseStream, Result},
parse_macro_input, DeriveInput, FieldsNamed, Ident, LitInt, Token, parse_macro_input, DeriveInput, Ident, LitInt, Token,
}; };
fn as_packet_derive(input: TokenStream, state: proc_macro2::TokenStream) -> TokenStream { fn as_packet_derive(input: TokenStream, state: proc_macro2::TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input); let DeriveInput { ident, data, .. } = parse_macro_input!(input);
let fields = match &data { let syn::Data::Struct(syn::DataStruct { fields, .. }) = &data else {
syn::Data::Struct(syn::DataStruct { fields, .. }) => fields, panic!("#[derive(*Packet)] can only be used on structs")
_ => panic!("#[derive(*Packet)] can only be used on structs"),
}; };
let FieldsNamed { named: _, .. } = match fields { let syn::Fields::Named(_) = fields else {
syn::Fields::Named(f) => f, panic!("#[derive(*Packet)] can only be used on structs with named fields")
_ => panic!("#[derive(*Packet)] can only be used on structs with named fields"),
}; };
let variant_name = variant_name_from(&ident); let variant_name = variant_name_from(&ident);

View file

@ -1,7 +1,7 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_core::Vec3; use azalea_core::{ResourceLocation, Vec3};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::{EntityData, EntityMetadata}; use azalea_world::entity::{metadata::apply_default_metadata, EntityBundle};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@ -10,10 +10,8 @@ pub struct ClientboundAddEntityPacket {
#[var] #[var]
pub id: u32, pub id: u32,
pub uuid: Uuid, pub uuid: Uuid,
pub entity_type: azalea_registry::EntityType, pub entity_type: azalea_registry::EntityKind,
pub x: f64, pub position: Vec3,
pub y: f64,
pub z: f64,
pub x_rot: i8, pub x_rot: i8,
pub y_rot: i8, pub y_rot: i8,
pub y_head_rot: i8, pub y_head_rot: i8,
@ -24,17 +22,31 @@ pub struct ClientboundAddEntityPacket {
pub z_vel: i16, pub z_vel: i16,
} }
impl From<&ClientboundAddEntityPacket> for EntityData { // impl From<&ClientboundAddEntityPacket> for EntityData {
fn from(p: &ClientboundAddEntityPacket) -> Self { // fn from(p: &ClientboundAddEntityPacket) -> Self {
Self::new( // Self::new(
p.uuid, // p.uuid,
Vec3 { // Vec3 {
x: p.x, // x: p.x,
y: p.y, // y: p.y,
z: p.z, // z: p.z,
}, // },
// default metadata for the entity type // // default metadata for the entity type
EntityMetadata::from(p.entity_type), // EntityMetadata::from(p.entity_type),
) // )
// }
// }
impl ClientboundAddEntityPacket {
/// Make the entity into a bundle that can be inserted into the ECS. You
/// must apply the metadata after inserting the bundle with
/// [`Self::apply_metadata`].
pub fn as_entity_bundle(&self, world_name: ResourceLocation) -> EntityBundle {
EntityBundle::new(self.uuid, self.position, self.entity_type, world_name)
}
/// Apply the default metadata for the given entity.
pub fn apply_metadata(&self, entity: &mut bevy_ecs::system::EntityCommands) {
apply_default_metadata(entity, self.entity_type);
} }
} }

View file

@ -1,7 +1,8 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_core::Vec3; use azalea_core::{ResourceLocation, Vec3};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::{metadata, EntityData, EntityMetadata}; use azalea_registry::EntityKind;
use azalea_world::entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle};
use uuid::Uuid; use uuid::Uuid;
/// This packet is sent by the server when a player comes into visible range, /// This packet is sent by the server when a player comes into visible range,
@ -11,23 +12,16 @@ pub struct ClientboundAddPlayerPacket {
#[var] #[var]
pub id: u32, pub id: u32,
pub uuid: Uuid, pub uuid: Uuid,
pub x: f64, pub position: Vec3,
pub y: f64,
pub z: f64,
pub x_rot: i8, pub x_rot: i8,
pub y_rot: i8, pub y_rot: i8,
} }
impl From<&ClientboundAddPlayerPacket> for EntityData { impl ClientboundAddPlayerPacket {
fn from(p: &ClientboundAddPlayerPacket) -> Self { pub fn as_player_bundle(&self, world_name: ResourceLocation) -> PlayerBundle {
Self::new( PlayerBundle {
p.uuid, entity: EntityBundle::new(self.uuid, self.position, EntityKind::Player, world_name),
Vec3 { metadata: PlayerMetadataBundle::default(),
x: p.x, }
y: p.y,
z: p.z,
},
EntityMetadata::Player(metadata::Player::default()),
)
} }
} }

View file

@ -16,7 +16,7 @@ pub enum Stat {
Broken(azalea_registry::Item), Broken(azalea_registry::Item),
PickedUp(azalea_registry::Item), PickedUp(azalea_registry::Item),
Dropped(azalea_registry::Item), Dropped(azalea_registry::Item),
Killed(azalea_registry::EntityType), Killed(azalea_registry::EntityKind),
KilledBy(azalea_registry::EntityType), KilledBy(azalea_registry::EntityKind),
Custom(azalea_registry::CustomStat), Custom(azalea_registry::CustomStat),
} }

View file

@ -5,6 +5,6 @@ use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundBlockEntityDataPacket { pub struct ClientboundBlockEntityDataPacket {
pub pos: BlockPos, pub pos: BlockPos,
pub block_entity_type: azalea_registry::BlockEntityType, pub block_entity_type: azalea_registry::BlockEntityKind,
pub tag: azalea_nbt::Tag, pub tag: azalea_nbt::Tag,
} }

View file

@ -1,7 +1,7 @@
use azalea_buf::{ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable, BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
}; };
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_core::FixedBitSet; use azalea_core::FixedBitSet;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use std::io::Cursor; use std::io::Cursor;
@ -19,7 +19,7 @@ pub enum Operation {
Add(AddOperation), Add(AddOperation),
Remove, Remove,
UpdateProgress(f32), UpdateProgress(f32),
UpdateName(Component), UpdateName(FormattedText),
UpdateStyle(Style), UpdateStyle(Style),
UpdateProperties(Properties), UpdateProperties(Properties),
} }
@ -31,7 +31,7 @@ impl McBufReadable for Operation {
0 => Operation::Add(AddOperation::read_from(buf)?), 0 => Operation::Add(AddOperation::read_from(buf)?),
1 => Operation::Remove, 1 => Operation::Remove,
2 => Operation::UpdateProgress(f32::read_from(buf)?), 2 => Operation::UpdateProgress(f32::read_from(buf)?),
3 => Operation::UpdateName(Component::read_from(buf)?), 3 => Operation::UpdateName(FormattedText::read_from(buf)?),
4 => Operation::UpdateStyle(Style::read_from(buf)?), 4 => Operation::UpdateStyle(Style::read_from(buf)?),
5 => Operation::UpdateProperties(Properties::read_from(buf)?), 5 => Operation::UpdateProperties(Properties::read_from(buf)?),
_ => { _ => {
@ -76,7 +76,7 @@ impl McBufWritable for Operation {
#[derive(Clone, Debug, McBuf)] #[derive(Clone, Debug, McBuf)]
pub struct AddOperation { pub struct AddOperation {
name: Component, name: FormattedText,
progress: f32, progress: f32,
style: Style, style: Style,
properties: Properties, properties: Properties,

View file

@ -1,9 +1,9 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundChatPreviewPacket { pub struct ClientboundChatPreviewPacket {
pub query_id: i32, pub query_id: i32,
pub preview: Option<Component>, pub preview: Option<FormattedText>,
} }

View file

@ -1,13 +1,13 @@
use azalea_brigadier::suggestion::Suggestions; use azalea_brigadier::suggestion::Suggestions;
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundCommandSuggestionsPacket { pub struct ClientboundCommandSuggestionsPacket {
#[var] #[var]
pub id: u32, pub id: u32,
pub suggestions: Suggestions<Component>, pub suggestions: Suggestions<FormattedText>,
} }
#[cfg(test)] #[cfg(test)]
@ -24,7 +24,7 @@ mod tests {
suggestions: vec![Suggestion { suggestions: vec![Suggestion {
text: "foo".to_string(), text: "foo".to_string(),
range: StringRange::new(1, 4), range: StringRange::new(1, 4),
tooltip: Some(Component::from("bar".to_string())), tooltip: Some(FormattedText::from("bar".to_string())),
}], }],
}; };
let mut buf = Vec::new(); let mut buf = Vec::new();

View file

@ -114,7 +114,7 @@ pub enum BrigadierParser {
ItemStack, ItemStack,
ItemPredicate, ItemPredicate,
Color, Color,
Component, FormattedText,
Message, Message,
NbtCompoundTag, NbtCompoundTag,
NbtTag, NbtTag,

View file

@ -1,8 +1,8 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundDisconnectPacket { pub struct ClientboundDisconnectPacket {
pub reason: Component, pub reason: FormattedText,
} }

View file

@ -1,10 +1,10 @@
use super::clientbound_player_chat_packet::ChatTypeBound; use super::clientbound_player_chat_packet::ChatTypeBound;
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundDisguisedChatPacket { pub struct ClientboundDisguisedChatPacket {
pub message: Component, pub message: FormattedText,
pub chat_type: ChatTypeBound, pub chat_type: ChatTypeBound,
} }

View file

@ -1,5 +1,5 @@
use azalea_buf::{McBuf, McBufReadable, McBufWritable}; use azalea_buf::{McBuf, McBufReadable, McBufWritable};
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, ClientboundGamePacket, McBuf)] #[derive(Clone, Debug, ClientboundGamePacket, McBuf)]
@ -20,7 +20,7 @@ pub struct MapDecoration {
/// Minecraft does & 15 on this value, azalea-protocol doesn't. I don't /// Minecraft does & 15 on this value, azalea-protocol doesn't. I don't
/// think it matters. /// think it matters.
pub rot: i8, pub rot: i8,
pub name: Option<Component>, pub name: Option<FormattedText>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@ -7,5 +7,5 @@ pub struct ClientboundOpenScreenPacket {
#[var] #[var]
pub container_id: u32, pub container_id: u32,
pub menu_type: azalea_registry::Menu, pub menu_type: azalea_registry::Menu,
pub title: Component, pub title: FormattedText,
} }

View file

@ -3,7 +3,7 @@ use azalea_buf::{
}; };
use azalea_chat::{ use azalea_chat::{
translatable_component::{StringOrComponent, TranslatableComponent}, translatable_component::{StringOrComponent, TranslatableComponent},
Component, FormattedText,
}; };
use azalea_core::BitSet; use azalea_core::BitSet;
use azalea_crypto::MessageSignature; use azalea_crypto::MessageSignature;
@ -18,7 +18,7 @@ pub struct ClientboundPlayerChatPacket {
pub index: u32, pub index: u32,
pub signature: Option<MessageSignature>, pub signature: Option<MessageSignature>,
pub body: PackedSignedMessageBody, pub body: PackedSignedMessageBody,
pub unsigned_content: Option<Component>, pub unsigned_content: Option<FormattedText>,
pub filter_mask: FilterMask, pub filter_mask: FilterMask,
pub chat_type: ChatTypeBound, pub chat_type: ChatTypeBound,
} }
@ -66,8 +66,8 @@ pub enum ChatType {
#[derive(Clone, Debug, McBuf, PartialEq)] #[derive(Clone, Debug, McBuf, PartialEq)]
pub struct ChatTypeBound { pub struct ChatTypeBound {
pub chat_type: ChatType, pub chat_type: ChatType,
pub name: Component, pub name: FormattedText,
pub target_name: Option<Component>, pub target_name: Option<FormattedText>,
} }
// must be in Client // must be in Client
@ -87,19 +87,19 @@ pub struct MessageSignatureCache {
// {} } // {} }
impl ClientboundPlayerChatPacket { impl ClientboundPlayerChatPacket {
/// Returns the content of the message. If you want to get the Component /// Returns the content of the message. If you want to get the FormattedText
/// for the whole message including the sender part, use /// for the whole message including the sender part, use
/// [`ClientboundPlayerChatPacket::message`]. /// [`ClientboundPlayerChatPacket::message`].
#[must_use] #[must_use]
pub fn content(&self) -> Component { pub fn content(&self) -> FormattedText {
self.unsigned_content self.unsigned_content
.clone() .clone()
.unwrap_or_else(|| Component::from(self.body.content.clone())) .unwrap_or_else(|| FormattedText::from(self.body.content.clone()))
} }
/// Get the full message, including the sender part. /// Get the full message, including the sender part.
#[must_use] #[must_use]
pub fn message(&self) -> Component { pub fn message(&self) -> FormattedText {
let sender = self.chat_type.name.clone(); let sender = self.chat_type.name.clone();
let content = self.content(); let content = self.content();
let target = self.chat_type.target_name.clone(); let target = self.chat_type.target_name.clone();
@ -107,16 +107,16 @@ impl ClientboundPlayerChatPacket {
let translation_key = self.chat_type.chat_type.chat_translation_key(); let translation_key = self.chat_type.chat_type.chat_translation_key();
let mut args = vec![ let mut args = vec![
StringOrComponent::Component(sender), StringOrComponent::FormattedText(sender),
StringOrComponent::Component(content), StringOrComponent::FormattedText(content),
]; ];
if let Some(target) = target { if let Some(target) = target {
args.push(StringOrComponent::Component(target)); args.push(StringOrComponent::FormattedText(target));
} }
let component = TranslatableComponent::new(translation_key.to_string(), args); let component = TranslatableComponent::new(translation_key.to_string(), args);
Component::Translatable(component) FormattedText::Translatable(component)
} }
} }

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
/// Used to send a respawn screen. /// Used to send a respawn screen.
@ -8,5 +8,5 @@ pub struct ClientboundPlayerCombatKillPacket {
#[var] #[var]
pub player_id: u32, pub player_id: u32,
pub killer_id: u32, pub killer_id: u32,
pub message: Component, pub message: FormattedText,
} }

View file

@ -2,7 +2,7 @@ use azalea_auth::game_profile::{GameProfile, ProfilePropertyValue};
use azalea_buf::{ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable, BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
}; };
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_core::{FixedBitSet, GameType}; use azalea_core::{FixedBitSet, GameType};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use std::{ use std::{
@ -25,7 +25,7 @@ pub struct PlayerInfoEntry {
pub listed: bool, pub listed: bool,
pub latency: i32, pub latency: i32,
pub game_mode: GameType, pub game_mode: GameType,
pub display_name: Option<Component>, pub display_name: Option<FormattedText>,
pub chat_session: Option<RemoteChatSessionData>, pub chat_session: Option<RemoteChatSessionData>,
} }
@ -53,7 +53,7 @@ pub struct UpdateLatencyAction {
} }
#[derive(Clone, Debug, McBuf)] #[derive(Clone, Debug, McBuf)]
pub struct UpdateDisplayNameAction { pub struct UpdateDisplayNameAction {
pub display_name: Option<Component>, pub display_name: Option<FormattedText>,
} }
impl McBufReadable for ClientboundPlayerInfoUpdatePacket { impl McBufReadable for ClientboundPlayerInfoUpdatePacket {

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@ -7,5 +7,5 @@ pub struct ClientboundResourcePackPacket {
pub url: String, pub url: String,
pub hash: String, pub hash: String,
pub required: bool, pub required: bool,
pub prompt: Option<Component>, pub prompt: Option<FormattedText>,
} }

View file

@ -1,10 +1,10 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundServerDataPacket { pub struct ClientboundServerDataPacket {
pub motd: Option<Component>, pub motd: Option<FormattedText>,
pub icon_base64: Option<String>, pub icon_base64: Option<String>,
pub enforces_secure_chat: bool, pub enforces_secure_chat: bool,
} }

View file

@ -1,8 +1,8 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundSetActionBarTextPacket { pub struct ClientboundSetActionBarTextPacket {
pub text: Component, pub text: FormattedText,
} }

View file

@ -1,5 +1,5 @@
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable}; use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
@ -48,7 +48,7 @@ impl McBufWritable for Method {
#[derive(McBuf, Clone, Debug)] #[derive(McBuf, Clone, Debug)]
pub struct DisplayInfo { pub struct DisplayInfo {
pub display_name: Component, pub display_name: FormattedText,
pub render_type: RenderType, pub render_type: RenderType,
} }

View file

@ -1,5 +1,5 @@
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable}; use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use azalea_chat::{style::ChatFormatting, Component}; use azalea_chat::{style::ChatFormatting, FormattedText};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
@ -61,13 +61,13 @@ impl McBufWritable for Method {
#[derive(McBuf, Clone, Debug)] #[derive(McBuf, Clone, Debug)]
pub struct Parameters { pub struct Parameters {
pub display_name: Component, pub display_name: FormattedText,
pub options: u8, pub options: u8,
pub nametag_visibility: String, pub nametag_visibility: String,
pub collision_rule: String, pub collision_rule: String,
pub color: ChatFormatting, pub color: ChatFormatting,
pub player_prefix: Component, pub player_prefix: FormattedText,
pub player_suffix: Component, pub player_suffix: FormattedText,
} }
type PlayerList = Vec<String>; type PlayerList = Vec<String>;

View file

@ -1,8 +1,8 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundSetSubtitleTextPacket { pub struct ClientboundSetSubtitleTextPacket {
pub text: Component, pub text: FormattedText,
} }

View file

@ -1,8 +1,8 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundSetTitleTextPacket { pub struct ClientboundSetTitleTextPacket {
pub text: Component, pub text: FormattedText,
} }

View file

@ -1,9 +1,9 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket, PartialEq)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket, PartialEq)]
pub struct ClientboundSystemChatPacket { pub struct ClientboundSystemChatPacket {
pub content: Component, pub content: FormattedText,
pub overlay: bool, pub overlay: bool,
} }

View file

@ -1,9 +1,9 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundTabListPacket { pub struct ClientboundTabListPacket {
pub header: Component, pub header: FormattedText,
pub footer: Component, pub footer: FormattedText,
} }

View file

@ -1,13 +1,12 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_core::Vec3;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundTeleportEntityPacket { pub struct ClientboundTeleportEntityPacket {
#[var] #[var]
pub id: u32, pub id: u32,
pub x: f64, pub position: Vec3,
pub y: f64,
pub z: f64,
pub y_rot: i8, pub y_rot: i8,
pub x_rot: i8, pub x_rot: i8,
pub on_ground: bool, pub on_ground: bool,

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_core::{ResourceLocation, Slot}; use azalea_core::{ResourceLocation, Slot};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use std::collections::HashMap; use std::collections::HashMap;
@ -23,8 +23,8 @@ pub struct Advancement {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DisplayInfo { pub struct DisplayInfo {
pub title: Component, pub title: FormattedText,
pub description: Component, pub description: FormattedText,
pub icon: Slot, pub icon: Slot,
pub frame: FrameType, pub frame: FrameType,
pub show_toast: bool, pub show_toast: bool,
@ -128,8 +128,8 @@ mod tests {
Advancement { Advancement {
parent_id: None, parent_id: None,
display: Some(DisplayInfo { display: Some(DisplayInfo {
title: Component::from("title".to_string()), title: FormattedText::from("title".to_string()),
description: Component::from("description".to_string()), description: FormattedText::from("description".to_string()),
icon: Slot::Empty, icon: Slot::Empty,
frame: FrameType::Task, frame: FrameType::Task,
show_toast: true, show_toast: true,

View file

@ -15,8 +15,7 @@ pub struct ServerboundSetJigsawBlockPacket {
pub target: ResourceLocation, pub target: ResourceLocation,
pub pool: ResourceLocation, pub pool: ResourceLocation,
pub final_state: String, pub final_state: String,
pub joint: String, /* TODO: Does JigsawBlockEntity$JointType::getSerializedName, may not be pub joint: String,
* implemented */
} }
pub enum JointType { pub enum JointType {

View file

@ -1,8 +1,8 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundLoginPacket; use azalea_protocol_macros::ClientboundLoginPacket;
#[derive(Clone, Debug, McBuf, ClientboundLoginPacket)] #[derive(Clone, Debug, McBuf, ClientboundLoginPacket)]
pub struct ClientboundLoginDisconnectPacket { pub struct ClientboundLoginDisconnectPacket {
pub reason: Component, pub reason: FormattedText,
} }

View file

@ -1,5 +1,5 @@
use azalea_buf::{BufReadError, McBufReadable, McBufWritable}; use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
use azalea_chat::Component; use azalea_chat::FormattedText;
use azalea_protocol_macros::ClientboundStatusPacket; use azalea_protocol_macros::ClientboundStatusPacket;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{value::Serializer, Value}; use serde_json::{value::Serializer, Value};
@ -28,7 +28,7 @@ pub struct Players {
// the entire packet is just json, which is why it has deserialize // the entire packet is just json, which is why it has deserialize
#[derive(Clone, Debug, Serialize, Deserialize, ClientboundStatusPacket)] #[derive(Clone, Debug, Serialize, Deserialize, ClientboundStatusPacket)]
pub struct ClientboundStatusResponsePacket { pub struct ClientboundStatusResponsePacket {
pub description: Component, pub description: FormattedText,
#[serde(default)] #[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub favicon: Option<String>, pub favicon: Option<String>,

View file

@ -3,11 +3,12 @@ description = "Use Minecraft's registries."
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "azalea-registry" name = "azalea-registry"
version = "0.5.0"
repository = "https://github.com/mat-1/azalea/tree/main/azalea-registry" repository = "https://github.com/mat-1/azalea/tree/main/azalea-registry"
version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
azalea-buf = {path = "../azalea-buf", version = "^0.5.0"} azalea-buf = {path = "../azalea-buf", version = "^0.5.0"}
azalea-registry-macros = {path = "./azalea-registry-macros", version = "^0.5.0"} azalea-registry-macros = {path = "./azalea-registry-macros", version = "^0.5.0"}
enum-as-inner = "0.5.1"

View file

@ -128,7 +128,7 @@ pub fn registry(input: TokenStream) -> TokenStream {
// Display that uses registry ids // Display that uses registry ids
let mut display_items = quote! {}; let mut display_items = quote! {};
for item in input.items.iter() { for item in &input.items {
let name = &item.name; let name = &item.name;
let id = &item.id; let id = &item.id;
display_items.extend(quote! { display_items.extend(quote! {

View file

@ -1103,7 +1103,7 @@ registry!(Block, {
ReinforcedDeepslate => "minecraft:reinforced_deepslate", ReinforcedDeepslate => "minecraft:reinforced_deepslate",
}); });
registry!(BlockEntityType, { registry!(BlockEntityKind, {
Furnace => "minecraft:furnace", Furnace => "minecraft:furnace",
Chest => "minecraft:chest", Chest => "minecraft:chest",
TrappedChest => "minecraft:trapped_chest", TrappedChest => "minecraft:trapped_chest",
@ -1144,7 +1144,7 @@ registry!(BlockEntityType, {
ChiseledBookshelf => "minecraft:chiseled_bookshelf", ChiseledBookshelf => "minecraft:chiseled_bookshelf",
}); });
registry!(BlockPredicateType, { registry!(BlockPredicateKind, {
MatchingBlocks => "minecraft:matching_blocks", MatchingBlocks => "minecraft:matching_blocks",
MatchingBlockTag => "minecraft:matching_block_tag", MatchingBlockTag => "minecraft:matching_block_tag",
MatchingFluids => "minecraft:matching_fluids", MatchingFluids => "minecraft:matching_fluids",
@ -1189,7 +1189,7 @@ registry!(ChunkStatus, {
Full => "minecraft:full", Full => "minecraft:full",
}); });
registry!(CommandArgumentType, { registry!(CommandArgumentKind, {
Bool => "brigadier:bool", Bool => "brigadier:bool",
Float => "brigadier:float", Float => "brigadier:float",
Double => "brigadier:double", Double => "brigadier:double",
@ -1360,7 +1360,7 @@ registry!(Enchantment, {
VanishingCurse => "minecraft:vanishing_curse", VanishingCurse => "minecraft:vanishing_curse",
}); });
registry!(EntityType, { registry!(EntityKind, {
Allay => "minecraft:allay", Allay => "minecraft:allay",
AreaEffectCloud => "minecraft:area_effect_cloud", AreaEffectCloud => "minecraft:area_effect_cloud",
ArmorStand => "minecraft:armor_stand", ArmorStand => "minecraft:armor_stand",
@ -1482,7 +1482,7 @@ registry!(EntityType, {
FishingBobber => "minecraft:fishing_bobber", FishingBobber => "minecraft:fishing_bobber",
}); });
registry!(FloatProviderType, { registry!(FloatProviderKind, {
Constant => "minecraft:constant", Constant => "minecraft:constant",
Uniform => "minecraft:uniform", Uniform => "minecraft:uniform",
ClampedNormal => "minecraft:clamped_normal", ClampedNormal => "minecraft:clamped_normal",
@ -1552,7 +1552,7 @@ registry!(GameEvent, {
Teleport => "minecraft:teleport", Teleport => "minecraft:teleport",
}); });
registry!(HeightProviderType, { registry!(HeightProviderKind, {
Constant => "minecraft:constant", Constant => "minecraft:constant",
Uniform => "minecraft:uniform", Uniform => "minecraft:uniform",
BiasedToBottom => "minecraft:biased_to_bottom", BiasedToBottom => "minecraft:biased_to_bottom",
@ -1572,7 +1572,7 @@ registry!(Instrument, {
DreamGoatHorn => "minecraft:dream_goat_horn", DreamGoatHorn => "minecraft:dream_goat_horn",
}); });
registry!(IntProviderType, { registry!(IntProviderKind, {
Constant => "minecraft:constant", Constant => "minecraft:constant",
Uniform => "minecraft:uniform", Uniform => "minecraft:uniform",
BiasedToBottom => "minecraft:biased_to_bottom", BiasedToBottom => "minecraft:biased_to_bottom",
@ -2770,7 +2770,7 @@ registry!(Item, {
EchoShard => "minecraft:echo_shard", EchoShard => "minecraft:echo_shard",
}); });
registry!(LootConditionType, { registry!(LootConditionKind, {
Inverted => "minecraft:inverted", Inverted => "minecraft:inverted",
Alternative => "minecraft:alternative", Alternative => "minecraft:alternative",
RandomChance => "minecraft:random_chance", RandomChance => "minecraft:random_chance",
@ -2790,7 +2790,7 @@ registry!(LootConditionType, {
ValueCheck => "minecraft:value_check", ValueCheck => "minecraft:value_check",
}); });
registry!(LootFunctionType, { registry!(LootFunctionKind, {
SetCount => "minecraft:set_count", SetCount => "minecraft:set_count",
EnchantWithLevels => "minecraft:enchant_with_levels", EnchantWithLevels => "minecraft:enchant_with_levels",
EnchantRandomly => "minecraft:enchant_randomly", EnchantRandomly => "minecraft:enchant_randomly",
@ -2818,19 +2818,19 @@ registry!(LootFunctionType, {
SetInstrument => "minecraft:set_instrument", SetInstrument => "minecraft:set_instrument",
}); });
registry!(LootNbtProviderType, { registry!(LootNbtProviderKind, {
Storage => "minecraft:storage", Storage => "minecraft:storage",
Context => "minecraft:context", Context => "minecraft:context",
}); });
registry!(LootNumberProviderType, { registry!(LootNumberProviderKind, {
Constant => "minecraft:constant", Constant => "minecraft:constant",
Uniform => "minecraft:uniform", Uniform => "minecraft:uniform",
Binomial => "minecraft:binomial", Binomial => "minecraft:binomial",
Score => "minecraft:score", Score => "minecraft:score",
}); });
registry!(LootPoolEntryType, { registry!(LootPoolEntryKind, {
Empty => "minecraft:empty", Empty => "minecraft:empty",
Item => "minecraft:item", Item => "minecraft:item",
LootTable => "minecraft:loot_table", LootTable => "minecraft:loot_table",
@ -2841,12 +2841,12 @@ registry!(LootPoolEntryType, {
Group => "minecraft:group", Group => "minecraft:group",
}); });
registry!(LootScoreProviderType, { registry!(LootScoreProviderKind, {
Fixed => "minecraft:fixed", Fixed => "minecraft:fixed",
Context => "minecraft:context", Context => "minecraft:context",
}); });
registry!(MemoryModuleType, { registry!(MemoryModuleKind, {
Dummy => "minecraft:dummy", Dummy => "minecraft:dummy",
Home => "minecraft:home", Home => "minecraft:home",
JobSite => "minecraft:job_site", JobSite => "minecraft:job_site",
@ -3038,7 +3038,7 @@ registry!(PaintingVariant, {
DonkeyKong => "minecraft:donkey_kong", DonkeyKong => "minecraft:donkey_kong",
}); });
registry!(ParticleType, { registry!(ParticleKind, {
AmbientEntityEffect => "minecraft:ambient_entity_effect", AmbientEntityEffect => "minecraft:ambient_entity_effect",
AngryVillager => "minecraft:angry_villager", AngryVillager => "minecraft:angry_villager",
Block => "minecraft:block", Block => "minecraft:block",
@ -3134,7 +3134,7 @@ registry!(ParticleType, {
Shriek => "minecraft:shriek", Shriek => "minecraft:shriek",
}); });
registry!(PointOfInterestType, { registry!(PointOfInterestKind, {
Armorer => "minecraft:armorer", Armorer => "minecraft:armorer",
Butcher => "minecraft:butcher", Butcher => "minecraft:butcher",
Cartographer => "minecraft:cartographer", Cartographer => "minecraft:cartographer",
@ -3163,7 +3163,7 @@ registry!(PosRuleTest, {
AxisAlignedLinearPos => "minecraft:axis_aligned_linear_pos", AxisAlignedLinearPos => "minecraft:axis_aligned_linear_pos",
}); });
registry!(PositionSourceType, { registry!(PositionSourceKind, {
Block => "minecraft:block", Block => "minecraft:block",
Entity => "minecraft:entity", Entity => "minecraft:entity",
}); });
@ -3238,7 +3238,7 @@ registry!(RecipeSerializer, {
Smithing => "minecraft:smithing", Smithing => "minecraft:smithing",
}); });
registry!(RecipeType, { registry!(RecipeKind, {
Crafting => "minecraft:crafting", Crafting => "minecraft:crafting",
Smelting => "minecraft:smelting", Smelting => "minecraft:smelting",
Blasting => "minecraft:blasting", Blasting => "minecraft:blasting",
@ -3264,7 +3264,7 @@ registry!(Schedule, {
VillagerDefault => "minecraft:villager_default", VillagerDefault => "minecraft:villager_default",
}); });
registry!(SensorType, { registry!(SensorKind, {
Dummy => "minecraft:dummy", Dummy => "minecraft:dummy",
NearestItems => "minecraft:nearest_items", NearestItems => "minecraft:nearest_items",
NearestLivingEntities => "minecraft:nearest_living_entities", NearestLivingEntities => "minecraft:nearest_living_entities",
@ -4684,7 +4684,7 @@ registry!(SoundEvent, {
EntityZombieVillagerStep => "minecraft:entity.zombie_villager.step", EntityZombieVillagerStep => "minecraft:entity.zombie_villager.step",
}); });
registry!(StatType, { registry!(StatKind, {
Mined => "minecraft:mined", Mined => "minecraft:mined",
Crafted => "minecraft:crafted", Crafted => "minecraft:crafted",
Used => "minecraft:used", Used => "minecraft:used",
@ -4714,7 +4714,7 @@ registry!(VillagerProfession, {
Weaponsmith => "minecraft:weaponsmith", Weaponsmith => "minecraft:weaponsmith",
}); });
registry!(VillagerType, { registry!(VillagerKind, {
Desert => "minecraft:desert", Desert => "minecraft:desert",
Jungle => "minecraft:jungle", Jungle => "minecraft:jungle",
Plains => "minecraft:plains", Plains => "minecraft:plains",
@ -4731,7 +4731,7 @@ registry!(WorldgenBiomeSource, {
TheEnd => "minecraft:the_end", TheEnd => "minecraft:the_end",
}); });
registry!(WorldgenBlockStateProviderType, { registry!(WorldgenBlockStateProviderKind, {
SimpleStateProvider => "minecraft:simple_state_provider", SimpleStateProvider => "minecraft:simple_state_provider",
WeightedStateProvider => "minecraft:weighted_state_provider", WeightedStateProvider => "minecraft:weighted_state_provider",
NoiseThresholdProvider => "minecraft:noise_threshold_provider", NoiseThresholdProvider => "minecraft:noise_threshold_provider",
@ -4753,7 +4753,7 @@ registry!(WorldgenChunkGenerator, {
Debug => "minecraft:debug", Debug => "minecraft:debug",
}); });
registry!(WorldgenDensityFunctionType, { registry!(WorldgenDensityFunctionKind, {
BlendAlpha => "minecraft:blend_alpha", BlendAlpha => "minecraft:blend_alpha",
BlendOffset => "minecraft:blend_offset", BlendOffset => "minecraft:blend_offset",
Beardifier => "minecraft:beardifier", Beardifier => "minecraft:beardifier",
@ -4852,12 +4852,12 @@ registry!(WorldgenFeature, {
SculkPatch => "minecraft:sculk_patch", SculkPatch => "minecraft:sculk_patch",
}); });
registry!(WorldgenFeatureSizeType, { registry!(WorldgenFeatureSizeKind, {
TwoLayersFeatureSize => "minecraft:two_layers_feature_size", TwoLayersFeatureSize => "minecraft:two_layers_feature_size",
ThreeLayersFeatureSize => "minecraft:three_layers_feature_size", ThreeLayersFeatureSize => "minecraft:three_layers_feature_size",
}); });
registry!(WorldgenFoliagePlacerType, { registry!(WorldgenFoliagePlacerKind, {
BlobFoliagePlacer => "minecraft:blob_foliage_placer", BlobFoliagePlacer => "minecraft:blob_foliage_placer",
SpruceFoliagePlacer => "minecraft:spruce_foliage_placer", SpruceFoliagePlacer => "minecraft:spruce_foliage_placer",
PineFoliagePlacer => "minecraft:pine_foliage_placer", PineFoliagePlacer => "minecraft:pine_foliage_placer",
@ -4891,7 +4891,7 @@ registry!(WorldgenMaterialRule, {
Condition => "minecraft:condition", Condition => "minecraft:condition",
}); });
registry!(WorldgenPlacementModifierType, { registry!(WorldgenPlacementModifierKind, {
BlockPredicateFilter => "minecraft:block_predicate_filter", BlockPredicateFilter => "minecraft:block_predicate_filter",
RarityFilter => "minecraft:rarity_filter", RarityFilter => "minecraft:rarity_filter",
SurfaceRelativeThresholdFilter => "minecraft:surface_relative_threshold_filter", SurfaceRelativeThresholdFilter => "minecraft:surface_relative_threshold_filter",
@ -4909,7 +4909,7 @@ registry!(WorldgenPlacementModifierType, {
CarvingMask => "minecraft:carving_mask", CarvingMask => "minecraft:carving_mask",
}); });
registry!(WorldgenRootPlacerType, { registry!(WorldgenRootPlacerKind, {
MangroveRootPlacer => "minecraft:mangrove_root_placer", MangroveRootPlacer => "minecraft:mangrove_root_placer",
}); });
@ -4998,7 +4998,7 @@ registry!(WorldgenStructureProcessor, {
ProtectedBlocks => "minecraft:protected_blocks", ProtectedBlocks => "minecraft:protected_blocks",
}); });
registry!(WorldgenStructureType, { registry!(WorldgenStructureKind, {
BuriedTreasure => "minecraft:buried_treasure", BuriedTreasure => "minecraft:buried_treasure",
DesertPyramid => "minecraft:desert_pyramid", DesertPyramid => "minecraft:desert_pyramid",
EndCity => "minecraft:end_city", EndCity => "minecraft:end_city",
@ -5017,7 +5017,7 @@ registry!(WorldgenStructureType, {
WoodlandMansion => "minecraft:woodland_mansion", WoodlandMansion => "minecraft:woodland_mansion",
}); });
registry!(WorldgenTreeDecoratorType, { registry!(WorldgenTreeDecoratorKind, {
TrunkVine => "minecraft:trunk_vine", TrunkVine => "minecraft:trunk_vine",
LeaveVine => "minecraft:leave_vine", LeaveVine => "minecraft:leave_vine",
Cocoa => "minecraft:cocoa", Cocoa => "minecraft:cocoa",
@ -5026,7 +5026,7 @@ registry!(WorldgenTreeDecoratorType, {
AttachedToLeaves => "minecraft:attached_to_leaves", AttachedToLeaves => "minecraft:attached_to_leaves",
}); });
registry!(WorldgenTrunkPlacerType, { registry!(WorldgenTrunkPlacerKind, {
StraightTrunkPlacer => "minecraft:straight_trunk_placer", StraightTrunkPlacer => "minecraft:straight_trunk_placer",
ForkingTrunkPlacer => "minecraft:forking_trunk_placer", ForkingTrunkPlacer => "minecraft:forking_trunk_placer",
GiantTrunkPlacer => "minecraft:giant_trunk_placer", GiantTrunkPlacer => "minecraft:giant_trunk_placer",

View file

@ -12,12 +12,16 @@ version = "0.5.0"
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.5.0"} azalea-block = {path = "../azalea-block", default-features = false, version = "^0.5.0"}
azalea-buf = {path = "../azalea-buf", version = "^0.5.0"} azalea-buf = {path = "../azalea-buf", version = "^0.5.0"}
azalea-chat = {path = "../azalea-chat", version = "^0.5.0"} azalea-chat = {path = "../azalea-chat", version = "^0.5.0"}
azalea-core = {path = "../azalea-core", version = "^0.5.0" } azalea-core = {path = "../azalea-core", version = "^0.5.0", features = ["bevy_ecs"]}
azalea-ecs = { version = "0.5.0", path = "../azalea-ecs" }
azalea-nbt = {path = "../azalea-nbt", version = "^0.5.0"} azalea-nbt = {path = "../azalea-nbt", version = "^0.5.0"}
azalea-registry = {path = "../azalea-registry", version = "^0.5.0"} azalea-registry = {path = "../azalea-registry", version = "^0.5.0"}
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
enum-as-inner = "0.5.1" enum-as-inner = "0.5.1"
iyes_loopless = "0.9.1"
log = "0.4.17" log = "0.4.17"
nohash-hasher = "0.2.0" nohash-hasher = "0.2.0"
once_cell = "1.16.0"
parking_lot = "^0.12.1" parking_lot = "^0.12.1"
thiserror = "1.0.34" thiserror = "1.0.34"
uuid = "1.1.2" uuid = "1.1.2"

View file

@ -15,13 +15,10 @@ use std::{
const SECTION_HEIGHT: u32 = 16; const SECTION_HEIGHT: u32 = 16;
/// An efficient storage of chunks for a client that has a limited render /// An efficient storage of chunks for a client that has a limited render
/// distance. This has support for using a shared [`WeakChunkStorage`]. If you /// distance. This has support for using a shared [`ChunkStorage`].
/// have an infinite render distance (like a server), you should use
/// [`ChunkStorage`] instead.
pub struct PartialChunkStorage { pub struct PartialChunkStorage {
/// Chunk storage that can be shared by clients. /// The center of the view, i.e. the chunk the player is currently in. You
pub shared: Arc<RwLock<WeakChunkStorage>>, /// can safely modify this.
pub view_center: ChunkPos, pub view_center: ChunkPos,
chunk_radius: u32, chunk_radius: u32,
view_range: u32, view_range: u32,
@ -33,23 +30,16 @@ pub struct PartialChunkStorage {
/// actively being used somewhere else they'll be forgotten. This is used for /// actively being used somewhere else they'll be forgotten. This is used for
/// shared worlds. /// shared worlds.
#[derive(Debug)] #[derive(Debug)]
pub struct WeakChunkStorage { pub struct ChunkStorage {
pub height: u32, pub height: u32,
pub min_y: i32, pub min_y: i32,
pub chunks: HashMap<ChunkPos, Weak<RwLock<Chunk>>>, pub chunks: HashMap<ChunkPos, Weak<RwLock<Chunk>>>,
} }
/// A storage of potentially infinite chunks in a world. Chunks are stored as
/// an `Arc<Mutex>` so they can be shared across threads.
pub struct ChunkStorage {
pub height: u32,
pub min_y: i32,
pub chunks: HashMap<ChunkPos, Arc<RwLock<Chunk>>>,
}
/// A single chunk in a world (16*?*16 blocks). This only contains the blocks /// A single chunk in a world (16*?*16 blocks). This only contains the blocks
/// and biomes. You can derive the height of the chunk from the number of /// and biomes. You can derive the height of the chunk from the number of
/// sections, but you need a [`ChunkStorage`] to get the minimum Y coordinate. /// sections, but you need a [`ChunkStorage`] to get the minimum Y
/// coordinate.
#[derive(Debug)] #[derive(Debug)]
pub struct Chunk { pub struct Chunk {
pub sections: Vec<Section>, pub sections: Vec<Section>,
@ -82,10 +72,9 @@ impl Default for Chunk {
} }
impl PartialChunkStorage { impl PartialChunkStorage {
pub fn new(chunk_radius: u32, shared: Arc<RwLock<WeakChunkStorage>>) -> Self { pub fn new(chunk_radius: u32) -> Self {
let view_range = chunk_radius * 2 + 1; let view_range = chunk_radius * 2 + 1;
PartialChunkStorage { PartialChunkStorage {
shared,
view_center: ChunkPos::new(0, 0), view_center: ChunkPos::new(0, 0),
chunk_radius, chunk_radius,
view_range, view_range,
@ -93,13 +82,6 @@ impl PartialChunkStorage {
} }
} }
pub fn min_y(&self) -> i32 {
self.shared.read().min_y
}
pub fn height(&self) -> u32 {
self.shared.read().height
}
fn get_index(&self, chunk_pos: &ChunkPos) -> usize { fn get_index(&self, chunk_pos: &ChunkPos) -> usize {
(i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32) (i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32)
+ i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize + i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize
@ -110,20 +92,28 @@ impl PartialChunkStorage {
&& (chunk_pos.z - self.view_center.z).unsigned_abs() <= self.chunk_radius && (chunk_pos.z - self.view_center.z).unsigned_abs() <= self.chunk_radius
} }
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> { pub fn set_block_state(
if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) { &self,
pos: &BlockPos,
state: BlockState,
chunk_storage: &mut ChunkStorage,
) -> Option<BlockState> {
if pos.y < chunk_storage.min_y
|| pos.y >= (chunk_storage.min_y + chunk_storage.height as i32)
{
return None; return None;
} }
let chunk_pos = ChunkPos::from(pos); let chunk_pos = ChunkPos::from(pos);
let chunk = self.get(&chunk_pos)?; let chunk_lock = chunk_storage.get(&chunk_pos)?;
let mut chunk = chunk.write(); let mut chunk = chunk_lock.write();
Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y())) Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, chunk_storage.min_y))
} }
pub fn replace_with_packet_data( pub fn replace_with_packet_data(
&mut self, &mut self,
pos: &ChunkPos, pos: &ChunkPos,
data: &mut Cursor<&[u8]>, data: &mut Cursor<&[u8]>,
chunk_storage: &mut ChunkStorage,
) -> Result<(), BufReadError> { ) -> Result<(), BufReadError> {
debug!("Replacing chunk at {:?}", pos); debug!("Replacing chunk at {:?}", pos);
if !self.in_range(pos) { if !self.in_range(pos) {
@ -135,19 +125,16 @@ impl PartialChunkStorage {
return Ok(()); return Ok(());
} }
let chunk = Arc::new(RwLock::new(Chunk::read_with_dimension_height( let chunk = Chunk::read_with_dimension_height(data, chunk_storage.height)?;
data,
self.height(),
)?));
trace!("Loaded chunk {:?}", pos); trace!("Loaded chunk {:?}", pos);
self.set(pos, Some(chunk)); self.set(pos, Some(chunk), chunk_storage);
Ok(()) Ok(())
} }
/// Get a [`Chunk`] within render distance, or `None` if it's not loaded. /// Get a [`Chunk`] within render distance, or `None` if it's not loaded.
/// Use [`PartialChunkStorage::get`] to get a chunk from the shared storage. /// Use [`ChunkStorage::get`] to get a chunk from the shared storage.
pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> { pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> {
if !self.in_range(pos) { if !self.in_range(pos) {
warn!( warn!(
@ -161,7 +148,7 @@ impl PartialChunkStorage {
self.chunks[index].as_ref() self.chunks[index].as_ref()
} }
/// Get a mutable reference to a [`Chunk`] within render distance, or /// Get a mutable reference to a [`Chunk`] within render distance, or
/// `None` if it's not loaded. Use [`PartialChunkStorage::get`] to get /// `None` if it's not loaded. Use [`ChunkStorage::get`] to get
/// a chunk from the shared storage. /// a chunk from the shared storage.
pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> { pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> {
if !self.in_range(pos) { if !self.in_range(pos) {
@ -172,26 +159,30 @@ impl PartialChunkStorage {
Some(&mut self.chunks[index]) Some(&mut self.chunks[index])
} }
/// Get a chunk,
pub fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
self.shared
.read()
.chunks
.get(pos)
.and_then(|chunk| chunk.upgrade())
}
/// Set a chunk in the shared storage and reference it from the limited /// Set a chunk in the shared storage and reference it from the limited
/// storage. /// storage. Use [`Self::set_with_shared_reference`] if you already have
/// an `Arc<RwLock<Chunk>>`.
/// ///
/// # Panics /// # Panics
/// If the chunk is not in the render distance. /// If the chunk is not in the render distance.
pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) { pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Chunk>, chunk_storage: &mut ChunkStorage) {
self.set_with_shared_reference(pos, chunk.map(|c| Arc::new(RwLock::new(c))), chunk_storage);
}
/// Set a chunk in the shared storage and reference it from the limited
/// storage. Use [`Self::set`] if you don't already have an
/// `Arc<RwLock<Chunk>>` (it'll make it for you).
///
/// # Panics
/// If the chunk is not in the render distance.
pub fn set_with_shared_reference(
&mut self,
pos: &ChunkPos,
chunk: Option<Arc<RwLock<Chunk>>>,
chunk_storage: &mut ChunkStorage,
) {
if let Some(chunk) = &chunk { if let Some(chunk) = &chunk {
self.shared chunk_storage.chunks.insert(*pos, Arc::downgrade(chunk));
.write()
.chunks
.insert(*pos, Arc::downgrade(chunk));
} else { } else {
// don't remove it from the shared storage, since it'll be removed // don't remove it from the shared storage, since it'll be removed
// automatically if this was the last reference // automatically if this was the last reference
@ -201,9 +192,9 @@ impl PartialChunkStorage {
} }
} }
} }
impl WeakChunkStorage { impl ChunkStorage {
pub fn new(height: u32, min_y: i32) -> Self { pub fn new(height: u32, min_y: i32) -> Self {
WeakChunkStorage { ChunkStorage {
height, height,
min_y, min_y,
chunks: HashMap::new(), chunks: HashMap::new(),
@ -280,7 +271,7 @@ impl Chunk {
// TODO: make sure the section exists // TODO: make sure the section exists
let section = &mut self.sections[section_index as usize]; let section = &mut self.sections[section_index as usize];
let chunk_section_pos = ChunkSectionBlockPos::from(pos); let chunk_section_pos = ChunkSectionBlockPos::from(pos);
section.set(chunk_section_pos, state) section.set(chunk_section_pos, state);
} }
} }
@ -295,12 +286,10 @@ impl McBufWritable for Chunk {
impl Debug for PartialChunkStorage { impl Debug for PartialChunkStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChunkStorage") f.debug_struct("PartialChunkStorage")
.field("view_center", &self.view_center) .field("view_center", &self.view_center)
.field("chunk_radius", &self.chunk_radius) .field("chunk_radius", &self.chunk_radius)
.field("view_range", &self.view_range) .field("view_range", &self.view_range)
.field("height", &self.height())
.field("min_y", &self.min_y())
// .field("chunks", &self.chunks) // .field("chunks", &self.chunks)
.field("chunks", &format_args!("{} items", self.chunks.len())) .field("chunks", &format_args!("{} items", self.chunks.len()))
.finish() .finish()
@ -373,10 +362,10 @@ impl Section {
impl Default for PartialChunkStorage { impl Default for PartialChunkStorage {
fn default() -> Self { fn default() -> Self {
Self::new(8, Arc::new(RwLock::new(WeakChunkStorage::default()))) Self::new(8)
} }
} }
impl Default for WeakChunkStorage { impl Default for ChunkStorage {
fn default() -> Self { fn default() -> Self {
Self::new(384, -64) Self::new(384, -64)
} }
@ -408,34 +397,26 @@ mod tests {
#[test] #[test]
fn test_out_of_bounds_y() { fn test_out_of_bounds_y() {
let mut chunk_storage = PartialChunkStorage::default(); let mut chunk_storage = ChunkStorage::default();
chunk_storage.set( let mut partial_chunk_storage = PartialChunkStorage::default();
partial_chunk_storage.set(
&ChunkPos { x: 0, z: 0 }, &ChunkPos { x: 0, z: 0 },
Some(Arc::new(RwLock::new(Chunk::default()))), Some(Chunk::default()),
&mut chunk_storage,
); );
assert!(chunk_storage assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: 319, z: 0 }) .get_block_state(&BlockPos { x: 0, y: 319, z: 0 })
.is_some()); .is_some());
assert!(chunk_storage assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: 320, z: 0 }) .get_block_state(&BlockPos { x: 0, y: 320, z: 0 })
.is_none()); .is_none());
assert!(chunk_storage assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: 338, z: 0 }) .get_block_state(&BlockPos { x: 0, y: 338, z: 0 })
.is_none()); .is_none());
assert!(chunk_storage assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: -64, z: 0 }) .get_block_state(&BlockPos { x: 0, y: -64, z: 0 })
.is_some()); .is_some());
assert!(chunk_storage assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: -65, z: 0 }) .get_block_state(&BlockPos { x: 0, y: -65, z: 0 })
.is_none()); .is_none());
} }

View file

@ -1,52 +1,76 @@
use crate::WeakWorld;
use azalea_core::ResourceLocation; use azalea_core::ResourceLocation;
use azalea_ecs::system::Resource;
use log::error; use log::error;
use nohash_hasher::IntMap;
use parking_lot::RwLock;
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
/// A container of [`WeakWorld`]s. Worlds are stored as a Weak pointer here, so use crate::{ChunkStorage, World};
/// A container of [`World`]s. Worlds are stored as a Weak pointer here, so
/// if no clients are using a world it will be forgotten. /// if no clients are using a world it will be forgotten.
#[derive(Default)] #[derive(Default, Resource)]
pub struct WeakWorldContainer { pub struct WorldContainer {
pub worlds: HashMap<ResourceLocation, Weak<WeakWorld>>, // We just refer to the chunks here and don't include entities because there's not that many
// cases where we'd want to get every entity in the world (just getting the entities in chunks
// should work fine).
// Entities are garbage collected (by manual reference counting in EntityInfos) so we don't
// need to worry about them here.
// If it looks like we're relying on the server giving us unique world names, that's because we
// are. An evil server could give us two worlds with the same name and then we'd have no way of
// telling them apart. We hope most servers are nice and don't do that though. It's only an
// issue when there's multiple clients with the same WorldContainer in different worlds
// anyways.
pub worlds: HashMap<ResourceLocation, Weak<RwLock<World>>>,
} }
impl WeakWorldContainer { impl WorldContainer {
pub fn new() -> Self { pub fn new() -> Self {
WeakWorldContainer { WorldContainer {
worlds: HashMap::new(), worlds: HashMap::new(),
} }
} }
/// Get a world from the container. /// Get a world from the container.
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<WeakWorld>> { pub fn get(&self, name: &ResourceLocation) -> Option<Arc<RwLock<World>>> {
self.worlds.get(name).and_then(|world| world.upgrade()) self.worlds.get(name).and_then(|world| world.upgrade())
} }
/// Add an empty world to the container (or not if it already exists) and /// Add an empty world to the container (or not if it already exists) and
/// returns a strong reference to the world. /// returns a strong reference to the world.
#[must_use = "the world will be immediately forgotten if unused"] #[must_use = "the world will be immediately forgotten if unused"]
pub fn insert(&mut self, name: ResourceLocation, height: u32, min_y: i32) -> Arc<WeakWorld> { pub fn insert(
if let Some(existing) = self.worlds.get(&name).and_then(|world| world.upgrade()) { &mut self,
if existing.height() != height { name: ResourceLocation,
height: u32,
min_y: i32,
) -> Arc<RwLock<World>> {
if let Some(existing_lock) = self.worlds.get(&name).and_then(|world| world.upgrade()) {
let existing = existing_lock.read();
if existing.chunks.height != height {
error!( error!(
"Shared dimension height mismatch: {} != {}", "Shared dimension height mismatch: {} != {}",
existing.height(), existing.chunks.height, height,
height,
); );
} }
if existing.min_y() != min_y { if existing.chunks.min_y != min_y {
error!( error!(
"Shared world min_y mismatch: {} != {}", "Shared world min_y mismatch: {} != {}",
existing.min_y(), existing.chunks.min_y, min_y,
min_y,
); );
} }
existing existing_lock.clone()
} else { } else {
let world = Arc::new(WeakWorld::new(height, min_y)); let world = Arc::new(RwLock::new(World {
chunks: ChunkStorage::new(height, min_y),
entities_by_chunk: HashMap::new(),
entity_by_id: IntMap::default(),
}));
self.worlds.insert(name, Arc::downgrade(&world)); self.worlds.insert(name, Arc::downgrade(&world));
world world
} }

View file

@ -6,11 +6,12 @@ use std::{
}; };
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable}; use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use azalea_ecs::component::Component;
use thiserror::Error; use thiserror::Error;
use uuid::{uuid, Uuid}; use uuid::{uuid, Uuid};
#[derive(Clone, Debug)] #[derive(Clone, Debug, Component)]
pub struct AttributeModifiers { pub struct Attributes {
pub speed: AttributeInstance, pub speed: AttributeInstance,
} }
@ -41,7 +42,7 @@ impl AttributeInstance {
_ => {} _ => {}
} }
if let AttributeModifierOperation::MultiplyTotal = modifier.operation { if let AttributeModifierOperation::MultiplyTotal = modifier.operation {
total *= 1.0 + modifier.amount total *= 1.0 + modifier.amount;
} }
} }
total total

View file

@ -1,15 +1,20 @@
//! Define some types needed for entity metadata.
use azalea_block::BlockState; use azalea_block::BlockState;
use azalea_buf::{BufReadError, McBufVarReadable, McBufVarWritable}; use azalea_buf::{
use azalea_buf::{McBuf, McBufReadable, McBufWritable}; BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
use azalea_chat::Component; };
use azalea_chat::FormattedText;
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot}; use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot};
use azalea_ecs::component::Component;
use derive_more::Deref;
use enum_as_inner::EnumAsInner; use enum_as_inner::EnumAsInner;
use nohash_hasher::IntSet; use nohash_hasher::IntSet;
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deref)]
pub struct EntityMetadataItems(pub Vec<EntityDataItem>); pub struct EntityMetadataItems(Vec<EntityDataItem>);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct EntityDataItem { pub struct EntityDataItem {
@ -52,8 +57,8 @@ pub enum EntityDataValue {
Long(i64), Long(i64),
Float(f32), Float(f32),
String(String), String(String),
Component(Component), FormattedText(FormattedText),
OptionalComponent(Option<Component>), OptionalFormattedText(Option<FormattedText>),
ItemStack(Slot), ItemStack(Slot),
Boolean(bool), Boolean(bool),
Rotations(Rotations), Rotations(Rotations),
@ -105,7 +110,7 @@ pub struct Rotations {
pub z: f32, pub z: f32,
} }
#[derive(Clone, Debug, Copy, McBuf, Default)] #[derive(Clone, Debug, Copy, McBuf, Default, Component)]
pub enum Pose { pub enum Pose {
#[default] #[default]
Standing = 0, Standing = 0,
@ -120,7 +125,7 @@ pub enum Pose {
#[derive(Debug, Clone, McBuf)] #[derive(Debug, Clone, McBuf)]
pub struct VillagerData { pub struct VillagerData {
pub kind: azalea_registry::VillagerType, pub kind: azalea_registry::VillagerKind,
pub profession: azalea_registry::VillagerProfession, pub profession: azalea_registry::VillagerProfession,
#[var] #[var]
pub level: u32, pub level: u32,

View file

@ -1,4 +1,7 @@
use azalea_core::{Vec3, AABB}; use azalea_core::{Vec3, AABB};
use azalea_ecs::{query::Changed, system::Query};
use super::{Physics, Position};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct EntityDimensions { pub struct EntityDimensions {
@ -21,3 +24,15 @@ impl EntityDimensions {
} }
} }
} }
/// Sets the position of the entity. This doesn't update the cache in
/// azalea-world, and should only be used within azalea-world!
///
/// # Safety
/// Cached position in the world must be updated.
pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed<Position>>) {
for (position, mut physics) in query.iter_mut() {
let bounding_box = physics.dimensions.make_bounding_box(position);
physics.bounding_box = bounding_box;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,68 +1,52 @@
#![allow(clippy::derived_hash_with_manual_eq)]
pub mod attributes; pub mod attributes;
mod data; mod data;
mod dimensions; mod dimensions;
pub mod metadata; pub mod metadata;
use self::attributes::{AttributeInstance, AttributeModifiers}; use crate::ChunkStorage;
pub use self::metadata::EntityMetadata;
use crate::WeakWorld; use self::{attributes::AttributeInstance, metadata::Health};
pub use attributes::Attributes;
use azalea_block::BlockState; use azalea_block::BlockState;
use azalea_core::{BlockPos, Vec3, AABB}; use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB};
use azalea_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
query::Changed,
system::{Commands, Query},
};
pub use data::*; pub use data::*;
pub use dimensions::*; use derive_more::{Deref, DerefMut};
use std::marker::PhantomData; pub use dimensions::{update_bounding_box, EntityDimensions};
use std::ops::{Deref, DerefMut}; use std::fmt::Debug;
use std::ptr::NonNull;
use uuid::Uuid; use uuid::Uuid;
/// A reference to an entity in a world. /// An entity ID used by Minecraft. These are not guaranteed to be unique in
#[derive(Debug)] /// shared worlds, that's what [`Entity`] is for.
pub struct Entity<'d, D = &'d WeakWorld> { #[derive(Component, Copy, Clone, Debug, PartialEq, Eq, Deref, DerefMut)]
/// The world this entity is in. pub struct MinecraftEntityId(pub u32);
pub world: D, impl std::hash::Hash for MinecraftEntityId {
/// The incrementing numerical id of the entity. fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
pub id: u32, hasher.write_u32(self.0);
pub data: NonNull<EntityData>,
_marker: PhantomData<&'d ()>,
}
impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
pub fn new(world: D, id: u32, data: NonNull<EntityData>) -> Self {
// TODO: have this be based on the entity type
Self {
world,
id,
data,
_marker: PhantomData,
} }
} }
} impl nohash_hasher::IsEnabled for MinecraftEntityId {}
pub fn set_rotation(physics: &mut Physics, y_rot: f32, x_rot: f32) {
impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> { physics.y_rot = y_rot % 360.0;
/// Sets the position of the entity. This doesn't update the cache in physics.x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
/// azalea-world, and should only be used within azalea-world!
///
/// # Safety
/// Cached position in the world must be updated.
pub unsafe fn move_unchecked(&mut self, new_pos: Vec3) {
self.pos = new_pos;
let bounding_box = self.make_bounding_box();
self.bounding_box = bounding_box;
}
pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
self.y_rot = y_rot % 360.0;
self.x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
// TODO: minecraft also sets yRotO and xRotO to xRot and yRot ... but // TODO: minecraft also sets yRotO and xRotO to xRot and yRot ... but
// idk what they're used for so // idk what they're used for so
} }
pub fn move_relative(&mut self, speed: f32, acceleration: &Vec3) { pub fn move_relative(physics: &mut Physics, speed: f32, acceleration: &Vec3) {
let input_vector = self.input_vector(speed, acceleration); let input_vector = input_vector(physics, speed, acceleration);
self.delta += input_vector; physics.delta += input_vector;
} }
pub fn input_vector(&self, speed: f32, acceleration: &Vec3) -> Vec3 { pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> Vec3 {
let distance = acceleration.length_squared(); let distance = acceleration.length_squared();
if distance < 1.0E-7 { if distance < 1.0E-7 {
return Vec3::default(); return Vec3::default();
@ -73,8 +57,8 @@ impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
*acceleration *acceleration
} }
.scale(speed as f64); .scale(speed as f64);
let y_rot = f32::sin(self.y_rot * 0.017453292f32); let y_rot = f32::sin(physics.y_rot * 0.017453292f32);
let x_rot = f32::cos(self.y_rot * 0.017453292f32); let x_rot = f32::cos(physics.y_rot * 0.017453292f32);
Vec3 { Vec3 {
x: acceleration.x * (x_rot as f64) - acceleration.z * (y_rot as f64), x: acceleration.x * (x_rot as f64) - acceleration.z * (y_rot as f64),
y: acceleration.y, y: acceleration.y,
@ -82,32 +66,9 @@ impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
} }
} }
/// Apply the given metadata items to the entity. Everything that isn't
/// included in items will be left unchanged. If an error occured, None
/// will be returned.
///
/// TODO: this should be changed to have a proper error.
pub fn apply_metadata(&mut self, items: &Vec<EntityDataItem>) -> Option<()> {
for item in items {
self.metadata.set_index(item.index, item.value.clone())?;
}
Some(())
}
}
impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
#[inline]
pub fn pos(&self) -> &Vec3 {
&self.pos
}
pub fn make_bounding_box(&self) -> AABB {
self.dimensions.make_bounding_box(self.pos())
}
/// Get the position of the block below the entity, but a little lower. /// Get the position of the block below the entity, but a little lower.
pub fn on_pos_legacy(&self) -> BlockPos { pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> BlockPos {
self.on_pos(0.2) on_pos(0.2, chunk_storage, position)
} }
// int x = Mth.floor(this.position.x); // int x = Mth.floor(this.position.x);
@ -122,18 +83,18 @@ impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
// } // }
// } // }
// return var5; // return var5;
pub fn on_pos(&self, offset: f32) -> BlockPos { pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: &Position) -> BlockPos {
let x = self.pos().x.floor() as i32; let x = pos.x.floor() as i32;
let y = (self.pos().y - offset as f64).floor() as i32; let y = (pos.y - offset as f64).floor() as i32;
let z = self.pos().z.floor() as i32; let z = pos.z.floor() as i32;
let pos = BlockPos { x, y, z }; let pos = BlockPos { x, y, z };
// TODO: check if block below is a fence, wall, or fence gate // TODO: check if block below is a fence, wall, or fence gate
let block_pos = pos.down(1); let block_pos = pos.down(1);
let block_state = self.world.get_block_state(&block_pos); let block_state = chunk_storage.get_block_state(&block_pos);
if block_state == Some(BlockState::Air) { if block_state == Some(BlockState::Air) {
let block_pos_below = block_pos.down(1); let block_pos_below = block_pos.down(1);
let block_state_below = self.world.get_block_state(&block_pos_below); let block_state_below = chunk_storage.get_block_state(&block_pos_below);
if let Some(_block_state_below) = block_state_below { if let Some(_block_state_below) = block_state_below {
// if block_state_below.is_fence() // if block_state_below.is_fence()
// || block_state_below.is_wall() // || block_state_below.is_wall()
@ -146,47 +107,84 @@ impl<'d, D: Deref<Target = WeakWorld>> Entity<'d, D> {
pos pos
} }
}
// impl< /// The Minecraft UUID of the entity. For players, this is their actual player
// 'd, /// UUID, and for other entities it's just random.
// D: Deref<Target = WeakWorld> + Deref<Target = WeakWorld>, #[derive(Component, Deref, DerefMut, Clone, Copy)]
// D2: Deref<Target = WeakWorld>, pub struct EntityUuid(Uuid);
// > From<Entity<'d, D>> for Entity<'d, D2> impl Debug for EntityUuid {
// { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// fn from(entity: Entity<'d, D>) -> Entity<'d, D> { (self.0).fmt(f)
// Entity {
// world: entity.world,
// id: entity.id,
// data: entity.data,
// _marker: PhantomData,
// }
// }
// }
impl<D: Deref<Target = WeakWorld>> DerefMut for Entity<'_, D> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { self.data.as_mut() }
} }
} }
impl<D: Deref<Target = WeakWorld>> Deref for Entity<'_, D> {
type Target = EntityData;
fn deref(&self) -> &Self::Target {
unsafe { self.data.as_ref() }
}
}
#[derive(Debug)]
pub struct EntityData {
pub uuid: Uuid,
/// The position of the entity right now. /// The position of the entity right now.
/// This can be changde with unsafe_move, but the correct way is with ///
/// world.move_entity /// You are free to change this; there's systems that update the indexes
pos: Vec3, /// automatically.
/// The position of the entity last tick. #[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub last_pos: Vec3, pub struct Position(Vec3);
impl From<Position> for ChunkPos {
fn from(value: Position) -> Self {
ChunkPos::from(&value.0)
}
}
impl From<Position> for BlockPos {
fn from(value: Position) -> Self {
BlockPos::from(&value.0)
}
}
impl From<&Position> for ChunkPos {
fn from(value: &Position) -> Self {
ChunkPos::from(value.0)
}
}
impl From<&Position> for BlockPos {
fn from(value: &Position) -> Self {
BlockPos::from(value.0)
}
}
/// The last position of the entity that was sent to the network.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct LastSentPosition(Vec3);
impl From<LastSentPosition> for ChunkPos {
fn from(value: LastSentPosition) -> Self {
ChunkPos::from(&value.0)
}
}
impl From<LastSentPosition> for BlockPos {
fn from(value: LastSentPosition) -> Self {
BlockPos::from(&value.0)
}
}
impl From<&LastSentPosition> for ChunkPos {
fn from(value: &LastSentPosition) -> Self {
ChunkPos::from(value.0)
}
}
impl From<&LastSentPosition> for BlockPos {
fn from(value: &LastSentPosition) -> Self {
BlockPos::from(value.0)
}
}
/// The name of the world the entity is in. If two entities share the same world
/// name, we assume they're in the same world.
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct WorldName(ResourceLocation);
/// A component for entities that can jump.
///
/// If this is true, the entity will try to jump every tick. (It's equivalent to
/// the space key being held in vanilla.)
#[derive(Debug, Component, Deref, DerefMut)]
pub struct Jumping(bool);
/// The physics data relating to the entity, such as position, velocity, and
/// bounding box.
#[derive(Debug, Component)]
pub struct Physics {
pub delta: Vec3, pub delta: Vec3,
/// X acceleration. /// X acceleration.
@ -211,30 +209,71 @@ pub struct EntityData {
/// unlike dimensions. /// unlike dimensions.
pub bounding_box: AABB, pub bounding_box: AABB,
/// Whether the entity will try to jump every tick
/// (equivalent to the space key being held down in vanilla).
pub jumping: bool,
pub has_impulse: bool, pub has_impulse: bool,
/// Stores some extra data about the entity, including the entity type.
pub metadata: EntityMetadata,
/// The attributes and modifiers that the entity has (for example, speed).
pub attributes: AttributeModifiers,
} }
impl EntityData { /// Marker component for entities that are dead.
pub fn new(uuid: Uuid, pos: Vec3, metadata: EntityMetadata) -> Self { ///
/// "Dead" means that the entity has 0 health.
#[derive(Component, Copy, Clone, Default)]
pub struct Dead;
/// System that adds the [`Dead`] marker component if an entity's health is set
/// to 0 (or less than 0). This will be present if an entity is doing the death
/// animation.
///
/// Entities that are dead can not be revived.
/// TODO: fact check this in-game by setting an entity's health to 0 and then
/// not 0
pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<Health>>) {
for (entity, health) in query.iter() {
if **health <= 0.0 {
commands.entity(entity).insert(Dead);
}
}
}
/// A component NewType for [`azalea_registry::EntityKind`].
///
/// Most of the time, you should be using `azalea_registry::EntityKind`
/// instead.
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref)]
pub struct EntityKind(azalea_registry::EntityKind);
/// A bundle of components that every entity has. This doesn't contain metadata,
/// that has to be added separately.
#[derive(Bundle)]
pub struct EntityBundle {
pub kind: EntityKind,
pub uuid: EntityUuid,
pub world_name: WorldName,
pub position: Position,
pub last_sent_position: LastSentPosition,
pub physics: Physics,
pub attributes: Attributes,
pub jumping: Jumping,
}
impl EntityBundle {
pub fn new(
uuid: Uuid,
pos: Vec3,
kind: azalea_registry::EntityKind,
world_name: ResourceLocation,
) -> Self {
// TODO: get correct entity dimensions by having them codegened somewhere
let dimensions = EntityDimensions { let dimensions = EntityDimensions {
width: 0.6, width: 0.6,
height: 1.8, height: 1.8,
}; };
Self { Self {
uuid, kind: EntityKind(kind),
pos, uuid: EntityUuid(uuid),
last_pos: pos, world_name: WorldName(world_name),
position: Position(pos),
last_sent_position: LastSentPosition(pos),
physics: Physics {
delta: Vec3::default(), delta: Vec3::default(),
xxa: 0., xxa: 0.,
@ -255,53 +294,44 @@ impl EntityData {
dimensions, dimensions,
has_impulse: false, has_impulse: false,
},
jumping: false, attributes: Attributes {
// TODO: do the correct defaults for everything, some
metadata, // entities have different defaults
attributes: AttributeModifiers {
// TODO: do the correct defaults for everything, some entities have different
// defaults
speed: AttributeInstance::new(0.1), speed: AttributeInstance::new(0.1),
}, },
jumping: Jumping(false),
}
} }
} }
/// Get the position of the entity in the world. /// A bundle of the components that are always present for a player.
#[inline] #[derive(Bundle)]
pub fn pos(&self) -> &Vec3 { pub struct PlayerBundle {
&self.pos pub entity: EntityBundle,
pub metadata: metadata::PlayerMetadataBundle,
} }
/// Convert this &self into a (mutable) pointer. // #[cfg(test)]
/// // mod tests {
/// # Safety // use super::*;
/// The entity MUST exist for at least as long as this pointer exists. // use crate::PartialWorld;
pub unsafe fn as_ptr(&self) -> NonNull<EntityData> {
// this is cursed
NonNull::new_unchecked(self as *const EntityData as *mut EntityData)
}
}
#[cfg(test)] // #[test]
mod tests { // fn from_mut_entity_to_ref_entity() {
use super::*; // let mut world = PartialWorld::default();
use crate::PartialWorld; // let uuid = Uuid::from_u128(100);
// world.add_entity(
#[test] // 0,
fn from_mut_entity_to_ref_entity() { // EntityData::new(
let mut world = PartialWorld::default(); // uuid,
let uuid = Uuid::from_u128(100); // Vec3::default(),
world.add_entity( // EntityMetadata::Player(metadata::Player::default()),
0, // ),
EntityData::new( // );
uuid, // let entity: Entity = world.entity_mut(0).unwrap();
Vec3::default(), // assert_eq!(entity.uuid, uuid);
EntityMetadata::Player(metadata::Player::default()), // }
), // }
);
let entity: Entity = world.entity_mut(0).unwrap();
assert_eq!(entity.uuid, uuid);
}
}

View file

@ -0,0 +1,357 @@
use crate::{
deduplicate_entities, deduplicate_local_entities,
entity::{
self, add_dead, update_bounding_box, EntityUuid, MinecraftEntityId, Position, WorldName,
},
update_entity_by_id_index, update_uuid_index, PartialWorld, WorldContainer,
};
use azalea_core::ChunkPos;
use azalea_ecs::{
app::{App, CoreStage, Plugin},
component::Component,
ecs::Ecs,
ecs::EntityMut,
entity::Entity,
query::{Added, Changed, With, Without},
schedule::{IntoSystemDescriptor, SystemSet},
system::{Command, Commands, Query, Res, ResMut, Resource},
};
use derive_more::{Deref, DerefMut};
use log::{debug, warn};
use nohash_hasher::IntMap;
use parking_lot::RwLock;
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
sync::Arc,
};
use uuid::Uuid;
/// Plugin handling some basic entity functionality.
pub struct EntityPlugin;
impl Plugin for EntityPlugin {
fn build(&self, app: &mut App) {
app.add_system_set(
SystemSet::new()
.after("tick")
.after("packet")
.with_system(update_entity_chunk_positions)
.with_system(remove_despawned_entities_from_indexes)
.with_system(update_bounding_box)
.with_system(add_dead)
.with_system(
add_updates_received
.after("deduplicate_entities")
.after("deduplicate_local_entities")
.label("add_updates_received"),
)
.with_system(
update_uuid_index
.label("update_uuid_index")
.after("deduplicate_local_entities")
.after("deduplicate_entities"),
)
.with_system(debug_detect_updates_received_on_local_entities)
.with_system(
update_entity_by_id_index
.label("update_entity_by_id_index")
.after("deduplicate_entities"),
)
.with_system(debug_new_entity),
)
.add_system_set_to_stage(
CoreStage::PostUpdate,
SystemSet::new()
.with_system(deduplicate_entities.label("deduplicate_entities"))
.with_system(
deduplicate_local_entities
.label("deduplicate_local_entities")
.before("update_uuid_index")
.before("update_entity_by_id_index"),
),
)
.init_resource::<EntityInfos>();
}
}
fn debug_new_entity(query: Query<Entity, Added<MinecraftEntityId>>) {
for entity in query.iter() {
debug!("new entity: {:?}", entity);
}
}
// How entity updates are processed (to avoid issues with shared worlds)
// - each bot contains a map of { entity id: updates received }
// - the shared world also contains a canonical "true" updates received for each
// entity
// - when a client loads an entity, its "updates received" is set to the same as
// the global "updates received"
// - when the shared world sees an entity for the first time, the "updates
// received" is set to 1.
// - clients can force the shared "updates received" to 0 to make it so certain
// entities (i.e. other bots in our swarm) don't get confused and updated by
// other bots
// - when a client gets an update to an entity, we check if our "updates
// received" is the same as the shared world's "updates received": if it is,
// then process the update and increment the client's and shared world's
// "updates received" if not, then we simply increment our local "updates
// received" and do nothing else
/// Keep track of certain metadatas that are only relevant for this partial
/// world.
#[derive(Debug, Default)]
pub struct PartialEntityInfos {
// note: using MinecraftEntityId for entity ids is acceptable here since
// there's no chance of collisions here
/// The entity id of the player that owns this partial world. This will
/// make [`RelativeEntityUpdate`] pretend the entity doesn't exist so
/// it doesn't get modified from outside sources.
pub owner_entity: Option<Entity>,
/// A counter for each entity that tracks how many updates we've observed
/// for it.
///
/// This is used for shared worlds (i.e. swarms), to make sure we don't
/// update entities twice on accident.
pub updates_received: IntMap<MinecraftEntityId, u32>,
}
impl PartialEntityInfos {
pub fn new(owner_entity: Option<Entity>) -> Self {
Self {
owner_entity,
updates_received: IntMap::default(),
}
}
}
/// A [`Command`] that applies a "relative update" to an entity, which means
/// this update won't be run multiple times by different clients in the same
/// world.
///
/// This is used to avoid a bug where when there's multiple clients in the same
/// world and an entity sends a relative move packet to all clients, its
/// position gets desynced since the relative move is applied multiple times.
///
/// Don't use this unless you actually got an entity update packet that all
/// other clients within render distance will get too. You usually don't need
/// this when the change isn't relative either.
pub struct RelativeEntityUpdate {
pub entity: Entity,
pub partial_world: Arc<RwLock<PartialWorld>>,
// a function that takes the entity and updates it
pub update: Box<dyn FnOnce(&mut EntityMut) + Send + Sync>,
}
impl Command for RelativeEntityUpdate {
fn write(self, world: &mut Ecs) {
let partial_entity_infos = &mut self.partial_world.write().entity_infos;
let mut entity = world.entity_mut(self.entity);
if Some(self.entity) == partial_entity_infos.owner_entity {
// if the entity owns this partial world, it's always allowed to update itself
(self.update)(&mut entity);
return;
};
let entity_id = *entity.get::<MinecraftEntityId>().unwrap();
let Some(updates_received) = entity.get_mut::<UpdatesReceived>() else {
// a client tried to update another client, which isn't allowed
return;
};
let this_client_updates_received = partial_entity_infos
.updates_received
.get(&entity_id)
.copied();
let can_update = this_client_updates_received.unwrap_or(1) == **updates_received;
if can_update {
let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
partial_entity_infos
.updates_received
.insert(entity_id, new_updates_received);
**entity.get_mut::<UpdatesReceived>().unwrap() = new_updates_received;
let mut entity = world.entity_mut(self.entity);
(self.update)(&mut entity);
}
}
}
/// Things that are shared between all the partial worlds.
#[derive(Resource, Default)]
pub struct EntityInfos {
/// An index of entities by their UUIDs
pub(crate) entity_by_uuid: HashMap<Uuid, Entity>,
}
impl EntityInfos {
pub fn new() -> Self {
Self {
entity_by_uuid: HashMap::default(),
}
}
pub fn get_entity_by_uuid(&self, uuid: &Uuid) -> Option<Entity> {
self.entity_by_uuid.get(uuid).copied()
}
}
/// Update the chunk position indexes in [`EntityInfos`].
fn update_entity_chunk_positions(
mut query: Query<
(
Entity,
&entity::Position,
&mut entity::LastSentPosition,
&entity::WorldName,
),
Changed<entity::Position>,
>,
world_container: Res<WorldContainer>,
) {
for (entity, pos, last_pos, world_name) in query.iter_mut() {
let world_lock = world_container.get(world_name).unwrap();
let mut world = world_lock.write();
let old_chunk = ChunkPos::from(*last_pos);
let new_chunk = ChunkPos::from(*pos);
if old_chunk != new_chunk {
// move the entity from the old chunk to the new one
if let Some(entities) = world.entities_by_chunk.get_mut(&old_chunk) {
entities.remove(&entity);
}
world
.entities_by_chunk
.entry(new_chunk)
.or_default()
.insert(entity);
}
}
}
/// A component that lists all the local player entities that have this entity
/// loaded. If this is empty, the entity will be removed from the ECS.
#[derive(Component, Clone, Deref, DerefMut)]
pub struct LoadedBy(pub HashSet<Entity>);
/// A component that counts the number of times this entity has been modified.
/// This is used for making sure two clients don't do the same relative update
/// on an entity.
///
/// If an entity is local (i.e. it's a client/localplayer), this component
/// should NOT be present in the entity.
#[derive(Component, Debug, Deref, DerefMut)]
pub struct UpdatesReceived(u32);
#[allow(clippy::type_complexity)]
pub fn add_updates_received(
mut commands: Commands,
query: Query<
Entity,
(
Changed<MinecraftEntityId>,
(Without<UpdatesReceived>, Without<Local>),
),
>,
) {
for entity in query.iter() {
// entities always start with 1 update received
commands.entity(entity).insert(UpdatesReceived(1));
}
}
/// A marker component that signifies that this entity is "local" and shouldn't
/// be updated by other clients.
#[derive(Component)]
pub struct Local;
/// The [`UpdatesReceived`] component should never be on [`Local`] entities.
/// This warns if an entity has both components.
fn debug_detect_updates_received_on_local_entities(
query: Query<Entity, (With<Local>, With<UpdatesReceived>)>,
) {
for entity in &query {
warn!("Entity {:?} has both Local and UpdatesReceived", entity);
}
}
/// Despawn entities that aren't being loaded by anything.
fn remove_despawned_entities_from_indexes(
mut commands: Commands,
mut entity_infos: ResMut<EntityInfos>,
world_container: Res<WorldContainer>,
query: Query<(Entity, &EntityUuid, &Position, &WorldName, &LoadedBy), Changed<LoadedBy>>,
) {
for (entity, uuid, position, world_name, loaded_by) in &query {
let world_lock = world_container.get(world_name).unwrap();
let mut world = world_lock.write();
// if the entity has no references left, despawn it
if !loaded_by.is_empty() {
continue;
}
// remove the entity from the chunk index
let chunk = ChunkPos::from(*position);
if let Some(entities_in_chunk) = world.entities_by_chunk.get_mut(&chunk) {
if entities_in_chunk.remove(&entity) {
// remove the chunk if there's no entities in it anymore
if entities_in_chunk.is_empty() {
world.entities_by_chunk.remove(&chunk);
}
} else {
warn!("Tried to remove entity from chunk {chunk:?} but the entity was not there.");
}
} else {
warn!("Tried to remove entity from chunk {chunk:?} but the chunk was not found.");
}
// remove it from the uuid index
if entity_infos.entity_by_uuid.remove(uuid).is_none() {
warn!("Tried to remove entity {entity:?} from the uuid index but it was not there.");
}
// and now remove the entity from the ecs
commands.entity(entity).despawn();
debug!("Despawned entity {entity:?} because it was not loaded by anything.");
return;
}
}
impl Debug for EntityInfos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntityInfos").finish()
}
}
// #[cfg(test)]
// mod tests {
// use crate::entity::metadata;
// use super::*;
// use azalea_core::Vec3;
// #[test]
// fn test_store_entity() {
// let mut storage = PartialEntityInfos::default();
// assert!(storage.limited_get_by_id(0).is_none());
// assert!(storage.shared.read().get_by_id(0).is_none());
// let uuid = Uuid::from_u128(100);
// storage.insert(
// 0,
// EntityData::new(
// uuid,
// Vec3::default(),
// EntityMetadata::Player(metadata::Player::default()),
// ),
// );
// assert_eq!(storage.limited_get_by_id(0).unwrap().uuid, uuid);
// assert_eq!(storage.shared.read().get_by_id(0).unwrap().uuid, uuid);
// storage.remove_by_id(0);
// assert!(storage.limited_get_by_id(0).is_none());
// assert!(storage.shared.read().get_by_id(0).is_none());
// }
// }

View file

@ -1,416 +0,0 @@
use crate::entity::EntityData;
use azalea_core::ChunkPos;
use log::warn;
use nohash_hasher::{IntMap, IntSet};
use parking_lot::RwLock;
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use uuid::Uuid;
// How entity updates are processed (to avoid issues with shared worlds)
// - each bot contains a map of { entity id: updates received }
// - the shared world also contains a canonical "true" updates received for each
// entity
// - when a client loads an entity, its "updates received" is set to the same as
// the global "updates received"
// - when the shared world sees an entity for the first time, the "updates
// received" is set to 1.
// - clients can force the shared "updates received" to 0 to make it so certain
// entities (i.e. other bots in our swarm) don't get confused and updated by
// other bots
// - when a client gets an update to an entity, we check if our "updates
// received" is the same as the shared world's "updates received": if it is,
// then process the update and increment the client's and shared world's
// "updates received" if not, then we simply increment our local "updates
// received" and do nothing else
/// Store a map of entities by ID. To get an iterator over all entities, use
/// `storage.shared.read().entities` [`WeakEntityStorage::entities`].
///
/// This is meant to be used with shared worlds.
///
/// You can access the shared storage with `world.shared.read()`.
#[derive(Debug, Default)]
pub struct PartialEntityStorage {
pub shared: Arc<RwLock<WeakEntityStorage>>,
/// The entity id of the player that owns this partial world. This will
/// make [`PartialWorld::entity_mut`] pretend the entity doesn't exist so
/// it doesn't get modified from outside sources.
///
/// [`PartialWorld::entity_mut`]: crate::PartialWorld::entity_mut
pub owner_entity_id: Option<u32>,
pub updates_received: IntMap<u32, u32>,
/// Strong references to the entities we have loaded.
data_by_id: IntMap<u32, Arc<EntityData>>,
}
/// Weakly store entities in a world. If the entities aren't being referenced
/// by anything else (like an [`PartialEntityStorage`]), they'll be forgotten.
#[derive(Debug, Default)]
pub struct WeakEntityStorage {
data_by_id: IntMap<u32, Weak<EntityData>>,
/// An index of all the entity ids we know are in a chunk
ids_by_chunk: HashMap<ChunkPos, IntSet<u32>>,
/// An index of entity ids by their UUIDs
id_by_uuid: HashMap<Uuid, u32>,
pub updates_received: IntMap<u32, u32>,
}
impl PartialEntityStorage {
pub fn new(shared: Arc<RwLock<WeakEntityStorage>>, owner_entity_id: Option<u32>) -> Self {
if let Some(owner_entity_id) = owner_entity_id {
shared.write().updates_received.insert(owner_entity_id, 0);
}
Self {
shared,
owner_entity_id,
updates_received: IntMap::default(),
data_by_id: IntMap::default(),
}
}
/// Add an entity to the storage.
#[inline]
pub fn insert(&mut self, id: u32, entity: EntityData) {
// if the entity is already in the shared world, we don't need to do anything
if self.shared.read().data_by_id.contains_key(&id) {
return;
}
// add the entity to the "indexes"
let mut shared = self.shared.write();
shared
.ids_by_chunk
.entry(ChunkPos::from(entity.pos()))
.or_default()
.insert(id);
shared.id_by_uuid.insert(entity.uuid, id);
// now store the actual entity data
let entity = Arc::new(entity);
shared.data_by_id.insert(id, Arc::downgrade(&entity));
self.data_by_id.insert(id, entity);
// set our updates_received to the shared updates_received, unless it's
// not there in which case set both to 1
if let Some(&shared_updates_received) = shared.updates_received.get(&id) {
// 0 means we're never tracking updates for this entity
if shared_updates_received != 0 || Some(id) == self.owner_entity_id {
self.updates_received.insert(id, 1);
}
} else {
shared.updates_received.insert(id, 1);
self.updates_received.insert(id, 1);
}
}
/// Remove an entity from this storage by its id. It will only be removed
/// from the shared storage if there are no other references to it.
#[inline]
pub fn remove_by_id(&mut self, id: u32) {
if let Some(entity) = self.data_by_id.remove(&id) {
let chunk = ChunkPos::from(entity.pos());
let uuid = entity.uuid;
self.updates_received.remove(&id);
drop(entity);
// maybe remove it from the storage
self.shared.write().remove_entity_if_unused(id, uuid, chunk);
} else {
warn!("Tried to remove entity with id {id} but it was not found.")
}
}
/// Whether the entity with the given id is being loaded by this storage.
/// If you want to check whether the entity is in the shared storage, use
/// [`WeakEntityStorage::contains_id`].
#[inline]
pub fn limited_contains_id(&self, id: &u32) -> bool {
self.data_by_id.contains_key(id)
}
/// Get a reference to an entity by its id, if it's being loaded by this
/// storage.
#[inline]
pub fn limited_get_by_id(&self, id: u32) -> Option<&Arc<EntityData>> {
self.data_by_id.get(&id)
}
/// Get a mutable reference to an entity by its id, if it's being loaded by
/// this storage.
#[inline]
pub fn limited_get_mut_by_id(&mut self, id: u32) -> Option<&mut Arc<EntityData>> {
self.data_by_id.get_mut(&id)
}
/// Returns whether we're allowed to update this entity (to prevent two
/// clients in a shared world updating it twice), and acknowleges that
/// we WILL update it if it's true. Don't call this unless you actually
/// got an entity update that all other clients within render distance
/// will get too.
pub fn maybe_update(&mut self, id: u32) -> bool {
let this_client_updates_received = self.updates_received.get(&id).copied();
let shared_updates_received = self.shared.read().updates_received.get(&id).copied();
let can_update = this_client_updates_received == shared_updates_received;
if can_update {
let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
self.updates_received.insert(id, new_updates_received);
self.shared
.write()
.updates_received
.insert(id, new_updates_received);
true
} else {
false
}
}
/// Get a reference to an entity by its UUID, if it's being loaded by this
/// storage.
#[inline]
pub fn limited_get_by_uuid(&self, uuid: &Uuid) -> Option<&Arc<EntityData>> {
self.shared
.read()
.id_by_uuid
.get(uuid)
.and_then(|id| self.data_by_id.get(id))
}
/// Get a mutable reference to an entity by its UUID, if it's being loaded
/// by this storage.
#[inline]
pub fn limited_get_mut_by_uuid(&mut self, uuid: &Uuid) -> Option<&mut Arc<EntityData>> {
self.shared
.read()
.id_by_uuid
.get(uuid)
.and_then(|id| self.data_by_id.get_mut(id))
}
/// Clear all entities in a chunk. This will not clear them from the
/// shared storage, unless there are no other references to them.
pub fn clear_chunk(&mut self, chunk: &ChunkPos) {
if let Some(entities) = self.shared.read().ids_by_chunk.get(chunk) {
for id in entities.iter() {
if let Some(entity) = self.data_by_id.remove(id) {
let uuid = entity.uuid;
drop(entity);
// maybe remove it from the storage
self.shared
.write()
.remove_entity_if_unused(*id, uuid, *chunk);
}
}
// for entity_id in entities {
// self.remove_by_id(entity_id);
// }
}
}
/// Move an entity from its old chunk to a new chunk.
#[inline]
pub fn update_entity_chunk(
&mut self,
entity_id: u32,
old_chunk: &ChunkPos,
new_chunk: &ChunkPos,
) {
self.shared
.write()
.update_entity_chunk(entity_id, old_chunk, new_chunk);
}
}
impl WeakEntityStorage {
pub fn new() -> Self {
Self {
data_by_id: IntMap::default(),
ids_by_chunk: HashMap::default(),
id_by_uuid: HashMap::default(),
updates_received: IntMap::default(),
}
}
/// Remove an entity from the storage if it has no strong references left.
/// Returns whether the entity was removed.
pub fn remove_entity_if_unused(&mut self, id: u32, uuid: Uuid, chunk: ChunkPos) -> bool {
if self.data_by_id.get(&id).and_then(|e| e.upgrade()).is_some() {
// if we could get the entity, that means there are still strong
// references to it
false
} else {
if self.ids_by_chunk.remove(&chunk).is_none() {
warn!("Tried to remove entity with id {id} from chunk {chunk:?} but it was not found.");
}
if self.id_by_uuid.remove(&uuid).is_none() {
warn!(
"Tried to remove entity with id {id} from uuid {uuid:?} but it was not found."
);
}
if self.updates_received.remove(&id).is_none() {
// if this happens it means we weren't tracking the updates_received for the
// client (bad)
warn!(
"Tried to remove entity with id {id} from updates_received but it was not found."
);
}
true
}
}
/// Remove a chunk from the storage if the entities in it have no strong
/// references left.
pub fn remove_chunk_if_unused(&mut self, chunk: &ChunkPos) {
if let Some(entities) = self.ids_by_chunk.get(chunk) {
if entities.is_empty() {
self.ids_by_chunk.remove(chunk);
}
}
}
/// Get an iterator over all entities in the shared storage. The iterator
/// is over `Weak<EntityData>`s, so you'll have to manually try to upgrade.
///
/// # Examples
///
/// ```rust
/// # use std::sync::Arc;
/// # use azalea_world::{PartialEntityStorage, entity::{EntityData, EntityMetadata, metadata}};
/// # use azalea_core::Vec3;
/// # use uuid::Uuid;
/// #
/// let mut storage = PartialEntityStorage::default();
/// storage.insert(
/// 0,
/// EntityData::new(
/// Uuid::nil(),
/// Vec3::default(),
/// EntityMetadata::Player(metadata::Player::default()),
/// ),
/// );
/// for entity in storage.shared.read().entities() {
/// if let Some(entity) = entity.upgrade() {
/// println!("Entity: {:?}", entity);
/// }
/// }
/// ```
pub fn entities(&self) -> std::collections::hash_map::Values<'_, u32, Weak<EntityData>> {
self.data_by_id.values()
}
/// Whether the entity with the given id is in the shared storage.
#[inline]
pub fn contains_id(&self, id: &u32) -> bool {
self.data_by_id.contains_key(id)
}
/// Get an entity by its id, if it exists.
#[inline]
pub fn get_by_id(&self, id: u32) -> Option<Arc<EntityData>> {
self.data_by_id.get(&id).and_then(|e| e.upgrade())
}
/// Get an entity in the shared storage by its UUID, if it exists.
#[inline]
pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<Arc<EntityData>> {
self.id_by_uuid
.get(uuid)
.and_then(|id| self.data_by_id.get(id).and_then(|e| e.upgrade()))
}
pub fn entity_by<F>(&self, mut f: F) -> Option<Arc<EntityData>>
where
F: FnMut(&Arc<EntityData>) -> bool,
{
for entity in self.entities() {
if let Some(entity) = entity.upgrade() {
if f(&entity) {
return Some(entity);
}
}
}
None
}
pub fn entities_by<F>(&self, mut f: F) -> Vec<Arc<EntityData>>
where
F: FnMut(&Arc<EntityData>) -> bool,
{
let mut entities = Vec::new();
for entity in self.entities() {
if let Some(entity) = entity.upgrade() {
if f(&entity) {
entities.push(entity);
}
}
}
entities
}
pub fn entity_by_in_chunk<F>(&self, chunk: &ChunkPos, mut f: F) -> Option<Arc<EntityData>>
where
F: FnMut(&EntityData) -> bool,
{
if let Some(entities) = self.ids_by_chunk.get(chunk) {
for entity_id in entities {
if let Some(entity) = self.data_by_id.get(entity_id).and_then(|e| e.upgrade()) {
if f(&entity) {
return Some(entity);
}
}
}
}
None
}
/// Move an entity from its old chunk to a new chunk.
#[inline]
pub fn update_entity_chunk(
&mut self,
entity_id: u32,
old_chunk: &ChunkPos,
new_chunk: &ChunkPos,
) {
if let Some(entities) = self.ids_by_chunk.get_mut(old_chunk) {
entities.remove(&entity_id);
}
self.ids_by_chunk
.entry(*new_chunk)
.or_default()
.insert(entity_id);
}
}
#[cfg(test)]
mod tests {
use crate::entity::{metadata, EntityMetadata};
use super::*;
use azalea_core::Vec3;
#[test]
fn test_store_entity() {
let mut storage = PartialEntityStorage::default();
assert!(storage.limited_get_by_id(0).is_none());
assert!(storage.shared.read().get_by_id(0).is_none());
let uuid = Uuid::from_u128(100);
storage.insert(
0,
EntityData::new(
uuid,
Vec3::default(),
EntityMetadata::Player(metadata::Player::default()),
),
);
assert_eq!(storage.limited_get_by_id(0).unwrap().uuid, uuid);
assert_eq!(storage.shared.read().get_by_id(0).unwrap().uuid, uuid);
storage.remove_by_id(0);
assert!(storage.limited_get_by_id(0).is_none());
assert!(storage.shared.read().get_by_id(0).is_none());
}
}

Some files were not shown because too many files have changed in this diff Show more