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

Modernize 1.19.2 (#63)

* Add reason for disconnect (#54)

* Add reason for disconnect

Handles messages like "The server is full!" and "Banned by an operator."
Ban reasons are shown like "You are banned from this server.\nReason: foo"

* keep the kick reason as a Component in the error

Co-authored-by: mat <github@matdoes.dev>

* Bump tokio from 1.22.0 to 1.23.1 (#55)

Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.22.0 to 1.23.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.22.0...tokio-1.23.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add function to get message sender's UUID (#56)

* Add uuid function for chat messages

* Does this please you, clippy?

* the repo is NOT called Cargo.toml

* update a thing in cargo.toml

* Allow using azalea-chat without azalea-buf to avoid unstable features (#58)

* Server functions and proxy example (#59)

* A couple useful things for servers

* Add proxy example

* Use Uuid's serde feature

* Add const options to proxy example

* Example crates go in dev-dependencies

* Warn instead of error

* Log address on login

* Requested changes

* add a test for deserializing game profile + random small changes

Co-authored-by: mat <github@matdoes.dev>

* clippy

* oops

accidentally left this random person's server ip in lmao

* Fix test and packets (#60)

* Fix test and packets

* Fix bug, fix a couple more packets

* add tests and fix stuff

* fix warnings

Co-authored-by: Ubuntu <github@matdoes.dev>

* replace some string ranges with function equivalents

* have docs for all crates

* More packet fixes, tests, handle error (#61)

* Fix packet, fix tests, fixedbitsets

* Clippy: Nightmare Mode

* Fix mistake

* simplify impl Display and make thing pub

---------

Co-authored-by: mat <github@matdoes.dev>

* Bump tokio from 1.23.1 to 1.24.2 (#62)

Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.1 to 1.24.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/commits)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Use an ECS (#52)

* add EntityData::kind

* start making metadata use hecs

* make entity codegen generate ecs stuff

* fix registry codegen

* get rid of worldhaver

it's not even used

* add bevy_ecs to deps

* rename Component to FormattedText

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

* generate metadata.rs correctly for bevy_ecs

* start switching more entity stuff to use ecs

* more ecs stuff for entity storage

* ok well it compiles but

it definitely doesn't work

* random fixes

* change a bunch of entity things to use the components

* some ecs stuff in az-client

* packet handler uses the ecs now

and other fun changes

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

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

* start converting some functions in az-client into systems

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

* start splitting client

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

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

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

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

* packet handling

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

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

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

* start figuring out how functions in Client will work

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

* make writing packets work synchronously*

* huh az-client compiles

* start fixing stuff

* start fixing some packets

* make packet handler work

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

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

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

* fix warnings

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

* simple example for azalea-client

* 🐛

* maybe fix deadlock idk

can't test it rn lmao

* make physicsstate its own component

* use the default plugins

* azalea compiles lol

* use systemstate for packet handler

* fix entities

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

* physics (ticking) works

* try to add a .entity_by function

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

* try to make entity_by work

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

* everything compiles

* start converting swarm to use builder

* continue switching swarm to builder and fix stuff

* make swarm use builder

still have to fix some stuff and make client use builder

* fix death event

* client builder

* fix some warnings

* document plugins a bit

* start trying to fix tests

* azalea-ecs

* azalea-ecs stuff compiles

* az-physics tests pass 🎉

* fix all the tests

* clippy on azalea-ecs-macros

* remove now-unnecessary trait_upcasting feature

* fix some clippy::pedantic warnings lol

* why did cargo fmt not remove the trailing spaces

* FIX ALL THE THINGS

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

* start adding task pool

* fix entity deduplication

* fix pathfinder not stopping

* fix some more random bugs

* fix panic that sometimes happens in swarms

* make pathfinder run in task

* fix some tests

* fix doctests and clippy

* deadlock

* fix systems running in wrong order

* fix non-swarm bots

* make task_pool mod public

* move 'bot' into its own example

* move 'bot' into its own example (actually)

* reword readme a bit

* improve docs

* shut up clippy

* Small change

* It works! Probably!

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: mat <github@matdoes.dev>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nemo157 <github@nemo157.com>
Co-authored-by: mat <27899617+mat-1@users.noreply.github.com>
This commit is contained in:
EightFactorial 2023-02-05 14:45:17 -08:00 committed by GitHub
parent 13f1228ecd
commit 8d3ad63012
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
177 changed files with 20319 additions and 13834 deletions

970
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -13,9 +13,6 @@ A collection of Rust crates for making Minecraft bots, clients, and tools.
## ⚠️ Azalea is still very unfinished, though most crates are in a somewhat useable state
I named this Azalea because it sounds like a cool word and this is a cool library.
This project was heavily inspired by [PrismarineJS](https://github.com/PrismarineJS).
## Docs
The "stable" documentation is available at [docs.rs/azalea](https://docs.rs/azalea) and the unstable docs are at [azalea.matdoes.dev](https://azalea.matdoes.dev)
@ -26,7 +23,9 @@ If you'd like to chat about Azalea, you can join the Matrix space at [#azalea:ma
## Why
This project was heavily inspired by [PrismarineJS](https://github.com/PrismarineJS).
I wanted a fun excuse to do something cool with Rust, and I also felt like I could do better than [Mineflayer](https://github.com/prismarinejs/mineflayer) in some areas.
Also it's named Azalea because it sounds like a cool word and this is a cool library.
## Goals

12
azalea-auth/Cargo.toml Executable file → Normal file
View file

@ -9,8 +9,8 @@ version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-buf = {path = "../azalea-buf", version = "^0.5.0" }
azalea-crypto = {path = "../azalea-crypto", version = "^0.5.0" }
azalea-buf = {path = "../azalea-buf", version = "^0.5.0"}
azalea-crypto = {path = "../azalea-crypto", version = "^0.5.0"}
chrono = {version = "0.4.22", default-features = false}
log = "0.4.17"
num-bigint = "0.4.3"
@ -18,9 +18,9 @@ reqwest = {version = "0.11.12", features = ["json"]}
serde = {version = "1.0.145", features = ["derive"]}
serde_json = "1.0.86"
thiserror = "1.0.37"
tokio = {version = "1.21.2", features = ["fs"]}
uuid = "^1.1.2"
tokio = {version = "1.24.2", features = ["fs"]}
uuid = {version = "^1.1.2", features = ["serde"]}
[dev-dependencies]
env_logger = "0.9.1"
tokio = {version = "1.21.2", features = ["full"]}
env_logger = "0.9.3"
tokio = {version = "1.24.2", features = ["full"]}

View file

@ -2,4 +2,26 @@
A port of Mojang's Authlib and launcher authentication.
# Examples
```no_run
use std::path::PathBuf;
#[tokio::main]
async fn main() {
let cache_file = PathBuf::from("example_cache.json");
let auth_result = azalea_auth::auth(
"example@example.com",
azalea_auth::AuthOpts {
cache_file: Some(cache_file),
..Default::default()
},
)
.await
.unwrap();
println!("{auth_result:?}");
}
```
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).

View file

@ -10,6 +10,7 @@ use std::{
time::{Instant, SystemTime, UNIX_EPOCH},
};
use thiserror::Error;
use uuid::Uuid;
#[derive(Default)]
pub struct AuthOpts {
@ -209,8 +210,7 @@ pub struct GameOwnershipItem {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProfileResponse {
// todo: make the id a uuid
pub id: String,
pub id: Uuid,
pub name: String,
pub skins: Vec<serde_json::Value>,
pub capes: Vec<serde_json::Value>,
@ -257,8 +257,7 @@ async fn interactive_get_ms_auth_token(
log::trace!("Polling to check if user has logged in...");
if let Ok(access_token_response) = client
.post(format!(
"https://login.live.com/oauth20_token.srf?client_id={}",
CLIENT_ID
"https://login.live.com/oauth20_token.srf?client_id={CLIENT_ID}"
))
.form(&vec![
("client_id", CLIENT_ID),

View file

@ -1,10 +1,13 @@
use azalea_buf::McBuf;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(McBuf, Debug, Clone)]
#[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)]
pub struct GameProfile {
/// The UUID of the player.
pub uuid: Uuid,
/// The username of the player.
pub name: String,
pub properties: HashMap<String, ProfilePropertyValue>,
}
@ -19,8 +22,100 @@ impl GameProfile {
}
}
#[derive(McBuf, Debug, Clone)]
impl From<SerializableGameProfile> for GameProfile {
fn from(value: SerializableGameProfile) -> Self {
let mut properties = HashMap::new();
for value in value.properties {
properties.insert(
value.name,
ProfilePropertyValue {
value: value.value,
signature: value.signature,
},
);
}
Self {
uuid: value.id,
name: value.name,
properties,
}
}
}
#[derive(McBuf, Debug, Clone, Eq, PartialEq)]
pub struct ProfilePropertyValue {
pub value: String,
pub signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableGameProfile {
pub id: Uuid,
pub name: String,
pub properties: Vec<SerializableProfilePropertyValue>,
}
impl From<GameProfile> for SerializableGameProfile {
fn from(value: GameProfile) -> Self {
let mut properties = Vec::new();
for (key, value) in value.properties {
properties.push(SerializableProfilePropertyValue {
name: key,
value: value.value,
signature: value.signature,
});
}
Self {
id: value.uuid,
name: value.name,
properties,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableProfilePropertyValue {
pub name: String,
pub value: String,
pub signature: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_game_profile() {
let json = r#"{
"id": "f1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6",
"name": "Notch",
"properties": [
{
"name": "qwer",
"value": "asdf",
"signature": "zxcv"
}
]
}"#;
let profile =
GameProfile::from(serde_json::from_str::<SerializableGameProfile>(json).unwrap());
assert_eq!(
profile,
GameProfile {
uuid: Uuid::parse_str("f1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6").unwrap(),
name: "Notch".to_string(),
properties: {
let mut map = HashMap::new();
map.insert(
"qwer".to_string(),
ProfilePropertyValue {
value: "asdf".to_string(),
signature: Some("zxcv".to_string()),
},
);
map
},
}
);
}
}

View file

@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
mod auth;
mod cache;
pub mod game_profile;

View file

@ -1,11 +1,15 @@
//! Tell Mojang you're joining a multiplayer server.
use log::debug;
use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::json;
use thiserror::Error;
use uuid::Uuid;
use crate::game_profile::{GameProfile, SerializableGameProfile};
#[derive(Debug, Error)]
pub enum SessionServerError {
pub enum ClientSessionServerError {
#[error("Error sending HTTP request to sessionserver: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Multiplayer is not enabled for this account")]
@ -20,10 +24,24 @@ pub enum SessionServerError {
Unknown(String),
#[error("Forbidden operation (expired session?)")]
ForbiddenOperation,
#[error("RateLimiter disallowed request")]
RateLimited,
#[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
UnexpectedResponse { status_code: u16, body: String },
}
#[derive(Debug, Error)]
pub enum ServerSessionServerError {
#[error("Error sending HTTP request to sessionserver: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Invalid or expired session")]
InvalidSession,
#[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
UnexpectedResponse { status_code: u16, body: String },
#[error("Unknown sessionserver error: {0}")]
Unknown(String),
}
#[derive(Deserialize)]
pub struct ForbiddenError {
pub error: String,
@ -39,7 +57,7 @@ pub async fn join(
private_key: &[u8],
uuid: &Uuid,
server_id: &str,
) -> Result<(), SessionServerError> {
) -> Result<(), ClientSessionServerError> {
let client = reqwest::Client::new();
let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
@ -63,28 +81,83 @@ pub async fn join(
.await?;
match res.status() {
reqwest::StatusCode::NO_CONTENT => Ok(()),
reqwest::StatusCode::FORBIDDEN => {
StatusCode::NO_CONTENT => Ok(()),
StatusCode::FORBIDDEN => {
let forbidden = res.json::<ForbiddenError>().await?;
match forbidden.error.as_str() {
"InsufficientPrivilegesException" => Err(SessionServerError::MultiplayerDisabled),
"UserBannedException" => Err(SessionServerError::Banned),
"AuthenticationUnavailableException" => {
Err(SessionServerError::AuthServersUnreachable)
"InsufficientPrivilegesException" => {
Err(ClientSessionServerError::MultiplayerDisabled)
}
"InvalidCredentialsException" => Err(SessionServerError::InvalidSession),
"ForbiddenOperationException" => Err(SessionServerError::ForbiddenOperation),
_ => Err(SessionServerError::Unknown(forbidden.error)),
"UserBannedException" => Err(ClientSessionServerError::Banned),
"AuthenticationUnavailableException" => {
Err(ClientSessionServerError::AuthServersUnreachable)
}
"InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession),
"ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation),
_ => Err(ClientSessionServerError::Unknown(forbidden.error)),
}
}
StatusCode::TOO_MANY_REQUESTS => Err(ClientSessionServerError::RateLimited),
status_code => {
// log the headers
log::debug!("Error headers: {:#?}", res.headers());
debug!("Error headers: {:#?}", res.headers());
let body = res.text().await?;
Err(SessionServerError::UnexpectedResponse {
Err(ClientSessionServerError::UnexpectedResponse {
status_code: status_code.as_u16(),
body,
})
}
}
}
/// Ask Mojang's servers if the player joining is authenticated.
/// Included in the reply is the player's skin and cape.
/// The IP field is optional and equivalent to enabling
/// 'prevent-proxy-connections' in server.properties
pub async fn serverside_auth(
username: &str,
public_key: &[u8],
private_key: &[u8; 16],
ip: Option<&str>,
) -> Result<GameProfile, ServerSessionServerError> {
let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
"".as_bytes(),
public_key,
private_key,
));
let url = reqwest::Url::parse_with_params(
"https://sessionserver.mojang.com/session/minecraft/hasJoined",
if let Some(ip) = ip {
vec![("username", username), ("serverId", &hash), ("ip", ip)]
} else {
vec![("username", username), ("serverId", &hash)]
},
)
.expect("URL should always be valid");
let res = reqwest::get(url).await?;
match res.status() {
StatusCode::OK => {}
StatusCode::NO_CONTENT => {
return Err(ServerSessionServerError::InvalidSession);
}
StatusCode::FORBIDDEN => {
return Err(ServerSessionServerError::Unknown(
res.json::<ForbiddenError>().await?.error,
))
}
status_code => {
// log the headers
debug!("Error headers: {:#?}", res.headers());
let body = res.text().await?;
return Err(ServerSessionServerError::UnexpectedResponse {
status_code: status_code.as_u16(),
body,
});
}
};
Ok(res.json::<SerializableGameProfile>().await?.into())
}

View file

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

View file

@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
mod behavior;
mod blocks;

View file

@ -1,3 +1,7 @@
# Azalea Brigadier
A Rust port of Mojang's [Brigadier](https://github.com/Mojang/brigadier) command parsing and dispatching library.
# Examples
See the [tests](https://github.com/mat-1/azalea/tree/main/azalea-brigadier/tests).

View file

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

View file

@ -83,17 +83,12 @@ impl fmt::Debug for BuiltInExceptions {
write!(f, "Unclosed quoted string")
}
BuiltInExceptions::ReaderInvalidEscape { character } => {
write!(
f,
"Invalid escape sequence '{}' in quoted string",
character
)
write!(f, "Invalid escape sequence '{character}' in quoted string")
}
BuiltInExceptions::ReaderInvalidBool { value } => {
write!(
f,
"Invalid bool, expected true or false but found '{}'",
value
"Invalid bool, expected true or false but found '{value}'"
)
}
BuiltInExceptions::ReaderInvalidInt { value } => {

View file

@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
pub mod arguments;
pub mod builder;
pub mod command_dispatcher;

View file

@ -77,7 +77,7 @@ impl StringReader {
}
pub fn is_allowed_number(c: char) -> bool {
('0'..='9').contains(&c) || c == '.' || c == '-'
c.is_ascii_digit() || c == '.' || c == '-'
}
pub fn is_quoted_string_start(c: char) -> bool {
@ -175,9 +175,9 @@ impl StringReader {
}
pub fn is_allowed_in_unquoted_string(c: char) -> bool {
('0'..='9').contains(&c)
|| ('A'..='Z').contains(&c)
|| ('a'..='z').contains(&c)
c.is_ascii_digit()
|| c.is_ascii_uppercase()
|| c.is_ascii_lowercase()
|| c == '_'
|| c == '-'
|| c == '.'

View file

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

View file

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

View file

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

View file

@ -9,11 +9,12 @@ version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-buf-macros = {path = "./azalea-buf-macros", version = "^0.5.0"}
azalea-buf-macros = {path = "./azalea-buf-macros", version = "^0.5.0" }
byteorder = "^1.4.3"
log = "0.4.17"
serde_json = {version = "^1.0", optional = true}
thiserror = "^1.0.34"
tokio = {version = "^1.21.2", features = ["io-util", "net", "macros"]}
thiserror = "1.0.37"
tokio = {version = "^1.24.2", features = ["io-util", "net", "macros"]}
uuid = "^1.1.2"
[features]

View file

@ -2,4 +2,4 @@
An implementation of Minecraft's FriendlyByteBuf. This is used frequently in the game for serialization and deserialization of data.
Note that there are some minor implementation differences such as using unsigned integers in places where Minecraft uses signed integers. This doesn't cause issues normally, but does technically make usage of azalea-buf detectable if a server really wants to since it won't error in places where vanilla Minecraft would.
Note that there are some minor implementation differences such as using unsigned integers in places where Minecraft uses signed integers. This doesn't cause issues normally, but does technically make usage of azalea-buf detectable if a server really wants to since it won't error in places where vanilla Minecraft would.

View file

@ -1,294 +1,30 @@
mod read;
mod write;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{self, parse_macro_input, Data, DeriveInput, FieldsNamed, Ident};
fn create_impl_mcbufreadable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let FieldsNamed { named, .. } = match fields {
syn::Fields::Named(f) => f,
_ => panic!("#[derive(McBuf)] can only be used on structs with named fields"),
};
let read_fields = named
.iter()
.map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if f.attrs.iter().any(|a| a.path.is_ident("var")) {
quote! {
let #field_name = azalea_buf::McBufVarReadable::var_read_from(buf)?;
}
} else {
quote! {
let #field_name = azalea_buf::McBufReadable::read_from(buf)?;
}
}
}
_ => panic!(
"Error reading field {}: {}",
field_name.clone().unwrap(),
field_type.to_token_stream()
),
}
})
.collect::<Vec<_>>();
let read_field_names = named.iter().map(|f| &f.ident).collect::<Vec<_>>();
quote! {
impl azalea_buf::McBufReadable for #ident {
fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
#(#read_fields)*
Ok(#ident {
#(#read_field_names: #read_field_names),*
})
}
}
}
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let mut match_contents = quote!();
let mut variant_discrim: u32 = 0;
let mut first = true;
let mut first_reader = None;
for variant in variants {
let variant_name = &variant.ident;
match &variant.discriminant.as_ref() {
Some(d) => {
variant_discrim = match &d.1 {
syn::Expr::Lit(e) => match &e.lit {
syn::Lit::Int(i) => i.base10_parse().unwrap(),
_ => panic!("Error parsing enum discriminant as int"),
},
syn::Expr::Unary(_) => {
panic!("Negative enum discriminants are not supported")
}
_ => {
panic!("Error parsing enum discriminant as literal (is {:?})", d.1)
}
}
}
None => {
if !first {
variant_discrim += 1;
}
}
}
let reader = match variant.fields {
syn::Fields::Named(_) => {
panic!("writing named fields in enums is not supported")
}
syn::Fields::Unnamed(_) => quote! {
Ok(Self::#variant_name(azalea_buf::McBufReadable::read_from(buf)?))
},
syn::Fields::Unit => quote! {
Ok(Self::#variant_name)
},
};
if first {
first_reader = Some(reader.clone());
first = false;
};
match_contents.extend(quote! {
#variant_discrim => {
#reader
},
});
}
let first_reader = first_reader.expect("There should be at least one variant");
quote! {
impl azalea_buf::McBufReadable for #ident {
fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result<Self, azalea_buf::BufReadError> {
let id = azalea_buf::McBufVarReadable::var_read_from(buf)?;
Self::read_from_id(buf, id)
}
}
impl #ident {
pub fn read_from_id(buf: &mut std::io::Cursor<&[u8]>, id: u32) -> Result<Self, azalea_buf::BufReadError> {
match id {
#match_contents
// you'd THINK this throws an error, but mojang decided to make it default for some reason
_ => #first_reader
}
}
}
}
}
_ => panic!("#[derive(McBuf)] can only be used on structs"),
}
}
fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let FieldsNamed { named, .. } = match fields {
syn::Fields::Named(f) => f,
_ => panic!("#[derive(McBuf)] can only be used on structs with named fields"),
};
let write_fields = named
.iter()
.map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if f.attrs.iter().any(|attr| attr.path.is_ident("var")) {
quote! {
azalea_buf::McBufVarWritable::var_write_into(&self.#field_name, buf)?;
}
} else {
quote! {
azalea_buf::McBufWritable::write_into(&self.#field_name, buf)?;
}
}
}
_ => panic!(
"Error writing field {}: {}",
field_name.clone().unwrap(),
field_type.to_token_stream()
),
}
})
.collect::<Vec<_>>();
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
#(#write_fields)*
Ok(())
}
}
}
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
// remember whether it's a data variant so we can do an optimization later
let mut is_data_enum = false;
let mut match_arms = quote!();
let mut match_arms_without_id = quote!();
let mut variant_discrim: u32 = 0;
let mut first = true;
for variant in variants {
match &variant.discriminant.as_ref() {
Some(d) => {
variant_discrim = match &d.1 {
syn::Expr::Lit(e) => match &e.lit {
syn::Lit::Int(i) => i.base10_parse().unwrap(),
_ => panic!("Error parsing enum discriminant as int"),
},
syn::Expr::Unary(_) => {
panic!("Negative enum discriminants are not supported")
}
_ => {
panic!("Error parsing enum discriminant as literal (is {:?})", d.1)
}
}
}
None => {
if first {
first = false;
} else {
variant_discrim += 1;
}
}
}
match &variant.fields {
syn::Fields::Named(_) => {
panic!("Enum variants with named fields are not supported yet");
}
syn::Fields::Unit => {
let variant_name = &variant.ident;
match_arms.extend(quote! {
Self::#variant_name => {
azalea_buf::McBufVarWritable::var_write_into(&#variant_discrim, buf)?;
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name => {}
});
}
syn::Fields::Unnamed(_) => {
is_data_enum = true;
let variant_name = &variant.ident;
match_arms.extend(quote! {
Self::#variant_name(data) => {
azalea_buf::McBufVarWritable::var_write_into(&#variant_discrim, buf)?;
azalea_buf::McBufWritable::write_into(data, buf)?;
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name(data) => {
azalea_buf::McBufWritable::write_into(data, buf)?;
}
});
}
}
}
if is_data_enum {
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
match self {
#match_arms
}
Ok(())
}
}
impl #ident {
pub fn write_without_id(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
match self {
#match_arms_without_id
}
Ok(())
}
}
}
} else {
// optimization: if it doesn't have data we can just do `as u32`
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
azalea_buf::McBufVarWritable::var_write_into(&(*self as u32), buf)
}
}
}
}
}
_ => panic!("#[derive(McBuf)] can only be used on structs"),
}
}
use quote::quote;
use syn::{self, parse_macro_input, DeriveInput};
#[proc_macro_derive(McBufReadable, attributes(var))]
pub fn derive_mcbufreadable(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
create_impl_mcbufreadable(&ident, &data).into()
read::create_impl_mcbufreadable(&ident, &data).into()
}
#[proc_macro_derive(McBufWritable, attributes(var))]
pub fn derive_mcbufwritable(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
create_impl_mcbufwritable(&ident, &data).into()
write::create_impl_mcbufwritable(&ident, &data).into()
}
#[proc_macro_derive(McBuf, attributes(var))]
pub fn derive_mcbuf(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
let writable = create_impl_mcbufwritable(&ident, &data);
let readable = create_impl_mcbufreadable(&ident, &data);
let writable = write::create_impl_mcbufwritable(&ident, &data);
let readable = read::create_impl_mcbufreadable(&ident, &data);
quote! {
#writable
#readable

View file

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

View file

@ -0,0 +1,192 @@
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{self, punctuated::Punctuated, token::Comma, Data, Field, FieldsNamed, Ident};
fn write_named_fields(
named: &Punctuated<Field, Comma>,
ident_name: Option<&Ident>,
) -> proc_macro2::TokenStream {
let write_fields = named.iter().map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
let ident_dot_field = match ident_name {
Some(ident) => quote! { &#ident.#field_name },
None => quote! { #field_name },
};
// do a different buf.write_* for each field depending on the type
// if it's a string, use buf.write_string
match field_type {
syn::Type::Path(_) | syn::Type::Array(_) => {
if f.attrs.iter().any(|attr| attr.path.is_ident("var")) {
quote! {
azalea_buf::McBufVarWritable::var_write_into(#ident_dot_field, buf)?;
}
} else {
quote! {
azalea_buf::McBufWritable::write_into(#ident_dot_field, buf)?;
}
}
}
_ => panic!(
"Error writing field {}: {}",
field_name.clone().unwrap(),
field_type.to_token_stream()
),
}
});
quote! { #(#write_fields)* }
}
pub fn create_impl_mcbufwritable(ident: &Ident, data: &Data) -> proc_macro2::TokenStream {
match data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => {
let syn::Fields::Named(FieldsNamed { named, .. }) = fields else {
panic!("#[derive(McBuf)] can only be used on structs with named fields")
};
let write_fields =
write_named_fields(named, Some(&Ident::new("self", Span::call_site())));
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
#write_fields
Ok(())
}
}
}
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
// remember whether it's a data variant so we can do an optimization later
let mut is_data_enum = false;
let mut match_arms = quote!();
let mut match_arms_without_id = quote!();
let mut variant_discrim: u32 = 0;
let mut first = true;
for variant in variants {
match &variant.discriminant.as_ref() {
Some(d) => {
variant_discrim = match &d.1 {
syn::Expr::Lit(e) => match &e.lit {
syn::Lit::Int(i) => i.base10_parse().unwrap(),
// syn::Lit::Str(s) => s.value(),
_ => panic!("Error parsing enum discriminant as int (is {e:?})"),
},
syn::Expr::Unary(_) => {
panic!("Negative enum discriminants are not supported")
}
_ => {
panic!("Error parsing enum discriminant as literal (is {:?})", d.1)
}
}
}
None => {
if first {
first = false;
} else {
variant_discrim += 1;
}
}
}
let variant_name = &variant.ident;
// the variant number that we're going to write
let write_the_variant = quote! {
azalea_buf::McBufVarWritable::var_write_into(&#variant_discrim, buf)?;
};
match &variant.fields {
syn::Fields::Named(f) => {
is_data_enum = true;
let field_names = f
.named
.iter()
.map(|f| f.ident.clone().unwrap())
.collect::<Vec<_>>();
let write_fields = write_named_fields(&f.named, None);
match_arms.extend(quote! {
Self::#variant_name { #(#field_names),* } => {
#write_the_variant
#write_fields
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name { #(#field_names),* } => {
#write_fields
}
});
}
syn::Fields::Unit => {
match_arms.extend(quote! {
Self::#variant_name => {
#write_the_variant
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name => {}
});
}
syn::Fields::Unnamed(fields) => {
is_data_enum = true;
let mut writers_code = quote! {};
let mut params_code = quote! {};
for (i, f) in fields.unnamed.iter().enumerate() {
let param_ident = Ident::new(&format!("data{i}"), Span::call_site());
params_code.extend(quote! { #param_ident, });
if f.attrs.iter().any(|attr| attr.path.is_ident("var")) {
writers_code.extend(quote! {
azalea_buf::McBufVarWritable::var_write_into(#param_ident, buf)?;
});
} else {
writers_code.extend(quote! {
azalea_buf::McBufWritable::write_into(#param_ident, buf)?;
});
}
}
match_arms.extend(quote! {
Self::#variant_name(#params_code) => {
#write_the_variant
#writers_code
}
});
match_arms_without_id.extend(quote! {
Self::#variant_name(data) => {
azalea_buf::McBufWritable::write_into(data, buf)?;
}
});
}
}
}
if is_data_enum {
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
match self {
#match_arms
}
Ok(())
}
}
impl #ident {
pub fn write_without_id(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
match self {
#match_arms_without_id
}
Ok(())
}
}
}
} else {
// optimization: if it doesn't have data we can just do `as u32`
quote! {
impl azalea_buf::McBufWritable for #ident {
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
azalea_buf::McBufVarWritable::var_write_into(&(*self as u32), buf)
}
}
}
}
}
_ => panic!("#[derive(McBuf)] can only be used on structs"),
}
}

View file

@ -1,5 +1,4 @@
//! Utilities for reading and writing for the Minecraft protocol
#![doc = include_str!("../README.md")]
#![feature(min_specialization)]
// these two are necessary for thiserror backtraces
#![feature(error_generic_member_access)]
@ -142,6 +141,21 @@ mod tests {
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 7178);
}
#[test]
fn test_write_varlong() {
let mut buf = Vec::new();
0u64.var_write_into(&mut buf).unwrap();
assert_eq!(buf, vec![0]);
let mut buf = Vec::new();
1u64.var_write_into(&mut buf).unwrap();
assert_eq!(buf, vec![1]);
let mut buf = Vec::new();
9223372036854775807u64.var_write_into(&mut buf).unwrap();
assert_eq!(buf, vec![255, 255, 255, 255, 255, 255, 255, 255, 127]);
}
#[test]
fn test_list() {
let original_vec = vec!["a".to_string(), "bc".to_string(), "def".to_string()];

View file

@ -1,6 +1,8 @@
use super::{UnsizedByteArray, MAX_STRING_LENGTH};
use byteorder::{ReadBytesExt, BE};
use log::warn;
use std::{
backtrace::Backtrace,
collections::HashMap,
hash::Hash,
io::{Cursor, Read},
@ -17,14 +19,18 @@ pub enum BufReadError {
CouldNotReadBytes,
#[error("The received encoded string buffer length is longer than maximum allowed ({length} > {max_length})")]
StringLengthTooLong { length: u32, max_length: u32 },
#[error("{0}")]
Io(
#[error("{source}")]
Io {
#[from]
#[backtrace]
std::io::Error,
),
#[error("Invalid UTF-8")]
InvalidUtf8,
source: std::io::Error,
},
#[error("Invalid UTF-8: {bytes:?} (lossy: {lossy:?})")]
InvalidUtf8 {
bytes: Vec<u8>,
lossy: String,
// backtrace: Backtrace,
},
#[error("Unexpected enum variant {id}")]
UnexpectedEnumVariant { id: i32 },
#[error("Unexpected enum variant {id}")]
@ -33,12 +39,17 @@ pub enum BufReadError {
UnexpectedEof {
attempted_read: usize,
actual_read: usize,
backtrace: Backtrace,
},
#[error("{0}")]
Custom(String),
#[cfg(feature = "serde_json")]
#[error("{0}")]
Deserialization(#[from] serde_json::Error),
#[error("{source}")]
Deserialization {
#[from]
#[backtrace]
source: serde_json::Error,
},
}
fn read_bytes<'a>(buf: &'a mut Cursor<&[u8]>, length: usize) -> Result<&'a [u8], BufReadError> {
@ -46,6 +57,7 @@ fn read_bytes<'a>(buf: &'a mut Cursor<&[u8]>, length: usize) -> Result<&'a [u8],
return Err(BufReadError::UnexpectedEof {
attempted_read: length,
actual_read: buf.get_ref().len() - buf.position() as usize,
backtrace: Backtrace::capture(),
});
}
let initial_position = buf.position() as usize;
@ -66,7 +78,11 @@ fn read_utf_with_len(buf: &mut Cursor<&[u8]>, max_length: u32) -> Result<String,
let buffer = read_bytes(buf, length as usize)?;
let string = std::str::from_utf8(buffer)
.map_err(|_| BufReadError::InvalidUtf8)?
.map_err(|_| BufReadError::InvalidUtf8 {
bytes: buffer.to_vec(),
lossy: String::from_utf8_lossy(buffer).to_string(),
// backtrace: Backtrace::capture(),
})?
.to_string();
if string.len() > length as usize {
return Err(BufReadError::StringLengthTooLong { length, max_length });
@ -266,7 +282,11 @@ impl McBufReadable for u64 {
impl McBufReadable for bool {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Ok(u8::read_from(buf)? != 0)
let byte = u8::read_from(buf)?;
if byte > 1 {
warn!("Boolean value was not 0 or 1, but {}", byte);
}
Ok(byte != 0)
}
}

View file

@ -132,15 +132,16 @@ impl McBufVarWritable for i64 {
fn var_write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
let mut buffer = [0];
let mut value = *self;
if value == 0 {
buf.write_all(&buffer).unwrap();
}
while value != 0 {
buffer[0] = (value & 0b0111_1111) as u8;
value = (value >> 7) & (i64::max_value() >> 6);
if value != 0 {
buffer[0] |= 0b1000_0000;
}
// this only writes a single byte, so write_all isn't necessary
// the let _ = is so clippy doesn't complain
let _ = buf.write(&buffer)?;
buf.write_all(&buffer)?;
}
Ok(())
}

View file

@ -8,11 +8,14 @@ version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["azalea-buf"]
[dependencies]
azalea-buf = {path = "../azalea-buf", features = [
"serde_json",
], version = "^0.5.0"}
azalea-language = {path = "../azalea-language", version = "^0.5.0"}
azalea-buf = { path = "../azalea-buf", features = [
"serde_json",
], version = "^0.5.0", optional = true }
azalea-language = { path = "../azalea-language", version = "^0.5.0" }
log = "0.4.17"
once_cell = "1.16.0"
serde = {version = "^1.0.148", features = ["derive"]}

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
//! Things for working with Minecraft chat messages.
//! This was inspired by Minecraft and prismarine-chat.
#![doc = include_str!("../README.md")]
pub mod base_component;
mod component;
@ -7,4 +6,4 @@ pub mod style;
pub mod text_component;
pub mod translatable_component;
pub use component::Component;
pub use component::FormattedText;

View file

@ -1,5 +1,6 @@
use std::{collections::HashMap, fmt};
#[cfg(feature = "azalea-buf")]
use azalea_buf::McBuf;
use once_cell::sync::Lazy;
use serde::{ser::SerializeStruct, Serialize, Serializer};
@ -86,7 +87,8 @@ impl Ansi {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, McBuf)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "azalea-buf", derive(McBuf))]
pub enum ChatFormatting {
Black,
DarkBlue,

View file

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

View file

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

View file

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

View file

@ -11,20 +11,30 @@ version = "0.5.0"
[dependencies]
anyhow = "1.0.59"
async-trait = "0.1.58"
azalea-auth = {path = "../azalea-auth", version = "0.5.0" }
azalea-block = {path = "../azalea-block", version = "0.5.0" }
azalea-chat = {path = "../azalea-chat", version = "0.5.0" }
azalea-core = {path = "../azalea-core", version = "0.5.0" }
azalea-crypto = {path = "../azalea-crypto", version = "0.5.0" }
azalea-physics = {path = "../azalea-physics", version = "0.5.0" }
azalea-protocol = {path = "../azalea-protocol", version = "0.5.0" }
azalea-world = {path = "../azalea-world", version = "0.5.0" }
azalea-auth = {path = "../azalea-auth", version = "0.5.0"}
azalea-block = {path = "../azalea-block", version = "0.5.0"}
azalea-chat = {path = "../azalea-chat", version = "0.5.0"}
azalea-core = {path = "../azalea-core", version = "0.5.0"}
azalea-crypto = {path = "../azalea-crypto", version = "0.5.0"}
azalea-ecs = {path = "../azalea-ecs", version = "0.5.0"}
azalea-physics = {path = "../azalea-physics", version = "0.5.0"}
azalea-protocol = {path = "../azalea-protocol", version = "0.5.0"}
azalea-registry = {path = "../azalea-registry", version = "0.5.0"}
azalea-world = {path = "../azalea-world", version = "0.5.0"}
bevy_tasks = "0.9.1"
bevy_time = "0.9.1"
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
futures = "0.3.25"
iyes_loopless = "0.9.1"
log = "0.4.17"
nohash-hasher = "0.2.0"
once_cell = "1.16.0"
parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]}
regex = "1.7.0"
thiserror = "^1.0.34"
tokio = {version = "^1.21.2", features = ["sync"]}
typemap_rev = "0.2.0"
tokio = {version = "^1.24.2", features = ["sync"]}
typemap_rev = "0.3.0"
uuid = "^1.1.2"
[dev-dependencies]
env_logger = "0.9.1"

View file

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

View file

@ -8,7 +8,8 @@ use uuid::Uuid;
/// Something that can join Minecraft servers.
///
/// To join a server using this account, use [`crate::Client::join`].
/// To join a server using this account, use [`Client::join`] or
/// [`azalea::ClientBuilder`].
///
/// # Examples
///
@ -21,6 +22,9 @@ use uuid::Uuid;
/// // or Account::offline("example");
/// # }
/// ```
///
/// [`Client::join`]: crate::Client::join
/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html
#[derive(Clone, Debug)]
pub struct Account {
/// The Minecraft username of the account.
@ -28,10 +32,10 @@ pub struct Account {
/// The access token for authentication. You can obtain one of these
/// manually from azalea-auth.
///
/// This is an Arc<Mutex> so it can be modified by [`Self::refresh`].
/// This is an `Arc<Mutex>` so it can be modified by [`Self::refresh`].
pub access_token: Option<Arc<Mutex<String>>>,
/// Only required for online-mode accounts.
pub uuid: Option<uuid::Uuid>,
pub uuid: Option<Uuid>,
/// The parameters (i.e. email) that were passed for creating this
/// [`Account`]. This is used to for automatic reauthentication when we get
@ -85,7 +89,7 @@ impl Account {
Ok(Self {
username: auth_result.profile.name,
access_token: Some(Arc::new(Mutex::new(auth_result.access_token))),
uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")),
uuid: Some(auth_result.profile.id),
auth_opts: AuthOpts::Microsoft {
email: email.to_string(),
},

View file

@ -1,7 +1,6 @@
//! Implementations of chat-related features.
use crate::Client;
use azalea_chat::Component;
use azalea_chat::FormattedText;
use azalea_crypto::MessageSignature;
use azalea_protocol::packets::game::{
clientbound_player_chat_packet::{ClientboundPlayerChatPacket, LastSeenMessagesUpdate},
@ -13,6 +12,9 @@ use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use uuid::Uuid;
use crate::client::Client;
/// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone, PartialEq)]
@ -30,7 +32,7 @@ macro_rules! regex {
impl ChatPacket {
/// Get the message shown in chat for this packet.
pub fn message(&self) -> Component {
pub fn message(&self) -> FormattedText {
match self {
ChatPacket::System(p) => p.content.clone(),
ChatPacket::Player(p) => p.message(false),
@ -73,6 +75,16 @@ impl ChatPacket {
self.split_sender_and_content().0
}
/// Get the UUID of the sender of the message. If it's not a
/// player-sent chat message, this will be None (this is sometimes the case
/// when a server uses a plugin to modify chat messages).
pub fn uuid(&self) -> Option<Uuid> {
match self {
ChatPacket::System(_) => None,
ChatPacket::Player(m) => Some(m.message.signed_header.sender),
}
}
/// Get the content part of the message as a string. This does not preserve
/// formatting codes. If it's not a player-sent chat message or the sender
/// couldn't be determined, this will contain the entire message.
@ -84,7 +96,7 @@ impl ChatPacket {
/// convenience function for testing.
pub fn new(message: &str) -> Self {
ChatPacket::System(Arc::new(ClientboundSystemChatPacket {
content: Component::from(message),
content: FormattedText::from(message),
overlay: false,
}))
}
@ -95,7 +107,7 @@ impl Client {
/// not the command packet. The [`Client::chat`] function handles checking
/// whether the message is a command and using the proper packet for you,
/// so you should use that instead.
pub async fn send_chat_packet(&self, message: &str) -> Result<(), std::io::Error> {
pub fn send_chat_packet(&self, message: &str) {
// TODO: chat signing
let signature = sign_message();
let packet = ServerboundChatPacket {
@ -112,12 +124,12 @@ impl Client {
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get();
self.write_packet(packet).await
self.write_packet(packet);
}
/// Send a command packet to the server. The `command` argument should not
/// include the slash at the front.
pub async fn send_command_packet(&self, command: &str) -> Result<(), std::io::Error> {
pub fn send_command_packet(&self, command: &str) {
// TODO: chat signing
let packet = ServerboundChatCommandPacket {
command: command.to_string(),
@ -133,7 +145,7 @@ impl Client {
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get();
self.write_packet(packet).await
self.write_packet(packet);
}
/// Send a message in chat.
@ -141,15 +153,15 @@ impl Client {
/// ```rust,no_run
/// # use azalea_client::{Client, Event};
/// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> {
/// bot.chat("Hello, world!").await.unwrap();
/// bot.chat("Hello, world!");
/// # Ok(())
/// # }
/// ```
pub async fn chat(&self, message: &str) -> Result<(), std::io::Error> {
pub fn chat(&self, message: &str) {
if let Some(command) = message.strip_prefix('/') {
self.send_command_packet(command).await
self.send_command_packet(command);
} else {
self.send_chat_packet(message).await
self.send_chat_packet(message);
}
}

File diff suppressed because it is too large Load diff

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ pub enum PingError {
#[error("{0}")]
Connection(#[from] ConnectionError),
#[error("{0}")]
ReadPacket(#[from] azalea_protocol::read::ReadPacketError),
ReadPacket(#[from] Box<azalea_protocol::read::ReadPacketError>),
#[error("{0}")]
WritePacket(#[from] io::Error),
#[error("The given address could not be parsed into a ServerAddress")]

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
# Azalea Core
Miscellaneous things in Azalea.
Random miscellaneous things like `bitsets` and `Vec3` that don't deserve their own crate.

View file

@ -1,4 +1,6 @@
use azalea_buf::McBuf;
use std::io::{Cursor, Write};
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
/// Represents Java's BitSet, a list of bits.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, McBuf)]
@ -23,9 +25,7 @@ impl BitSet {
fn check_range(&self, from_index: usize, to_index: usize) {
assert!(
from_index <= to_index,
"fromIndex: {} > toIndex: {}",
from_index,
to_index
"fromIndex: {from_index} > toIndex: {to_index}",
);
}
@ -108,6 +108,82 @@ impl BitSet {
}
}
impl From<Vec<u64>> for BitSet {
fn from(data: Vec<u64>) -> Self {
BitSet { data }
}
}
impl From<Vec<u8>> for BitSet {
fn from(data: Vec<u8>) -> Self {
let mut words = vec![0; data.len().div_ceil(8)];
for (i, byte) in data.iter().enumerate() {
words[i / 8] |= (*byte as u64) << ((i % 8) * 8);
}
BitSet { data: words }
}
}
/// A list of bits with a known fixed size.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FixedBitSet<const N: usize>
where
[(); N.div_ceil(8)]: Sized,
{
data: [u8; N.div_ceil(8)],
}
impl<const N: usize> FixedBitSet<N>
where
[u8; N.div_ceil(8)]: Sized,
{
pub fn new() -> Self {
FixedBitSet {
data: [0; N.div_ceil(8)],
}
}
pub fn index(&self, index: usize) -> bool {
(self.data[index / 8] & (1u8 << (index % 8))) != 0
}
pub fn set(&mut self, bit_index: usize) {
self.data[bit_index / 8] |= 1u8 << (bit_index % 8);
}
}
impl<const N: usize> McBufReadable for FixedBitSet<N>
where
[u8; N.div_ceil(8)]: Sized,
{
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let mut data = [0; N.div_ceil(8)];
for item in data.iter_mut().take(N.div_ceil(8)) {
*item = u8::read_from(buf)?;
}
Ok(FixedBitSet { data })
}
}
impl<const N: usize> McBufWritable for FixedBitSet<N>
where
[u8; N.div_ceil(8)]: Sized,
{
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
for i in 0..N.div_ceil(8) {
self.data[i].write_into(buf)?;
}
Ok(())
}
}
impl<const N: usize> Default for FixedBitSet<N>
where
[u8; N.div_ceil(8)]: Sized,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

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

View file

@ -1,6 +1,7 @@
//! Random miscellaneous things like UUIDs that don't deserve their own crate.
#![doc = include_str!("../README.md")]
#![feature(int_roundings)]
#![allow(incomplete_features)]
#![feature(generic_const_exprs)]
mod difficulty;
pub use difficulty::*;

View file

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

View file

@ -1,5 +1,5 @@
use crate::ResourceLocation;
use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use std::{
io::{Cursor, Write},
ops::{Add, AddAssign, Mul, Rem, Sub},
@ -109,7 +109,9 @@ macro_rules! vec3_impl {
};
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
/// Used to represent an exact position in the world where an entity could be.
/// For blocks, [`BlockPos`] is used instead.
#[derive(Clone, Copy, Debug, Default, PartialEq, McBuf)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
@ -117,6 +119,8 @@ pub struct Vec3 {
}
vec3_impl!(Vec3, f64);
/// The coordinates of a block in the world. For entities (if the coordinate
/// with decimals), use [`Vec3`] instead.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct BlockPos {
pub x: i32,
@ -137,6 +141,8 @@ impl BlockPos {
}
}
/// Chunk coordinates are used to represent where a chunk is in the world. You
/// can convert the x and z to block coordinates by multiplying them by 16.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ChunkPos {
pub x: i32,
@ -270,12 +276,22 @@ impl From<&Vec3> for BlockPos {
}
}
}
impl From<Vec3> for BlockPos {
fn from(pos: Vec3) -> Self {
BlockPos::from(&pos)
}
}
impl From<&Vec3> for ChunkPos {
fn from(pos: &Vec3) -> Self {
ChunkPos::from(&BlockPos::from(pos))
}
}
impl From<Vec3> for ChunkPos {
fn from(pos: Vec3) -> Self {
ChunkPos::from(&pos)
}
}
const PACKED_X_LENGTH: u64 = 1 + 25; // minecraft does something a bit more complicated to get this 25
const PACKED_Z_LENGTH: u64 = PACKED_X_LENGTH;

View file

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

View file

@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
mod signing;
use aes::cipher::inout::InOutBuf;

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -2,3 +2,8 @@
Translate Minecraft strings from their id.
# Examples
```
assert_eq!(azalea_language::get("translation.test.none"), Some("Hello, world!"));
```

View file

@ -1,4 +1,4 @@
//! Translate Minecraft strings from their id.
#![doc = include_str!("../README.md")]
use once_cell::sync::Lazy;
use std::collections::HashMap;
@ -9,13 +9,3 @@ pub static STORAGE: Lazy<HashMap<String, String>> =
pub fn get(key: &str) -> Option<&str> {
STORAGE.get(key).map(|s| s.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get() {
assert_eq!(get("translation.test.none"), Some("Hello, world!"));
}
}

View file

@ -1,3 +1,26 @@
# Azalea NBT
A fast NBT serializer and deserializer.
# Examples
```
use ahash::AHashMap;
use azalea_nbt::Tag;
use std::{io::{Cursor, Read}, fs::File};
let mut file = File::open("tests/hello_world.nbt").unwrap();
let mut buf = vec![];
file.read_to_end(&mut buf).unwrap();
let tag = Tag::read(&mut Cursor::new(&buf[..])).unwrap();
assert_eq!(
tag,
Tag::Compound(AHashMap::from_iter(vec![(
"hello world".to_string(),
Tag::Compound(AHashMap::from_iter(vec![(
"name".to_string(),
Tag::String("Bananrama".to_string()),
)]))
)]))
);
```

View file

@ -35,6 +35,8 @@ fn read_string(stream: &mut Cursor<&[u8]>) -> Result<String, Error> {
}
impl Tag {
/// Read the NBT data when you already know the ID of the tag. You usually
/// want [`Tag::read`] if you're reading an NBT file.
#[inline]
fn read_known(stream: &mut Cursor<&[u8]>, id: u8) -> Result<Tag, Error> {
Ok(match id {
@ -129,6 +131,7 @@ impl Tag {
})
}
/// Read the NBT data. This will return a compound tag with a single item.
pub fn read(stream: &mut Cursor<&[u8]>) -> Result<Tag, Error> {
// default to compound tag
@ -145,6 +148,7 @@ impl Tag {
Ok(Tag::Compound(map))
}
/// Read the NBT data compressed wtih zlib.
pub fn read_zlib(stream: &mut impl BufRead) -> Result<Tag, Error> {
let mut gz = ZlibDecoder::new(stream);
let mut buf = Vec::new();
@ -152,6 +156,7 @@ impl Tag {
Tag::read(&mut Cursor::new(&buf))
}
/// Read the NBT data compressed wtih gzip.
pub fn read_gzip(stream: &mut Cursor<Vec<u8>>) -> Result<Tag, Error> {
let mut gz = GzDecoder::new(stream);
let mut buf = Vec::new();

View file

@ -6,8 +6,6 @@ use byteorder::{WriteBytesExt, BE};
use flate2::write::{GzEncoder, ZlibEncoder};
use std::io::Write;
// who needs friends when you've got code that runs in nanoseconds?
#[inline]
fn write_string(writer: &mut dyn Write, string: &str) -> Result<(), Error> {
writer.write_u16::<BE>(string.len() as u16)?;
@ -28,62 +26,62 @@ fn write_compound(
Tag::Byte(value) => {
writer.write_u8(1)?;
write_string(writer, key)?;
writer.write_i8(*value)?
writer.write_i8(*value)?;
}
Tag::Short(value) => {
writer.write_u8(2)?;
write_string(writer, key)?;
writer.write_i16::<BE>(*value)?
writer.write_i16::<BE>(*value)?;
}
Tag::Int(value) => {
writer.write_u8(3)?;
write_string(writer, key)?;
writer.write_i32::<BE>(*value)?
writer.write_i32::<BE>(*value)?;
}
Tag::Long(value) => {
writer.write_u8(4)?;
write_string(writer, key)?;
writer.write_i64::<BE>(*value)?
writer.write_i64::<BE>(*value)?;
}
Tag::Float(value) => {
writer.write_u8(5)?;
write_string(writer, key)?;
writer.write_f32::<BE>(*value)?
writer.write_f32::<BE>(*value)?;
}
Tag::Double(value) => {
writer.write_u8(6)?;
write_string(writer, key)?;
writer.write_f64::<BE>(*value)?
writer.write_f64::<BE>(*value)?;
}
Tag::ByteArray(value) => {
writer.write_u8(7)?;
write_string(writer, key)?;
write_bytearray(writer, value)?
write_bytearray(writer, value)?;
}
Tag::String(value) => {
writer.write_u8(8)?;
write_string(writer, key)?;
write_string(writer, value)?
write_string(writer, value)?;
}
Tag::List(value) => {
writer.write_u8(9)?;
write_string(writer, key)?;
write_list(writer, value)?
write_list(writer, value)?;
}
Tag::Compound(value) => {
writer.write_u8(10)?;
write_string(writer, key)?;
write_compound(writer, value, true)?
write_compound(writer, value, true)?;
}
Tag::IntArray(value) => {
writer.write_u8(11)?;
write_string(writer, key)?;
write_intarray(writer, value)?
write_intarray(writer, value)?;
}
Tag::LongArray(value) => {
writer.write_u8(12)?;
write_string(writer, key)?;
write_longarray(writer, value)?
write_longarray(writer, value)?;
}
}
}
@ -166,6 +164,10 @@ fn write_longarray(writer: &mut dyn Write, value: &Vec<i64>) -> Result<(), Error
}
impl Tag {
/// Write the tag as unnamed, uncompressed NBT data. If you're writing a
/// compound tag and the length of the NBT is already known, use
/// [`Tag::write`] to avoid the `End` tag (this is used when writing NBT to
/// a file).
#[inline]
pub fn write_without_end(&self, writer: &mut dyn Write) -> Result<(), Error> {
match self {
@ -187,6 +189,11 @@ impl Tag {
Ok(())
}
/// Write the compound tag as NBT data.
///
/// # Errors
///
/// Returns an `Err` if it's not a Compound or End tag.
pub fn write(&self, writer: &mut impl Write) -> Result<(), Error> {
match self {
Tag::Compound(value) => {
@ -201,11 +208,21 @@ impl Tag {
}
}
/// Write the compound tag as NBT data compressed wtih zlib.
///
/// # Errors
///
/// Returns an `Err` if it's not a Compound or End tag.
pub fn write_zlib(&self, writer: &mut impl Write) -> Result<(), Error> {
let mut encoder = ZlibEncoder::new(writer, flate2::Compression::default());
self.write(&mut encoder)
}
/// Write the compound tag as NBT data compressed wtih gzip.
///
/// # Errors
///
/// Returns an `Err` if it's not a Compound or End tag.
pub fn write_gzip(&self, writer: &mut impl Write) -> Result<(), Error> {
let mut encoder = GzEncoder::new(writer, flate2::Compression::default());
self.write(&mut encoder)

View file

@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
mod decode;
mod encode;
mod error;

View file

@ -1,10 +1,12 @@
use ahash::AHashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// An NBT value.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
#[serde(untagged)]
pub enum Tag {
End, // 0
#[default]
End, // 0
Byte(i8), // 1
Short(i16), // 2
Int(i32), // 3
@ -19,13 +21,8 @@ pub enum Tag {
LongArray(Vec<i64>), // 12
}
impl Default for Tag {
fn default() -> Self {
Tag::End
}
}
impl Tag {
/// Get the numerical ID of the tag type.
#[inline]
pub fn id(&self) -> u8 {
match self {
@ -45,6 +42,7 @@ impl Tag {
}
}
/// If the type is a byte, return the [`i8`].
#[inline]
pub fn as_byte(&self) -> Option<&i8> {
if let Tag::Byte(v) = self {
@ -54,6 +52,7 @@ impl Tag {
}
}
/// If the type is a short, return the [`i16`].
#[inline]
pub fn as_short(&self) -> Option<&i16> {
if let Tag::Short(v) = self {
@ -63,6 +62,7 @@ impl Tag {
}
}
/// If the type is an int, return the [`i32`].
#[inline]
pub fn as_int(&self) -> Option<&i32> {
if let Tag::Int(v) = self {
@ -72,6 +72,7 @@ impl Tag {
}
}
/// If the type is a long, return the [`i64`].
#[inline]
pub fn as_long(&self) -> Option<&i64> {
if let Tag::Long(v) = self {
@ -81,6 +82,7 @@ impl Tag {
}
}
/// If the type is a float, return the [`f32`].
#[inline]
pub fn as_float(&self) -> Option<&f32> {
if let Tag::Float(v) = self {
@ -90,6 +92,7 @@ impl Tag {
}
}
/// If the type is a double, return the [`f64`].
#[inline]
pub fn as_double(&self) -> Option<&f64> {
if let Tag::Double(v) = self {
@ -99,6 +102,7 @@ impl Tag {
}
}
/// If the type is a string, return the [`str`].
#[inline]
pub fn as_string(&self) -> Option<&str> {
if let Tag::String(v) = self {
@ -108,6 +112,7 @@ impl Tag {
}
}
/// If the type is a compound, return the `AHashMap<String, Tag>`.
#[inline]
pub fn as_compound(&self) -> Option<&AHashMap<String, Tag>> {
if let Tag::Compound(v) = self {
@ -117,6 +122,7 @@ impl Tag {
}
}
/// If the type is a bytearray, return the `[u8]`.
#[inline]
pub fn as_bytearray(&self) -> Option<&[u8]> {
if let Tag::ByteArray(v) = self {
@ -126,6 +132,7 @@ impl Tag {
}
}
/// If the type is an intarray, return the `Vec<i32>`.
#[inline]
pub fn as_intarray(&self) -> Option<&Vec<i32>> {
if let Tag::IntArray(v) = self {
@ -135,6 +142,7 @@ impl Tag {
}
}
/// If the type is a longarray, return the `Vec<i64>`.
#[inline]
pub fn as_longarray(&self) -> Option<&Vec<i64>> {
if let Tag::LongArray(v) = self {
@ -144,6 +152,7 @@ impl Tag {
}
}
/// If the type is a list, return the `[Tag]`.
#[inline]
pub fn as_list(&self) -> Option<&[Tag]> {
if let Tag::List(v) = self {

View file

@ -9,11 +9,14 @@ version = "0.5.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-block = {path = "../azalea-block", version = "^0.5.0" }
azalea-core = {path = "../azalea-core", version = "^0.5.0" }
azalea-world = {path = "../azalea-world", version = "^0.5.0" }
azalea-block = { path = "../azalea-block", version = "^0.5.0" }
azalea-core = { path = "../azalea-core", version = "^0.5.0" }
azalea-world = { path = "../azalea-world", version = "^0.5.0" }
azalea-registry = { path = "../azalea-registry", version = "^0.5.0" }
iyes_loopless = "0.9.1"
once_cell = "1.16.0"
parking_lot = "^0.12.1"
azalea-ecs = { version = "0.5.0", path = "../azalea-ecs" }
[dev-dependencies]
uuid = "^1.1.2"

View file

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

View file

@ -5,13 +5,15 @@ mod shape;
mod world_collisions;
use azalea_core::{Axis, Vec3, AABB, EPSILON};
use azalea_world::entity::{Entity, EntityData};
use azalea_world::{MoveEntityError, WeakWorld};
use azalea_world::{
entity::{self},
MoveEntityError, World,
};
pub use blocks::BlockWithShape;
pub use discrete_voxel_shape::*;
pub use shape::*;
use std::ops::Deref;
use world_collisions::CollisionGetter;
use self::world_collisions::get_block_collisions;
pub enum MoverType {
Own,
@ -21,192 +23,170 @@ pub enum MoverType {
Shulker,
}
pub trait HasCollision {
fn collide(&self, movement: &Vec3, entity: &EntityData) -> Vec3;
// private Vec3 collide(Vec3 var1) {
// AABB var2 = this.getBoundingBox();
// List var3 = this.level.getEntityCollisions(this,
// var2.expandTowards(var1)); Vec3 var4 = var1.lengthSqr() == 0.0D ?
// var1 : collideBoundingBox(this, var1, var2, this.level, var3);
// boolean var5 = var1.x != var4.x;
// boolean var6 = var1.y != var4.y;
// boolean var7 = var1.z != var4.z;
// boolean var8 = this.onGround || var6 && var1.y < 0.0D;
// if (this.maxUpStep > 0.0F && var8 && (var5 || var7)) {
// Vec3 var9 = collideBoundingBox(this, new Vec3(var1.x,
// (double)this.maxUpStep, var1.z), var2, this.level, var3); Vec3
// var10 = collideBoundingBox(this, new Vec3(0.0D, (double)this.maxUpStep,
// 0.0D), var2.expandTowards(var1.x, 0.0D, var1.z), this.level, var3);
// if (var10.y < (double)this.maxUpStep) {
// Vec3 var11 = collideBoundingBox(this, new Vec3(var1.x, 0.0D,
// var1.z), var2.move(var10), this.level, var3).add(var10); if
// (var11.horizontalDistanceSqr() > var9.horizontalDistanceSqr()) {
// var9 = var11;
// }
// }
// if (var9.horizontalDistanceSqr() > var4.horizontalDistanceSqr()) {
// return var9.add(collideBoundingBox(this, new Vec3(0.0D, -var9.y +
// var1.y, 0.0D), var2.move(var9), this.level, var3)); }
// }
// return var4;
// }
fn collide(movement: &Vec3, world: &World, physics: &entity::Physics) -> Vec3 {
let entity_bounding_box = physics.bounding_box;
// TODO: get_entity_collisions
// let entity_collisions = world.get_entity_collisions(self,
// entity_bounding_box.expand_towards(movement));
let entity_collisions = Vec::new();
if movement.length_sqr() == 0.0 {
*movement
} else {
collide_bounding_box(movement, &entity_bounding_box, world, entity_collisions)
}
// TODO: stepping (for stairs and stuff)
// collided_movement
}
pub trait MovableEntity {
fn move_colliding(
&mut self,
mover_type: &MoverType,
movement: &Vec3,
) -> Result<(), MoveEntityError>;
}
/// Move an entity by a given delta, checking for collisions.
pub fn move_colliding(
_mover_type: &MoverType,
movement: &Vec3,
world: &World,
position: &mut entity::Position,
physics: &mut entity::Physics,
) -> Result<(), MoveEntityError> {
// TODO: do all these
impl<D: Deref<Target = WeakWorld>> HasCollision for D {
// private Vec3 collide(Vec3 var1) {
// AABB var2 = this.getBoundingBox();
// List var3 = this.level.getEntityCollisions(this,
// var2.expandTowards(var1)); Vec3 var4 = var1.lengthSqr() == 0.0D ?
// var1 : collideBoundingBox(this, var1, var2, this.level, var3);
// boolean var5 = var1.x != var4.x;
// boolean var6 = var1.y != var4.y;
// boolean var7 = var1.z != var4.z;
// boolean var8 = this.onGround || var6 && var1.y < 0.0D;
// if (this.maxUpStep > 0.0F && var8 && (var5 || var7)) {
// Vec3 var9 = collideBoundingBox(this, new Vec3(var1.x,
// (double)this.maxUpStep, var1.z), var2, this.level, var3); Vec3
// var10 = collideBoundingBox(this, new Vec3(0.0D, (double)this.maxUpStep,
// 0.0D), var2.expandTowards(var1.x, 0.0D, var1.z), this.level, var3);
// if (var10.y < (double)this.maxUpStep) {
// Vec3 var11 = collideBoundingBox(this, new Vec3(var1.x, 0.0D,
// var1.z), var2.move(var10), this.level, var3).add(var10); if
// (var11.horizontalDistanceSqr() > var9.horizontalDistanceSqr()) {
// var9 = var11;
// }
// }
// if self.no_physics {
// return;
// };
// if (var9.horizontalDistanceSqr() > var4.horizontalDistanceSqr()) {
// return var9.add(collideBoundingBox(this, new Vec3(0.0D, -var9.y +
// var1.y, 0.0D), var2.move(var9), this.level, var3)); }
// if (var1 == MoverType.PISTON) {
// var2 = this.limitPistonMovement(var2);
// if (var2.equals(Vec3.ZERO)) {
// return;
// }
// return var4;
// }
fn collide(&self, movement: &Vec3, entity: &EntityData) -> Vec3 {
let entity_bounding_box = entity.bounding_box;
// TODO: get_entity_collisions
// let entity_collisions = world.get_entity_collisions(self,
// entity_bounding_box.expand_towards(movement));
let entity_collisions = Vec::new();
if movement.length_sqr() == 0.0 {
*movement
} else {
collide_bounding_box(
Some(entity),
movement,
&entity_bounding_box,
self,
entity_collisions,
)
}
// TODO: stepping (for stairs and stuff)
// if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) {
// var2 = var2.multiply(this.stuckSpeedMultiplier);
// this.stuckSpeedMultiplier = Vec3.ZERO;
// this.setDeltaMovement(Vec3.ZERO);
// }
// collided_movement
}
}
// movement = this.maybeBackOffFromEdge(movement, moverType);
impl<D: Deref<Target = WeakWorld>> MovableEntity for Entity<'_, D> {
/// Move an entity by a given delta, checking for collisions.
fn move_colliding(
&mut self,
_mover_type: &MoverType,
movement: &Vec3,
) -> Result<(), MoveEntityError> {
// TODO: do all these
let collide_result = collide(movement, world, physics);
// if self.no_physics {
// return;
// };
let move_distance = collide_result.length_sqr();
// if (var1 == MoverType.PISTON) {
// var2 = this.limitPistonMovement(var2);
// if (var2.equals(Vec3.ZERO)) {
// return;
// }
// }
if move_distance > EPSILON {
// TODO: fall damage
// if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) {
// var2 = var2.multiply(this.stuckSpeedMultiplier);
// this.stuckSpeedMultiplier = Vec3.ZERO;
// this.setDeltaMovement(Vec3.ZERO);
// }
// movement = this.maybeBackOffFromEdge(movement, moverType);
let collide_result = { self.world.collide(movement, self) };
let move_distance = collide_result.length_sqr();
if move_distance > EPSILON {
// TODO: fall damage
let new_pos = {
let entity_pos = self.pos();
Vec3 {
x: entity_pos.x + collide_result.x,
y: entity_pos.y + collide_result.y,
z: entity_pos.z + collide_result.z,
}
};
self.world.set_entity_pos(self.id, new_pos)?;
}
let x_collision = movement.x != collide_result.x;
let z_collision = movement.z != collide_result.z;
let horizontal_collision = x_collision || z_collision;
let vertical_collision = movement.y != collide_result.y;
let on_ground = vertical_collision && movement.y < 0.;
self.on_ground = on_ground;
// TODO: minecraft checks for a "minor" horizontal collision here
let _block_pos_below = self.on_pos_legacy();
// let _block_state_below = self
// .world
// .get_block_state(&block_pos_below)
// .expect("Couldn't get block state below");
// self.check_fall_damage(collide_result.y, on_ground, block_state_below,
// block_pos_below);
// if self.isRemoved() { return; }
if horizontal_collision {
let delta_movement = &self.delta;
self.delta = Vec3 {
x: if x_collision { 0. } else { delta_movement.x },
y: delta_movement.y,
z: if z_collision { 0. } else { delta_movement.z },
let new_pos = {
Vec3 {
x: position.x + collide_result.x,
y: position.y + collide_result.y,
z: position.z + collide_result.z,
}
}
};
if vertical_collision {
// blockBelow.updateEntityAfterFallOn(this.level, this);
// the default implementation of updateEntityAfterFallOn sets the y movement to
// 0
self.delta.y = 0.;
}
if on_ground {
// blockBelow.stepOn(this.level, blockPosBelow, blockStateBelow,
// this);
}
// sounds
// this.tryCheckInsideBlocks();
// float var25 = this.getBlockSpeedFactor();
// this.setDeltaMovement(this.getDeltaMovement().multiply((double)var25, 1.0D,
// (double)var25)); if (this.level.getBlockStatesIfLoaded(this.
// getBoundingBox().deflate(1.0E-6D)).noneMatch((var0) -> {
// return var0.is(BlockTags.FIRE) || var0.is(Blocks.LAVA);
// })) {
// if (this.remainingFireTicks <= 0) {
// this.setRemainingFireTicks(-this.getFireImmuneTicks());
// }
// if (this.wasOnFire && (this.isInPowderSnow ||
// this.isInWaterRainOrBubble())) { this.
// playEntityOnFireExtinguishedSound(); }
// }
// if (this.isOnFire() && (this.isInPowderSnow || this.isInWaterRainOrBubble()))
// { this.setRemainingFireTicks(-this.getFireImmuneTicks());
// }
Ok(())
**position = new_pos;
}
let x_collision = movement.x != collide_result.x;
let z_collision = movement.z != collide_result.z;
let horizontal_collision = x_collision || z_collision;
let vertical_collision = movement.y != collide_result.y;
let on_ground = vertical_collision && movement.y < 0.;
physics.on_ground = on_ground;
// TODO: minecraft checks for a "minor" horizontal collision here
let _block_pos_below = entity::on_pos_legacy(&world.chunks, position);
// let _block_state_below = self
// .world
// .get_block_state(&block_pos_below)
// .expect("Couldn't get block state below");
// self.check_fall_damage(collide_result.y, on_ground, block_state_below,
// block_pos_below);
// if self.isRemoved() { return; }
if horizontal_collision {
let delta_movement = &physics.delta;
physics.delta = Vec3 {
x: if x_collision { 0. } else { delta_movement.x },
y: delta_movement.y,
z: if z_collision { 0. } else { delta_movement.z },
}
}
if vertical_collision {
// blockBelow.updateEntityAfterFallOn(this.level, this);
// the default implementation of updateEntityAfterFallOn sets the y movement to
// 0
physics.delta.y = 0.;
}
if on_ground {
// blockBelow.stepOn(this.level, blockPosBelow, blockStateBelow,
// this);
}
// sounds
// this.tryCheckInsideBlocks();
// float var25 = this.getBlockSpeedFactor();
// this.setDeltaMovement(this.getDeltaMovement().multiply((double)var25, 1.0D,
// (double)var25)); if (this.level.getBlockStatesIfLoaded(this.
// getBoundingBox().deflate(1.0E-6D)).noneMatch((var0) -> {
// return var0.is(BlockTags.FIRE) || var0.is(Blocks.LAVA);
// })) {
// if (this.remainingFireTicks <= 0) {
// this.setRemainingFireTicks(-this.getFireImmuneTicks());
// }
// if (this.wasOnFire && (this.isInPowderSnow ||
// this.isInWaterRainOrBubble())) { this.
// playEntityOnFireExtinguishedSound(); }
// }
// if (this.isOnFire() && (this.isInPowderSnow || this.isInWaterRainOrBubble()))
// { this.setRemainingFireTicks(-this.getFireImmuneTicks());
// }
Ok(())
}
fn collide_bounding_box(
entity: Option<&EntityData>,
movement: &Vec3,
entity_bounding_box: &AABB,
world: &WeakWorld,
world: &World,
entity_collisions: Vec<VoxelShape>,
) -> Vec3 {
let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
@ -218,7 +198,7 @@ fn collide_bounding_box(
// TODO: world border
let block_collisions =
world.get_block_collisions(entity, entity_bounding_box.expand_towards(movement));
get_block_collisions(world, entity_bounding_box.expand_towards(movement));
let block_collisions = block_collisions.collect::<Vec<_>>();
collision_boxes.extend(block_collisions);
collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)

View file

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

View file

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

View file

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

11
azalea-protocol/Cargo.toml Executable file → Normal file
View file

@ -22,6 +22,7 @@ azalea-nbt = {path = "../azalea-nbt", version = "^0.5.0" }
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.5.0" }
azalea-registry = {path = "../azalea-registry", version = "^0.5.0" }
azalea-world = {path = "../azalea-world", version = "^0.5.0" }
bevy_ecs = { version = "0.9.1", default-features = false }
byteorder = "^1.4.3"
bytes = "^1.1.0"
flate2 = "1.0.23"
@ -30,8 +31,8 @@ futures-util = "0.3.24"
log = "0.4.17"
serde = {version = "1.0.130", features = ["serde_derive"]}
serde_json = "^1.0.72"
thiserror = "^1.0.34"
tokio = {version = "^1.21.2", features = ["io-util", "net", "macros"]}
thiserror = "1.0.37"
tokio = {version = "^1.24.2", features = ["io-util", "net", "macros"]}
tokio-util = {version = "0.7.4", features = ["codec"]}
trust-dns-resolver = {version = "^0.22.0", default-features = false, features = ["tokio-runtime"]}
uuid = "1.1.2"
@ -40,3 +41,9 @@ uuid = "1.1.2"
connecting = []
default = ["packets"]
packets = ["connecting", "dep:async-compression", "dep:azalea-core"]
[dev-dependencies]
anyhow = "^1.0.65"
tracing = "^0.1.36"
tracing-subscriber = "^0.3.15"
once_cell = "1.17.0"

View file

@ -3,19 +3,17 @@ use quote::quote;
use syn::{
self, braced,
parse::{Parse, ParseStream, Result},
parse_macro_input, DeriveInput, FieldsNamed, Ident, LitInt, Token,
parse_macro_input, DeriveInput, Ident, LitInt, Token,
};
fn as_packet_derive(input: TokenStream, state: proc_macro2::TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
let fields = match &data {
syn::Data::Struct(syn::DataStruct { fields, .. }) => fields,
_ => panic!("#[derive(*Packet)] can only be used on structs"),
let syn::Data::Struct(syn::DataStruct { fields, .. }) = &data else {
panic!("#[derive(*Packet)] can only be used on structs")
};
let FieldsNamed { named: _, .. } = match fields {
syn::Fields::Named(f) => f,
_ => panic!("#[derive(*Packet)] can only be used on structs with named fields"),
let syn::Fields::Named(_) = fields else {
panic!("#[derive(*Packet)] can only be used on structs with named fields")
};
let variant_name = variant_name_from(&ident);
@ -221,11 +219,19 @@ pub fn declare_state_packets(input: TokenStream) -> TokenStream {
});
serverbound_read_match_contents.extend(quote! {
#id => {
let data = #module::#name::read(buf).map_err(|e| crate::read::ReadPacketError::Parse { source: e, packet_id: #id, packet_name: #name_litstr.to_string() })?;
let mut leftover = Vec::new();
let _ = std::io::Read::read_to_end(buf, &mut leftover);
if !leftover.is_empty() {
return Err(crate::read::ReadPacketError::LeftoverData { packet_name: #name_litstr.to_string(), data: leftover });
let data = #module::#name::read(buf).map_err(|e| crate::read::ReadPacketError::Parse {
source: e,
packet_id: #id,
backtrace: Box::new(std::backtrace::Backtrace::capture()),
packet_name: #name_litstr.to_string(),
})?;
#[cfg(debug_assertions)]
{
let mut leftover = Vec::new();
let _ = std::io::Read::read_to_end(buf, &mut leftover);
if !leftover.is_empty() {
return Err(Box::new(crate::read::ReadPacketError::LeftoverData { packet_name: #name_litstr.to_string(), data: leftover }));
}
}
data
},
@ -246,14 +252,25 @@ pub fn declare_state_packets(input: TokenStream) -> TokenStream {
});
clientbound_read_match_contents.extend(quote! {
#id => {
let data = #module::#name::read(buf).map_err(|e| crate::read::ReadPacketError::Parse { source: e, packet_id: #id, packet_name: #name_litstr.to_string() })?;
let data = #module::#name::read(buf).map_err(|e| crate::read::ReadPacketError::Parse {
source: e,
packet_id: #id,
backtrace: Box::new(std::backtrace::Backtrace::capture()),
packet_name: #name_litstr.to_string(),
})?;
#[cfg(debug_assertions)]
{
let mut leftover = Vec::new();
let _ = std::io::Read::read_to_end(buf, &mut leftover);
if !leftover.is_empty() {
return Err(crate::read::ReadPacketError::LeftoverData { packet_name: #name_litstr.to_string(), data: leftover });
return Err(
Box::new(
crate::read::ReadPacketError::LeftoverData {
packet_name: #name_litstr.to_string(),
data: leftover
}
)
);
}
}
data
@ -314,13 +331,13 @@ pub fn declare_state_packets(input: TokenStream) -> TokenStream {
fn read(
id: u32,
buf: &mut std::io::Cursor<&[u8]>,
) -> Result<#serverbound_state_name, crate::read::ReadPacketError>
) -> Result<#serverbound_state_name, Box<crate::read::ReadPacketError>>
where
Self: Sized,
{
Ok(match id {
#serverbound_read_match_contents
_ => return Err(crate::read::ReadPacketError::UnknownPacketId { state_name: #state_name_litstr.to_string(), id }),
_ => return Err(Box::new(crate::read::ReadPacketError::UnknownPacketId { state_name: #state_name_litstr.to_string(), id })),
})
}
}
@ -345,13 +362,13 @@ pub fn declare_state_packets(input: TokenStream) -> TokenStream {
fn read(
id: u32,
buf: &mut std::io::Cursor<&[u8]>,
) -> Result<#clientbound_state_name, crate::read::ReadPacketError>
) -> Result<#clientbound_state_name, Box<crate::read::ReadPacketError>>
where
Self: Sized,
{
Ok(match id {
#clientbound_read_match_contents
_ => return Err(crate::read::ReadPacketError::UnknownPacketId { state_name: #state_name_litstr.to_string(), id }),
_ => return Err(Box::new(crate::read::ReadPacketError::UnknownPacketId { state_name: #state_name_litstr.to_string(), id })),
})
}
}

View file

@ -0,0 +1,225 @@
//! A "simple" server that gets login information and proxies connections.
//! After login all connections are encrypted and Azalea cannot read them.
use azalea_protocol::{
connect::Connection,
packets::{
handshake::{
client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket,
ServerboundHandshakePacket,
},
login::{serverbound_hello_packet::ServerboundHelloPacket, ServerboundLoginPacket},
status::{
clientbound_pong_response_packet::ClientboundPongResponsePacket,
clientbound_status_response_packet::{
ClientboundStatusResponsePacket, Players, Version,
},
ServerboundStatusPacket,
},
ConnectionProtocol, PROTOCOL_VERSION,
},
read::ReadPacketError,
};
use futures::FutureExt;
use log::{error, info, warn};
use once_cell::sync::Lazy;
use std::error::Error;
use tokio::{
io::{self, AsyncWriteExt},
net::{TcpListener, TcpStream},
};
use tracing::Level;
const LISTEN_ADDR: &str = "127.0.0.1:25566";
const PROXY_ADDR: &str = "127.0.0.1:25565";
const PROXY_DESC: &str = "An Azalea Minecraft Proxy";
// String must be formatted like "data:image/png;base64,<data>"
static PROXY_FAVICON: Lazy<Option<String>> = Lazy::new(|| None);
static PROXY_VERSION: Lazy<Version> = Lazy::new(|| Version {
name: "1.19.3".to_string(),
protocol: PROTOCOL_VERSION as i32,
});
const PROXY_PLAYERS: Players = Players {
max: 1,
online: 0,
sample: Vec::new(),
};
const PROXY_PREVIEWS_CHAT: Option<bool> = Some(false);
const PROXY_SECURE_CHAT: Option<bool> = Some(false);
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
// Bind to an address and port
let listener = TcpListener::bind(LISTEN_ADDR).await?;
loop {
// When a connection is made, pass it off to another thread
let (stream, _) = listener.accept().await?;
tokio::spawn(handle_connection(stream));
}
}
async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> {
stream.set_nodelay(true)?;
let ip = stream.peer_addr()?;
let mut conn: Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> =
Connection::wrap(stream);
// The first packet sent from a client is the intent packet.
// This specifies whether the client is pinging
// the server or is going to join the game.
let intent = match conn.read().await {
Ok(packet) => match packet {
ServerboundHandshakePacket::ClientIntention(packet) => {
info!(
"New connection: {0}, Version {1}, {2:?}",
ip.ip(),
packet.protocol_version,
packet.intention
);
packet
}
},
Err(e) => {
let e = e.into();
warn!("Error during intent: {e}");
return Err(e);
}
};
match intent.intention {
// If the client is pinging the proxy,
// reply with the information below.
ConnectionProtocol::Status => {
let mut conn = conn.status();
loop {
match conn.read().await {
Ok(p) => match p {
ServerboundStatusPacket::StatusRequest(_) => {
conn.write(
ClientboundStatusResponsePacket {
description: PROXY_DESC.into(),
favicon: PROXY_FAVICON.clone(),
players: PROXY_PLAYERS.clone(),
version: PROXY_VERSION.clone(),
previews_chat: PROXY_PREVIEWS_CHAT,
enforces_secure_chat: PROXY_SECURE_CHAT,
}
.get(),
)
.await?;
}
ServerboundStatusPacket::PingRequest(p) => {
conn.write(ClientboundPongResponsePacket { time: p.time }.get())
.await?;
break;
}
},
Err(e) => match *e {
ReadPacketError::ConnectionClosed => {
break;
}
e => {
warn!("Error during status: {e}");
return Err(e.into());
}
},
}
}
}
// If the client intends to join the proxy,
// wait for them to send the `Hello` packet to
// log their username and uuid, then forward the
// connection along to the proxy target.
ConnectionProtocol::Login => {
let mut conn = conn.login();
loop {
match conn.read().await {
Ok(p) => {
if let ServerboundLoginPacket::Hello(hello) = p {
info!(
"Player \'{0}\' from {1} logging in with uuid: {2}",
hello.username,
ip.ip(),
if let Some(id) = hello.profile_id {
id.to_string()
} else {
String::new()
}
);
tokio::spawn(transfer(conn.unwrap()?, intent, hello).map(|r| {
if let Err(e) = r {
error!("Failed to proxy: {e}");
}
}));
break;
}
}
Err(e) => match *e {
ReadPacketError::ConnectionClosed => {
break;
}
e => {
warn!("Error during login: {e}");
return Err(e.into());
}
},
}
}
}
_ => {
warn!("Client provided weird intent: {:?}", intent.intention);
}
}
Ok(())
}
async fn transfer(
mut inbound: TcpStream,
intent: ClientIntentionPacket,
hello: ServerboundHelloPacket,
) -> Result<(), Box<dyn Error>> {
let outbound = TcpStream::connect(PROXY_ADDR).await?;
let name = hello.username.clone();
outbound.set_nodelay(true)?;
// Repeat the intent and hello packet
// recieved earlier to the proxy target
let mut outbound_conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> =
Connection::wrap(outbound);
outbound_conn.write(intent.get()).await?;
let mut outbound_conn = outbound_conn.login();
outbound_conn.write(hello.get()).await?;
let mut outbound = outbound_conn.unwrap()?;
// Split the incoming and outgoing connections in
// halves and handle each pair on separate threads.
let (mut ri, mut wi) = inbound.split();
let (mut ro, mut wo) = outbound.split();
let client_to_server = async {
io::copy(&mut ri, &mut wo).await?;
wo.shutdown().await
};
let server_to_client = async {
io::copy(&mut ro, &mut wi).await?;
wi.shutdown().await
};
tokio::try_join!(client_to_server, server_to_client)?;
info!("Player \'{name}\' left the game");
Ok(())
}

View file

@ -8,7 +8,8 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
use crate::packets::ProtocolPacket;
use crate::read::{read_packet, ReadPacketError};
use crate::write::write_packet;
use azalea_auth::sessionserver::SessionServerError;
use azalea_auth::game_profile::GameProfile;
use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError};
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
use bytes::BytesMut;
use log::{error, info};
@ -17,7 +18,7 @@ use std::marker::PhantomData;
use std::net::SocketAddr;
use thiserror::Error;
use tokio::io::AsyncWriteExt;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError};
use tokio::net::TcpStream;
use uuid::Uuid;
@ -52,7 +53,7 @@ pub struct WriteConnection<W: ProtocolPacket> {
/// login::{
/// ClientboundLoginPacket,
/// serverbound_hello_packet::ServerboundHelloPacket,
/// serverbound_key_packet::{ServerboundKeyPacket, NonceOrSaltSignature}
/// serverbound_key_packet::ServerboundKeyPacket
/// },
/// handshake::client_intention_packet::ClientIntentionPacket
/// }
@ -80,8 +81,7 @@ pub struct WriteConnection<W: ProtocolPacket> {
/// // login
/// conn.write(
/// ServerboundHelloPacket {
/// username: "bot".to_string(),
/// public_key: None,
/// name: "bot".to_string(),
/// profile_id: None,
/// }
/// .get(),
@ -96,8 +96,8 @@ pub struct WriteConnection<W: ProtocolPacket> {
///
/// conn.write(
/// ServerboundKeyPacket {
/// nonce_or_salt_signature: NonceOrSaltSignature::Nonce(e.encrypted_nonce),
/// key_bytes: e.encrypted_public_key,
/// encrypted_challenge: e.encrypted_nonce,
/// }
/// .get(),
/// )
@ -131,7 +131,7 @@ where
R: ProtocolPacket + Debug,
{
/// Read a packet from the stream.
pub async fn read(&mut self) -> Result<R, ReadPacketError> {
pub async fn read(&mut self) -> Result<R, Box<ReadPacketError>> {
read_packet::<R, _>(
&mut self.read_stream,
&mut self.buffer,
@ -179,7 +179,7 @@ where
W: ProtocolPacket + Debug,
{
/// Read a packet from the other side of the connection.
pub async fn read(&mut self) -> Result<R, ReadPacketError> {
pub async fn read(&mut self) -> Result<R, Box<ReadPacketError>> {
self.reader.read().await
}
@ -189,6 +189,7 @@ where
}
/// Split the reader and writer into two objects. This doesn't allocate.
#[must_use]
pub fn into_split(self) -> (ReadConnection<R>, WriteConnection<W>) {
(self.reader, self.writer)
}
@ -229,12 +230,14 @@ impl Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> {
/// Change our state from handshake to login. This is the state that is used
/// for logging in.
#[must_use]
pub fn login(self) -> Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
Connection::from(self)
}
/// Change our state from handshake to status. This is the state that is
/// used for pinging the server.
#[must_use]
pub fn status(self) -> Connection<ClientboundStatusPacket, ServerboundStatusPacket> {
Connection::from(self)
}
@ -265,6 +268,7 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
/// Change our state from login to game. This is the state that's used when
/// you're actually in the game.
#[must_use]
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
Connection::from(self)
}
@ -280,7 +284,7 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
/// use azalea_protocol::connect::Connection;
/// use azalea_protocol::packets::login::{
/// ClientboundLoginPacket,
/// serverbound_key_packet::{ServerboundKeyPacket, NonceOrSaltSignature}
/// serverbound_key_packet::ServerboundKeyPacket
/// };
/// use uuid::Uuid;
/// # use azalea_protocol::ServerAddress;
@ -305,14 +309,14 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
/// let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap();
/// conn.authenticate(
/// &access_token,
/// &Uuid::parse_str(&profile.id).expect("Invalid UUID"),
/// &profile.id,
/// e.secret_key,
/// &p
/// ).await?;
/// conn.write(
/// ServerboundKeyPacket {
/// nonce_or_salt_signature: NonceOrSaltSignature::Nonce(e.encrypted_nonce),
/// key_bytes: e.encrypted_public_key,
/// encrypted_challenge: e.encrypted_nonce,
/// }.get()
/// ).await?;
/// conn.set_encryption_key(e.secret_key);
@ -328,7 +332,7 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
uuid: &Uuid,
private_key: [u8; 16],
packet: &ClientboundHelloPacket,
) -> Result<(), SessionServerError> {
) -> Result<(), ClientSessionServerError> {
azalea_auth::sessionserver::join(
access_token,
&packet.public_key,
@ -340,6 +344,66 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
}
}
impl Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> {
/// Change our state from handshake to login. This is the state that is used
/// for logging in.
#[must_use]
pub fn login(self) -> Connection<ServerboundLoginPacket, ClientboundLoginPacket> {
Connection::from(self)
}
/// Change our state from handshake to status. This is the state that is
/// used for pinging the server.
#[must_use]
pub fn status(self) -> Connection<ServerboundStatusPacket, ClientboundStatusPacket> {
Connection::from(self)
}
}
impl Connection<ServerboundLoginPacket, ClientboundLoginPacket> {
/// Set our compression threshold, i.e. the maximum size that a packet is
/// allowed to be without getting compressed. If you set it to less than 0
/// then compression gets disabled.
pub fn set_compression_threshold(&mut self, threshold: i32) {
// if you pass a threshold of less than 0, compression is disabled
if threshold >= 0 {
self.reader.compression_threshold = Some(threshold as u32);
self.writer.compression_threshold = Some(threshold as u32);
} else {
self.reader.compression_threshold = None;
self.writer.compression_threshold = None;
}
}
/// Set the encryption key that is used to encrypt and decrypt packets. It's
/// the same for both reading and writing.
pub fn set_encryption_key(&mut self, key: [u8; 16]) {
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
self.reader.dec_cipher = Some(dec_cipher);
self.writer.enc_cipher = Some(enc_cipher);
}
/// Change our state from login to game. This is the state that's used when
/// the client is actually in the game.
#[must_use]
pub fn game(self) -> Connection<ServerboundGamePacket, ClientboundGamePacket> {
Connection::from(self)
}
/// Verify connecting clients have authenticated with Minecraft's servers.
/// This must happen after the client sends a `ServerboundLoginPacket::Key`
/// packet.
pub async fn authenticate(
&self,
username: &str,
public_key: &[u8],
private_key: &[u8; 16],
ip: Option<&str>,
) -> Result<GameProfile, ServerSessionServerError> {
azalea_auth::sessionserver::serverside_auth(username, public_key, private_key, ip).await
}
}
// rust doesn't let us implement From because allegedly it conflicts with
// `core`'s "impl<T> From<T> for T" so we do this instead
impl<R1, W1> Connection<R1, W1>
@ -349,6 +413,7 @@ where
{
/// Creates a `Connection` of a type from a `Connection` of another type.
/// Useful for servers or custom packets.
#[must_use]
pub fn from<R2, W2>(connection: Connection<R1, W1>) -> Connection<R2, W2>
where
R2: ProtocolPacket + Debug,
@ -391,4 +456,9 @@ where
},
}
}
/// Convert from a `Connection` into a `TcpStream`. Useful for servers.
pub fn unwrap(self) -> Result<TcpStream, ReuniteError> {
self.reader.read_stream.reunite(self.writer.write_stream)
}
}

View file

@ -1,7 +1,7 @@
//! A low-level crate to send and receive Minecraft packets.
//!
//! You should probably use [`azalea`] or [`azalea_client`] instead, as
//! azalea_protocol delegates much of the work, such as auth, to the user of
//! `azalea_protocol` delegates much of the work, such as auth, to the user of
//! the crate.
//!
//! [`azalea`]: https://crates.io/crates/azalea
@ -13,7 +13,7 @@
#![feature(error_generic_member_access)]
#![feature(provide_any)]
use std::{net::SocketAddr, str::FromStr};
use std::{fmt::Display, net::SocketAddr, str::FromStr};
#[cfg(feature = "connecting")]
pub mod connect;
@ -27,7 +27,7 @@ pub mod write;
///
/// # Examples
///
/// ServerAddress implements TryFrom<&str>, so you can use it like this:
/// `ServerAddress` implements TryFrom<&str>, so you can use it like this:
/// ```
/// use azalea_protocol::ServerAddress;
///
@ -45,7 +45,7 @@ impl<'a> TryFrom<&'a str> for ServerAddress {
type Error = String;
/// Convert a Minecraft server address (host:port, the port is optional) to
/// a ServerAddress
/// a `ServerAddress`
fn try_from(string: &str) -> Result<Self, Self::Error> {
if string.is_empty() {
return Err("Empty string".to_string());
@ -60,9 +60,9 @@ impl<'a> TryFrom<&'a str> for ServerAddress {
}
impl From<SocketAddr> for ServerAddress {
/// Convert an existing SocketAddr into a ServerAddress. This just converts
/// the ip to a string and passes along the port. The resolver will realize
/// it's already an IP address and not do any DNS requests.
/// Convert an existing `SocketAddr` into a `ServerAddress`. This just
/// converts the ip to a string and passes along the port. The resolver
/// will realize it's already an IP address and not do any DNS requests.
fn from(addr: SocketAddr) -> Self {
ServerAddress {
host: addr.ip().to_string(),
@ -71,6 +71,12 @@ impl From<SocketAddr> for ServerAddress {
}
}
impl Display for ServerAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.host, self.port)
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_chat::Component;
use azalea_chat::FormattedText;
use azalea_core::FixedBitSet;
use azalea_protocol_macros::ClientboundGamePacket;
use std::io::Cursor;
use std::io::Write;
@ -18,7 +19,7 @@ pub enum Operation {
Add(AddOperation),
Remove,
UpdateProgress(f32),
UpdateName(Component),
UpdateName(FormattedText),
UpdateStyle(Style),
UpdateProperties(Properties),
}
@ -30,7 +31,7 @@ impl McBufReadable for Operation {
0 => Operation::Add(AddOperation::read_from(buf)?),
1 => Operation::Remove,
2 => Operation::UpdateProgress(f32::read_from(buf)?),
3 => Operation::UpdateName(Component::read_from(buf)?),
3 => Operation::UpdateName(FormattedText::read_from(buf)?),
4 => Operation::UpdateStyle(Style::read_from(buf)?),
5 => Operation::UpdateProperties(Properties::read_from(buf)?),
_ => {
@ -75,7 +76,7 @@ impl McBufWritable for Operation {
#[derive(Clone, Debug, McBuf)]
pub struct AddOperation {
name: Component,
name: FormattedText,
progress: f32,
style: Style,
properties: Properties,
@ -116,28 +117,28 @@ pub struct Properties {
impl McBufReadable for Properties {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let byte = u8::read_from(buf)?;
let set = FixedBitSet::<3>::read_from(buf)?;
Ok(Self {
darken_screen: byte & 1 != 0,
play_music: byte & 2 != 0,
create_world_fog: byte & 4 != 0,
darken_screen: set.index(0),
play_music: set.index(1),
create_world_fog: set.index(2),
})
}
}
impl McBufWritable for Properties {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
let mut byte = 0;
let mut set = FixedBitSet::<3>::new();
if self.darken_screen {
byte |= 1;
set.set(0);
}
if self.play_music {
byte |= 2;
set.set(1);
}
if self.create_world_fog {
byte |= 4;
set.set(2);
}
u8::write_into(&byte, buf)?;
set.write_into(buf)?;
Ok(())
}
}

View file

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

View file

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

View file

@ -15,7 +15,7 @@ pub struct ClientboundCommandsPacket {
pub root_index: u32,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct BrigadierNodeStub {
pub is_executable: bool,
pub children: Vec<u32>,
@ -23,7 +23,7 @@ pub struct BrigadierNodeStub {
pub node_type: NodeType,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq)]
pub struct BrigadierNumber<T> {
pub min: Option<T>,
pub max: Option<T>,
@ -33,6 +33,19 @@ impl<T> BrigadierNumber<T> {
BrigadierNumber { min, max }
}
}
impl<T: PartialEq> PartialEq for BrigadierNumber<T> {
fn eq(&self, other: &Self) -> bool {
match (&self.min, &self.max, &other.min, &other.max) {
(Some(f_min), None, Some(s_min), None) => f_min == s_min,
(None, Some(f_max), None, Some(s_max)) => f_max == s_max,
(Some(f_min), Some(f_max), Some(s_min), Some(s_max)) => {
f_min == s_min && f_max == s_max
}
(None, None, None, None) => true,
_ => false,
}
}
}
impl<T: McBufReadable> McBufReadable for BrigadierNumber<T> {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let flags = u8::read_from(buf)?;
@ -69,7 +82,7 @@ impl<T: McBufWritable> McBufWritable for BrigadierNumber<T> {
}
}
#[derive(Debug, Clone, Copy, McBuf)]
#[derive(Debug, Clone, Copy, McBuf, PartialEq, Eq)]
pub enum BrigadierString {
/// Reads a single word
SingleWord = 0,
@ -80,7 +93,7 @@ pub enum BrigadierString {
GreedyPhrase = 2,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum BrigadierParser {
Bool,
Double(BrigadierNumber<f64>),
@ -515,7 +528,7 @@ impl McBufWritable for BrigadierNodeStub {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum NodeType {
Root,
Literal {
@ -537,3 +550,59 @@ impl BrigadierNodeStub {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_brigadier_node_stub_root() {
let data = BrigadierNodeStub {
is_executable: false,
children: vec![1, 2],
redirect_node: None,
node_type: NodeType::Root,
};
let mut buf = Vec::new();
data.write_into(&mut buf).unwrap();
let mut data_cursor: Cursor<&[u8]> = Cursor::new(&buf);
let read_data = BrigadierNodeStub::read_from(&mut data_cursor).unwrap();
assert_eq!(data, read_data);
}
#[test]
fn test_brigadier_node_stub_literal() {
let data = BrigadierNodeStub {
is_executable: true,
children: vec![],
redirect_node: None,
node_type: NodeType::Literal {
name: "String".to_string(),
},
};
let mut buf = Vec::new();
data.write_into(&mut buf).unwrap();
let mut data_cursor: Cursor<&[u8]> = Cursor::new(&buf);
let read_data = BrigadierNodeStub::read_from(&mut data_cursor).unwrap();
assert_eq!(data, read_data);
}
#[test]
fn test_brigadier_node_stub_argument() {
let data = BrigadierNodeStub {
is_executable: false,
children: vec![6, 9],
redirect_node: Some(5),
node_type: NodeType::Argument {
name: "position".to_string(),
parser: BrigadierParser::Vec3,
suggestions_type: Some(ResourceLocation::new("minecraft:test_suggestion").unwrap()),
},
};
let mut buf = Vec::new();
data.write_into(&mut buf).unwrap();
let mut data_cursor: Cursor<&[u8]> = Cursor::new(&buf);
let read_data = BrigadierNodeStub::read_from(&mut data_cursor).unwrap();
assert_eq!(data, read_data);
}
}

View file

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

View file

@ -30,9 +30,9 @@ impl McBufReadable for ClientboundExplodePacket {
let mut to_blow = Vec::with_capacity(to_blow_len as usize);
for _ in 0..to_blow_len {
// the bytes are offsets from the main x y z
let x = x_floor + i8::read_from(buf)? as i32;
let y = y_floor + i8::read_from(buf)? as i32;
let z = z_floor + i8::read_from(buf)? as i32;
let x = x_floor + i32::from(i8::read_from(buf)?);
let y = y_floor + i32::from(i8::read_from(buf)?);
let z = z_floor + i32::from(i8::read_from(buf)?);
to_blow.push(BlockPos { x, y, z });
}

View file

@ -1,7 +1,7 @@
use azalea_buf::McBuf;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
#[derive(Clone, Debug, ClientboundGamePacket, McBuf)]
pub struct ClientboundInitializeBorderPacket {
pub new_center_x: f64,
pub new_center_z: f64,

View file

@ -1,4 +1,4 @@
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufWritable};
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable};
use azalea_core::ParticleData;
use azalea_protocol_macros::ClientboundGamePacket;
use std::io::{Cursor, Write};
@ -51,7 +51,18 @@ impl McBufReadable for ClientboundLevelParticlesPacket {
}
impl McBufWritable for ClientboundLevelParticlesPacket {
fn write_into(&self, _buf: &mut impl Write) -> Result<(), std::io::Error> {
todo!();
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.particle_id.var_write_into(buf)?;
self.override_limiter.write_into(buf)?;
self.x.write_into(buf)?;
self.y.write_into(buf)?;
self.z.write_into(buf)?;
self.x_dist.write_into(buf)?;
self.y_dist.write_into(buf)?;
self.z_dist.write_into(buf)?;
self.max_speed.write_into(buf)?;
self.count.write_into(buf)?;
self.data.write_without_id(buf)?;
Ok(())
}
}

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