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

merge main and finish implementing components for 25w09b

This commit is contained in:
mat 2025-03-16 20:46:02 +00:00
commit 60d6ff4cfe
231 changed files with 7047 additions and 2757 deletions

View file

@ -1,45 +1,44 @@
name: Doc
on:
push:
branches:
- main
workflow_dispatch:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
pages: write
id-token: write
contents: write
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
group: "pages"
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
- run: cargo doc --workspace --no-deps
- uses: "finnp/create-file-action@master"
env:
FILE_NAME: "./target/doc/index.html"
FILE_DATA: '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=''./azalea''"/></head></html>' # Redirect to default page
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './target/doc/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
- run: cargo doc --workspace --no-deps
- uses: "finnp/create-file-action@master"
env:
FILE_NAME: "./target/doc/index.html"
FILE_DATA: '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=''./azalea''"/></head></html>' # Redirect to default page
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./target/doc/"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

3
.gitignore vendored
View file

@ -17,3 +17,6 @@ perf.data.old
heaptrack.*
rustc-ice-*
# not created by azalea itself, sometimes used for debugging since the docs specifically mentions using azalea.log
azalea.log

498
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -23,62 +23,63 @@ resolver = "2"
[workspace.package]
version = "0.11.0+mc25w09b"
edition = "2021"
edition = "2024"
license = "MIT"
repository = "https://github.com/azalea-rs/azalea"
# homepage = "https://github.com/azalea-rs/azalea"
[workspace.dependencies]
aes = "0.8.4"
anyhow = "1.0.94"
anyhow = "1.0.95"
async-recursion = "1.1.1"
async-trait = "0.1.83"
base64 = "0.22.1"
bevy_app = "0.15.0"
bevy_ecs = { version = "0.15.0", default-features = false }
bevy_log = "0.15.0"
bevy_tasks = "0.15.0"
bevy_time = "0.15.0"
bevy_app = "0.15.2"
bevy_ecs = { version = "0.15.2", default-features = false }
bevy_log = "0.15.2"
bevy_tasks = "0.15.2"
bevy_time = "0.15.2"
byteorder = "1.5.0"
cfb8 = "0.8.1"
chrono = { version = "0.4.39", default-features = false }
criterion = "0.5.1"
derive_more = "1.0.0"
derive_more = "2.0.1"
enum-as-inner = "0.6.1"
env_logger = "0.11.6"
flate2 = "1.0.35"
futures = "0.3.31"
futures-lite = "2.5.0"
log = "0.4.22"
futures-lite = "2.6.0"
md-5 = "0.10.6"
minecraft_folder_path = "0.1.2"
nohash-hasher = "0.2.0"
num-bigint = "0.4.6"
num-traits = "0.2.19"
parking_lot = "0.12.3"
proc-macro2 = "1.0.92"
quote = "1.0.37"
rand = "0.8.5"
proc-macro2 = "1.0.93"
quote = "1.0.38"
rand = "0.8.0"
regex = "1.11.1"
reqwest = { version = "0.12.9", default-features = false }
reqwest = { version = "0.12.12", default-features = false }
rsa = "0.9.7"
rsa_public_encrypt_pkcs1 = "0.4.0"
rustc-hash = "2.1.0"
serde = "1.0.216"
serde_json = "1.0.133"
rustc-hash = "2.1.1"
serde = "1.0.217"
serde_json = "1.0.138"
sha-1 = "0.10.1"
sha2 = "0.10.8"
simdnbt = "0.6"
socks5-impl = "0.6.0"
syn = "2.0.90"
thiserror = "2.0.8"
tokio = "1.42.0"
simdnbt = "0.7"
socks5-impl = "0.6.1"
syn = "2.0.98"
thiserror = "2.0.11"
tokio = "1.43.0"
tokio-util = "0.7.13"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
hickory-resolver = { version = "0.24.2", default-features = false }
uuid = "1.11.0"
hickory-resolver = { version = "0.24.3", default-features = false }
uuid = "1.12.1"
num-format = "0.4.4"
indexmap = "2.7.1"
paste = "1.0.15"
compact_str = "0.8.1"
# --- Profile Settings ---

View file

@ -8,7 +8,6 @@ A collection of Rust crates for making Minecraft bots, clients, and tools.
<img src="https://github.com/azalea-rs/azalea/assets/27899617/b98a42df-5cf0-4d1f-ae7c-ecca333e3cab" alt="Azalea" height="200">
</p>
<!-- The line below is automatically read and updated by the migrate script, so don't change it manually. -->
_Currently supported Minecraft version: `25w09b`._
@ -18,7 +17,7 @@ _Currently supported Minecraft version: `25w09b`._
## Features
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity collisions and elytras aren't yet implemented)
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity pushing and elytras aren't yet implemented)
- [Pathfinder](https://azalea.matdoes.dev/azalea/pathfinder/index.html)
- [Swarms](https://azalea.matdoes.dev/azalea/swarm/index.html)
- [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine)
@ -49,18 +48,23 @@ If you'd like to chat about Azalea, you can join the Matrix space at [#azalea:ma
- Bedrock edition.
- Graphics.
## Branches
## Real-world bots using Azalea
There are several branches in the Azalea repository that target older Minecraft versions.
Most of them are severely outdated compared to the latest version of Azalea.
If you'd like to update them or add more, please open a PR.
Here's an incomplete list of bots built using Azalea, primarily intended as a reference in addition to the existing documentation and examples:
- [1.21.2-1.21.3](https://github.com/azalea-rs/azalea/tree/1.21.3)
- [1.21-1.21.1](https://github.com/azalea-rs/azalea/tree/1.21.1)
- [1.20.5-1.20.6](https://github.com/azalea-rs/azalea/tree/1.20.6)
- [1.20.4](https://github.com/azalea-rs/azalea/tree/1.20.4)
- [1.20.2](https://github.com/azalea-rs/azalea/tree/1.20.2)
- [1.20-1.20.1](https://github.com/azalea-rs/azalea/tree/1.20.1)
- [1.19.4](https://github.com/azalea-rs/azalea/tree/1.19.4)
- [1.19.3](https://github.com/azalea-rs/azalea/tree/1.19.3)
- [1.19.2](https://github.com/azalea-rs/azalea/tree/1.19.2)
- [ShayBox/ShaysBot](https://github.com/ShayBox/ShaysBot) - Pearl statis bot featuring a Discord bot, an HTTP API, and more.
- [EnderKill98/statis-bot](https://github.com/EnderKill98/stasis-bot) - This bot can automatically detect thrown pearls and later walk there and pull them for you.
- [as1100k/aether](https://github.com/as1100k/aether) - Collection of Minecraft bots and plugins.
- [mat-1/potato-bot-2](https://github.com/mat-1/potato-bot-2) - Hardened Discord chat bridge created for the LiveOverflow SMP.
- [ErrorNoInternet/ErrorNoWatcher](https://github.com/ErrorNoInternet/ErrorNoWatcher) - A Minecraft bot with Lua scripting support.
You can see more projects built with Azalea in the [GitHub dependency graph](https://github.com/azalea-rs/azalea/network/dependents).
## Plugins
Azalea has support for Bevy plugins, which can significantly alter its functionality. Here's some plugins you may find useful:
- [azalea-rs/azalea-viaversion](https://github.com/azalea-rs/azalea-viaversion) - Multi-version compatibility for your Azalea bots using ViaProxy.
- [azalea-rs/azalea-hax](https://github.com/azalea-rs/azalea-hax) - Anti-knockback.
If you've created your own plugin for Azalea, please create a PR to add it to this list :).

View file

@ -1,26 +1,26 @@
[package]
name = "azalea-auth"
description = "A port of Mojang's Authlib and launcher authentication."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.11.0" }
azalea-crypto = { path = "../azalea-crypto", version = "0.11.0" }
base64 = { workspace = true }
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
md-5 = { workspace = true }
md-5.workspace = true
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
rsa = { workspace = true }
rsa.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["fs"] }
tracing = { workspace = true }
tracing.workspace = true
uuid = { workspace = true, features = ["serde", "v3"] }
[dev-dependencies]
env_logger = { workspace = true }
env_logger.workspace = true
tokio = { workspace = true, features = ["full"] }

View file

@ -24,4 +24,4 @@ async fn main() {
}
```
Thanks to [wiki.vg contributors](https://wiki.vg/Microsoft_Authentication_Scheme), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth).
Thanks to [wiki contributors](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Microsoft_Authentication_Scheme), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth).

View file

@ -360,7 +360,7 @@ pub async fn get_ms_auth_token(
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
trace!("Polling to check if user has logged in...");
if let Ok(access_token_response) = client
let res = client
.post(format!(
"https://login.live.com/oauth20_token.srf?client_id={client_id}"
))
@ -372,8 +372,8 @@ pub async fn get_ms_auth_token(
.send()
.await?
.json::<AccessTokenResponse>()
.await
{
.await;
if let Ok(access_token_response) = res {
trace!("access_token_response: {:?}", access_token_response);
let expires_at = SystemTime::now()
+ std::time::Duration::from_secs(access_token_response.expires_in);

View file

@ -1,6 +1,6 @@
use base64::Engine;
use chrono::{DateTime, Utc};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey};
use serde::Deserialize;
use thiserror::Error;
use tracing::trace;

View file

@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use azalea_buf::AzBuf;
use serde::{Deserialize, Serialize};
@ -10,7 +10,7 @@ pub struct GameProfile {
pub uuid: Uuid,
/// The username of the player.
pub name: String,
pub properties: HashMap<String, ProfilePropertyValue>,
pub properties: Arc<HashMap<String, ProfilePropertyValue>>,
}
impl GameProfile {
@ -18,7 +18,7 @@ impl GameProfile {
GameProfile {
uuid,
name,
properties: HashMap::new(),
properties: Arc::new(HashMap::new()),
}
}
}
@ -38,7 +38,7 @@ impl From<SerializableGameProfile> for GameProfile {
Self {
uuid: value.id,
name: value.name,
properties,
properties: Arc::new(properties),
}
}
}
@ -59,11 +59,11 @@ pub struct SerializableGameProfile {
impl From<GameProfile> for SerializableGameProfile {
fn from(value: GameProfile) -> Self {
let mut properties = Vec::new();
for (key, value) in value.properties {
for (key, value) in &*value.properties {
properties.push(SerializableProfilePropertyValue {
name: key,
value: value.value,
signature: value.signature,
name: key.clone(),
value: value.value.clone(),
signature: value.signature.clone(),
});
}
Self {
@ -114,7 +114,7 @@ mod tests {
signature: Some("zxcv".to_string()),
},
);
map
map.into()
},
}
);

View file

@ -159,7 +159,7 @@ pub async fn serverside_auth(
StatusCode::FORBIDDEN => {
return Err(ServerSessionServerError::Unknown(
res.json::<ForbiddenError>().await?.error,
))
));
}
status_code => {
// log the headers

View file

@ -1,10 +1,10 @@
[package]
name = "azalea-block"
description = "Representation of Minecraft block states."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-block-macros = { path = "./azalea-block-macros", version = "0.11.0" }

View file

@ -1,15 +1,15 @@
[package]
name = "azalea-block-macros"
description = "Proc macros used by azalea-block."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true

View file

@ -9,13 +9,13 @@ use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::quote;
use syn::{
braced,
Expr, Ident, LitStr, Token, braced,
ext::IdentExt,
parenthesized,
parse::{Parse, ParseStream, Result},
parse_macro_input,
punctuated::Punctuated,
token, Expr, Ident, LitStr, Token,
token,
};
use utils::{combinations_of, to_pascal_case};
@ -511,13 +511,13 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
Ident::new(&combination[i].to_string(), proc_macro2::Span::call_site());
// this terrible code just gets the property default as a string
let property_default_as_string = if let TokenTree::Ident(ident) =
property.default.clone().into_iter().last().unwrap()
{
ident.to_string()
} else {
panic!()
};
let property_default_as_string =
match property.default.clone().into_iter().last().unwrap() {
TokenTree::Ident(ident) => ident.to_string(),
_ => {
panic!()
}
};
if property_default_as_string != combination[i] {
is_default = false;
}
@ -565,15 +565,16 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let Some(default_state_id) = default_state_id else {
let defaults = properties_with_name
.iter()
.map(|p| {
if let TokenTree::Ident(i) = p.default.clone().into_iter().last().unwrap() {
i.to_string()
} else {
.map(|p| match p.default.clone().into_iter().last().unwrap() {
TokenTree::Ident(i) => i.to_string(),
_ => {
panic!()
}
})
.collect::<Vec<_>>();
panic!("Couldn't get default state id for {block_name_pascal_case}, combinations={block_properties_vec:?}, defaults={defaults:?}")
panic!(
"Couldn't get default state id for {block_name_pascal_case}, combinations={block_properties_vec:?}, defaults={defaults:?}"
)
};
// 7035..=7058 => {

View file

@ -56,7 +56,6 @@ impl BlockBehavior {
self
}
// TODO: currently unused
pub fn force_solid(mut self, force_solid: bool) -> Self {
self.force_solid = Some(force_solid);
self

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![feature(trait_upcasting)]
mod behavior;
pub mod block_state;

View file

@ -1,9 +1,9 @@
use std::{
collections::{hash_set, HashSet},
collections::{HashSet, hash_set},
ops::{Add, RangeInclusive},
};
use crate::{block_state::BlockStateIntegerRepr, BlockState};
use crate::{BlockState, block_state::BlockStateIntegerRepr};
#[derive(Debug, Clone)]
pub struct BlockStates {

View file

@ -1,19 +1,19 @@
[package]
name = "azalea-brigadier"
description = "A port of Mojang's Brigadier command parsing and dispatching library."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dev-dependencies]
bevy_app = { workspace = true }
bevy_ecs = { workspace = true }
bevy_app.workspace = true
bevy_ecs.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.11.0", optional = true }
azalea-chat = { path = "../azalea-chat", version = "0.11.0", optional = true }
parking_lot = { workspace = true }
parking_lot.workspace = true
[features]
azalea-buf = ["dep:azalea-buf", "dep:azalea-chat", "azalea-chat/azalea-buf"]

View file

@ -108,23 +108,30 @@ impl<S> CommandDispatcher<S> {
1
}) {
reader.skip();
if let Some(redirect) = &child.read().redirect {
let child_context =
CommandContextBuilder::new(self, source, redirect.clone(), reader.cursor);
let parse = self
.parse_nodes(redirect, &reader, child_context)
.expect("Parsing nodes failed");
context.with_child(Rc::new(parse.context));
return Ok(ParseResults {
context,
reader: parse.reader,
exceptions: parse.exceptions,
});
} else {
let parse = self
.parse_nodes(&child, &reader, context)
.expect("Parsing nodes failed");
potentials.push(parse);
match &child.read().redirect {
Some(redirect) => {
let child_context = CommandContextBuilder::new(
self,
source,
redirect.clone(),
reader.cursor,
);
let parse = self
.parse_nodes(redirect, &reader, child_context)
.expect("Parsing nodes failed");
context.with_child(Rc::new(parse.context));
return Ok(ParseResults {
context,
reader: parse.reader,
exceptions: parse.exceptions,
});
}
_ => {
let parse = self
.parse_nodes(&child, &reader, context)
.expect("Parsing nodes failed");
potentials.push(parse);
}
}
} else {
potentials.push(ParseResults {
@ -215,11 +222,14 @@ impl<S> CommandDispatcher<S> {
pub fn find_node(&self, path: &[&str]) -> Option<Arc<RwLock<CommandNode<S>>>> {
let mut node = self.root.clone();
for name in path {
if let Some(child) = node.clone().read().child(name) {
node = child;
} else {
return None;
}
match node.clone().read().child(name) {
Some(child) => {
node = child;
}
_ => {
return None;
}
};
}
Some(node)
}
@ -258,15 +268,20 @@ impl<S> CommandDispatcher<S> {
let modifier = &context.modifier;
if let Some(modifier) = modifier {
let results = modifier(context);
if let Ok(results) = results {
if !results.is_empty() {
next.extend(results.iter().map(|s| child.copy_for(s.clone())));
match results {
Ok(results) => {
if !results.is_empty() {
next.extend(
results.iter().map(|s| child.copy_for(s.clone())),
);
}
}
} else {
// TODO
// self.consumer.on_command_complete(context, false, 0);
if !forked {
return Err(results.err().unwrap());
_ => {
// TODO
// self.consumer.on_command_complete(context, false, 0);
if !forked {
return Err(results.err().unwrap());
}
}
}
} else {
@ -281,8 +296,8 @@ impl<S> CommandDispatcher<S> {
// consumer.on_command_complete(context, true, value);
successful_forks += 1;
// TODO: allow context_command to error and handle those
// errors
// TODO: allow context_command to error and handle
// those errors
}
}
@ -332,32 +347,35 @@ impl<S> CommandDispatcher<S> {
if node.command.is_some() {
result.push(prefix.to_owned());
}
if let Some(redirect) = &node.redirect {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
};
if prefix.is_empty() {
result.push(format!("{} {redirect}", node.usage_text()));
} else {
result.push(format!("{prefix} {redirect}"));
match &node.redirect {
Some(redirect) => {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
};
if prefix.is_empty() {
result.push(format!("{} {redirect}", node.usage_text()));
} else {
result.push(format!("{prefix} {redirect}"));
}
}
} else {
for child in node.children.values() {
let child = child.read();
self.get_all_usage_recursive(
&child,
source,
result,
if prefix.is_empty() {
child.usage_text()
} else {
format!("{prefix} {}", child.usage_text())
}
.as_str(),
restricted,
);
_ => {
for child in node.children.values() {
let child = child.read();
self.get_all_usage_recursive(
&child,
source,
result,
if prefix.is_empty() {
child.usage_text()
} else {
format!("{prefix} {}", child.usage_text())
}
.as_str(),
restricted,
);
}
}
}
}

View file

@ -2,7 +2,7 @@ use std::{any::Any, collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
use parking_lot::RwLock;
use super::{parsed_command_node::ParsedCommandNode, string_range::StringRange, ParsedArgument};
use super::{ParsedArgument, parsed_command_node::ParsedCommandNode, string_range::StringRange};
use crate::{
modifier::RedirectModifier,
tree::{Command, CommandNode},

View file

@ -3,8 +3,8 @@ use std::{collections::HashMap, fmt::Debug, rc::Rc, sync::Arc};
use parking_lot::RwLock;
use super::{
command_context::CommandContext, parsed_command_node::ParsedCommandNode,
string_range::StringRange, suggestion_context::SuggestionContext, ParsedArgument,
ParsedArgument, command_context::CommandContext, parsed_command_node::ParsedCommandNode,
string_range::StringRange, suggestion_context::SuggestionContext,
};
use crate::{
command_dispatcher::CommandDispatcher,
@ -107,18 +107,18 @@ impl<'a, S> CommandContextBuilder<'a, S> {
}
if self.range.end() < cursor {
if let Some(child) = &self.child {
child.find_suggestion_context(cursor)
} else if let Some(last) = self.nodes.last() {
SuggestionContext {
parent: Arc::clone(&last.node),
start_pos: last.range.end() + 1,
}
} else {
SuggestionContext {
parent: Arc::clone(&self.root),
start_pos: self.range.start(),
}
match &self.child {
Some(child) => child.find_suggestion_context(cursor),
_ => match self.nodes.last() {
Some(last) => SuggestionContext {
parent: Arc::clone(&last.node),
start_pos: last.range.end() + 1,
},
_ => SuggestionContext {
parent: Arc::clone(&self.root),
start_pos: self.range.start(),
},
},
}
} else {
let mut prev = &self.root;

View file

@ -292,18 +292,27 @@ impl<S> PartialEq for CommandNode<S> {
}
}
if let Some(selfexecutes) = &self.command {
// idk how to do this better since we can't compare `dyn Fn`s
if let Some(otherexecutes) = &other.command {
#[allow(ambiguous_wide_pointer_comparisons)]
if !Arc::ptr_eq(selfexecutes, otherexecutes) {
match &self.command {
Some(selfexecutes) => {
// idk how to do this better since we can't compare `dyn Fn`s
match &other.command {
Some(otherexecutes) =>
{
#[allow(ambiguous_wide_pointer_comparisons)]
if !Arc::ptr_eq(selfexecutes, otherexecutes) {
return false;
}
}
_ => {
return false;
}
}
}
_ => {
if other.command.is_some() {
return false;
}
} else {
return false;
}
} else if other.command.is_some() {
return false;
}
true
}

View file

@ -10,11 +10,13 @@ fn test_arguments() {
let builder = builder.then(argument.clone());
assert_eq!(builder.arguments().children.len(), 1);
let built_argument = Rc::new(argument.build());
assert!(builder
.arguments()
.children
.values()
.any(|e| *e.read() == *built_argument));
assert!(
builder
.arguments()
.children
.values()
.any(|e| *e.read() == *built_argument)
);
}
// @Test

View file

@ -1,19 +1,19 @@
[package]
name = "azalea-buf"
description = "Serialize and deserialize buffers from Minecraft."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-buf-macros = { path = "./azalea-buf-macros", version = "0.11.0" }
byteorder = { workspace = true }
byteorder.workspace = true
serde_json = { workspace = true, optional = true }
simdnbt = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
simdnbt.workspace = true
thiserror.workspace = true
tracing.workspace = true
uuid.workspace = true
[features]
serde_json = ["dep:serde_json"]

View file

@ -1,15 +1,15 @@
[package]
name = "azalea-buf-macros"
description = "#[derive(AzBuf)]"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
proc-macro2.workspace = true
quote.workspace = true
syn = { workspace = true, features = ["extra-traits"] }

View file

@ -3,7 +3,7 @@ mod write;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::{DeriveInput, parse_macro_input};
#[proc_macro_derive(AzaleaRead, attributes(var))]
pub fn derive_azalearead(input: TokenStream) -> TokenStream {

View file

@ -1,5 +1,5 @@
use quote::{quote, ToTokens};
use syn::{punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
use quote::{ToTokens, quote};
use syn::{Data, Field, FieldsNamed, Ident, punctuated::Punctuated, token::Comma};
fn read_named_fields(
named: &Punctuated<Field, Comma>,

View file

@ -1,6 +1,6 @@
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
use quote::{ToTokens, quote};
use syn::{Data, Field, FieldsNamed, Ident, punctuated::Punctuated, token::Comma};
fn write_named_fields(
named: &Punctuated<Field, Comma>,

View file

@ -3,13 +3,14 @@ use std::{
collections::HashMap,
hash::Hash,
io::{Cursor, Read},
sync::Arc,
};
use byteorder::{ReadBytesExt, BE};
use byteorder::{BE, ReadBytesExt};
use thiserror::Error;
use tracing::warn;
use super::{UnsizedByteArray, MAX_STRING_LENGTH};
use super::{MAX_STRING_LENGTH, UnsizedByteArray};
#[derive(Error, Debug)]
pub enum BufReadError {
@ -19,8 +20,12 @@ pub enum BufReadError {
InvalidVarLong,
#[error("Error reading bytes")]
CouldNotReadBytes,
#[error("The received encoded string buffer length is longer than maximum allowed ({length} > {max_length})")]
#[error(
"The received encoded string buffer length is longer than maximum allowed ({length} > {max_length})"
)]
StringLengthTooLong { length: u32, max_length: u32 },
#[error("The received Vec length is longer than maximum allowed ({length} > {max_length})")]
VecLengthTooLong { length: u32, max_length: u32 },
#[error("{source}")]
Io {
#[from]
@ -183,7 +188,7 @@ impl AzaleaRead for UnsizedByteArray {
}
}
impl<T: AzaleaRead + Send> AzaleaRead for Vec<T> {
impl<T: AzaleaRead> AzaleaRead for Vec<T> {
default fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let length = u32::azalea_read_var(buf)? as usize;
// we limit the capacity to not get exploited into allocating a bunch
@ -194,6 +199,33 @@ impl<T: AzaleaRead + Send> AzaleaRead for Vec<T> {
Ok(contents)
}
}
impl<T: AzaleaRead> AzaleaRead for Box<[T]> {
default fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Vec::<T>::azalea_read(buf).map(Vec::into_boxed_slice)
}
}
impl<T: AzaleaRead> AzaleaReadLimited for Vec<T> {
fn azalea_read_limited(buf: &mut Cursor<&[u8]>, limit: usize) -> Result<Self, BufReadError> {
let length = u32::azalea_read_var(buf)? as usize;
if length > limit {
return Err(BufReadError::VecLengthTooLong {
length: length as u32,
max_length: limit as u32,
});
}
let mut contents = Vec::with_capacity(usize::min(length, 65536));
for _ in 0..length {
contents.push(T::azalea_read(buf)?);
}
Ok(contents)
}
}
impl<T: AzaleaRead> AzaleaReadLimited for Box<[T]> {
fn azalea_read_limited(buf: &mut Cursor<&[u8]>, limit: usize) -> Result<Self, BufReadError> {
Vec::<T>::azalea_read_limited(buf, limit).map(Vec::into_boxed_slice)
}
}
impl<K: AzaleaRead + Send + Eq + Hash, V: AzaleaRead + Send> AzaleaRead for HashMap<K, V> {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
@ -275,6 +307,11 @@ impl<T: AzaleaReadVar> AzaleaReadVar for Vec<T> {
Ok(contents)
}
}
impl<T: AzaleaReadVar> AzaleaReadVar for Box<[T]> {
fn azalea_read_var(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Vec::<T>::azalea_read_var(buf).map(Vec::into_boxed_slice)
}
}
impl AzaleaRead for i64 {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
@ -343,6 +380,16 @@ impl<T: AzaleaReadVar> AzaleaReadVar for Option<T> {
})
}
}
impl<T: AzaleaReadLimited> AzaleaReadLimited for Option<T> {
fn azalea_read_limited(buf: &mut Cursor<&[u8]>, limit: usize) -> Result<Self, BufReadError> {
let present = bool::azalea_read(buf)?;
Ok(if present {
Some(T::azalea_read_limited(buf, limit)?)
} else {
None
})
}
}
// [String; 4]
impl<T: AzaleaRead, const N: usize> AzaleaRead for [T; N] {
@ -386,3 +433,15 @@ where
Ok(Box::new(T::azalea_read(buf)?))
}
}
impl<A: AzaleaRead, B: AzaleaRead> AzaleaRead for (A, B) {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Ok((A::azalea_read(buf)?, B::azalea_read(buf)?))
}
}
impl<T: AzaleaRead> AzaleaRead for Arc<T> {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Ok(Arc::new(T::azalea_read(buf)?))
}
}

View file

@ -2,7 +2,7 @@ use std::io::{Cursor, Write};
use uuid::Uuid;
use crate::{read::BufReadError, AzaleaRead, AzaleaWrite};
use crate::{AzaleaRead, AzaleaWrite, read::BufReadError};
pub trait SerializableUuid {
fn to_int_array(&self) -> [u32; 4];

View file

@ -1,14 +1,14 @@
use std::{collections::HashMap, io::Write};
use std::{
collections::HashMap,
io::{self, Write},
sync::Arc,
};
use byteorder::{BigEndian, WriteBytesExt};
use super::{UnsizedByteArray, MAX_STRING_LENGTH};
use super::{MAX_STRING_LENGTH, UnsizedByteArray};
fn write_utf_with_len(
buf: &mut impl Write,
string: &str,
len: usize,
) -> Result<(), std::io::Error> {
fn write_utf_with_len(buf: &mut impl Write, string: &str, len: usize) -> Result<(), io::Error> {
if string.len() > len {
panic!(
"String too big (was {} bytes encoded, max {})",
@ -21,21 +21,21 @@ fn write_utf_with_len(
}
pub trait AzaleaWrite {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error>;
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error>;
}
pub trait AzaleaWriteVar {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error>;
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error>;
}
impl AzaleaWrite for i32 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
WriteBytesExt::write_i32::<BigEndian>(buf, *self)
}
}
impl AzaleaWriteVar for i32 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
let mut buffer = [0];
let mut value = *self;
if value == 0 {
@ -54,19 +54,24 @@ impl AzaleaWriteVar for i32 {
}
impl AzaleaWrite for UnsizedByteArray {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
buf.write_all(self)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Vec<T> {
default fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
default fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
self[..].azalea_write(buf)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Box<[T]> {
default fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
self[..].azalea_write(buf)
}
}
impl<T: AzaleaWrite> AzaleaWrite for [T] {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
(self.len() as u32).azalea_write_var(buf)?;
for item in self {
T::azalea_write(item, buf)?;
@ -76,7 +81,7 @@ impl<T: AzaleaWrite> AzaleaWrite for [T] {
}
impl<K: AzaleaWrite, V: AzaleaWrite> AzaleaWrite for HashMap<K, V> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
u32::azalea_write_var(&(self.len() as u32), buf)?;
for (key, value) in self {
key.azalea_write(buf)?;
@ -88,7 +93,7 @@ impl<K: AzaleaWrite, V: AzaleaWrite> AzaleaWrite for HashMap<K, V> {
}
impl<K: AzaleaWrite, V: AzaleaWriteVar> AzaleaWriteVar for HashMap<K, V> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
u32::azalea_write_var(&(self.len() as u32), buf)?;
for (key, value) in self {
key.azalea_write(buf)?;
@ -100,38 +105,38 @@ impl<K: AzaleaWrite, V: AzaleaWriteVar> AzaleaWriteVar for HashMap<K, V> {
}
impl AzaleaWrite for Vec<u8> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
(self.len() as u32).azalea_write_var(buf)?;
buf.write_all(self)
}
}
impl AzaleaWrite for String {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
write_utf_with_len(buf, self, MAX_STRING_LENGTH.into())
}
}
impl AzaleaWrite for &str {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
write_utf_with_len(buf, self, MAX_STRING_LENGTH.into())
}
}
impl AzaleaWrite for u32 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
i32::azalea_write(&(*self as i32), buf)
}
}
impl AzaleaWriteVar for u32 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
i32::azalea_write_var(&(*self as i32), buf)
}
}
impl AzaleaWriteVar for i64 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
let mut buffer = [0];
let mut value = *self;
if value == 0 {
@ -150,25 +155,25 @@ impl AzaleaWriteVar for i64 {
}
impl AzaleaWriteVar for u64 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
i64::azalea_write_var(&(*self as i64), buf)
}
}
impl AzaleaWrite for u16 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
i16::azalea_write(&(*self as i16), buf)
}
}
impl AzaleaWriteVar for u16 {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
i32::azalea_write_var(&(*self as i32), buf)
}
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Vec<T> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
impl<T: AzaleaWriteVar> AzaleaWriteVar for [T] {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
u32::azalea_write_var(&(self.len() as u32), buf)?;
for i in self {
i.azalea_write_var(buf)?;
@ -176,58 +181,68 @@ impl<T: AzaleaWriteVar> AzaleaWriteVar for Vec<T> {
Ok(())
}
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Vec<T> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
self[..].azalea_write_var(buf)
}
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Box<[T]> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
self[..].azalea_write_var(buf)
}
}
impl AzaleaWrite for u8 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
WriteBytesExt::write_u8(buf, *self)
}
}
impl AzaleaWrite for i16 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
WriteBytesExt::write_i16::<BigEndian>(buf, *self)
}
}
impl AzaleaWrite for i64 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
WriteBytesExt::write_i64::<BigEndian>(buf, *self)
}
}
impl AzaleaWrite for u64 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
i64::azalea_write(&(*self as i64), buf)
}
}
impl AzaleaWrite for bool {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
let byte = u8::from(*self);
byte.azalea_write(buf)
}
}
impl AzaleaWrite for i8 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
(*self as u8).azalea_write(buf)
}
}
impl AzaleaWrite for f32 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
WriteBytesExt::write_f32::<BigEndian>(buf, *self)
}
}
impl AzaleaWrite for f64 {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
WriteBytesExt::write_f64::<BigEndian>(buf, *self)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Option<T> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
if let Some(s) = self {
true.azalea_write(buf)?;
s.azalea_write(buf)?;
@ -239,7 +254,7 @@ impl<T: AzaleaWrite> AzaleaWrite for Option<T> {
}
impl<T: AzaleaWriteVar> AzaleaWriteVar for Option<T> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write_var(&self, buf: &mut impl Write) -> Result<(), io::Error> {
if let Some(s) = self {
true.azalea_write(buf)?;
s.azalea_write_var(buf)?;
@ -252,7 +267,7 @@ impl<T: AzaleaWriteVar> AzaleaWriteVar for Option<T> {
// [T; N]
impl<T: AzaleaWrite, const N: usize> AzaleaWrite for [T; N] {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
for i in self {
i.azalea_write(buf)?;
}
@ -261,7 +276,7 @@ impl<T: AzaleaWrite, const N: usize> AzaleaWrite for [T; N] {
}
impl AzaleaWrite for simdnbt::owned::NbtTag {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
let mut data = Vec::new();
self.write(&mut data);
buf.write_all(&data)
@ -269,7 +284,7 @@ impl AzaleaWrite for simdnbt::owned::NbtTag {
}
impl AzaleaWrite for simdnbt::owned::NbtCompound {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
let mut data = Vec::new();
simdnbt::owned::NbtTag::Compound(self.clone()).write(&mut data);
buf.write_all(&data)
@ -277,7 +292,7 @@ impl AzaleaWrite for simdnbt::owned::NbtCompound {
}
impl AzaleaWrite for simdnbt::owned::Nbt {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
let mut data = Vec::new();
self.write_unnamed(&mut data);
buf.write_all(&data)
@ -288,7 +303,20 @@ impl<T> AzaleaWrite for Box<T>
where
T: AzaleaWrite,
{
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
T::azalea_write(&**self, buf)
}
}
impl<A: AzaleaWrite, B: AzaleaWrite> AzaleaWrite for (A, B) {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
self.0.azalea_write(buf)?;
self.1.azalea_write(buf)
}
}
impl<T: AzaleaWrite> AzaleaWrite for Arc<T> {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
T::azalea_write(&**self, buf)
}
}

View file

@ -1,10 +1,10 @@
[package]
name = "azalea-chat"
description = "Parse Minecraft chat messages."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[features]
default = []
@ -19,6 +19,6 @@ azalea-buf = { path = "../azalea-buf", version = "0.11.0", optional = true, feat
azalea-language = { path = "../azalea-language", version = "0.11.0" }
azalea-registry = { path = "../azalea-registry", version = "0.11.0", optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_json.workspace = true
simdnbt = { workspace = true, optional = true }
tracing = { workspace = true }
tracing.workspace = true

View file

@ -1,6 +1,6 @@
use serde::Serialize;
use crate::{style::Style, FormattedText};
use crate::{FormattedText, style::Style};
#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]
pub struct BaseComponent {

View file

@ -2,7 +2,7 @@ use std::{fmt::Display, sync::LazyLock};
#[cfg(feature = "azalea-buf")]
use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
use serde::{de, Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Deserializer, Serialize, de};
#[cfg(feature = "simdnbt")]
use simdnbt::{Deserialize as _, FromNbtTag as _, Serialize as _};
use tracing::{debug, trace, warn};
@ -371,7 +371,9 @@ impl FormattedText {
} else if let Some(s) = primitive.string() {
with_array.push(StringOrComponent::String(s.to_string()));
} else {
warn!("couldn't parse {item:?} as FormattedText because it has a disallowed primitive");
warn!(
"couldn't parse {item:?} as FormattedText because it has a disallowed primitive"
);
with_array.push(StringOrComponent::String("?".to_string()));
}
} else if let Some(c) = FormattedText::from_nbt_compound(item) {
@ -392,7 +394,9 @@ impl FormattedText {
}
}
} else {
warn!("couldn't parse {with:?} as FormattedText because it's not a list of compounds");
warn!(
"couldn't parse {with:?} as FormattedText because it's not a list of compounds"
);
return None;
}
component =
@ -456,12 +460,11 @@ impl From<&simdnbt::Mutf8Str> for FormattedText {
impl AzaleaRead for FormattedText {
fn azalea_read(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, BufReadError> {
let nbt = simdnbt::borrow::read_optional_tag(buf)?;
if let Some(nbt) = nbt {
FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
match nbt {
Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom(
"couldn't convert nbt to chat message".to_owned(),
))
} else {
Ok(FormattedText::default())
)),
_ => Ok(FormattedText::default()),
}
}
}

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt, sync::LazyLock};
#[cfg(feature = "azalea-buf")]
use azalea_buf::AzBuf;
use serde::{ser::SerializeStruct, Serialize, Serializer};
use serde::{Serialize, Serializer, ser::SerializeStruct};
use serde_json::Value;
#[cfg(feature = "simdnbt")]
use simdnbt::owned::{NbtCompound, NbtTag};
@ -334,10 +334,15 @@ fn simdnbt_serialize_field(
default: impl simdnbt::ToNbtTag,
reset: bool,
) {
if let Some(value) = value {
compound.insert(name, value);
} else if reset {
compound.insert(name, default);
match value {
Some(value) => {
compound.insert(name, value);
}
_ => {
if reset {
compound.insert(name, default);
}
}
}
}

View file

@ -1,8 +1,8 @@
use std::fmt::Display;
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::SerializeMap};
use crate::{base_component::BaseComponent, style::ChatFormatting, FormattedText};
use crate::{FormattedText, base_component::BaseComponent, style::ChatFormatting};
/// A component that contains text that's the same in all locales.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]

View file

@ -1,11 +1,11 @@
use std::fmt::{self, Display, Formatter};
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::SerializeMap};
#[cfg(feature = "simdnbt")]
use simdnbt::Serialize as _;
use crate::{
base_component::BaseComponent, style::Style, text_component::TextComponent, FormattedText,
FormattedText, base_component::BaseComponent, style::Style, text_component::TextComponent,
};
#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]

View file

@ -1,6 +1,6 @@
use azalea_chat::{
style::{Ansi, ChatFormatting, TextColor},
FormattedText,
style::{Ansi, ChatFormatting, TextColor},
};
use serde::Deserialize;
use serde_json::Value;

View file

@ -1,10 +1,10 @@
[package]
name = "azalea-client"
description = "A headless Minecraft client."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-auth = { path = "../azalea-auth", version = "0.11.0" }
@ -19,24 +19,25 @@ azalea-physics = { path = "../azalea-physics", version = "0.11.0" }
azalea-protocol = { path = "../azalea-protocol", version = "0.11.0" }
azalea-registry = { path = "../azalea-registry", version = "0.11.0" }
azalea-world = { path = "../azalea-world", version = "0.11.0" }
bevy_app = { workspace = true }
bevy_ecs = { workspace = true }
bevy_app.workspace = true
bevy_ecs.workspace = true
bevy_log = { workspace = true, optional = true }
bevy_tasks = { workspace = true }
bevy_time = { workspace = true }
bevy_tasks.workspace = true
bevy_time.workspace = true
derive_more = { workspace = true, features = ["deref", "deref_mut"] }
minecraft_folder_path = { workspace = true }
parking_lot = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true }
simdnbt = { workspace = true }
thiserror = { workspace = true }
minecraft_folder_path.workspace = true
parking_lot.workspace = true
paste.workspace = true
regex.workspace = true
reqwest.workspace = true
simdnbt.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true }
uuid = { workspace = true }
tracing.workspace = true
uuid.workspace = true
[dev-dependencies]
anyhow = { workspace = true }
anyhow.workspace = true
[features]
default = ["log"]

View file

@ -2,8 +2,8 @@
use std::sync::Arc;
use azalea_auth::certs::{Certificates, FetchCertificatesError};
use azalea_auth::AccessTokenResponse;
use azalea_auth::certs::{Certificates, FetchCertificatesError};
use bevy_ecs::component::Component;
use parking_lot::Mutex;
use thiserror::Error;

View file

@ -9,31 +9,34 @@ use std::{
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
use azalea_core::{position::Vec3, tick::GameTick};
use azalea_core::{
data_registry::ResolvableDataRegistry, position::Vec3, resource_location::ResourceLocation,
tick::GameTick,
};
use azalea_entity::{
EntityPlugin, EntityUpdateSet, EyeHeight, LocalEntity, Position,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::Health,
EntityPlugin, EntityUpdateSet, EyeHeight, LocalEntity, Position,
};
use azalea_physics::PhysicsPlugin;
use azalea_protocol::{
ServerAddress,
common::client_information::ClientInformation,
connect::{Connection, ConnectionError, Proxy},
packets::{
self,
self, ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, Packet,
config::{ClientboundConfigPacket, ServerboundConfigPacket},
game::ServerboundGamePacket,
handshake::{
s_intention::ServerboundIntention, ClientboundHandshakePacket,
ServerboundHandshakePacket,
ClientboundHandshakePacket, ServerboundHandshakePacket,
s_intention::ServerboundIntention,
},
login::{
s_hello::ServerboundHello, s_key::ServerboundKey,
s_login_acknowledged::ServerboundLoginAcknowledged, ClientboundLoginPacket,
ClientboundLoginPacket, s_hello::ServerboundHello, s_key::ServerboundKey,
s_login_acknowledged::ServerboundLoginAcknowledged,
},
ClientIntention, ConnectionProtocol, Packet, PROTOCOL_VERSION,
},
resolver, ServerAddress,
resolver,
};
use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance};
use bevy_app::{App, Plugin, PluginGroup, PluginGroupBuilder, Update};
@ -48,38 +51,42 @@ use bevy_ecs::{
use bevy_time::TimePlugin;
use derive_more::Deref;
use parking_lot::{Mutex, RwLock};
use simdnbt::owned::NbtCompound;
use thiserror::Error;
use tokio::{
sync::{broadcast, mpsc},
sync::{
broadcast,
mpsc::{self, error::TrySendError},
},
time,
};
use tracing::{debug, error};
use uuid::Uuid;
use crate::{
Account, PlayerInfo,
attack::{self, AttackPlugin},
brand::BrandPlugin,
chat::ChatPlugin,
chunks::{ChunkBatchInfo, ChunkPlugin},
configuration::ConfigurationPlugin,
chunks::{ChunkBatchInfo, ChunksPlugin},
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents},
events::{Event, EventsPlugin, LocalPlayerEvents},
interact::{CurrentSequenceNumber, InteractPlugin},
inventory::{Inventory, InventoryPlugin},
local_player::{
death_event, GameProfileComponent, Hunger, InstanceHolder, PermissionLevel,
PlayerAbilities, TabList,
GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
},
mining::{self, MinePlugin},
movement::{LastSentLookDirection, PhysicsState, PlayerMovePlugin},
packet_handling::{
login::{self, LoginSendPacketQueue},
PacketHandlerPlugin,
mining::{self, MiningPlugin},
movement::{LastSentLookDirection, MovementPlugin, PhysicsState},
packet::{
PacketPlugin,
login::{self, InLoginState, LoginSendPacketQueue},
},
player::retroactively_add_game_profile_component,
raw_connection::RawConnection,
respawn::RespawnPlugin,
task_pool::TaskPoolPlugin,
Account, PlayerInfo,
tick_end::TickEndPlugin,
};
/// `Client` has the things that a user interacting with the library will want.
@ -111,7 +118,7 @@ pub struct Client {
pub ecs: Arc<Mutex<World>>,
/// Use this to force the client to run the schedule outside of a tick.
pub run_schedule_sender: mpsc::UnboundedSender<()>,
pub run_schedule_sender: mpsc::Sender<()>,
}
/// An error that happened while joining the server.
@ -141,7 +148,7 @@ pub struct StartClientOpts<'a> {
pub address: &'a ServerAddress,
pub resolved_address: &'a SocketAddr,
pub proxy: Option<Proxy>,
pub run_schedule_sender: mpsc::UnboundedSender<()>,
pub run_schedule_sender: mpsc::Sender<()>,
}
impl<'a> StartClientOpts<'a> {
@ -151,7 +158,7 @@ impl<'a> StartClientOpts<'a> {
resolved_address: &'a SocketAddr,
) -> StartClientOpts<'a> {
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1);
let mut app = App::new();
app.add_plugins(DefaultPlugins);
@ -183,7 +190,7 @@ impl Client {
profile: GameProfile,
entity: Entity,
ecs: Arc<Mutex<World>>,
run_schedule_sender: mpsc::UnboundedSender<()>,
run_schedule_sender: mpsc::Sender<()>,
) -> Self {
Self {
profile,
@ -324,8 +331,11 @@ impl Client {
game_profile: GameProfileComponent(game_profile),
client_information: crate::ClientInformation::default(),
instance_holder,
metadata: azalea_entity::metadata::PlayerMetadataBundle::default(),
},
InConfigurationState,
InConfigState,
// this component is never removed
LocalEntity,
));
Ok((client, rx))
@ -364,7 +374,8 @@ impl Client {
let (ecs_packets_tx, mut ecs_packets_rx) = mpsc::unbounded_channel();
ecs_lock.lock().entity_mut(entity).insert((
LoginSendPacketQueue { tx: ecs_packets_tx },
login::IgnoreQueryIds::default(),
crate::packet::login::IgnoreQueryIds::default(),
InLoginState,
));
// login
@ -451,7 +462,8 @@ impl Client {
p.game_profile
);
conn.write(ServerboundLoginAcknowledged {}).await?;
break (conn.configuration(), p.game_profile);
break (conn.config(), p.game_profile);
}
ClientboundLoginPacket::LoginDisconnect(p) => {
debug!("Got disconnect {:?}", p);
@ -460,7 +472,7 @@ impl Client {
ClientboundLoginPacket::CustomQuery(p) => {
debug!("Got custom query {:?}", p);
// replying to custom query is done in
// packet_handling::login::process_packet_events
// packet::login::process_packet_events
}
ClientboundLoginPacket::CookieRequest(p) => {
debug!("Got cookie request {:?}", p);
@ -479,7 +491,8 @@ impl Client {
.lock()
.entity_mut(entity)
.remove::<login::IgnoreQueryIds>()
.remove::<LoginSendPacketQueue>();
.remove::<LoginSendPacketQueue>()
.remove::<InLoginState>();
Ok((conn, profile))
}
@ -549,6 +562,11 @@ impl Client {
self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
}
/// Get a resource from the ECS. This will clone the resource and return it.
pub fn resource<T: Resource + Clone>(&self) -> T {
self.ecs.lock().resource::<T>().clone()
}
/// Get a required component for this client and call the given function.
///
/// Similar to [`Self::component`], but doesn't clone the component since
@ -711,7 +729,55 @@ impl Client {
///
/// This is a shortcut for `*bot.component::<TabList>()`.
pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> {
self.component::<TabList>().deref().clone()
(*self.component::<TabList>()).clone()
}
/// Call the given function with the client's [`RegistryHolder`].
///
/// The player's instance (aka world) will be locked during this time, which
/// may result in a deadlock if you try to access the instance again while
/// in the function.
///
/// [`RegistryHolder`]: azalea_core::registry_holder::RegistryHolder
pub fn with_registry_holder<R>(
&self,
f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R,
) -> R {
let instance = self.world();
let registries = &instance.read().registries;
f(registries)
}
/// Resolve the given registry to its name.
///
/// This is necessary for data-driven registries like [`Enchantment`].
///
/// [`Enchantment`]: azalea_registry::Enchantment
pub fn resolve_registry_name(
&self,
registry: &impl ResolvableDataRegistry,
) -> Option<ResourceLocation> {
self.with_registry_holder(|registries| registry.resolve_name(registries))
}
/// Resolve the given registry to its name and data and call the given
/// function with it.
///
/// This is necessary for data-driven registries like [`Enchantment`].
///
/// If you just want the value name, use [`Self::resolve_registry_name`]
/// instead.
///
/// [`Enchantment`]: azalea_registry::Enchantment
pub fn with_resolved_registry<R>(
&self,
registry: impl ResolvableDataRegistry,
f: impl FnOnce(&ResourceLocation, &NbtCompound) -> R,
) -> Option<R> {
self.with_registry_holder(|registries| {
registry
.resolve(registries)
.map(|(name, data)| f(name, data))
})
}
}
@ -719,8 +785,7 @@ impl Client {
/// `configuration` or `game` state.
///
/// For the components that are only present in the `game` state, see
/// [`JoinedClientBundle`] and for the ones in the `configuration` state, see
/// [`ConfigurationClientBundle`].
/// [`JoinedClientBundle`].
#[derive(Bundle)]
pub struct LocalPlayerBundle {
pub raw_connection: RawConnection,
@ -728,12 +793,14 @@ pub struct LocalPlayerBundle {
pub game_profile: GameProfileComponent,
pub client_information: ClientInformation,
pub instance_holder: InstanceHolder,
pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
}
/// A bundle for the components that are present on a local player that is
/// currently in the `game` protocol state. If you want to filter for this, just
/// use [`LocalEntity`].
#[derive(Bundle)]
/// currently in the `game` protocol state. If you want to filter for this, use
/// [`InGameState`].
#[derive(Bundle, Default)]
pub struct JoinedClientBundle {
// note that InstanceHolder isn't here because it's set slightly before we fully join the world
pub physics_state: PhysicsState,
@ -751,13 +818,17 @@ pub struct JoinedClientBundle {
pub mining: mining::MineBundle,
pub attack: attack::AttackBundle,
pub _local_entity: LocalEntity,
pub in_game_state: InGameState,
}
/// A marker component for local players that are currently in the
/// `game` state.
#[derive(Component, Clone, Debug, Default)]
pub struct InGameState;
/// A marker component for local players that are currently in the
/// `configuration` state.
#[derive(Component)]
pub struct InConfigurationState;
#[derive(Component, Clone, Debug, Default)]
pub struct InConfigState;
pub struct AzaleaPlugin;
impl Plugin for AzaleaPlugin {
@ -765,8 +836,6 @@ impl Plugin for AzaleaPlugin {
app.add_systems(
Update,
(
// fire the Death event when the player dies.
death_event,
// add GameProfileComponent when we get an AddPlayerEvent
retroactively_add_game_profile_component.after(EntityUpdateSet::Index),
),
@ -783,8 +852,8 @@ impl Plugin for AzaleaPlugin {
#[doc(hidden)]
pub fn start_ecs_runner(
mut app: App,
run_schedule_receiver: mpsc::UnboundedReceiver<()>,
run_schedule_sender: mpsc::UnboundedSender<()>,
run_schedule_receiver: mpsc::Receiver<()>,
run_schedule_sender: mpsc::Sender<()>,
) -> Arc<Mutex<World>> {
// all resources should have been added by now so we can take the ecs from the
// app
@ -803,19 +872,17 @@ pub fn start_ecs_runner(
async fn run_schedule_loop(
ecs: Arc<Mutex<World>>,
outer_schedule_label: InternedScheduleLabel,
mut run_schedule_receiver: mpsc::UnboundedReceiver<()>,
mut run_schedule_receiver: mpsc::Receiver<()>,
) {
let mut last_tick: Option<Instant> = None;
loop {
// get rid of any queued events
while let Ok(()) = run_schedule_receiver.try_recv() {}
// whenever we get an event from run_schedule_receiver, run the schedule
run_schedule_receiver.recv().await;
let mut ecs = ecs.lock();
// if last tick is None or more than 50ms ago, run the GameTick schedule
ecs.run_schedule(outer_schedule_label);
if last_tick
.map(|last_tick| last_tick.elapsed() > Duration::from_millis(50))
.unwrap_or(true)
@ -828,23 +895,21 @@ async fn run_schedule_loop(
ecs.run_schedule(GameTick);
}
ecs.run_schedule(outer_schedule_label);
ecs.clear_trackers();
}
}
/// Send an event to run the schedule every 50 milliseconds. It will stop when
/// the receiver is dropped.
pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<()>) {
let mut game_tick_interval = time::interval(time::Duration::from_millis(50));
pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) {
let mut game_tick_interval = time::interval(Duration::from_millis(50));
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
loop {
game_tick_interval.tick().await;
if let Err(e) = run_schedule_sender.send(()) {
println!("tick_run_schedule_loop error: {e}");
if let Err(TrySendError::Closed(())) = run_schedule_sender.try_send(()) {
error!("tick_run_schedule_loop failed because run_schedule_sender was closed");
// the sender is closed so end the task
return;
}
@ -912,22 +977,23 @@ impl PluginGroup for DefaultPlugins {
let mut group = PluginGroupBuilder::start::<Self>()
.add(AmbiguityLoggerPlugin)
.add(TimePlugin)
.add(PacketHandlerPlugin)
.add(PacketPlugin)
.add(AzaleaPlugin)
.add(EntityPlugin)
.add(PhysicsPlugin)
.add(EventPlugin)
.add(EventsPlugin)
.add(TaskPoolPlugin::default())
.add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
.add(PlayerMovePlugin)
.add(MovementPlugin)
.add(InteractPlugin)
.add(RespawnPlugin)
.add(MinePlugin)
.add(MiningPlugin)
.add(AttackPlugin)
.add(ChunkPlugin)
.add(ConfigurationPlugin)
.add(ChunksPlugin)
.add(TickEndPlugin)
.add(BrandPlugin)
.add(TickBroadcastPlugin);
#[cfg(feature = "log")]
{

View file

@ -1,49 +0,0 @@
use azalea_buf::AzaleaWrite;
use azalea_core::resource_location::ResourceLocation;
use azalea_protocol::{
common::client_information::ClientInformation,
packets::config::{
s_client_information::ServerboundClientInformation,
s_custom_payload::ServerboundCustomPayload,
},
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use crate::{client::InConfigurationState, packet_handling::configuration::SendConfigurationEvent};
pub struct ConfigurationPlugin;
impl Plugin for ConfigurationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
handle_in_configuration_state
.after(crate::packet_handling::configuration::handle_send_packet_event),
);
}
}
fn handle_in_configuration_state(
query: Query<(Entity, &ClientInformation), Added<InConfigurationState>>,
mut send_packet_events: EventWriter<SendConfigurationEvent>,
) {
for (entity, client_information) in query.iter() {
let mut brand_data = Vec::new();
// they don't have to know :)
"vanilla".azalea_write(&mut brand_data).unwrap();
send_packet_events.send(SendConfigurationEvent::new(
entity,
ServerboundCustomPayload {
identifier: ResourceLocation::new("brand"),
data: brand_data.into(),
},
));
send_packet_events.send(SendConfigurationEvent::new(
entity,
ServerboundClientInformation {
information: client_information.clone(),
},
));
}
}

View file

@ -106,9 +106,7 @@ where
fn find(&self, ecs_lock: Arc<Mutex<World>>) -> 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
query.iter(&ecs).find(|(_, q)| (self)(q)).map(|(e, _)| e)
}
}

View file

@ -5,35 +5,26 @@
//! [`azalea_protocol`]: https://docs.rs/azalea-protocol
//! [`azalea`]: https://docs.rs/azalea
#![allow(incomplete_features)]
#![feature(error_generic_member_access)]
#![feature(never_type)]
mod account;
pub mod attack;
pub mod chat;
pub mod chunks;
mod client;
pub mod configuration;
pub mod disconnect;
mod entity_query;
pub mod events;
pub mod interact;
pub mod inventory;
mod local_player;
pub mod mining;
pub mod movement;
pub mod packet_handling;
pub mod ping;
mod player;
mod plugins;
pub mod raw_connection;
pub mod respawn;
pub mod task_pool;
#[doc(hidden)]
pub mod test_simulation;
pub use account::{Account, AccountOpts};
pub use azalea_protocol::common::client_information::ClientInformation;
pub use client::{
start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, StartClientOpts,
TickBroadcast,
Client, DefaultPlugins, InConfigState, InGameState, JoinError, JoinedClientBundle,
LocalPlayerBundle, StartClientOpts, TickBroadcast, start_ecs_runner,
};
pub use events::Event;
pub use local_player::{GameProfileComponent, Hunger, InstanceHolder, TabList};
@ -41,3 +32,4 @@ pub use movement::{
PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
};
pub use player::PlayerInfo;
pub use plugins::*;

View file

@ -2,7 +2,6 @@ use std::{collections::HashMap, io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::game_type::GameMode;
use azalea_entity::Dead;
use azalea_protocol::packets::game::c_player_abilities::ClientboundPlayerAbilities;
use azalea_world::{Instance, PartialInstance};
use bevy_ecs::{component::Component, prelude::*};
@ -13,21 +12,27 @@ use tokio::sync::mpsc;
use tracing::error;
use uuid::Uuid;
use crate::{
events::{Event as AzaleaEvent, LocalPlayerEvents},
ClientInformation, PlayerInfo,
};
use crate::{ClientInformation, PlayerInfo, events::Event as AzaleaEvent};
/// A component that keeps strong references to our [`PartialInstance`] and
/// [`Instance`] for local players.
///
/// This can also act as a convenience for accessing the player's Instance since
/// the alternative is to look up the player's [`InstanceName`] in the
/// [`InstanceContainer`].
///
/// [`InstanceContainer`]: azalea_world::InstanceContainer
/// [`InstanceName`]: azalea_world::InstanceName
#[derive(Component, Clone)]
pub struct InstanceHolder {
/// The partial instance is the world this client currently has loaded. It
/// has a limited render distance.
pub partial_instance: Arc<RwLock<PartialInstance>>,
/// The world is the combined [`PartialInstance`]s of all clients in the
/// same world. (Only relevant if you're using a shared world, i.e. a
/// swarm)
/// same world.
///
/// This is only relevant if you're using a shared world (i.e. a
/// swarm).
pub instance: Arc<RwLock<Instance>>,
}
@ -91,6 +96,13 @@ pub struct PermissionLevel(pub u8);
/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency);
/// }
/// # }
/// ```
///
/// For convenience, `TabList` is also a resource in the ECS.
/// It's set to be the same as the tab list for the last client whose tab list
/// was updated.
/// This means you should avoid using `TabList` as a resource unless you know
/// all of your clients will have the same tab list.
#[derive(Component, Resource, Clone, Debug, Deref, DerefMut, Default)]
pub struct TabList(HashMap<Uuid, PlayerInfo>);
@ -114,12 +126,16 @@ impl Default for Hunger {
}
impl InstanceHolder {
/// Create a new `InstanceHolder`.
pub fn new(entity: Entity, world: Arc<RwLock<Instance>>) -> Self {
/// Create a new `InstanceHolder` for the given entity.
///
/// The partial instance will be created for you. The render distance will
/// be set to a default value, which you can change by creating a new
/// partial_instance.
pub fn new(entity: Entity, instance: Arc<RwLock<Instance>>) -> Self {
let client_information = ClientInformation::default();
InstanceHolder {
instance: world,
instance,
partial_instance: Arc::new(RwLock::new(PartialInstance::new(
azalea_world::chunk_storage::calculate_chunk_storage_range(
client_information.view_distance.into(),
@ -130,13 +146,6 @@ impl InstanceHolder {
}
}
/// Send the "Death" event for [`LocalEntity`]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(AzaleaEvent::Death(None)).unwrap();
}
}
#[derive(Error, Debug)]
pub enum HandlePacketError {
#[error("{0}")]

View file

@ -1,271 +0,0 @@
use std::io::Cursor;
use azalea_entity::indexing::EntityIdIndex;
use azalea_protocol::packets::config::s_finish_configuration::ServerboundFinishConfiguration;
use azalea_protocol::packets::config::s_keep_alive::ServerboundKeepAlive;
use azalea_protocol::packets::config::s_select_known_packs::ServerboundSelectKnownPacks;
use azalea_protocol::packets::config::{
self, ClientboundConfigPacket, ServerboundConfigPacket, ServerboundCookieResponse,
ServerboundResourcePack,
};
use azalea_protocol::packets::{ConnectionProtocol, Packet};
use azalea_protocol::read::deserialize_packet;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState;
use tracing::{debug, error, warn};
use crate::client::InConfigurationState;
use crate::disconnect::DisconnectEvent;
use crate::local_player::Hunger;
use crate::packet_handling::game::KeepAliveEvent;
use crate::raw_connection::RawConnection;
use crate::InstanceHolder;
#[derive(Event, Debug, Clone)]
pub struct ConfigurationEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
pub packet: ClientboundConfigPacket,
}
pub fn send_packet_events(
query: Query<(Entity, &RawConnection), With<InConfigurationState>>,
mut packet_events: ResMut<Events<ConfigurationEvent>>,
) {
// we manually clear and send the events at the beginning of each update
// since otherwise it'd cause issues with events in process_packet_events
// running twice
packet_events.clear();
for (player_entity, raw_conn) in &query {
let packets_lock = raw_conn.incoming_packet_queue();
let mut packets = packets_lock.lock();
if !packets.is_empty() {
for raw_packet in packets.iter() {
let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new(
raw_packet,
)) {
Ok(packet) => packet,
Err(err) => {
error!("failed to read packet: {err:?}");
debug!("packet bytes: {raw_packet:?}");
continue;
}
};
packet_events.send(ConfigurationEvent {
entity: player_entity,
packet,
});
}
// clear the packets right after we read them
packets.clear();
}
}
}
pub fn process_packet_events(ecs: &mut World) {
let mut events_owned = Vec::new();
let mut system_state: SystemState<EventReader<ConfigurationEvent>> = SystemState::new(ecs);
let mut events = system_state.get_mut(ecs);
for ConfigurationEvent {
entity: player_entity,
packet,
} in events.read()
{
// we do this so `ecs` isn't borrowed for the whole loop
events_owned.push((*player_entity, packet.clone()));
}
for (player_entity, packet) in events_owned {
match packet {
ClientboundConfigPacket::RegistryData(p) => {
let mut system_state: SystemState<Query<&mut InstanceHolder>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let instance_holder = query.get_mut(player_entity).unwrap();
let mut instance = instance_holder.instance.write();
// add the new registry data
instance.registries.append(p.registry_id, p.entries);
}
ClientboundConfigPacket::CustomPayload(p) => {
debug!("Got custom payload packet {p:?}");
}
ClientboundConfigPacket::Disconnect(p) => {
warn!("Got disconnect packet {p:?}");
let mut system_state: SystemState<EventWriter<DisconnectEvent>> =
SystemState::new(ecs);
let mut disconnect_events = system_state.get_mut(ecs);
disconnect_events.send(DisconnectEvent {
entity: player_entity,
reason: Some(p.reason.clone()),
});
}
ClientboundConfigPacket::FinishConfiguration(p) => {
debug!("got FinishConfiguration packet: {p:?}");
let mut system_state: SystemState<Query<&mut RawConnection>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let mut raw_conn = query.get_mut(player_entity).unwrap();
raw_conn
.write_packet(ServerboundFinishConfiguration {})
.expect(
"we should be in the right state and encoding this packet shouldn't fail",
);
raw_conn.set_state(ConnectionProtocol::Game);
// these components are added now that we're going to be in the Game state
ecs.entity_mut(player_entity)
.remove::<InConfigurationState>()
.insert(crate::JoinedClientBundle {
physics_state: crate::PhysicsState::default(),
inventory: crate::inventory::Inventory::default(),
tab_list: crate::local_player::TabList::default(),
current_sequence_number: crate::interact::CurrentSequenceNumber::default(),
last_sent_direction: crate::movement::LastSentLookDirection::default(),
abilities: crate::local_player::PlayerAbilities::default(),
permission_level: crate::local_player::PermissionLevel::default(),
hunger: Hunger::default(),
chunk_batch_info: crate::chunks::ChunkBatchInfo::default(),
entity_id_index: EntityIdIndex::default(),
mining: crate::mining::MineBundle::default(),
attack: crate::attack::AttackBundle::default(),
_local_entity: azalea_entity::LocalEntity,
});
}
ClientboundConfigPacket::KeepAlive(p) => {
debug!("Got keep alive packet (in configuration) {p:?} for {player_entity:?}");
let mut system_state: SystemState<(
Query<&RawConnection>,
EventWriter<KeepAliveEvent>,
)> = SystemState::new(ecs);
let (query, mut keepalive_events) = system_state.get_mut(ecs);
let raw_conn = query.get(player_entity).unwrap();
keepalive_events.send(KeepAliveEvent {
entity: player_entity,
id: p.id,
});
raw_conn
.write_packet(ServerboundKeepAlive { id: p.id })
.unwrap();
}
ClientboundConfigPacket::Ping(p) => {
debug!("Got ping packet {p:?}");
let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let raw_conn = query.get_mut(player_entity).unwrap();
raw_conn
.write_packet(config::s_pong::ServerboundPong { id: p.id })
.unwrap();
}
ClientboundConfigPacket::ResourcePackPush(p) => {
debug!("Got resource pack packet {p:?}");
let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let raw_conn = query.get_mut(player_entity).unwrap();
// always accept resource pack
raw_conn
.write_packet(ServerboundResourcePack {
id: p.id,
action: config::s_resource_pack::Action::Accepted,
})
.unwrap();
}
ClientboundConfigPacket::ResourcePackPop(_) => {
// we can ignore this
}
ClientboundConfigPacket::UpdateEnabledFeatures(p) => {
debug!("Got update enabled features packet {p:?}");
}
ClientboundConfigPacket::UpdateTags(_p) => {
debug!("Got update tags packet");
}
ClientboundConfigPacket::CookieRequest(p) => {
debug!("Got cookie request packet {p:?}");
let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let raw_conn = query.get_mut(player_entity).unwrap();
raw_conn
.write_packet(ServerboundCookieResponse {
key: p.key,
// cookies aren't implemented
payload: None,
})
.unwrap();
}
ClientboundConfigPacket::ResetChat(p) => {
debug!("Got reset chat packet {p:?}");
}
ClientboundConfigPacket::StoreCookie(p) => {
debug!("Got store cookie packet {p:?}");
}
ClientboundConfigPacket::Transfer(p) => {
debug!("Got transfer packet {p:?}");
}
ClientboundConfigPacket::SelectKnownPacks(p) => {
debug!("Got select known packs packet {p:?}");
let mut system_state: SystemState<Query<&RawConnection>> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let raw_conn = query.get_mut(player_entity).unwrap();
// resource pack management isn't implemented
raw_conn
.write_packet(ServerboundSelectKnownPacks {
known_packs: vec![],
})
.unwrap();
}
ClientboundConfigPacket::ServerLinks(_) => {}
ClientboundConfigPacket::CustomReportDetails(_) => {}
}
}
}
/// An event for sending a packet to the server while we're in the
/// `configuration` state.
#[derive(Event)]
pub struct SendConfigurationEvent {
pub sent_by: Entity,
pub packet: ServerboundConfigPacket,
}
impl SendConfigurationEvent {
pub fn new(sent_by: Entity, packet: impl Packet<ServerboundConfigPacket>) -> Self {
let packet = packet.into_variant();
Self { sent_by, packet }
}
}
pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendConfigurationEvent>,
mut query: Query<(&mut RawConnection, Option<&InConfigurationState>)>,
) {
for event in send_packet_events.read() {
if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
if in_configuration_state.is_none() {
error!(
"Tried to send a configuration packet {:?} while not in configuration state",
event.packet
);
continue;
}
debug!("Sending packet: {:?}", event.packet);
if let Err(e) = raw_conn.write_packet(event.packet.clone()) {
error!("Failed to send packet: {e}");
}
}
}
}

View file

@ -3,19 +3,20 @@
use std::io;
use azalea_protocol::{
ServerAddress,
connect::{Connection, ConnectionError, Proxy},
packets::{
ClientIntention, PROTOCOL_VERSION,
handshake::{
s_intention::ServerboundIntention, ClientboundHandshakePacket,
ServerboundHandshakePacket,
ClientboundHandshakePacket, ServerboundHandshakePacket,
s_intention::ServerboundIntention,
},
status::{
c_status_response::ClientboundStatusResponse,
s_status_request::ServerboundStatusRequest, ClientboundStatusPacket,
ClientboundStatusPacket, c_status_response::ClientboundStatusResponse,
s_status_request::ServerboundStatusRequest,
},
ClientIntention, PROTOCOL_VERSION,
},
resolver, ServerAddress,
resolver,
};
use thiserror::Error;

View file

@ -8,7 +8,7 @@ use bevy_ecs::{
};
use uuid::Uuid;
use crate::{packet_handling::game::AddPlayerEvent, GameProfileComponent};
use crate::{GameProfileComponent, packet::game::AddPlayerEvent};
/// A player in the tab list.
#[derive(Debug, Clone)]

View file

@ -1,7 +1,8 @@
use azalea_core::{game_type::GameMode, tick::GameTick};
use azalea_entity::{
Attributes, Physics,
metadata::{ShiftKeyDown, Sprinting},
update_bounding_box, Attributes, Physics,
update_bounding_box,
};
use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::s_interact::{self, ServerboundInteract};
@ -10,9 +11,10 @@ use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use super::packet::game::SendPacketEvent;
use crate::{
interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSet,
packet_handling::game::SendPacketEvent, respawn::perform_respawn, Client,
Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSet,
respawn::perform_respawn,
};
pub struct AttackPlugin;
@ -86,7 +88,7 @@ pub fn handle_attack_event(
send_packet_events.send(SendPacketEvent::new(
event.entity,
ServerboundInteract {
entity_id: *event.target,
entity_id: event.target,
action: s_interact::ActionType::Attack,
using_secondary_action: **sneaking,
},

View file

@ -0,0 +1,63 @@
use azalea_buf::AzaleaWrite;
use azalea_core::resource_location::ResourceLocation;
use azalea_protocol::{
common::client_information::ClientInformation,
packets::config::{
s_client_information::ServerboundClientInformation,
s_custom_payload::ServerboundCustomPayload,
},
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use tracing::{debug, warn};
use super::packet::config::SendConfigPacketEvent;
use crate::packet::login::InLoginState;
pub struct BrandPlugin;
impl Plugin for BrandPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
handle_end_login_state.before(crate::packet::config::handle_send_packet_event),
);
}
}
pub fn handle_end_login_state(
mut removed: RemovedComponents<InLoginState>,
query: Query<&ClientInformation>,
mut send_packet_events: EventWriter<SendConfigPacketEvent>,
) {
for entity in removed.read() {
let mut brand_data = Vec::new();
// azalea pretends to be vanilla everywhere else so it makes sense to lie here
// too
"vanilla".azalea_write(&mut brand_data).unwrap();
send_packet_events.send(SendConfigPacketEvent::new(
entity,
ServerboundCustomPayload {
identifier: ResourceLocation::new("brand"),
data: brand_data.into(),
},
));
let client_information = match query.get(entity).ok() {
Some(i) => i,
None => {
warn!(
"ClientInformation component was not set before leaving login state, using a default"
);
&ClientInformation::default()
}
};
debug!("Writing ClientInformation while in config state: {client_information:?}");
send_packet_events.send(SendConfigPacketEvent::new(
entity,
ServerboundClientInformation {
information: client_information.clone(),
},
));
}
}

View file

@ -0,0 +1,63 @@
use std::time::{SystemTime, UNIX_EPOCH};
use azalea_protocol::packets::{
Packet,
game::{ServerboundChat, ServerboundChatCommand, s_chat::LastSeenMessagesUpdate},
};
use bevy_ecs::prelude::*;
use super::ChatKind;
use crate::packet::game::SendPacketEvent;
/// Send a chat packet to the server of a specific kind (chat message or
/// command). Usually you just want [`SendChatEvent`] instead.
///
/// Usually setting the kind to `Message` will make it send a chat message even
/// if it starts with a slash, but some server implementations will always do a
/// command if it starts with a slash.
///
/// If you're wondering why this isn't two separate events, it's so ordering is
/// preserved if multiple chat messages and commands are sent at the same time.
///
/// [`SendChatEvent`]: super::SendChatEvent
#[derive(Event)]
pub struct SendChatKindEvent {
pub entity: Entity,
pub content: String,
pub kind: ChatKind,
}
pub fn handle_send_chat_kind_event(
mut events: EventReader<SendChatKindEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
) {
for event in events.read() {
let content = event
.content
.chars()
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
.take(256)
.collect::<String>();
let packet = match event.kind {
ChatKind::Message => ServerboundChat {
message: content,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.into_variant(),
ChatKind::Command => {
// TODO: chat signing
ServerboundChatCommand { command: content }.into_variant()
}
};
send_packet_events.send(SendPacketEvent::new(event.entity, packet));
}
}

View file

@ -1,20 +1,13 @@
//! Implementations of chat-related features.
use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
pub mod handler;
use std::sync::Arc;
use azalea_chat::FormattedText;
use azalea_protocol::packets::{
game::{
c_disguised_chat::ClientboundDisguisedChat,
c_player_chat::ClientboundPlayerChat,
c_system_chat::ClientboundSystemChat,
s_chat::{LastSeenMessagesUpdate, ServerboundChat},
s_chat_command::ServerboundChatCommand,
},
Packet,
use azalea_protocol::packets::game::{
c_disguised_chat::ClientboundDisguisedChat, c_player_chat::ClientboundPlayerChat,
c_system_chat::ClientboundSystemChat,
};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
@ -23,12 +16,28 @@ use bevy_ecs::{
prelude::Event,
schedule::IntoSystemConfigs,
};
use handler::{SendChatKindEvent, handle_send_chat_kind_event};
use uuid::Uuid;
use crate::{
client::Client,
packet_handling::game::{handle_send_packet_event, SendPacketEvent},
};
use super::packet::game::handle_outgoing_packets;
use crate::client::Client;
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) {
app.add_event::<SendChatEvent>()
.add_event::<SendChatKindEvent>()
.add_event::<ChatReceivedEvent>()
.add_systems(
Update,
(
handle_send_chat_event,
handle_send_chat_kind_event.after(handle_outgoing_packets),
)
.chain(),
);
}
}
/// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone, PartialEq)]
@ -148,25 +157,28 @@ impl Client {
content: message.to_string(),
kind: ChatKind::Message,
});
self.run_schedule_sender.send(()).unwrap();
let _ = self.run_schedule_sender.try_send(());
}
/// Send a command packet to the server. The `command` argument should not
/// include the slash at the front.
///
/// You can also just use [`Client::chat`] and start your message with a `/`
/// to send a command.
pub fn send_command_packet(&self, command: &str) {
self.ecs.lock().send_event(SendChatKindEvent {
entity: self.entity,
content: command.to_string(),
kind: ChatKind::Command,
});
self.run_schedule_sender.send(()).unwrap();
let _ = self.run_schedule_sender.try_send(());
}
/// Send a message in chat.
///
/// ```rust,no_run
/// # use azalea_client::{Client, Event};
/// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> {
/// # use azalea_client::Client;
/// # async fn example(bot: Client) -> anyhow::Result<()> {
/// bot.chat("Hello, world!");
/// # Ok(())
/// # }
@ -176,24 +188,7 @@ impl Client {
entity: self.entity,
content: content.to_string(),
});
self.run_schedule_sender.send(()).unwrap();
}
}
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) {
app.add_event::<SendChatEvent>()
.add_event::<SendChatKindEvent>()
.add_event::<ChatReceivedEvent>()
.add_systems(
Update,
(
handle_send_chat_event,
handle_send_chat_kind_event.after(handle_send_packet_event),
)
.chain(),
);
let _ = self.run_schedule_sender.try_send(());
}
}
@ -232,63 +227,12 @@ pub fn handle_send_chat_event(
}
}
/// Send a chat packet to the server of a specific kind (chat message or
/// command). Usually you just want [`SendChatEvent`] instead.
///
/// Usually setting the kind to `Message` will make it send a chat message even
/// if it starts with a slash, but some server implementations will always do a
/// command if it starts with a slash.
///
/// If you're wondering why this isn't two separate events, it's so ordering is
/// preserved if multiple chat messages and commands are sent at the same time.
#[derive(Event)]
pub struct SendChatKindEvent {
pub entity: Entity,
pub content: String,
pub kind: ChatKind,
}
/// A kind of chat packet, either a chat message or a command.
pub enum ChatKind {
Message,
Command,
}
pub fn handle_send_chat_kind_event(
mut events: EventReader<SendChatKindEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
) {
for event in events.read() {
let content = event
.content
.chars()
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
.take(256)
.collect::<String>();
let packet = match event.kind {
ChatKind::Message => ServerboundChat {
message: content,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.into_variant(),
ChatKind::Command => {
// TODO: chat signing
ServerboundChatCommand { command: content }.into_variant()
}
};
send_packet_events.send(SendPacketEvent::new(event.entity, packet));
}
}
// TODO
// MessageSigner, ChatMessageContent, LastSeenMessages
// fn sign_message() -> MessageSignature {

View file

@ -18,16 +18,14 @@ use bevy_ecs::prelude::*;
use simdnbt::owned::BaseNbt;
use tracing::{error, trace};
use super::packet::game::handle_outgoing_packets;
use crate::{
interact::handle_block_interact_event,
inventory::InventorySet,
packet_handling::game::{handle_send_packet_event, SendPacketEvent},
respawn::perform_respawn,
InstanceHolder,
InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet,
packet::game::SendPacketEvent, respawn::perform_respawn,
};
pub struct ChunkPlugin;
impl Plugin for ChunkPlugin {
pub struct ChunksPlugin;
impl Plugin for ChunksPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
@ -37,7 +35,7 @@ impl Plugin for ChunkPlugin {
handle_chunk_batch_finished_event,
)
.chain()
.before(handle_send_packet_event)
.before(handle_outgoing_packets)
.before(InventorySet)
.before(handle_block_interact_event)
.before(perform_respawn),
@ -111,7 +109,10 @@ pub fn handle_receive_chunk_events(
heightmaps,
&mut instance.chunks,
) {
error!("Couldn't set chunk data: {e}");
error!(
"Couldn't set chunk data: {e}. World height: {}",
instance.chunks.height
);
}
}
}

View file

@ -1,7 +1,7 @@
//! Disconnect a client from the server.
use azalea_chat::FormattedText;
use azalea_entity::LocalEntity;
use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{
component::Component,
@ -13,8 +13,12 @@ use bevy_ecs::{
system::{Commands, Query},
};
use derive_more::Deref;
use tracing::trace;
use crate::{client::JoinedClientBundle, events::LocalPlayerEvents, raw_connection::RawConnection};
use crate::{
InstanceHolder, client::JoinedClientBundle, events::LocalPlayerEvents,
raw_connection::RawConnection,
};
pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
@ -38,22 +42,35 @@ pub struct DisconnectEvent {
pub reason: Option<FormattedText>,
}
/// System that removes the [`JoinedClientBundle`] from the entity when it
/// receives a [`DisconnectEvent`].
/// A system that removes the several components from our clients when they get
/// a [`DisconnectEvent`].
pub fn remove_components_from_disconnected_players(
mut commands: Commands,
mut events: EventReader<DisconnectEvent>,
mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>,
) {
for DisconnectEvent { entity, .. } in events.read() {
trace!("Got DisconnectEvent for {entity:?}");
commands
.entity(*entity)
.remove::<JoinedClientBundle>()
.remove::<EntityBundle>()
.remove::<InstanceHolder>()
.remove::<PlayerMetadataBundle>()
.remove::<InLoadedChunk>()
// this makes it close the tcp connection
.remove::<RawConnection>()
// swarm detects when this tx gets dropped to fire SwarmEvent::Disconnect
.remove::<LocalPlayerEvents>();
// note that we don't remove the client from the ECS, so if they decide
// to reconnect they'll keep their state
// now we have to remove ourselves from the LoadedBy for every entity.
// in theory this could be inefficient if we have massive swarms... but in
// practice this is fine.
for mut loaded_by in &mut loaded_by_query.iter_mut() {
loaded_by.remove(entity);
}
}
}

View file

@ -5,8 +5,9 @@ use std::sync::Arc;
use azalea_chat::FormattedText;
use azalea_core::tick::GameTick;
use azalea_entity::Dead;
use azalea_protocol::packets::game::{
c_player_combat_kill::ClientboundPlayerCombatKill, ClientboundGamePacket,
ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill,
};
use azalea_world::{InstanceName, MinecraftEntityId};
use bevy_app::{App, Plugin, PreUpdate, Update};
@ -21,31 +22,36 @@ use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc;
use crate::{
PlayerInfo,
chat::{ChatPacket, ChatReceivedEvent},
disconnect::DisconnectEvent,
packet_handling::game::{
AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent,
packet::game::{
AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, RemovePlayerEvent,
UpdatePlayerEvent,
},
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")
// - Add it as an ECS event first:
// - Make a struct that contains an entity field and some data fields (look
// in packet/game/events.rs for examples. These structs should always have
// their names end with "Event".
// - (the `entity` field is the local player entity that's receiving the
// event)
// - In the GamePacketHandler, you always have a `player` field that you can
// use.
// - Add the event struct in PacketPlugin::build
// - (in the `impl Plugin for PacketPlugin`)
// - To get the event writer, you have to get an EventWriter<ThingEvent>.
// Look at other packets in packet/game/mod.rs for examples.
//
// - 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`
// At this point, you've created a new ECS event. That's annoying for bots to
// use though, so you might wanna add it to the Event enum too:
// - In this file, add a new variant to that Event enum with the same name
// as your event (without the "Event" suffix).
// - Create a new system function like the other ones here, and put that
// system function in the `impl Plugin for EventsPlugin`
/// Something that happened in-game, such as a tick passing or chat message
/// being sent.
@ -111,8 +117,8 @@ pub enum Event {
#[derive(Component, Deref, DerefMut)]
pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
pub struct EventPlugin;
impl Plugin for EventPlugin {
pub struct EventsPlugin;
impl Plugin for EventsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
@ -130,7 +136,7 @@ impl Plugin for EventPlugin {
)
.add_systems(
PreUpdate,
init_listener.before(crate::packet_handling::game::process_packet_events),
init_listener.before(crate::packet::game::process_packet_events),
)
.add_systems(GameTick, tick_listener);
}
@ -166,7 +172,10 @@ pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) {
}
}
pub fn packet_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<PacketEvent>) {
pub fn packet_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<ReceivePacketEvent>,
) {
for event in events.read() {
let local_player_events = query
.get(event.entity)
@ -219,6 +228,15 @@ pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<
}
}
/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
///
/// [`LocalEntity`]: azalea_entity::LocalEntity
pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
for local_player_events in &query {
local_player_events.send(Event::Death(None)).unwrap();
}
}
pub fn keepalive_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<KeepAliveEvent>,

View file

@ -8,9 +8,9 @@ use azalea_core::{
position::{BlockPos, Vec3},
};
use azalea_entity::{
clamp_look_direction, view_vector, Attributes, EyeHeight, LocalEntity, LookDirection, Position,
Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
};
use azalea_inventory::{components, ItemStack, ItemStackData};
use azalea_inventory::{ItemStack, ItemStackData, components};
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_protocol::packets::game::{
s_interact::InteractionHand,
@ -30,14 +30,15 @@ use bevy_ecs::{
use derive_more::{Deref, DerefMut};
use tracing::warn;
use super::packet::game::handle_outgoing_packets;
use crate::{
Client,
attack::handle_attack_event,
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
packet_handling::game::{handle_send_packet_event, SendPacketEvent},
packet::game::SendPacketEvent,
respawn::perform_respawn,
Client,
};
/// A plugin that allows clients to interact with blocks in the world.
@ -54,7 +55,7 @@ impl Plugin for InteractPlugin {
handle_block_interact_event,
handle_swing_arm_event,
)
.before(handle_send_packet_event)
.before(handle_outgoing_packets)
.after(InventorySet)
.after(perform_respawn)
.after(handle_attack_event)
@ -245,15 +246,16 @@ pub fn check_is_interaction_restricted(
// way of modifying that
let held_item = inventory.held_item();
if let ItemStack::Present(item) = &held_item {
let block = instance.chunks.get_block_state(block_pos);
let Some(block) = block else {
// block isn't loaded so just say that it is restricted
return true;
};
check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
} else {
true
match &held_item {
ItemStack::Present(item) => {
let block = instance.chunks.get_block_state(block_pos);
let Some(block) = block else {
// block isn't loaded so just say that it is restricted
return true;
};
check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
}
_ => true,
}
}
GameMode::Spectator => true,

View file

@ -25,11 +25,9 @@ use bevy_ecs::{
};
use tracing::warn;
use super::packet::game::handle_outgoing_packets;
use crate::{
local_player::PlayerAbilities,
packet_handling::game::{handle_send_packet_event, SendPacketEvent},
respawn::perform_respawn,
Client,
Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn,
};
pub struct InventoryPlugin;
@ -48,7 +46,7 @@ impl Plugin for InventoryPlugin {
handle_menu_opened_event,
handle_set_container_content_event,
handle_container_click_event,
handle_container_close_event.before(handle_send_packet_event),
handle_container_close_event.before(handle_outgoing_packets),
handle_client_side_close_container_event,
)
.chain()
@ -124,10 +122,9 @@ impl Inventory {
///
/// Use [`Self::menu_mut`] if you need a mutable reference.
pub fn menu(&self) -> &azalea_inventory::Menu {
if let Some(menu) = &self.container_menu {
menu
} else {
&self.inventory_menu
match &self.container_menu {
Some(menu) => menu,
_ => &self.inventory_menu,
}
}
@ -137,10 +134,9 @@ impl Inventory {
///
/// Use [`Self::menu`] if you don't need a mutable reference.
pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
if let Some(menu) = &mut self.container_menu {
menu
} else {
&mut self.inventory_menu
match &mut self.container_menu {
Some(menu) => menu,
_ => &mut self.inventory_menu,
}
}
@ -286,10 +282,9 @@ impl Inventory {
carried_count -= new_carried.count - slot_item_count;
// we have to inline self.menu_mut() here to avoid the borrow checker
// complaining
let menu = if let Some(menu) = &mut self.container_menu {
menu
} else {
&mut self.inventory_menu
let menu = match &mut self.container_menu {
Some(menu) => menu,
_ => &mut self.inventory_menu,
};
*menu.slot_mut(slot_index as usize).unwrap() =
ItemStack::Present(new_carried);

View file

@ -1,6 +1,6 @@
use azalea_block::{fluid_state::FluidState, Block, BlockState};
use azalea_block::{Block, BlockState, fluid_state::FluidState};
use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress};
use azalea_inventory::ItemStack;
use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
@ -10,20 +10,20 @@ use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use crate::{
Client,
interact::{
can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
HitResultComponent, SwingArmEvent,
CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks,
check_is_interaction_restricted,
},
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
packet_handling::game::SendPacketEvent,
Client,
packet::game::SendPacketEvent,
};
/// A plugin that allows clients to break blocks in the world.
pub struct MinePlugin;
impl Plugin for MinePlugin {
pub struct MiningPlugin;
impl Plugin for MiningPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartMiningBlockEvent>()
.add_event::<StartMiningBlockWithDirectionEvent>()
@ -59,6 +59,7 @@ impl Plugin for MinePlugin {
}
}
/// The Bevy system set for things related to mining.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct MiningSet;

View file

@ -0,0 +1,14 @@
pub mod attack;
pub mod brand;
pub mod chat;
pub mod chunks;
pub mod disconnect;
pub mod events;
pub mod interact;
pub mod inventory;
pub mod mining;
pub mod movement;
pub mod packet;
pub mod respawn;
pub mod task_pool;
pub mod tick_end;

View file

@ -2,23 +2,24 @@ use std::backtrace::Backtrace;
use azalea_core::position::Vec3;
use azalea_core::tick::GameTick;
use azalea_entity::{metadata::Sprinting, Attributes, Jumping};
use azalea_entity::{Attributes, Jumping, metadata::Sprinting};
use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position};
use azalea_physics::{ai_step, PhysicsSet};
use azalea_protocol::packets::game::ServerboundPlayerCommand;
use azalea_physics::{PhysicsSet, ai_step};
use azalea_protocol::packets::game::{ServerboundPlayerCommand, ServerboundPlayerInput};
use azalea_protocol::packets::{
Packet,
game::{
s_move_player_pos::ServerboundMovePlayerPos,
s_move_player_pos_rot::ServerboundMovePlayerPosRot,
s_move_player_rot::ServerboundMovePlayerRot,
s_move_player_status_only::ServerboundMovePlayerStatusOnly,
},
Packet,
};
use azalea_world::{MinecraftEntityId, MoveEntityError};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::{Event, EventWriter};
use bevy_ecs::schedule::SystemSet;
use bevy_ecs::system::Commands;
use bevy_ecs::{
component::Component, entity::Entity, event::EventReader, query::With,
schedule::IntoSystemConfigs, system::Query,
@ -26,7 +27,7 @@ use bevy_ecs::{
use thiserror::Error;
use crate::client::Client;
use crate::packet_handling::game::SendPacketEvent;
use crate::packet::game::SendPacketEvent;
#[derive(Error, Debug)]
pub enum MovePlayerError {
@ -46,9 +47,9 @@ impl From<MoveEntityError> for MovePlayerError {
}
}
pub struct PlayerMovePlugin;
pub struct MovementPlugin;
impl Plugin for PlayerMovePlugin {
impl Plugin for MovementPlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartWalkEvent>()
.add_event::<StartSprintEvent>()
@ -68,6 +69,7 @@ impl Plugin for PlayerMovePlugin {
.before(ai_step)
.before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_player_input_packet,
send_position.after(PhysicsSet),
)
.chain(),
@ -251,6 +253,41 @@ pub fn send_position(
}
}
#[derive(Debug, Default, Component, Clone, PartialEq, Eq)]
pub struct LastSentInput(pub ServerboundPlayerInput);
pub fn send_player_input_packet(
mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
mut send_packet_events: EventWriter<SendPacketEvent>,
mut commands: Commands,
) {
for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
let dir = physics_state.move_direction;
type D = WalkDirection;
let input = ServerboundPlayerInput {
forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
jump: **jumping,
// TODO: implement sneaking
shift: false,
sprint: physics_state.trying_to_sprint,
};
// if LastSentInput isn't present, we default to assuming we're not pressing any
// keys and insert it anyways every time it changes
let last_sent_input = last_sent_input.cloned().unwrap_or_default();
if input != last_sent_input.0 {
send_packet_events.send(SendPacketEvent {
sent_by: entity,
packet: input.clone().into_variant(),
});
commands.entity(entity).insert(LastSentInput(input));
}
}
}
fn send_sprinting_if_needed(
mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
mut send_packet_events: EventWriter<SendPacketEvent>,
@ -266,7 +303,7 @@ fn send_sprinting_if_needed(
send_packet_events.send(SendPacketEvent::new(
entity,
ServerboundPlayerCommand {
id: **minecraft_entity_id,
id: *minecraft_entity_id,
action: sprinting_action,
data: 0,
},
@ -510,7 +547,7 @@ pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader<
}
}
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WalkDirection {
#[default]
None,

View file

@ -0,0 +1,111 @@
use std::io::Cursor;
use azalea_protocol::{
packets::{
Packet,
config::{ClientboundConfigPacket, ServerboundConfigPacket},
},
read::deserialize_packet,
};
use bevy_ecs::prelude::*;
use tracing::{debug, error};
use crate::{InConfigState, raw_connection::RawConnection};
#[derive(Event, Debug, Clone)]
pub struct ReceiveConfigPacketEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
pub packet: ClientboundConfigPacket,
}
/// An event for sending a packet to the server while we're in the
/// `configuration` state.
#[derive(Event)]
pub struct SendConfigPacketEvent {
pub sent_by: Entity,
pub packet: ServerboundConfigPacket,
}
impl SendConfigPacketEvent {
pub fn new(sent_by: Entity, packet: impl Packet<ServerboundConfigPacket>) -> Self {
let packet = packet.into_variant();
Self { sent_by, packet }
}
}
pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendConfigPacketEvent>,
mut query: Query<(&mut RawConnection, Option<&InConfigState>)>,
) {
for event in send_packet_events.read() {
if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {
if in_configuration_state.is_none() {
error!(
"Tried to send a configuration packet {:?} while not in configuration state",
event.packet
);
continue;
}
debug!("Sending packet: {:?}", event.packet);
if let Err(e) = raw_conn.write_packet(event.packet.clone()) {
error!("Failed to send packet: {e}");
}
}
}
}
pub fn emit_receive_config_packet_events(
query: Query<(Entity, &RawConnection), With<InConfigState>>,
mut packet_events: ResMut<Events<ReceiveConfigPacketEvent>>,
) {
// we manually clear and send the events at the beginning of each update
// since otherwise it'd cause issues with events in process_packet_events
// running twice
packet_events.clear();
for (player_entity, raw_conn) in &query {
let packets_lock = raw_conn.incoming_packet_queue();
let mut packets = packets_lock.lock();
if !packets.is_empty() {
let mut packets_read = 0;
for raw_packet in packets.iter() {
packets_read += 1;
let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new(
raw_packet,
)) {
Ok(packet) => packet,
Err(err) => {
error!("failed to read packet: {err:?}");
debug!("packet bytes: {raw_packet:?}");
continue;
}
};
let should_interrupt = packet_interrupts(&packet);
packet_events.send(ReceiveConfigPacketEvent {
entity: player_entity,
packet,
});
if should_interrupt {
break;
}
}
packets.drain(0..packets_read);
}
}
}
/// Whether the given packet should make us stop deserializing the received
/// packets until next update.
///
/// This is used for packets that can switch the client state.
fn packet_interrupts(packet: &ClientboundConfigPacket) -> bool {
matches!(
packet,
ClientboundConfigPacket::FinishConfiguration(_)
| ClientboundConfigPacket::Disconnect(_)
| ClientboundConfigPacket::Transfer(_)
)
}

View file

@ -0,0 +1,229 @@
mod events;
use azalea_entity::LocalEntity;
use azalea_protocol::packets::ConnectionProtocol;
use azalea_protocol::packets::config::*;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState;
pub use events::*;
use tracing::{debug, warn};
use super::as_system;
use crate::client::InConfigState;
use crate::disconnect::DisconnectEvent;
use crate::packet::game::KeepAliveEvent;
use crate::packet::game::ResourcePackEvent;
use crate::raw_connection::RawConnection;
use crate::{InstanceHolder, declare_packet_handlers};
pub fn process_packet_events(ecs: &mut World) {
let mut events_owned = Vec::new();
let mut system_state: SystemState<EventReader<ReceiveConfigPacketEvent>> =
SystemState::new(ecs);
let mut events = system_state.get_mut(ecs);
for ReceiveConfigPacketEvent {
entity: player_entity,
packet,
} in events.read()
{
// we do this so `ecs` isn't borrowed for the whole loop
events_owned.push((*player_entity, packet.clone()));
}
for (player_entity, packet) in events_owned {
let mut handler = ConfigPacketHandler {
player: player_entity,
ecs,
};
declare_packet_handlers!(
ClientboundConfigPacket,
packet,
handler,
[
cookie_request,
custom_payload,
disconnect,
finish_configuration,
keep_alive,
ping,
reset_chat,
registry_data,
resource_pack_pop,
resource_pack_push,
store_cookie,
transfer,
update_enabled_features,
update_tags,
select_known_packs,
custom_report_details,
server_links,
]
);
}
}
pub struct ConfigPacketHandler<'a> {
pub ecs: &'a mut World,
pub player: Entity,
}
impl ConfigPacketHandler<'_> {
pub fn registry_data(&mut self, p: ClientboundRegistryData) {
as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| {
let instance_holder = query.get_mut(self.player).unwrap();
let mut instance = instance_holder.instance.write();
// add the new registry data
instance.registries.append(p.registry_id, p.entries);
});
}
pub fn custom_payload(&mut self, p: ClientboundCustomPayload) {
debug!("Got custom payload packet {p:?}");
}
pub fn disconnect(&mut self, p: ClientboundDisconnect) {
warn!("Got disconnect packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(DisconnectEvent {
entity: self.player,
reason: Some(p.reason),
});
});
}
pub fn finish_configuration(&mut self, p: ClientboundFinishConfiguration) {
debug!("got FinishConfiguration packet: {p:?}");
as_system::<(Commands, Query<&mut RawConnection>)>(
self.ecs,
|(mut commands, mut query)| {
let mut raw_conn = query.get_mut(self.player).unwrap();
raw_conn
.write_packet(ServerboundFinishConfiguration)
.expect(
"we should be in the right state and encoding this packet shouldn't fail",
);
raw_conn.set_state(ConnectionProtocol::Game);
// these components are added now that we're going to be in the Game state
commands
.entity(self.player)
.remove::<InConfigState>()
.insert((
crate::JoinedClientBundle::default(),
// localentity should already be added, but in case the user forgot or
// something we also add it here
LocalEntity,
));
},
);
}
pub fn keep_alive(&mut self, p: ClientboundKeepAlive) {
debug!(
"Got keep alive packet (in configuration) {p:?} for {:?}",
self.player
);
as_system::<(Query<&RawConnection>, EventWriter<_>)>(self.ecs, |(query, mut events)| {
let raw_conn = query.get(self.player).unwrap();
events.send(KeepAliveEvent {
entity: self.player,
id: p.id,
});
raw_conn
.write_packet(ServerboundKeepAlive { id: p.id })
.unwrap();
});
}
pub fn ping(&mut self, p: ClientboundPing) {
debug!("Got ping packet (in configuration) {p:?}");
as_system::<Query<&RawConnection>>(self.ecs, |query| {
let raw_conn = query.get(self.player).unwrap();
raw_conn.write_packet(ServerboundPong { id: p.id }).unwrap();
});
}
pub fn resource_pack_push(&mut self, p: ClientboundResourcePackPush) {
debug!("Got resource pack push packet {p:?}");
as_system::<EventWriter<_>>(self.ecs, |mut events| {
events.send(ResourcePackEvent {
entity: self.player,
id: p.id,
url: p.url.to_owned(),
hash: p.hash.to_owned(),
required: p.required,
prompt: p.prompt.to_owned(),
});
});
}
pub fn resource_pack_pop(&mut self, p: ClientboundResourcePackPop) {
debug!("Got resource pack pop packet {p:?}");
}
pub fn update_enabled_features(&mut self, p: ClientboundUpdateEnabledFeatures) {
debug!("Got update enabled features packet {p:?}");
}
pub fn update_tags(&mut self, _p: ClientboundUpdateTags) {
debug!("Got update tags packet");
}
pub fn cookie_request(&mut self, p: ClientboundCookieRequest) {
debug!("Got cookie request packet {p:?}");
as_system::<Query<&RawConnection>>(self.ecs, |query| {
let raw_conn = query.get(self.player).unwrap();
raw_conn
.write_packet(ServerboundCookieResponse {
key: p.key,
// cookies aren't implemented
payload: None,
})
.unwrap();
});
}
pub fn reset_chat(&mut self, p: ClientboundResetChat) {
debug!("Got reset chat packet {p:?}");
}
pub fn store_cookie(&mut self, p: ClientboundStoreCookie) {
debug!("Got store cookie packet {p:?}");
}
pub fn transfer(&mut self, p: ClientboundTransfer) {
debug!("Got transfer packet {p:?}");
}
pub fn select_known_packs(&mut self, p: ClientboundSelectKnownPacks) {
debug!("Got select known packs packet {p:?}");
as_system::<Query<&RawConnection>>(self.ecs, |query| {
let raw_conn = query.get(self.player).unwrap();
// resource pack management isn't implemented
raw_conn
.write_packet(ServerboundSelectKnownPacks {
known_packs: vec![],
})
.unwrap();
});
}
pub fn server_links(&mut self, p: ClientboundServerLinks) {
debug!("Got server links packet {p:?}");
}
pub fn custom_report_details(&mut self, p: ClientboundCustomReportDetails) {
debug!("Got custom report details packet {p:?}");
}
}

View file

@ -0,0 +1,206 @@
use std::{
io::Cursor,
sync::{Arc, Weak},
};
use azalea_chat::FormattedText;
use azalea_core::resource_location::ResourceLocation;
use azalea_protocol::{
packets::{
Packet,
game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
},
read::deserialize_packet,
};
use azalea_world::Instance;
use bevy_ecs::prelude::*;
use parking_lot::RwLock;
use tracing::{debug, error};
use uuid::Uuid;
use crate::{PlayerInfo, client::InGameState, raw_connection::RawConnection};
/// An event that's sent when we receive a packet.
/// ```
/// # use azalea_client::packet::game::ReceivePacketEvent;
/// # use azalea_protocol::packets::game::ClientboundGamePacket;
/// # use bevy_ecs::event::EventReader;
///
/// fn handle_packets(mut events: EventReader<ReceivePacketEvent>) {
/// for ReceivePacketEvent {
/// entity,
/// packet,
/// } in events.read() {
/// match packet.as_ref() {
/// ClientboundGamePacket::LevelParticles(p) => {
/// // ...
/// }
/// _ => {}
/// }
/// }
/// }
/// ```
#[derive(Event, Debug, Clone)]
pub struct ReceivePacketEvent {
/// The client entity that received the packet.
pub entity: Entity,
/// The packet that was actually received.
pub packet: Arc<ClientboundGamePacket>,
}
/// An event for sending a packet to the server while we're in the `game` state.
#[derive(Event)]
pub struct SendPacketEvent {
pub sent_by: Entity,
pub packet: ServerboundGamePacket,
}
impl SendPacketEvent {
pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self {
let packet = packet.into_variant();
Self { sent_by, packet }
}
}
pub fn handle_outgoing_packets(
mut send_packet_events: EventReader<SendPacketEvent>,
mut query: Query<(&mut RawConnection, Option<&InGameState>)>,
) {
for event in send_packet_events.read() {
if let Ok((raw_connection, in_game_state)) = query.get_mut(event.sent_by) {
if in_game_state.is_none() {
error!(
"Tried to send a game packet {:?} while not in game state",
event.packet
);
continue;
}
// debug!("Sending packet: {:?}", event.packet);
if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
error!("Failed to send packet: {e}");
}
}
}
}
pub fn emit_receive_packet_events(
query: Query<(Entity, &RawConnection), With<InGameState>>,
mut packet_events: ResMut<Events<ReceivePacketEvent>>,
) {
// we manually clear and send the events at the beginning of each update
// since otherwise it'd cause issues with events in process_packet_events
// running twice
packet_events.clear();
for (player_entity, raw_connection) in &query {
let packets_lock = raw_connection.incoming_packet_queue();
let mut packets = packets_lock.lock();
if !packets.is_empty() {
let mut packets_read = 0;
for raw_packet in packets.iter() {
packets_read += 1;
let packet =
match deserialize_packet::<ClientboundGamePacket>(&mut Cursor::new(raw_packet))
{
Ok(packet) => packet,
Err(err) => {
error!("failed to read packet: {err:?}");
debug!("packet bytes: {raw_packet:?}");
continue;
}
};
let should_interrupt = packet_interrupts(&packet);
packet_events.send(ReceivePacketEvent {
entity: player_entity,
packet: Arc::new(packet),
});
if should_interrupt {
break;
}
}
packets.drain(0..packets_read);
}
}
}
/// Whether the given packet should make us stop deserializing the received
/// packets until next update.
///
/// This is used for packets that can switch the client state.
fn packet_interrupts(packet: &ClientboundGamePacket) -> bool {
matches!(
packet,
ClientboundGamePacket::StartConfiguration(_)
| ClientboundGamePacket::Disconnect(_)
| ClientboundGamePacket::Transfer(_)
)
}
/// A player joined the game (or more specifically, was added to the tab
/// list of a local player).
#[derive(Event, Debug, Clone)]
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(Event, Debug, Clone)]
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(Event, Debug, Clone)]
pub struct UpdatePlayerEvent {
/// The local player entity that received this event.
pub entity: Entity,
pub info: PlayerInfo,
}
/// Event for when an entity dies. dies. If it's a local player and there's a
/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will
/// be included.
#[derive(Event, Debug, Clone)]
pub struct DeathEvent {
pub entity: Entity,
pub packet: Option<ClientboundPlayerCombatKill>,
}
/// A KeepAlive packet is sent from the server to verify that the client is
/// still connected.
#[derive(Event, Debug, Clone)]
pub struct KeepAliveEvent {
pub entity: Entity,
/// The ID of the keepalive. This is an arbitrary number, but vanilla
/// servers use the time to generate this.
pub id: u64,
}
#[derive(Event, Debug, Clone)]
pub struct ResourcePackEvent {
pub entity: Entity,
/// The random ID for this request to download the resource pack. The packet
/// for replying to a resource pack push must contain the same ID.
pub id: Uuid,
pub url: String,
pub hash: String,
pub required: bool,
pub prompt: Option<FormattedText>,
}
/// An instance (aka world, dimension) was loaded by a client.
///
/// Since the instance is given to you as a weak reference, it won't be able to
/// be `upgrade`d if all local players leave it.
#[derive(Event, Debug, Clone)]
pub struct InstanceLoadedEvent {
pub entity: Entity,
pub name: ResourceLocation,
pub instance: Weak<RwLock<Instance>>,
}

File diff suppressed because it is too large Load diff

View file

@ -4,11 +4,11 @@
use std::{collections::HashSet, sync::Arc};
use azalea_protocol::packets::{
login::{
s_custom_query_answer::ServerboundCustomQueryAnswer, ClientboundLoginPacket,
ServerboundLoginPacket,
},
Packet,
login::{
ClientboundLoginPacket, ServerboundLoginPacket,
s_custom_query_answer::ServerboundCustomQueryAnswer,
},
};
use bevy_ecs::{prelude::*, system::SystemState};
use derive_more::{Deref, DerefMut};
@ -20,7 +20,7 @@ use tracing::error;
/// An event that's sent when we receive a login packet from the server. Note
/// that if you want to handle this in a system, you must add
/// `.before(azalea::packet_handling::login::process_packet_events)` to it
/// `.before(azalea::packet::login::process_packet_events)` to it
/// because that system clears the events.
#[derive(Event, Debug, Clone)]
pub struct LoginPacketEvent {
@ -48,6 +48,11 @@ pub struct LoginSendPacketQueue {
pub tx: mpsc::UnboundedSender<ServerboundLoginPacket>,
}
/// A marker component for local players that are currently in the
/// `login` state.
#[derive(Component, Clone, Debug)]
pub struct InLoginState;
pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendLoginPacketEvent>,
mut query: Query<&mut LoginSendPacketQueue>,

View file

@ -1,6 +1,9 @@
use azalea_entity::{metadata::Health, EntityUpdateSet};
use azalea_entity::{EntityUpdateSet, metadata::Health};
use bevy_app::{App, First, Plugin, PreUpdate, Update};
use bevy_ecs::prelude::*;
use bevy_ecs::{
prelude::*,
system::{SystemParam, SystemState},
};
use self::{
game::{
@ -11,11 +14,11 @@ use self::{
};
use crate::{chat::ChatReceivedEvent, events::death_listener};
pub mod configuration;
pub mod config;
pub mod game;
pub mod login;
pub struct PacketHandlerPlugin;
pub struct PacketPlugin;
pub fn death_event_on_0_health(
query: Query<(Entity, &Health), Changed<Health>>,
@ -31,11 +34,14 @@ pub fn death_event_on_0_health(
}
}
impl Plugin for PacketHandlerPlugin {
impl Plugin for PacketPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
First,
(game::send_packet_events, configuration::send_packet_events),
(
game::emit_receive_packet_events,
config::emit_receive_config_packet_events,
),
)
.add_systems(
PreUpdate,
@ -43,7 +49,7 @@ impl Plugin for PacketHandlerPlugin {
game::process_packet_events
// we want to index and deindex right after
.before(EntityUpdateSet::Deindex),
configuration::process_packet_events,
config::process_packet_events,
login::handle_send_packet_event,
login::process_packet_events,
),
@ -52,18 +58,18 @@ impl Plugin for PacketHandlerPlugin {
Update,
(
(
configuration::handle_send_packet_event,
game::handle_send_packet_event,
config::handle_send_packet_event,
game::handle_outgoing_packets,
)
.chain(),
death_event_on_0_health.before(death_listener),
),
)
// we do this instead of add_event so we can handle the events ourselves
.init_resource::<Events<game::PacketEvent>>()
.init_resource::<Events<configuration::ConfigurationEvent>>()
.init_resource::<Events<game::ReceivePacketEvent>>()
.init_resource::<Events<config::ReceiveConfigPacketEvent>>()
.add_event::<game::SendPacketEvent>()
.add_event::<configuration::SendConfigurationEvent>()
.add_event::<config::SendConfigPacketEvent>()
.add_event::<AddPlayerEvent>()
.add_event::<RemovePlayerEvent>()
.add_event::<UpdatePlayerEvent>()
@ -76,3 +82,31 @@ impl Plugin for PacketHandlerPlugin {
.add_event::<SendLoginPacketEvent>();
}
}
#[macro_export]
macro_rules! declare_packet_handlers {
(
$packetenum:ident,
$packetvar:expr,
$handler:ident,
[$($packet:path),+ $(,)?]
) => {
paste::paste! {
match $packetvar {
$(
$packetenum::[< $packet:camel >](p) => $handler.$packet(p),
)+
}
}
};
}
pub(crate) fn as_system<T>(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>))
where
T: SystemParam + 'static,
{
let mut system_state = SystemState::<T>::new(ecs);
let values = system_state.get_mut(ecs);
f(values);
system_state.apply(ecs);
}

View file

@ -2,7 +2,8 @@ use azalea_protocol::packets::game::s_client_command::{self, ServerboundClientCo
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use crate::packet_handling::game::{handle_send_packet_event, SendPacketEvent};
use super::packet::game::handle_outgoing_packets;
use crate::packet::game::SendPacketEvent;
/// Tell the server that we're respawning.
#[derive(Event, Debug, Clone)]
@ -15,7 +16,7 @@ pub struct RespawnPlugin;
impl Plugin for RespawnPlugin {
fn build(&self, app: &mut App) {
app.add_event::<PerformRespawnEvent>()
.add_systems(Update, perform_respawn.before(handle_send_packet_event));
.add_systems(Update, perform_respawn.before(handle_outgoing_packets));
}
}

View file

@ -5,8 +5,8 @@ use std::marker::PhantomData;
use bevy_app::{App, Last, Plugin};
use bevy_ecs::system::{NonSend, Resource};
use bevy_tasks::{
tick_global_task_pools_on_main_thread, AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool,
TaskPoolBuilder,
AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder,
tick_global_task_pools_on_main_thread,
};
/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`,

View file

@ -0,0 +1,36 @@
//! Clients send a [`ServerboundClientTickEnd`] packet every tick.
use azalea_core::tick::GameTick;
use azalea_entity::LocalEntity;
use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::ServerboundClientTickEnd;
use azalea_world::InstanceName;
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use crate::{mining::MiningSet, packet::game::SendPacketEvent};
/// A plugin that makes clients send a [`ServerboundClientTickEnd`] packet every
/// tick.
pub struct TickEndPlugin;
impl Plugin for TickEndPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
GameTick,
// this has to happen after every other event that might send packets
game_tick_packet
.after(PhysicsSet)
.after(MiningSet)
.after(crate::movement::send_position),
);
}
}
pub fn game_tick_packet(
query: Query<Entity, (With<LocalEntity>, With<InstanceName>)>,
mut send_packets: EventWriter<SendPacketEvent>,
) {
for entity in query.iter() {
send_packets.send(SendPacketEvent::new(entity, ServerboundClientTickEnd));
}
}

View file

@ -10,7 +10,10 @@ use azalea_protocol::{
use bevy_ecs::prelude::*;
use parking_lot::Mutex;
use thiserror::Error;
use tokio::sync::mpsc::{self, error::SendError};
use tokio::sync::mpsc::{
self,
error::{SendError, TrySendError},
};
use tracing::error;
/// A component for clients that can read and write packets to the server. This
@ -18,26 +21,26 @@ use tracing::error;
/// yourself. It will do the compression and encryption for you though.
#[derive(Component)]
pub struct RawConnection {
reader: RawConnectionReader,
writer: RawConnectionWriter,
pub reader: RawConnectionReader,
pub writer: RawConnectionWriter,
/// Packets sent to this will be sent to the server.
/// A task that reads packets from the server. The client is disconnected
/// when this task ends.
read_packets_task: tokio::task::JoinHandle<()>,
pub read_packets_task: tokio::task::JoinHandle<()>,
/// A task that writes packets from the server.
write_packets_task: tokio::task::JoinHandle<()>,
pub write_packets_task: tokio::task::JoinHandle<()>,
connection_protocol: ConnectionProtocol,
pub connection_protocol: ConnectionProtocol,
}
#[derive(Clone)]
struct RawConnectionReader {
pub struct RawConnectionReader {
pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>,
pub run_schedule_sender: mpsc::UnboundedSender<()>,
pub run_schedule_sender: mpsc::Sender<()>,
}
#[derive(Clone)]
struct RawConnectionWriter {
pub struct RawConnectionWriter {
pub outgoing_packets_sender: mpsc::UnboundedSender<Box<[u8]>>,
}
@ -60,7 +63,7 @@ pub enum WritePacketError {
impl RawConnection {
pub fn new(
run_schedule_sender: mpsc::UnboundedSender<()>,
run_schedule_sender: mpsc::Sender<()>,
connection_protocol: ConnectionProtocol,
raw_read_connection: RawReadConnection,
raw_write_connection: RawWriteConnection,
@ -133,21 +136,42 @@ impl RawConnectionReader {
/// 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: RawReadConnection) {
fn log_for_error(error: &ReadPacketError) {
if !matches!(*error, ReadPacketError::ConnectionClosed) {
error!("Error reading packet from Client: {error:?}");
}
}
loop {
match read_conn.read().await {
Ok(raw_packet) => {
self.incoming_packet_queue.lock().push(raw_packet);
let mut incoming_packet_queue = self.incoming_packet_queue.lock();
incoming_packet_queue.push(raw_packet);
// this makes it so packets received at the same time are guaranteed to be
// handled in the same tick. this is also an attempt at making it so we can't
// receive any packets in the ticks/updates after being disconnected.
loop {
let raw_packet = match read_conn.try_read() {
Ok(p) => p,
Err(err) => {
log_for_error(&err);
return;
}
};
let Some(raw_packet) = raw_packet else { break };
incoming_packet_queue.push(raw_packet);
}
// tell the client to run all the systems
if self.run_schedule_sender.send(()).is_err() {
if self.run_schedule_sender.try_send(()) == Err(TrySendError::Closed(())) {
// the client was dropped
break;
}
}
Err(error) => {
if !matches!(*error, ReadPacketError::ConnectionClosed) {
error!("Error reading packet from Client: {error:?}");
}
break;
Err(err) => {
log_for_error(&err);
return;
}
}
}
@ -158,6 +182,8 @@ impl RawConnectionWriter {
/// 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.
///
/// [`ServerboundGamePacket`]: azalea_protocol::packets::game::ServerboundGamePacket
pub async fn write_task(
self,
mut write_conn: RawWriteConnection,

View file

@ -0,0 +1,321 @@
use std::{fmt::Debug, sync::Arc, time::Duration};
use azalea_auth::game_profile::GameProfile;
use azalea_buf::AzaleaWrite;
use azalea_core::delta::PositionDelta8;
use azalea_core::game_type::{GameMode, OptionalGameType};
use azalea_core::position::{ChunkPos, Vec3};
use azalea_core::resource_location::ResourceLocation;
use azalea_core::tick::GameTick;
use azalea_entity::metadata::PlayerMetadataBundle;
use azalea_protocol::packets::common::CommonPlayerSpawnInfo;
use azalea_protocol::packets::config::{ClientboundFinishConfiguration, ClientboundRegistryData};
use azalea_protocol::packets::game::c_level_chunk_with_light::ClientboundLevelChunkPacketData;
use azalea_protocol::packets::game::c_light_update::ClientboundLightUpdatePacketData;
use azalea_protocol::packets::game::{
ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn,
};
use azalea_protocol::packets::{ConnectionProtocol, Packet, ProtocolPacket};
use azalea_registry::{DimensionType, EntityKind};
use azalea_world::palette::{PalettedContainer, PalettedContainerKind};
use azalea_world::{Chunk, Instance, MinecraftEntityId, Section};
use bevy_app::App;
use bevy_ecs::{prelude::*, schedule::ExecutorKind};
use parking_lot::{Mutex, RwLock};
use simdnbt::owned::{Nbt, NbtCompound, NbtTag};
use tokio::task::JoinHandle;
use tokio::{sync::mpsc, time::sleep};
use uuid::Uuid;
use crate::disconnect::DisconnectEvent;
use crate::{
ClientInformation, GameProfileComponent, InConfigState, InstanceHolder, LocalPlayerBundle,
events::LocalPlayerEvents,
raw_connection::{RawConnection, RawConnectionReader, RawConnectionWriter},
};
/// A way to simulate a client in a server, used for some internal tests.
pub struct Simulation {
pub app: App,
pub entity: Entity,
// the runtime needs to be kept around for the tasks to be considered alive
pub rt: tokio::runtime::Runtime,
pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>,
pub clear_outgoing_packets_receiver_task: JoinHandle<!>,
}
impl Simulation {
pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self {
let mut app = create_simulation_app();
let mut entity = app.world_mut().spawn_empty();
let (player, clear_outgoing_packets_receiver_task, incoming_packet_queue, rt) =
create_local_player_bundle(entity.id(), ConnectionProtocol::Configuration);
entity.insert(player);
let entity = entity.id();
tick_app(&mut app);
// start in the config state
app.world_mut().entity_mut(entity).insert(InConfigState);
tick_app(&mut app);
let mut simulation = Self {
app,
entity,
rt,
incoming_packet_queue,
clear_outgoing_packets_receiver_task,
};
#[allow(clippy::single_match)]
match initial_connection_protocol {
ConnectionProtocol::Configuration => {}
ConnectionProtocol::Game => {
simulation.receive_packet(ClientboundRegistryData {
registry_id: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![(
ResourceLocation::new("minecraft:overworld"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(384)),
("min_y".into(), NbtTag::Int(-64)),
])),
)]
.into_iter()
.collect(),
});
simulation.receive_packet(ClientboundFinishConfiguration);
simulation.tick();
}
_ => unimplemented!("unsupported ConnectionProtocol {initial_connection_protocol:?}"),
}
simulation
}
pub fn receive_packet<P: ProtocolPacket + Debug>(&mut self, packet: impl Packet<P>) {
let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap();
self.incoming_packet_queue.lock().push(buf);
}
pub fn tick(&mut self) {
tick_app(&mut self.app);
}
pub fn component<T: Component + Clone>(&self) -> T {
self.app.world().get::<T>(self.entity).unwrap().clone()
}
pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
self.app.world().get::<T>(self.entity).cloned()
}
pub fn has_component<T: Component>(&self) -> bool {
self.app.world().get::<T>(self.entity).is_some()
}
pub fn chunk(&self, chunk_pos: ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
self.component::<InstanceHolder>()
.instance
.read()
.chunks
.get(&chunk_pos)
}
pub fn disconnect(&mut self) {
// send DisconnectEvent
self.app.world_mut().send_event(DisconnectEvent {
entity: self.entity,
reason: None,
});
}
}
#[allow(clippy::type_complexity)]
fn create_local_player_bundle(
entity: Entity,
connection_protocol: ConnectionProtocol,
) -> (
LocalPlayerBundle,
JoinHandle<!>,
Arc<Mutex<Vec<Box<[u8]>>>>,
tokio::runtime::Runtime,
) {
// unused since we'll trigger ticks ourselves
let (run_schedule_sender, _run_schedule_receiver) = mpsc::channel(1);
let (outgoing_packets_sender, mut outgoing_packets_receiver) = mpsc::unbounded_channel();
let incoming_packet_queue = Arc::new(Mutex::new(Vec::new()));
let reader = RawConnectionReader {
incoming_packet_queue: incoming_packet_queue.clone(),
run_schedule_sender,
};
let writer = RawConnectionWriter {
outgoing_packets_sender,
};
let rt = tokio::runtime::Runtime::new().unwrap();
// the tasks can't die since that would make us send a DisconnectEvent
let read_packets_task = rt.spawn(async {
loop {
sleep(Duration::from_secs(60)).await;
}
});
let write_packets_task = rt.spawn(async {
loop {
sleep(Duration::from_secs(60)).await;
}
});
let clear_outgoing_packets_receiver_task = rt.spawn(async move {
loop {
let _ = outgoing_packets_receiver.recv().await;
}
});
let raw_connection = RawConnection {
reader,
writer,
read_packets_task,
write_packets_task,
connection_protocol,
};
let (local_player_events_sender, _local_player_events_receiver) = mpsc::unbounded_channel();
let instance = Instance::default();
let instance_holder = InstanceHolder::new(entity, Arc::new(RwLock::new(instance)));
let local_player_bundle = LocalPlayerBundle {
raw_connection,
local_player_events: LocalPlayerEvents(local_player_events_sender),
game_profile: GameProfileComponent(GameProfile::new(Uuid::nil(), "azalea".to_owned())),
client_information: ClientInformation::default(),
instance_holder,
metadata: PlayerMetadataBundle::default(),
};
(
local_player_bundle,
clear_outgoing_packets_receiver_task,
incoming_packet_queue,
rt,
)
}
fn create_simulation_app() -> App {
let mut app = App::new();
#[cfg(feature = "log")]
app.add_plugins(
bevy_app::PluginGroup::build(crate::DefaultPlugins).disable::<bevy_log::LogPlugin>(),
);
app.edit_schedule(bevy_app::Main, |schedule| {
// makes test results more reproducible
schedule.set_executor_kind(ExecutorKind::SingleThreaded);
});
app
}
fn tick_app(app: &mut App) {
app.update();
app.world_mut().run_schedule(GameTick);
}
pub fn make_basic_login_packet(
dimension_type: DimensionType,
dimension: ResourceLocation,
) -> ClientboundLogin {
ClientboundLogin {
player_id: MinecraftEntityId(0),
hardcore: false,
levels: vec![],
max_players: 20,
chunk_radius: 8,
simulation_distance: 8,
reduced_debug_info: false,
show_death_screen: true,
do_limited_crafting: false,
common: CommonPlayerSpawnInfo {
dimension_type,
dimension,
seed: 0,
game_type: GameMode::Survival,
previous_game_type: OptionalGameType(None),
is_debug: false,
is_flat: false,
last_death_location: None,
portal_cooldown: 0,
sea_level: 63,
},
enforces_secure_chat: false,
}
}
pub fn make_basic_respawn_packet(
dimension_type: DimensionType,
dimension: ResourceLocation,
) -> ClientboundRespawn {
ClientboundRespawn {
common: CommonPlayerSpawnInfo {
dimension_type,
dimension,
seed: 0,
game_type: GameMode::Survival,
previous_game_type: OptionalGameType(None),
is_debug: false,
is_flat: false,
last_death_location: None,
portal_cooldown: 0,
sea_level: 63,
},
data_to_keep: 0,
}
}
pub fn make_basic_empty_chunk(
pos: ChunkPos,
section_count: usize,
) -> ClientboundLevelChunkWithLight {
let mut chunk_bytes = Vec::new();
let mut sections = Vec::new();
for _ in 0..section_count {
sections.push(Section {
block_count: 0,
states: PalettedContainer::new(PalettedContainerKind::BlockStates),
biomes: PalettedContainer::new(PalettedContainerKind::Biomes),
});
}
sections.azalea_write(&mut chunk_bytes).unwrap();
ClientboundLevelChunkWithLight {
x: pos.x,
z: pos.z,
chunk_data: ClientboundLevelChunkPacketData {
heightmaps: Nbt::None,
data: Arc::new(chunk_bytes.into()),
block_entities: vec![],
},
light_data: ClientboundLightUpdatePacketData::default(),
}
}
pub fn make_basic_add_entity(
entity_type: EntityKind,
id: i32,
position: impl Into<Vec3>,
) -> ClientboundAddEntity {
ClientboundAddEntity {
id: id.into(),
uuid: Uuid::from_u128(1234),
entity_type,
position: position.into(),
x_rot: 0,
y_rot: 0,
y_head_rot: 0,
data: 0,
velocity: PositionDelta8::default(),
}
}

View file

@ -0,0 +1,155 @@
use azalea_client::{InConfigState, InGameState, test_simulation::*};
use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
use azalea_entity::LocalEntity;
use azalea_protocol::packets::{
ConnectionProtocol, Packet,
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
};
use azalea_registry::DimensionType;
use azalea_world::InstanceName;
use bevy_log::tracing_subscriber;
use simdnbt::owned::{NbtCompound, NbtTag};
#[test]
fn test_change_dimension_to_nether_and_back() {
generic_test_change_dimension_to_nether_and_back(true);
generic_test_change_dimension_to_nether_and_back(false);
}
fn generic_test_change_dimension_to_nether_and_back(using_respawn: bool) {
let make_basic_login_or_respawn_packet = if using_respawn {
|dimension: DimensionType, instance_name: ResourceLocation| {
make_basic_respawn_packet(dimension, instance_name).into_variant()
}
} else {
|dimension: DimensionType, instance_name: ResourceLocation| {
make_basic_login_packet(dimension, instance_name).into_variant()
}
};
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
assert!(simulation.has_component::<InConfigState>());
assert!(!simulation.has_component::<InGameState>());
simulation.receive_packet(ClientboundRegistryData {
registry_id: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![
(
// this dimension should never be created. it just exists to make sure we're not
// hard-coding the dimension type id anywhere.
ResourceLocation::new("azalea:fakedimension"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(16)),
("min_y".into(), NbtTag::Int(0)),
])),
),
(
ResourceLocation::new("minecraft:overworld"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(384)),
("min_y".into(), NbtTag::Int(-64)),
])),
),
(
ResourceLocation::new("minecraft:nether"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(256)),
("min_y".into(), NbtTag::Int(0)),
])),
),
]
.into_iter()
.collect(),
});
simulation.tick();
simulation.receive_packet(ClientboundFinishConfiguration);
simulation.tick();
assert!(!simulation.has_component::<InConfigState>());
assert!(simulation.has_component::<InGameState>());
assert!(simulation.has_component::<LocalEntity>());
//
// OVERWORLD
//
simulation.receive_packet(make_basic_login_packet(
DimensionType::new_raw(1), // overworld
ResourceLocation::new("azalea:a"),
));
simulation.tick();
assert_eq!(
*simulation.component::<InstanceName>(),
ResourceLocation::new("azalea:a"),
"InstanceName should be azalea:a after setting dimension to that"
);
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
simulation.tick();
// make sure the chunk exists
simulation
.chunk(ChunkPos::new(0, 0))
.expect("chunk should exist");
//
// NETHER
//
simulation.receive_packet(make_basic_login_or_respawn_packet(
DimensionType::new_raw(2), // nether
ResourceLocation::new("azalea:b"),
));
simulation.tick();
assert!(
simulation.chunk(ChunkPos::new(0, 0)).is_none(),
"chunk should not exist immediately after changing dimensions"
);
assert_eq!(
*simulation.component::<InstanceName>(),
ResourceLocation::new("azalea:b"),
"InstanceName should be azalea:b after changing dimensions to that"
);
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), 256 / 16));
simulation.tick();
// make sure the chunk exists
simulation
.chunk(ChunkPos::new(0, 0))
.expect("chunk should exist");
simulation.receive_packet(make_basic_login_or_respawn_packet(
DimensionType::new_raw(2), // nether
ResourceLocation::new("minecraft:nether"),
));
simulation.tick();
//
// BACK TO OVERWORLD
//
simulation.receive_packet(make_basic_login_packet(
DimensionType::new_raw(1), // overworld
ResourceLocation::new("azalea:a"),
));
simulation.tick();
assert_eq!(
*simulation.component::<InstanceName>(),
ResourceLocation::new("azalea:a"),
"InstanceName should be azalea:a after setting dimension back to that"
);
assert!(
simulation.chunk(ChunkPos::new(0, 0)).is_none(),
"chunk should not exist immediately after switching back to overworld"
);
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
simulation.tick();
// make sure the chunk exists
simulation
.chunk(ChunkPos::new(0, 0))
.expect("chunk should exist");
}

View file

@ -0,0 +1,21 @@
use azalea_client::test_simulation::*;
use azalea_protocol::packets::ConnectionProtocol;
use azalea_world::InstanceName;
use bevy_log::tracing_subscriber;
#[test]
fn test_client_disconnect() {
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Game);
simulation.disconnect();
simulation.tick();
// make sure we're disconnected
let is_connected = simulation.has_component::<InstanceName>();
assert!(!is_connected);
// tick again to make sure nothing goes wrong
simulation.tick();
}

View file

@ -0,0 +1,80 @@
use azalea_client::test_simulation::*;
use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
use azalea_entity::metadata::Cow;
use azalea_protocol::packets::{
ConnectionProtocol,
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
};
use azalea_registry::{DimensionType, EntityKind};
use bevy_ecs::query::With;
use bevy_log::tracing_subscriber;
use simdnbt::owned::{NbtCompound, NbtTag};
#[test]
fn test_despawn_entities_when_changing_dimension() {
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
simulation.receive_packet(ClientboundRegistryData {
registry_id: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![
(
ResourceLocation::new("minecraft:overworld"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(384)),
("min_y".into(), NbtTag::Int(-64)),
])),
),
(
ResourceLocation::new("minecraft:nether"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(256)),
("min_y".into(), NbtTag::Int(0)),
])),
),
]
.into_iter()
.collect(),
});
simulation.tick();
simulation.receive_packet(ClientboundFinishConfiguration);
simulation.tick();
//
// OVERWORLD
//
simulation.receive_packet(make_basic_login_packet(
DimensionType::new_raw(0), // overworld
ResourceLocation::new("azalea:a"),
));
simulation.tick();
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
simulation.tick();
// spawn a cow
simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5)));
simulation.tick();
// make sure it's spawned
let mut cow_query = simulation.app.world_mut().query_filtered::<(), With<Cow>>();
let cow_iter = cow_query.iter(simulation.app.world());
assert_eq!(cow_iter.count(), 1, "cow should be spawned");
//
// NETHER
//
simulation.receive_packet(make_basic_respawn_packet(
DimensionType::new_raw(1), // nether
ResourceLocation::new("azalea:b"),
));
simulation.tick();
// cow should be completely deleted from the ecs
let cow_iter = cow_query.iter(simulation.app.world());
assert_eq!(
cow_iter.count(),
0,
"cow should be despawned after switching dimensions"
);
}

View file

@ -0,0 +1,43 @@
use azalea_client::{InConfigState, test_simulation::*};
use azalea_core::resource_location::ResourceLocation;
use azalea_entity::metadata::Health;
use azalea_protocol::packets::{
ConnectionProtocol,
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
game::ClientboundSetHealth,
};
use bevy_log::tracing_subscriber;
use simdnbt::owned::{NbtCompound, NbtTag};
#[test]
fn test_fast_login() {
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
assert!(simulation.has_component::<InConfigState>());
simulation.receive_packet(ClientboundRegistryData {
registry_id: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![(
ResourceLocation::new("minecraft:overworld"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(384)),
("min_y".into(), NbtTag::Int(-64)),
])),
)]
.into_iter()
.collect(),
});
simulation.receive_packet(ClientboundFinishConfiguration);
// note that there's no simulation tick here
simulation.receive_packet(ClientboundSetHealth {
health: 15.,
food: 20,
saturation: 20.,
});
simulation.tick();
// we need a second tick to handle the state switch properly
simulation.tick();
assert_eq!(*simulation.component::<Health>(), 15.);
}

View file

@ -0,0 +1,51 @@
use azalea_client::test_simulation::*;
use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
use azalea_entity::metadata::Cow;
use azalea_protocol::packets::{ConnectionProtocol, game::ClientboundMoveEntityRot};
use azalea_registry::{DimensionType, EntityKind};
use azalea_world::MinecraftEntityId;
use bevy_ecs::query::With;
use bevy_log::tracing_subscriber;
#[test]
fn test_move_despawned_entity() {
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Game);
simulation.receive_packet(make_basic_login_packet(
DimensionType::new_raw(0),
ResourceLocation::new("azalea:overworld"),
));
simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
simulation.tick();
// spawn a cow
simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5)));
simulation.tick();
// make sure it's spawned
let mut cow_query = simulation.app.world_mut().query_filtered::<(), With<Cow>>();
let cow_iter = cow_query.iter(simulation.app.world());
assert_eq!(cow_iter.count(), 1, "cow should be despawned");
// despawn the cow by receiving a login packet
simulation.receive_packet(make_basic_login_packet(
DimensionType::new_raw(0),
ResourceLocation::new("azalea:overworld"),
));
simulation.tick();
// make sure it's despawned
let mut cow_query = simulation.app.world_mut().query_filtered::<(), With<Cow>>();
let cow_iter = cow_query.iter(simulation.app.world());
assert_eq!(cow_iter.count(), 0, "cow should be despawned");
// send a move_entity_rot
simulation.receive_packet(ClientboundMoveEntityRot {
entity_id: MinecraftEntityId(123),
y_rot: 0,
x_rot: 0,
on_ground: false,
});
simulation.tick();
}

View file

@ -0,0 +1,55 @@
use azalea_client::{InConfigState, test_simulation::*};
use azalea_core::resource_location::ResourceLocation;
use azalea_entity::{LocalEntity, metadata::Health};
use azalea_protocol::packets::{
ConnectionProtocol,
config::{ClientboundFinishConfiguration, ClientboundRegistryData},
game::ClientboundSetHealth,
};
use azalea_registry::DimensionType;
use bevy_log::tracing_subscriber;
use simdnbt::owned::{NbtCompound, NbtTag};
#[test]
fn test_set_health_before_login() {
let _ = tracing_subscriber::fmt::try_init();
let mut simulation = Simulation::new(ConnectionProtocol::Configuration);
assert!(simulation.has_component::<InConfigState>());
simulation.receive_packet(ClientboundRegistryData {
registry_id: ResourceLocation::new("minecraft:dimension_type"),
entries: vec![(
ResourceLocation::new("minecraft:overworld"),
Some(NbtCompound::from_values(vec![
("height".into(), NbtTag::Int(384)),
("min_y".into(), NbtTag::Int(-64)),
])),
)]
.into_iter()
.collect(),
});
simulation.tick();
simulation.receive_packet(ClientboundFinishConfiguration);
simulation.tick();
assert!(!simulation.has_component::<InConfigState>());
assert!(simulation.has_component::<LocalEntity>());
simulation.receive_packet(ClientboundSetHealth {
health: 15.,
food: 20,
saturation: 20.,
});
simulation.tick();
assert_eq!(*simulation.component::<Health>(), 15.);
simulation.receive_packet(make_basic_login_packet(
DimensionType::new_raw(0), // overworld
ResourceLocation::new("minecraft:overworld"),
));
simulation.tick();
// health should stay the same
assert_eq!(*simulation.component::<Health>(), 15.);
}

View file

@ -1,21 +1,22 @@
[package]
name = "azalea-core"
description = "Miscellaneous things in Azalea."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "0.11.0" }
azalea-registry = { path = "../azalea-registry", version = "0.11.0" }
bevy_ecs = { workspace = true, optional = true }
nohash-hasher = { workspace = true }
num-traits = { workspace = true }
nohash-hasher.workspace = true
num-traits.workspace = true
serde = { workspace = true, optional = true }
simdnbt = { workspace = true }
tracing = { workspace = true }
simdnbt.workspace = true
tracing.workspace = true
azalea-chat = { path = "../azalea-chat", version = "0.11.0" }
indexmap.workspace = true
[features]
bevy_ecs = ["dep:bevy_ecs"]

View file

@ -26,26 +26,26 @@ pub struct ClipPointOpts<'a> {
}
impl AABB {
pub fn contract(&self, x: f64, y: f64, z: f64) -> AABB {
pub fn contract(&self, amount: Vec3) -> AABB {
let mut min = self.min;
let mut max = self.max;
if x < 0.0 {
min.x -= x;
} else if x > 0.0 {
max.x -= x;
if amount.x < 0.0 {
min.x -= amount.x;
} else if amount.x > 0.0 {
max.x -= amount.x;
}
if y < 0.0 {
min.y -= y;
} else if y > 0.0 {
max.y -= y;
if amount.y < 0.0 {
min.y -= amount.y;
} else if amount.y > 0.0 {
max.y -= amount.y;
}
if z < 0.0 {
min.z -= z;
} else if z > 0.0 {
max.z -= z;
if amount.z < 0.0 {
min.z -= amount.z;
} else if amount.z > 0.0 {
max.z -= amount.z;
}
AABB { min, max }
@ -84,20 +84,23 @@ impl AABB {
}
}
pub fn inflate(&self, x: f64, y: f64, z: f64) -> AABB {
let min_x = self.min.x - x;
let min_y = self.min.y - y;
let min_z = self.min.z - z;
pub fn inflate(&self, amount: Vec3) -> AABB {
let min_x = self.min.x - amount.x;
let min_y = self.min.y - amount.y;
let min_z = self.min.z - amount.z;
let max_x = self.max.x + x;
let max_y = self.max.y + y;
let max_z = self.max.z + z;
let max_x = self.max.x + amount.x;
let max_y = self.max.y + amount.y;
let max_z = self.max.z + amount.z;
AABB {
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
}
}
pub fn inflate_all(&self, amount: f64) -> AABB {
self.inflate(Vec3::new(amount, amount, amount))
}
pub fn intersect(&self, other: &AABB) -> AABB {
let min_x = self.min.x.max(other.min.x);
@ -144,17 +147,17 @@ impl AABB {
&& self.min.z < other.max.z
&& self.max.z > other.min.z
}
pub fn intersects_vec3(&self, other: &Vec3, other2: &Vec3) -> bool {
pub fn intersects_vec3(&self, corner1: &Vec3, corner2: &Vec3) -> bool {
self.intersects_aabb(&AABB {
min: Vec3::new(
other.x.min(other2.x),
other.y.min(other2.y),
other.z.min(other2.z),
corner1.x.min(corner2.x),
corner1.y.min(corner2.y),
corner1.z.min(corner2.z),
),
max: Vec3::new(
other.x.max(other2.x),
other.y.max(other2.y),
other.z.max(other2.z),
corner1.x.max(corner2.x),
corner1.y.max(corner2.y),
corner1.z.max(corner2.z),
),
})
}
@ -183,12 +186,11 @@ impl AABB {
)
}
pub fn deflate(&mut self, x: f64, y: f64, z: f64) -> AABB {
self.inflate(-x, -y, -z)
pub fn deflate(&self, amount: Vec3) -> AABB {
self.inflate(Vec3::new(-amount.x, -amount.y, -amount.z))
}
pub fn deflate_all(&mut self, amount: f64) -> AABB {
self.deflate(amount, amount, amount)
pub fn deflate_all(&self, amount: f64) -> AABB {
self.deflate(Vec3::new(amount, amount, amount))
}
pub fn clip(&self, min: &Vec3, max: &Vec3) -> Option<Vec3> {
@ -434,11 +436,11 @@ impl AABB {
let new_center = center + vector;
for aabb in boxes {
let inflated = aabb.inflate(
let inflated = aabb.inflate(Vec3::new(
self.get_size(Axis::X) * 0.5,
self.get_size(Axis::Y) * 0.5,
self.get_size(Axis::Z) * 0.5,
);
));
if inflated.contains(&new_center) || inflated.contains(&center) {
return true;
}

View file

@ -0,0 +1,47 @@
use std::{io::Cursor, str::FromStr};
use azalea_registry::DataRegistry;
use simdnbt::owned::NbtCompound;
use crate::{registry_holder::RegistryHolder, resource_location::ResourceLocation};
pub trait ResolvableDataRegistry: DataRegistry {
fn resolve_name(&self, registries: &RegistryHolder) -> Option<ResourceLocation> {
self.resolve(registries).map(|(name, _)| name.clone())
}
fn resolve<'a>(
&self,
registries: &'a RegistryHolder,
) -> Option<(&'a ResourceLocation, &'a NbtCompound)> {
let name_resourcelocation = ResourceLocation::from_str(Self::NAME).unwrap_or_else(|_| {
panic!(
"Name for registry should be a valid ResourceLocation: {}",
Self::NAME
)
});
let registry_values = registries.map.get(&name_resourcelocation)?;
let resolved = registry_values.get_index(self.protocol_id() as usize)?;
Some(resolved)
}
fn resolve_and_deserialize<T: simdnbt::Deserialize>(
&self,
registries: &RegistryHolder,
) -> Option<Result<(ResourceLocation, T), simdnbt::DeserializeError>> {
let (name, value) = self.resolve(registries)?;
let mut nbt_bytes = Vec::new();
value.write(&mut nbt_bytes);
let nbt_borrow_compound =
simdnbt::borrow::read_compound(&mut Cursor::new(&nbt_bytes)).ok()?;
let value = match T::from_compound((&nbt_borrow_compound).into()) {
Ok(value) => value,
Err(err) => {
return Some(Err(err));
}
};
Some(Ok((name.clone(), value)))
}
}
impl<T: DataRegistry> ResolvableDataRegistry for T {}

View file

@ -51,8 +51,8 @@ impl Vec3 {
pub fn normalize(&self) -> Vec3 {
let length = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
if length < 1e-4 {
return Vec3::default();
if length < 1e-5 {
return Vec3::ZERO;
}
Vec3 {
x: self.x / length,

View file

@ -17,9 +17,9 @@ pub enum Direction {
impl Direction {
pub const HORIZONTAL: [Direction; 4] = [
Direction::North,
Direction::East,
Direction::South,
Direction::West,
Direction::East,
];
pub const VERTICAL: [Direction; 2] = [Direction::Down, Direction::Up];

View file

@ -0,0 +1,55 @@
use std::io::Cursor;
use azalea_buf::{AzaleaRead, AzaleaReadLimited, AzaleaReadVar, AzaleaWrite};
/// Used for written books.
pub struct Filterable<T> {
pub raw: T,
pub filtered: Option<T>,
}
impl<T: AzaleaWrite> azalea_buf::AzaleaWrite for Filterable<T> {
fn azalea_write(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
self.raw.azalea_write(buf)?;
self.filtered.azalea_write(buf)?;
Ok(())
}
}
impl<T: AzaleaRead> azalea_buf::AzaleaRead for Filterable<T> {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
let raw = AzaleaRead::azalea_read(buf)?;
let filtered = AzaleaRead::azalea_read(buf)?;
Ok(Self { raw, filtered })
}
}
impl<T: AzaleaReadLimited> azalea_buf::AzaleaReadLimited for Filterable<T> {
fn azalea_read_limited(
buf: &mut Cursor<&[u8]>,
limit: usize,
) -> Result<Self, azalea_buf::BufReadError> {
let raw = AzaleaReadLimited::azalea_read_limited(buf, limit)?;
let filtered = AzaleaReadLimited::azalea_read_limited(buf, limit)?;
Ok(Self { raw, filtered })
}
}
impl<T: AzaleaReadVar> azalea_buf::AzaleaReadVar for Filterable<T> {
fn azalea_read_var(buf: &mut Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
let raw = AzaleaReadVar::azalea_read_var(buf)?;
let filtered = AzaleaReadVar::azalea_read_var(buf)?;
Ok(Self { raw, filtered })
}
}
impl<T: Clone> Clone for Filterable<T> {
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
filtered: self.filtered.clone(),
}
}
}
impl<T: PartialEq> PartialEq for Filterable<T> {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw && self.filtered == other.filtered
}
}

View file

@ -1,21 +1,22 @@
#![doc = include_str!("../README.md")]
#![feature(trait_upcasting)]
#![allow(incomplete_features)]
pub mod aabb;
pub mod bitset;
pub mod block_hit_result;
pub mod color;
pub mod cursor3d;
pub mod data_registry;
pub mod delta;
pub mod difficulty;
pub mod direction;
pub mod filterable;
pub mod game_type;
pub mod math;
pub mod objectives;
pub mod position;
pub mod registry_holder;
pub mod resource_location;
pub mod sound;
#[cfg(feature = "bevy_ecs")]
pub mod tick;
pub mod tier;

View file

@ -91,18 +91,10 @@ pub fn to_degrees(radians: f64) -> f64 {
///
/// This function exists because f64::signum doesn't check for 0.
pub fn sign(num: f64) -> f64 {
if num == 0. {
0.
} else {
num.signum()
}
if num == 0. { 0. } else { num.signum() }
}
pub fn sign_as_int(num: f64) -> i32 {
if num == 0. {
0
} else {
num.signum() as i32
}
if num == 0. { 0 } else { num.signum() as i32 }
}
#[cfg(test)]

View file

@ -125,6 +125,16 @@ macro_rules! vec3_impl {
z: self.z,
}
}
pub fn with_x(&self, x: $type) -> Self {
Self { x, ..*self }
}
pub fn with_y(&self, y: $type) -> Self {
Self { y, ..*self }
}
pub fn with_z(&self, z: $type) -> Self {
Self { z, ..*self }
}
}
impl Add for &$name {
@ -309,6 +319,21 @@ impl Vec3 {
let z = self.z * (x_delta as f64) - self.x * (y_delta as f64);
Vec3 { x, y, z }
}
pub fn to_block_pos_floor(&self) -> BlockPos {
BlockPos {
x: self.x.floor() as i32,
y: self.y.floor() as i32,
z: self.z.floor() as i32,
}
}
pub fn to_block_pos_ceil(&self) -> BlockPos {
BlockPos {
x: self.x.ceil() as i32,
y: self.y.ceil() as i32,
z: self.z.ceil() as i32,
}
}
}
/// The coordinates of a block in the world. For entities (if the coordinate
@ -612,6 +637,16 @@ impl From<ChunkSectionPos> for ChunkPos {
ChunkPos { x: pos.x, z: pos.z }
}
}
impl From<&Vec3> for ChunkSectionPos {
fn from(pos: &Vec3) -> Self {
ChunkSectionPos::from(&BlockPos::from(pos))
}
}
impl From<Vec3> for ChunkSectionPos {
fn from(pos: Vec3) -> Self {
ChunkSectionPos::from(&pos)
}
}
impl From<&BlockPos> for ChunkBlockPos {
#[inline]

View file

@ -7,9 +7,10 @@
use std::{collections::HashMap, io::Cursor};
use indexmap::IndexMap;
use simdnbt::{
owned::{NbtCompound, NbtTag},
Deserialize, FromNbtTag, Serialize, ToNbtTag,
owned::{NbtCompound, NbtTag},
};
use tracing::error;
@ -18,23 +19,28 @@ use crate::resource_location::ResourceLocation;
/// The base of the registry.
///
/// This is the registry that is sent to the client upon login.
///
/// Note that `azalea-client` stores registries per-world instead of per-client
/// like you might expect. This is an optimization for swarms to reduce memory
/// usage, since registries are expected to be the same for every client in a
/// world.
#[derive(Default, Debug, Clone)]
pub struct RegistryHolder {
pub map: HashMap<ResourceLocation, HashMap<ResourceLocation, NbtCompound>>,
pub map: HashMap<ResourceLocation, IndexMap<ResourceLocation, NbtCompound>>,
}
impl RegistryHolder {
pub fn append(
&mut self,
id: ResourceLocation,
entries: HashMap<ResourceLocation, Option<NbtCompound>>,
entries: Vec<(ResourceLocation, Option<NbtCompound>)>,
) {
let map = self.map.entry(id).or_default();
for (key, value) in entries {
if let Some(value) = value {
map.insert(key, value);
} else {
map.remove(&key);
map.shift_remove(&key);
}
}
}
@ -152,7 +158,7 @@ pub struct DimensionTypeElement {
pub natural: bool,
pub piglin_safe: bool,
pub respawn_anchor_works: bool,
pub ultrawarm: bool,
pub ultrawarm: Option<bool>,
}
/// Dimension attributes.
@ -161,7 +167,7 @@ pub struct DimensionTypeElement {
pub struct DimensionTypeElement {
pub height: u32,
pub min_y: i32,
pub ultrawarm: bool,
pub ultrawarm: Option<bool>,
#[simdnbt(flatten)]
pub _extra: HashMap<String, NbtTag>,
}

View file

@ -8,8 +8,8 @@ use std::{
use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use simdnbt::{owned::NbtTag, FromNbtTag, ToNbtTag};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use simdnbt::{FromNbtTag, ToNbtTag, owned::NbtTag};
#[derive(Hash, Clone, PartialEq, Eq)]
pub struct ResourceLocation {

9
azalea-core/src/sound.rs Normal file
View file

@ -0,0 +1,9 @@
use azalea_buf::AzBuf;
use crate::resource_location::ResourceLocation;
#[derive(Clone, Debug, PartialEq, AzBuf)]
pub struct CustomSound {
pub location: ResourceLocation,
pub fixed_range: Option<f32>,
}

View file

@ -4,5 +4,7 @@ use bevy_ecs::schedule::ScheduleLabel;
///
/// Many client systems run on this schedule, the most important one being
/// physics.
///
/// This schedule runs either zero or one times after every Bevy `Update`.
#[derive(ScheduleLabel, Hash, Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct GameTick;

View file

@ -1,25 +1,25 @@
[package]
name = "azalea-crypto"
description = "Cryptography features used in Minecraft."
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dev-dependencies]
criterion = { workspace = true }
criterion.workspace = true
[dependencies]
aes = { workspace = true }
aes.workspace = true
azalea-buf = { path = "../azalea-buf", version = "0.11.0" }
cfb8 = { workspace = true }
num-bigint = { workspace = true }
cfb8.workspace = true
num-bigint.workspace = true
rand = { workspace = true, features = ["getrandom"] }
rsa = { workspace = true, features = ["sha2"] }
rsa_public_encrypt_pkcs1 = { workspace = true }
sha-1 = { workspace = true }
sha2 = { workspace = true }
uuid = { workspace = true }
rsa_public_encrypt_pkcs1.workspace = true
sha-1.workspace = true
sha2.workspace = true
uuid.workspace = true
[[bench]]
harness = false

View file

@ -1,5 +1,5 @@
use azalea_crypto::{create_cipher, decrypt_packet, encrypt_packet};
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
fn bench(c: &mut Criterion) {
let (mut enc, dec) = create_cipher(b"0123456789abcdef");

View file

@ -4,10 +4,10 @@ mod signing;
use aes::cipher::inout::InOutBuf;
use aes::{
cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit},
Aes128,
cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit},
};
use rand::{rngs::OsRng, RngCore};
use rand::{RngCore, rngs::OsRng};
use sha1::{Digest, Sha1};
pub use signing::*;

View file

@ -2,8 +2,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
use azalea_buf::{AzBuf, AzaleaWrite};
use rsa::{
signature::{RandomizedSigner, SignatureEncoding},
RsaPrivateKey,
signature::{RandomizedSigner, SignatureEncoding},
};
use sha2::Sha256;
use uuid::Uuid;

View file

@ -1,10 +1,10 @@
[package]
name = "azalea-entity"
description = "Things related to Minecraft entities used by Azalea"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
azalea-block = { path = "../azalea-block", version = "0.11.0" }
@ -16,13 +16,13 @@ azalea-core = { path = "../azalea-core", version = "0.11.0" }
azalea-inventory = { path = "../azalea-inventory", version = "0.11.0" }
azalea-registry = { path = "../azalea-registry", version = "0.11.0" }
azalea-world = { path = "../azalea-world", version = "0.11.0" }
bevy_app = { workspace = true }
bevy_ecs = { workspace = true }
derive_more = { workspace = true }
enum-as-inner = { workspace = true }
nohash-hasher = { workspace = true }
parking_lot = { workspace = true }
simdnbt = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
bevy_app.workspace = true
bevy_ecs.workspace = true
derive_more.workspace = true
enum-as-inner.workspace = true
nohash-hasher.workspace = true
parking_lot.workspace = true
simdnbt.workspace = true
thiserror.workspace = true
tracing.workspace = true
uuid.workspace = true

View file

@ -1,6 +1,6 @@
//! See <https://minecraft.fandom.com/wiki/Attribute>.
use std::collections::{hash_map, HashMap};
use std::collections::{HashMap, hash_map};
use azalea_buf::AzBuf;
use azalea_core::resource_location::ResourceLocation;

View file

@ -103,6 +103,7 @@ impl From<EntityKind> for EntityDimensions {
EntityKind::JungleChestBoat => EntityDimensions::new(1.375, 0.5625),
EntityKind::LeashKnot => EntityDimensions::new(0.375, 0.5),
EntityKind::LightningBolt => EntityDimensions::new(0.0, 0.0),
EntityKind::LingeringPotion => EntityDimensions::new(0.25, 0.25),
EntityKind::Llama => EntityDimensions::new(0.9, 1.87),
EntityKind::LlamaSpit => EntityDimensions::new(0.25, 0.25),
EntityKind::MagmaCube => EntityDimensions::new(0.52, 0.52),
@ -128,7 +129,6 @@ impl From<EntityKind> for EntityDimensions {
EntityKind::Pillager => EntityDimensions::new(0.6, 1.95),
EntityKind::Player => EntityDimensions::new(0.6, 1.8),
EntityKind::PolarBear => EntityDimensions::new(1.4, 1.4),
EntityKind::Potion => EntityDimensions::new(0.25, 0.25),
EntityKind::Pufferfish => EntityDimensions::new(0.7, 0.7),
EntityKind::Rabbit => EntityDimensions::new(0.4, 0.5),
EntityKind::Ravager => EntityDimensions::new(1.95, 2.2),
@ -147,6 +147,7 @@ impl From<EntityKind> for EntityDimensions {
EntityKind::SpawnerMinecart => EntityDimensions::new(0.98, 0.7),
EntityKind::SpectralArrow => EntityDimensions::new(0.5, 0.5),
EntityKind::Spider => EntityDimensions::new(1.4, 0.9),
EntityKind::SplashPotion => EntityDimensions::new(0.25, 0.25),
EntityKind::SpruceBoat => EntityDimensions::new(1.375, 0.5625),
EntityKind::SpruceChestBoat => EntityDimensions::new(1.375, 0.5625),
EntityKind::Squid => EntityDimensions::new(0.8, 0.8),

View file

@ -1,4 +1,4 @@
pub fn get_enchant_level(
pub fn _get_enchant_level(
_enchantment: azalea_registry::Enchantment,
_player_inventory: &azalea_inventory::Menu,
) -> u32 {

View file

@ -17,7 +17,7 @@ use std::{
};
pub use attributes::Attributes;
use azalea_block::{fluid_state::FluidKind, BlockState};
use azalea_block::{BlockState, fluid_state::FluidKind};
use azalea_buf::AzBuf;
use azalea_core::{
aabb::AABB,
@ -209,8 +209,8 @@ impl From<&LastSentPosition> for BlockPos {
///
/// 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, Copy, Clone, Deref, DerefMut, Default)]
pub struct Jumping(bool);
#[derive(Debug, Component, Copy, Clone, Deref, DerefMut, Default, PartialEq, Eq)]
pub struct Jumping(pub bool);
/// A component that contains the direction an entity is looking.
#[derive(Debug, Component, Copy, Clone, Default, PartialEq, AzBuf)]
@ -478,18 +478,13 @@ impl EntityBundle {
}
}
/// A bundle of the components that are always present for a player.
#[derive(Bundle)]
pub struct PlayerBundle {
pub entity: EntityBundle,
pub metadata: metadata::PlayerMetadataBundle,
}
/// A marker component that signifies that this entity is "local" and shouldn't
/// be updated by other clients.
///
/// If this is for a client then all of our clients will have this.
#[derive(Component, Clone)]
///
/// This component is not removed from clients when they disconnect.
#[derive(Component, Clone, Debug, Default)]
pub struct LocalEntity;
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]

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