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:
parent
13f1228ecd
commit
8d3ad63012
177 changed files with 20319 additions and 13834 deletions
970
Cargo.lock
generated
970
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -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
|
||||
|
|
|
@ -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
12
azalea-auth/Cargo.toml
Executable file → Normal 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"]}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod auth;
|
||||
mod cache;
|
||||
pub mod game_profile;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod behavior;
|
||||
mod blocks;
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 } => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod arguments;
|
||||
pub mod builder;
|
||||
pub mod command_dispatcher;
|
||||
|
|
|
@ -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 == '.'
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
192
azalea-buf/azalea-buf-macros/src/write.rs
Normal file
192
azalea-buf/azalea-buf-macros/src/write.rs
Normal 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"),
|
||||
}
|
||||
}
|
|
@ -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()];
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"]}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
```
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
49
azalea-client/examples/echo.rs
Normal file
49
azalea-client/examples/echo.rs
Normal 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);
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -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
109
azalea-client/src/entity_query.rs
Normal file
109
azalea-client/src/entity_query.rs
Normal 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
189
azalea-client/src/events.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
170
azalea-client/src/local_player.rs
Normal file
170
azalea-client/src/local_player.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
999
azalea-client/src/packet_handling.rs
Normal file
999
azalea-client/src/packet_handling.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
177
azalea-client/src/task_pool.rs
Normal file
177
azalea-client/src/task_pool.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Azalea Core
|
||||
|
||||
Miscellaneous things in Azalea.
|
||||
Random miscellaneous things like `bitsets` and `Vec3` that don't deserve their own crate.
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod signing;
|
||||
|
||||
use aes::cipher::inout::InOutBuf;
|
||||
|
|
13
azalea-ecs/Cargo.toml
Normal file
13
azalea-ecs/Cargo.toml
Normal 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"]}
|
15
azalea-ecs/azalea-ecs-macros/Cargo.toml
Executable file
15
azalea-ecs/azalea-ecs-macros/Cargo.toml
Executable 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"
|
125
azalea-ecs/azalea-ecs-macros/src/component.rs
Normal file
125
azalea-ecs/azalea-ecs-macros/src/component.rs
Normal 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 }
|
||||
}
|
466
azalea-ecs/azalea-ecs-macros/src/fetch.rs
Normal file
466
azalea-ecs/azalea-ecs-macros/src/fetch.rs
Normal 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 }
|
||||
}
|
523
azalea-ecs/azalea-ecs-macros/src/lib.rs
Executable file
523
azalea-ecs/azalea-ecs-macros/src/lib.rs
Executable 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 = ¶ms[0..param_count];
|
||||
let param_fetch = ¶ms_fetch[0..param_count];
|
||||
let meta = &metas[0..param_count];
|
||||
let param_fn_mut = ¶m_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)
|
||||
}
|
45
azalea-ecs/azalea-ecs-macros/src/utils/attrs.rs
Normal file
45
azalea-ecs/azalea-ecs-macros/src/utils/attrs.rs
Normal 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} = ...`"
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
224
azalea-ecs/azalea-ecs-macros/src/utils/mod.rs
Normal file
224
azalea-ecs/azalea-ecs-macros/src/utils/mod.rs
Normal 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()
|
||||
}
|
21
azalea-ecs/azalea-ecs-macros/src/utils/shape.rs
Normal file
21
azalea-ecs/azalea-ecs-macros/src/utils/shape.rs
Normal 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",
|
||||
)),
|
||||
}
|
||||
}
|
35
azalea-ecs/azalea-ecs-macros/src/utils/symbol.rs
Normal file
35
azalea-ecs/azalea-ecs-macros/src/utils/symbol.rs
Normal 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
148
azalea-ecs/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -2,3 +2,8 @@
|
|||
|
||||
Translate Minecraft strings from their id.
|
||||
|
||||
# Examples
|
||||
|
||||
```
|
||||
assert_eq!(azalea_language::get("translation.test.none"), Some("Hello, world!"));
|
||||
```
|
|
@ -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!"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
)]))
|
||||
)]))
|
||||
);
|
||||
```
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod decode;
|
||||
mod encode;
|
||||
mod error;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -539,7 +539,7 @@ impl VoxelShape {
|
|||
x_coords[var7 as usize],
|
||||
y_coords[var8 as usize],
|
||||
z_coords[var9 as usize],
|
||||
)
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
11
azalea-protocol/Cargo.toml
Executable file → Normal 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"
|
|
@ -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 })),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
225
azalea-protocol/examples/handshake_proxy.rs
Normal file
225
azalea-protocol/examples/handshake_proxy.rs
Normal 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(())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue