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:
commit
60d6ff4cfe
231 changed files with 7047 additions and 2757 deletions
71
.github/workflows/doc.yml
vendored
71
.github/workflows/doc.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -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
498
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
51
Cargo.toml
51
Cargo.toml
|
@ -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 ---
|
||||
|
||||
|
|
34
README.md
34
README.md
|
@ -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 :).
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
@ -1,5 +1,4 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![feature(trait_upcasting)]
|
||||
|
||||
mod behavior;
|
||||
pub mod block_state;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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)?))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use azalea_chat::{
|
||||
style::{Ansi, ChatFormatting, TextColor},
|
||||
FormattedText,
|
||||
style::{Ansi, ChatFormatting, TextColor},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")]
|
||||
{
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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}")]
|
||||
|
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
},
|
63
azalea-client/src/plugins/brand.rs
Normal file
63
azalea-client/src/plugins/brand.rs
Normal 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(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
63
azalea-client/src/plugins/chat/handler.rs
Normal file
63
azalea-client/src/plugins/chat/handler.rs
Normal 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));
|
||||
}
|
||||
}
|
124
azalea-client/src/chat.rs → azalea-client/src/plugins/chat/mod.rs
Executable file → Normal file
124
azalea-client/src/chat.rs → azalea-client/src/plugins/chat/mod.rs
Executable file → Normal 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 {
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
|
@ -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,
|
|
@ -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);
|
|
@ -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;
|
||||
|
14
azalea-client/src/plugins/mod.rs
Normal file
14
azalea-client/src/plugins/mod.rs
Normal 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;
|
|
@ -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,
|
111
azalea-client/src/plugins/packet/config/events.rs
Normal file
111
azalea-client/src/plugins/packet/config/events.rs
Normal 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(_)
|
||||
)
|
||||
}
|
229
azalea-client/src/plugins/packet/config/mod.rs
Normal file
229
azalea-client/src/plugins/packet/config/mod.rs
Normal 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:?}");
|
||||
}
|
||||
}
|
206
azalea-client/src/plugins/packet/game/events.rs
Normal file
206
azalea-client/src/plugins/packet/game/events.rs
Normal 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>>,
|
||||
}
|
1609
azalea-client/src/plugins/packet/game/mod.rs
Normal file
1609
azalea-client/src/plugins/packet/game/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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>,
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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`,
|
36
azalea-client/src/plugins/tick_end.rs
Normal file
36
azalea-client/src/plugins/tick_end.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
321
azalea-client/src/test_simulation.rs
Normal file
321
azalea-client/src/test_simulation.rs
Normal 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(),
|
||||
}
|
||||
}
|
155
azalea-client/tests/change_dimension_to_nether_and_back.rs
Normal file
155
azalea-client/tests/change_dimension_to_nether_and_back.rs
Normal 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");
|
||||
}
|
21
azalea-client/tests/client_disconnect.rs
Normal file
21
azalea-client/tests/client_disconnect.rs
Normal 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();
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
}
|
43
azalea-client/tests/fast_login.rs
Normal file
43
azalea-client/tests/fast_login.rs
Normal 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.);
|
||||
}
|
51
azalea-client/tests/move_despawned_entity.rs
Normal file
51
azalea-client/tests/move_despawned_entity.rs
Normal 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();
|
||||
}
|
55
azalea-client/tests/set_health_before_login.rs
Normal file
55
azalea-client/tests/set_health_before_login.rs
Normal 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.);
|
||||
}
|
|
@ -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"]
|
||||
|
|
|
@ -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(¢er) {
|
||||
return true;
|
||||
}
|
||||
|
|
47
azalea-core/src/data_registry.rs
Normal file
47
azalea-core/src/data_registry.rs
Normal 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 {}
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
55
azalea-core/src/filterable.rs
Normal file
55
azalea-core/src/filterable.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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
9
azalea-core/src/sound.rs
Normal 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>,
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub fn get_enchant_level(
|
||||
pub fn _get_enchant_level(
|
||||
_enchantment: azalea_registry::Enchantment,
|
||||
_player_inventory: &azalea_inventory::Menu,
|
||||
) -> u32 {
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue