mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
merge
This commit is contained in:
commit
3a72485aba
91 changed files with 3328 additions and 4118 deletions
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
|
@ -1,4 +1,6 @@
|
|||
on: push
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
name: Clippy check
|
||||
jobs:
|
||||
clippy_check:
|
||||
|
|
1076
Cargo.lock
generated
1076
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,6 @@ members = [
|
|||
"azalea-buf",
|
||||
"azalea-physics",
|
||||
"azalea-registry",
|
||||
"azalea-ecs",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
|
@ -9,18 +9,21 @@ version = "0.6.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.6.0" }
|
||||
azalea-crypto = {path = "../azalea-crypto", version = "^0.6.0" }
|
||||
chrono = {version = "0.4.22", default-features = false}
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
|
||||
azalea-crypto = { path = "../azalea-crypto", version = "^0.6.0" }
|
||||
chrono = { version = "0.4.22", default-features = false }
|
||||
log = "0.4.17"
|
||||
num-bigint = "0.4.3"
|
||||
reqwest = {version = "0.11.12", features = ["json"]}
|
||||
serde = {version = "1.0.145", features = ["derive"]}
|
||||
serde_json = "1.0.86"
|
||||
reqwest = { version = "0.11.12", default-features = false, features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
] }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.93"
|
||||
thiserror = "1.0.37"
|
||||
tokio = {version = "1.24.2", features = ["fs"]}
|
||||
uuid = {version = "^1.1.2", features = ["serde"]}
|
||||
tokio = { version = "1.24.2", features = ["fs"] }
|
||||
uuid = { version = "^1.1.2", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.9.3"
|
||||
tokio = {version = "1.24.2", features = ["full"]}
|
||||
tokio = { version = "1.24.2", features = ["full"] }
|
||||
|
|
|
@ -8,11 +8,11 @@ There's three block types, used for different things. You can (mostly) convert b
|
|||
|
||||
```
|
||||
# use azalea_block::BlockState;
|
||||
let block_state: BlockState = azalea_block::CobblestoneWallBlock {
|
||||
east: azalea_block::EastWall::Low,
|
||||
north: azalea_block::NorthWall::Low,
|
||||
south: azalea_block::SouthWall::Low,
|
||||
west: azalea_block::WestWall::Low,
|
||||
let block_state: BlockState = azalea_block::blocks::CobblestoneWall {
|
||||
east: azalea_block::properties::EastWall::Low,
|
||||
north: azalea_block::properties::NorthWall::Low,
|
||||
south: azalea_block::properties::SouthWall::Low,
|
||||
west: azalea_block::properties::WestWall::Low,
|
||||
up: false,
|
||||
waterlogged: false,
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ let block = Box::<dyn Block>::from(block_state);
|
|||
```
|
||||
# use azalea_block::{Block, BlockState};
|
||||
# let block_state: BlockState = azalea_registry::Block::Jukebox.into();
|
||||
if let Some(jukebox) = Box::<dyn Block>::from(block_state).downcast_ref::<azalea_block::JukeboxBlock>() {
|
||||
if let Some(jukebox) = Box::<dyn Block>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
|
|
@ -38,7 +38,7 @@ struct PropertyDefinitions {
|
|||
properties: Vec<PropertyDefinition>,
|
||||
}
|
||||
|
||||
/// `snowy: false` or `axis: Axis::Y`
|
||||
/// `snowy: false` or `axis: properties::Axis::Y`
|
||||
#[derive(Debug)]
|
||||
struct PropertyWithNameAndDefault {
|
||||
name: Ident,
|
||||
|
@ -59,7 +59,7 @@ struct BlockDefinition {
|
|||
}
|
||||
impl Parse for PropertyWithNameAndDefault {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
// `snowy: false` or `axis: Axis::Y`
|
||||
// `snowy: false` or `axis: properties::Axis::Y`
|
||||
let property_name = input.parse()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
|
||||
|
@ -74,7 +74,7 @@ impl Parse for PropertyWithNameAndDefault {
|
|||
is_enum = true;
|
||||
property_type = first_ident;
|
||||
let variant = input.parse::<Ident>()?;
|
||||
property_default.extend(quote! { ::#variant });
|
||||
property_default = quote! { properties::#property_default::#variant };
|
||||
} else if first_ident_string == "true" || first_ident_string == "false" {
|
||||
property_type = Ident::new("bool", first_ident.span());
|
||||
} else {
|
||||
|
@ -310,6 +310,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
let mut from_state_to_block_match = quote! {};
|
||||
let mut from_registry_block_to_block_match = quote! {};
|
||||
let mut from_registry_block_to_blockstate_match = quote! {};
|
||||
let mut from_registry_block_to_blockstates_match = quote! {};
|
||||
|
||||
for block in &input.block_definitions.blocks {
|
||||
let block_property_names = &block
|
||||
|
@ -386,13 +387,16 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
for PropertyWithNameAndDefault {
|
||||
property_type: struct_name,
|
||||
name,
|
||||
is_enum,
|
||||
..
|
||||
} in &properties_with_name
|
||||
{
|
||||
// let property_name_snake =
|
||||
// Ident::new(&property.to_string(), proc_macro2::Span::call_site());
|
||||
block_struct_fields.extend(quote! {
|
||||
pub #name: #struct_name,
|
||||
block_struct_fields.extend(if *is_enum {
|
||||
quote! { pub #name: properties::#struct_name, }
|
||||
} else {
|
||||
quote! { pub #name: #struct_name, }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -400,10 +404,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
&to_pascal_case(&block.name.to_string()),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let block_struct_name = Ident::new(
|
||||
&format!("{block_name_pascal_case}Block"),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let block_struct_name = Ident::new(&block_name_pascal_case.to_string(), proc_macro2::Span::call_site());
|
||||
|
||||
let mut from_block_to_state_match_inner = quote! {};
|
||||
|
||||
|
@ -445,7 +446,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
|
||||
let property_type = if property.is_enum {
|
||||
quote! {#property_struct_name_ident::#variant}
|
||||
quote! {properties::#property_struct_name_ident::#variant}
|
||||
} else {
|
||||
quote! {#variant}
|
||||
};
|
||||
|
@ -476,9 +477,9 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
// 7035..=7058 => {
|
||||
// let b = b - 7035;
|
||||
// &AcaciaButtonBlock {
|
||||
// powered: Powered::from((b / 1) % 2),
|
||||
// facing: Facing::from((b / 2) % 4),
|
||||
// face: Face::from((b / 8) % 3),
|
||||
// powered: properties::Powered::from((b / 1) % 2),
|
||||
// facing: properties::Facing::from((b / 2) % 4),
|
||||
// face: properties::Face::from((b / 8) % 3),
|
||||
// }
|
||||
// }
|
||||
let mut from_state_to_block_inner = quote! {};
|
||||
|
@ -498,7 +499,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
// this is not a mistake, it starts with true for some reason
|
||||
quote! {(b / #division) % #property_variants_count == 0}
|
||||
} else {
|
||||
quote! {#property_struct_name_ident::from((b / #division) % #property_variants_count)}
|
||||
quote! {properties::#property_struct_name_ident::from((b / #division) % #property_variants_count)}
|
||||
}
|
||||
};
|
||||
from_state_to_block_inner.extend(quote! {
|
||||
|
@ -523,6 +524,9 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
from_registry_block_to_blockstate_match.extend(quote! {
|
||||
azalea_registry::Block::#block_name_pascal_case => BlockState { id: #default_state_id },
|
||||
});
|
||||
from_registry_block_to_blockstates_match.extend(quote! {
|
||||
azalea_registry::Block::#block_name_pascal_case => BlockStates::from(#first_state_id..=#last_state_id),
|
||||
});
|
||||
|
||||
let mut block_default_fields = quote! {};
|
||||
for PropertyWithNameAndDefault {
|
||||
|
@ -560,14 +564,14 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
fn id(&self) -> &'static str {
|
||||
#block_id
|
||||
}
|
||||
fn as_blockstate(&self) -> BlockState {
|
||||
fn as_block_state(&self) -> BlockState {
|
||||
#from_block_to_state_match
|
||||
}
|
||||
}
|
||||
|
||||
impl From<#block_struct_name> for BlockState {
|
||||
fn from(b: #block_struct_name) -> Self {
|
||||
b.as_blockstate()
|
||||
b.as_block_state()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -585,21 +589,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
|
||||
let last_state_id = state_id - 1;
|
||||
let mut generated = quote! {
|
||||
#property_enums
|
||||
|
||||
/// A representation of a state a block can be in. (for example, a stone
|
||||
/// block only has one state but each possible stair rotation is a
|
||||
/// different state).
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Default)]
|
||||
pub struct BlockState {
|
||||
/// The protocol ID for the block state. IDs may change every
|
||||
/// version, so you shouldn't hard-code them or store them in databases.
|
||||
pub id: u32
|
||||
}
|
||||
|
||||
impl BlockState {
|
||||
pub const AIR: BlockState = BlockState { id: 0 };
|
||||
|
||||
/// Returns the highest possible state ID.
|
||||
#[inline]
|
||||
pub fn max_state() -> u32 {
|
||||
|
@ -607,38 +597,50 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BlockState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BlockState(id: {}, {:?})", self.id, Box::<dyn Block>::from(*self))
|
||||
}
|
||||
pub mod properties {
|
||||
use super::*;
|
||||
|
||||
#property_enums
|
||||
}
|
||||
};
|
||||
|
||||
generated.extend(quote! {
|
||||
#block_structs
|
||||
pub mod blocks {
|
||||
use super::*;
|
||||
|
||||
impl From<BlockState> for Box<dyn Block> {
|
||||
fn from(block_state: BlockState) -> Self {
|
||||
let b = block_state.id;
|
||||
match b {
|
||||
#from_state_to_block_match
|
||||
_ => panic!("Invalid block state: {}", b),
|
||||
#block_structs
|
||||
|
||||
impl From<BlockState> for Box<dyn Block> {
|
||||
fn from(block_state: BlockState) -> Self {
|
||||
let b = block_state.id;
|
||||
match b {
|
||||
#from_state_to_block_match
|
||||
_ => panic!("Invalid block state: {}", b),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<azalea_registry::Block> for Box<dyn Block> {
|
||||
fn from(block: azalea_registry::Block) -> Self {
|
||||
match block {
|
||||
#from_registry_block_to_block_match
|
||||
_ => unreachable!("There should always be a block struct for every azalea_registry::Block variant")
|
||||
impl From<azalea_registry::Block> for Box<dyn Block> {
|
||||
fn from(block: azalea_registry::Block) -> Self {
|
||||
match block {
|
||||
#from_registry_block_to_block_match
|
||||
_ => unreachable!("There should always be a block struct for every azalea_registry::Block variant")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<azalea_registry::Block> for BlockState {
|
||||
fn from(block: azalea_registry::Block) -> Self {
|
||||
match block {
|
||||
#from_registry_block_to_blockstate_match
|
||||
_ => unreachable!("There should always be a block state for every azalea_registry::Block variant")
|
||||
impl From<azalea_registry::Block> for BlockState {
|
||||
fn from(block: azalea_registry::Block) -> Self {
|
||||
match block {
|
||||
#from_registry_block_to_blockstate_match
|
||||
_ => unreachable!("There should always be a block state for every azalea_registry::Block variant")
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<azalea_registry::Block> for BlockStates {
|
||||
fn from(block: azalea_registry::Block) -> Self {
|
||||
match block {
|
||||
#from_registry_block_to_blockstates_match
|
||||
_ => unreachable!("There should always be a block state for every azalea_registry::Block variant")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crate::BlockBehavior;
|
||||
use crate::{Block, BlockBehavior, BlockState, BlockStates};
|
||||
use azalea_block_macros::make_block_states;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait Block: Debug + Any {
|
||||
fn behavior(&self) -> BlockBehavior;
|
||||
fn id(&self) -> &'static str;
|
||||
fn as_blockstate(&self) -> BlockState;
|
||||
}
|
||||
impl dyn Block {
|
||||
pub fn downcast_ref<T: Block>(&self) -> Option<&T> {
|
||||
(self as &dyn Any).downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
make_block_states! {
|
||||
Properties => {
|
||||
"snowy" => bool,
|
|
@ -2,14 +2,49 @@
|
|||
#![feature(trait_upcasting)]
|
||||
|
||||
mod behavior;
|
||||
mod blocks;
|
||||
mod generated;
|
||||
mod range;
|
||||
|
||||
pub use generated::{blocks, properties};
|
||||
|
||||
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable};
|
||||
pub use behavior::BlockBehavior;
|
||||
pub use blocks::*;
|
||||
use std::io::{Cursor, Write};
|
||||
use core::fmt::Debug;
|
||||
pub use range::BlockStates;
|
||||
use std::{
|
||||
any::Any,
|
||||
io::{Cursor, Write},
|
||||
};
|
||||
|
||||
pub trait Block: Debug + Any {
|
||||
fn behavior(&self) -> BlockBehavior;
|
||||
/// Get the Minecraft ID for this block. For example `stone` or
|
||||
/// `grass_block`.
|
||||
fn id(&self) -> &'static str;
|
||||
/// Convert the block to a block state. This is lossless, as the block
|
||||
/// contains all the state data.
|
||||
fn as_block_state(&self) -> BlockState;
|
||||
}
|
||||
impl dyn Block {
|
||||
pub fn downcast_ref<T: Block>(&self) -> Option<&T> {
|
||||
(self as &dyn Any).downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A representation of a state a block can be in.
|
||||
///
|
||||
/// For example, a stone block only has one state but each possible stair
|
||||
/// rotation is a different state.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)]
|
||||
pub struct BlockState {
|
||||
/// The protocol ID for the block state. IDs may change every
|
||||
/// version, so you shouldn't hard-code them or store them in databases.
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
impl BlockState {
|
||||
pub const AIR: BlockState = BlockState { id: 0 };
|
||||
|
||||
/// Transmutes a u32 to a block state.
|
||||
///
|
||||
/// # Safety
|
||||
|
@ -52,6 +87,17 @@ impl McBufWritable for BlockState {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BlockState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"BlockState(id: {}, {:?})",
|
||||
self.id,
|
||||
Box::<dyn Block>::from(*self)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -80,18 +126,14 @@ mod tests {
|
|||
"{:?}",
|
||||
BlockState::from(azalea_registry::Block::FloweringAzalea)
|
||||
);
|
||||
assert!(
|
||||
formatted.ends_with(", FloweringAzaleaBlock)"),
|
||||
"{}",
|
||||
formatted
|
||||
);
|
||||
assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
|
||||
|
||||
let formatted = format!(
|
||||
"{:?}",
|
||||
BlockState::from(azalea_registry::Block::BigDripleafStem)
|
||||
);
|
||||
assert!(
|
||||
formatted.ends_with(", BigDripleafStemBlock { facing: North, waterlogged: false })"),
|
||||
formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
|
||||
"{}",
|
||||
formatted
|
||||
);
|
||||
|
|
33
azalea-block/src/range.rs
Normal file
33
azalea-block/src/range.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use std::{collections::HashSet, ops::RangeInclusive};
|
||||
|
||||
use crate::BlockState;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockStates {
|
||||
pub set: HashSet<BlockState>,
|
||||
}
|
||||
|
||||
impl From<RangeInclusive<u32>> for BlockStates {
|
||||
fn from(range: RangeInclusive<u32>) -> Self {
|
||||
let mut set = HashSet::with_capacity((range.end() - range.start() + 1) as usize);
|
||||
for id in range {
|
||||
set.insert(BlockState { id });
|
||||
}
|
||||
Self { set }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for BlockStates {
|
||||
type Item = BlockState;
|
||||
type IntoIter = std::collections::hash_set::IntoIter<BlockState>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.set.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockStates {
|
||||
pub fn contains(&self, state: &BlockState) -> bool {
|
||||
self.set.contains(state)
|
||||
}
|
||||
}
|
|
@ -18,5 +18,5 @@ azalea-buf = { path = "../azalea-buf", features = [
|
|||
azalea-language = { path = "../azalea-language", version = "^0.6.0" }
|
||||
log = "0.4.17"
|
||||
once_cell = "1.16.0"
|
||||
serde = {version = "^1.0.148", features = ["derive"]}
|
||||
serde_json = "^1.0.72"
|
||||
serde = { version = "^1.0.152", features = ["derive"] }
|
||||
serde_json = "^1.0.93"
|
||||
|
|
|
@ -11,29 +11,28 @@ version = "0.6.0"
|
|||
[dependencies]
|
||||
anyhow = "1.0.59"
|
||||
async-trait = "0.1.58"
|
||||
azalea-auth = {path = "../azalea-auth", version = "0.6.0"}
|
||||
azalea-block = {path = "../azalea-block", version = "0.6.0"}
|
||||
azalea-chat = {path = "../azalea-chat", version = "0.6.0"}
|
||||
azalea-core = {path = "../azalea-core", version = "0.6.0"}
|
||||
azalea-crypto = {path = "../azalea-crypto", version = "0.6.0"}
|
||||
azalea-ecs = {path = "../azalea-ecs", version = "0.6.0"}
|
||||
azalea-physics = {path = "../azalea-physics", version = "0.6.0"}
|
||||
azalea-protocol = {path = "../azalea-protocol", version = "0.6.0"}
|
||||
azalea-registry = {path = "../azalea-registry", version = "0.6.0"}
|
||||
azalea-world = {path = "../azalea-world", version = "0.6.0"}
|
||||
bevy_tasks = "0.9.1"
|
||||
bevy_time = "0.9.1"
|
||||
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
|
||||
azalea-auth = { path = "../azalea-auth", version = "0.6.0" }
|
||||
azalea-block = { path = "../azalea-block", version = "0.6.0" }
|
||||
azalea-chat = { path = "../azalea-chat", version = "0.6.0" }
|
||||
azalea-core = { path = "../azalea-core", version = "0.6.0" }
|
||||
azalea-crypto = { path = "../azalea-crypto", version = "0.6.0" }
|
||||
azalea-physics = { path = "../azalea-physics", version = "0.6.0" }
|
||||
azalea-protocol = { path = "../azalea-protocol", version = "0.6.0" }
|
||||
azalea-registry = { path = "../azalea-registry", version = "0.6.0" }
|
||||
azalea-world = { path = "../azalea-world", version = "0.6.0" }
|
||||
bevy_app = "0.10.0"
|
||||
bevy_ecs = "0.10.0"
|
||||
bevy_log = "0.10.0"
|
||||
bevy_tasks = "0.10.0"
|
||||
bevy_time = "0.10.0"
|
||||
derive_more = { version = "0.99.17", features = ["deref", "deref_mut"] }
|
||||
futures = "0.3.25"
|
||||
log = "0.4.17"
|
||||
nohash-hasher = "0.2.0"
|
||||
once_cell = "1.16.0"
|
||||
parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]}
|
||||
parking_lot = { version = "^0.12.1", features = ["deadlock_detection"] }
|
||||
regex = "1.7.0"
|
||||
thiserror = "^1.0.34"
|
||||
tokio = {version = "^1.24.2", features = ["sync"]}
|
||||
tokio = { version = "^1.24.2", features = ["sync"] }
|
||||
typemap_rev = "0.3.0"
|
||||
uuid = "^1.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.9.1"
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
//! 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);
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,25 +2,28 @@
|
|||
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_crypto::MessageSignature;
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
entity::Entity,
|
||||
event::{EventReader, EventWriter},
|
||||
schedule::IntoSystemDescriptor,
|
||||
};
|
||||
use azalea_protocol::packets::game::{
|
||||
clientbound_player_chat_packet::{ClientboundPlayerChatPacket, LastSeenMessagesUpdate},
|
||||
clientbound_system_chat_packet::ClientboundSystemChatPacket,
|
||||
serverbound_chat_command_packet::ServerboundChatCommandPacket,
|
||||
serverbound_chat_packet::ServerboundChatPacket,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
event::{EventReader, EventWriter},
|
||||
schedule::{IntoSystemConfig, IntoSystemConfigs},
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{client::Client, local_player::SendPacketEvent};
|
||||
use crate::{
|
||||
client::Client,
|
||||
local_player::{handle_send_packet_event, SendPacketEvent},
|
||||
};
|
||||
|
||||
/// A chat packet, either a system message or a chat message.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -147,6 +150,7 @@ impl Client {
|
|||
entity: self.entity,
|
||||
content: content.to_string(),
|
||||
});
|
||||
self.run_schedule_sender.send(()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,15 +160,12 @@ impl Plugin for ChatPlugin {
|
|||
app.add_event::<SendChatEvent>()
|
||||
.add_event::<SendChatKindEvent>()
|
||||
.add_event::<ChatReceivedEvent>()
|
||||
.add_system(
|
||||
handle_send_chat_event
|
||||
.label("handle_send_chat_event")
|
||||
.after("packet"),
|
||||
)
|
||||
.add_system(
|
||||
handle_send_chat_kind_event
|
||||
.label("handle_send_chat_kind_event")
|
||||
.after("handle_send_chat_event"),
|
||||
.add_systems(
|
||||
(
|
||||
handle_send_chat_event,
|
||||
handle_send_chat_kind_event.after(handle_send_packet_event),
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -233,9 +234,15 @@ fn handle_send_chat_kind_event(
|
|||
mut send_packet_events: EventWriter<SendPacketEvent>,
|
||||
) {
|
||||
for event in events.iter() {
|
||||
let content = event
|
||||
.content
|
||||
.chars()
|
||||
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
|
||||
.take(256)
|
||||
.collect::<String>();
|
||||
let packet = match event.kind {
|
||||
ChatPacketKind::Message => ServerboundChatPacket {
|
||||
message: event.content.to_string(),
|
||||
message: content,
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time shouldn't be before epoch")
|
||||
|
@ -251,7 +258,7 @@ fn handle_send_chat_kind_event(
|
|||
ChatPacketKind::Command => {
|
||||
// TODO: chat signing
|
||||
ServerboundChatCommandPacket {
|
||||
command: event.content.to_string(),
|
||||
command: content,
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time shouldn't be before epoch")
|
||||
|
|
|
@ -6,25 +6,16 @@ use crate::{
|
|||
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
|
||||
LocalPlayer, PhysicsState, SendPacketEvent,
|
||||
},
|
||||
movement::{local_player_ai_step, send_position, sprint_listener, walk_listener},
|
||||
movement::PlayerMovePlugin,
|
||||
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
|
||||
player::retroactively_add_game_profile_component,
|
||||
task_pool::TaskPoolPlugin,
|
||||
Account, PlayerInfo, StartSprintEvent, StartWalkEvent,
|
||||
Account, PlayerInfo,
|
||||
};
|
||||
|
||||
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin, PluginGroup, PluginGroupBuilder},
|
||||
bundle::Bundle,
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
schedule::{IntoSystemDescriptor, Schedule, Stage, SystemSet},
|
||||
AppTickExt,
|
||||
};
|
||||
use azalea_ecs::{ecs::Ecs, TickPlugin};
|
||||
use azalea_physics::PhysicsPlugin;
|
||||
use azalea_physics::{PhysicsPlugin, PhysicsSet};
|
||||
use azalea_protocol::{
|
||||
connect::{Connection, ConnectionError},
|
||||
packets::{
|
||||
|
@ -47,18 +38,28 @@ use azalea_protocol::{
|
|||
resolver, ServerAddress,
|
||||
};
|
||||
use azalea_world::{
|
||||
entity::{EntityPlugin, Local, WorldName},
|
||||
PartialWorld, World, WorldContainer,
|
||||
entity::{EntityPlugin, EntityUpdateSet, Local, WorldName},
|
||||
Instance, InstanceContainer, PartialInstance,
|
||||
};
|
||||
use bevy_app::{App, CoreSchedule, Plugin, PluginGroup, PluginGroupBuilder};
|
||||
use bevy_ecs::{
|
||||
bundle::Bundle,
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
schedule::IntoSystemConfig,
|
||||
schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
|
||||
world::World,
|
||||
};
|
||||
use bevy_log::LogPlugin;
|
||||
use bevy_time::{prelude::FixedTime, TimePlugin};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use log::{debug, error};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc};
|
||||
use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration};
|
||||
use thiserror::Error;
|
||||
use tokio::{sync::mpsc, time};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type ClientInformation = ServerboundClientInformationPacket;
|
||||
|
||||
/// `Client` has the things that a user interacting with the library will want.
|
||||
/// Things that a player in the world will want to know are in [`LocalPlayer`].
|
||||
///
|
||||
|
@ -75,23 +76,32 @@ pub struct Client {
|
|||
/// and skin data.
|
||||
///
|
||||
/// This is immutable; the server cannot change it. To get the username and
|
||||
/// skin the server chose for you, get your player from
|
||||
/// [`Self::players`].
|
||||
/// skin the server chose for you, get your player from the [`TabList`]
|
||||
/// component.
|
||||
pub profile: GameProfile,
|
||||
/// The entity for this client in the ECS.
|
||||
pub entity: Entity,
|
||||
/// The world that this client is in.
|
||||
pub world: Arc<RwLock<PartialWorld>>,
|
||||
pub world: Arc<RwLock<PartialInstance>>,
|
||||
|
||||
/// The entity component system. You probably don't need to access this
|
||||
/// directly. Note that if you're using a shared world (i.e. a swarm), this
|
||||
/// will contain all entities in all worlds.
|
||||
pub ecs: Arc<Mutex<Ecs>>,
|
||||
pub ecs: Arc<Mutex<World>>,
|
||||
|
||||
/// Use this to force the client to run the schedule outside of a tick.
|
||||
pub run_schedule_sender: mpsc::UnboundedSender<()>,
|
||||
}
|
||||
|
||||
/// A component that contains some of the "settings" for this client that are
|
||||
/// sent to the server, such as render distance.
|
||||
pub type ClientInformation = ServerboundClientInformationPacket;
|
||||
|
||||
/// A component that contains a map of player UUIDs to their information in the
|
||||
/// tab list
|
||||
#[derive(Component, Clone, Debug, Deref, DerefMut, Default)]
|
||||
pub struct TabList(HashMap<Uuid, PlayerInfo>);
|
||||
|
||||
/// An error that happened while joining the server.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum JoinError {
|
||||
|
@ -120,14 +130,14 @@ impl Client {
|
|||
pub fn new(
|
||||
profile: GameProfile,
|
||||
entity: Entity,
|
||||
ecs: Arc<Mutex<Ecs>>,
|
||||
ecs: Arc<Mutex<World>>,
|
||||
run_schedule_sender: mpsc::UnboundedSender<()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
// default our id to 0, it'll be set later
|
||||
entity,
|
||||
world: Arc::new(RwLock::new(PartialWorld::default())),
|
||||
world: Arc::new(RwLock::new(PartialInstance::default())),
|
||||
|
||||
ecs,
|
||||
|
||||
|
@ -180,7 +190,7 @@ impl Client {
|
|||
/// Create a [`Client`] when you already have the ECS made with
|
||||
/// [`start_ecs`]. You'd usually want to use [`Self::join`] instead.
|
||||
pub async fn start_client(
|
||||
ecs_lock: Arc<Mutex<Ecs>>,
|
||||
ecs_lock: Arc<Mutex<World>>,
|
||||
account: &Account,
|
||||
address: &ServerAddress,
|
||||
resolved_address: &SocketAddr,
|
||||
|
@ -226,7 +236,7 @@ impl Client {
|
|||
packet_writer_sender,
|
||||
// default to an empty world, it'll be set correctly later when we
|
||||
// get the login packet
|
||||
Arc::new(RwLock::new(World::default())),
|
||||
Arc::new(RwLock::new(Instance::default())),
|
||||
read_packets_task,
|
||||
write_packets_task,
|
||||
);
|
||||
|
@ -237,6 +247,8 @@ impl Client {
|
|||
game_profile: GameProfileComponent(game_profile),
|
||||
physics_state: PhysicsState::default(),
|
||||
local_player_events: LocalPlayerEvents(tx),
|
||||
client_information: ClientInformation::default(),
|
||||
tab_list: TabList::default(),
|
||||
_local: Local,
|
||||
});
|
||||
|
||||
|
@ -383,13 +395,13 @@ impl Client {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn local_player<'a>(&'a self, ecs: &'a mut Ecs) -> &'a LocalPlayer {
|
||||
pub fn local_player<'a>(&'a self, ecs: &'a mut World) -> &'a LocalPlayer {
|
||||
self.query::<&LocalPlayer>(ecs)
|
||||
}
|
||||
pub fn local_player_mut<'a>(
|
||||
&'a self,
|
||||
ecs: &'a mut Ecs,
|
||||
) -> azalea_ecs::ecs::Mut<'a, LocalPlayer> {
|
||||
ecs: &'a mut World,
|
||||
) -> bevy_ecs::world::Mut<'a, LocalPlayer> {
|
||||
self.query::<&mut LocalPlayer>(ecs)
|
||||
}
|
||||
|
||||
|
@ -413,14 +425,14 @@ impl Client {
|
|||
|
||||
/// Get a reference to our (potentially shared) world.
|
||||
///
|
||||
/// This gets the [`World`] from our world container. If it's a normal
|
||||
/// This gets the [`Instance`] from our world container. If it's a normal
|
||||
/// client, then it'll be the same as the world the client has loaded.
|
||||
/// If the client using a shared world, then the shared world will be a
|
||||
/// superset of the client's world.
|
||||
pub fn world(&self) -> Arc<RwLock<World>> {
|
||||
pub fn world(&self) -> Arc<RwLock<Instance>> {
|
||||
let world_name = self.component::<WorldName>();
|
||||
let ecs = self.ecs.lock();
|
||||
let world_container = ecs.resource::<WorldContainer>();
|
||||
let world_container = ecs.resource::<InstanceContainer>();
|
||||
world_container.get(&world_name).unwrap()
|
||||
}
|
||||
|
||||
|
@ -451,32 +463,21 @@ impl Client {
|
|||
client_information: ServerboundClientInformationPacket,
|
||||
) -> Result<(), std::io::Error> {
|
||||
{
|
||||
self.local_player_mut(&mut self.ecs.lock())
|
||||
.client_information = client_information;
|
||||
let mut ecs = self.ecs.lock();
|
||||
let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs);
|
||||
*client_information_mut = client_information.clone();
|
||||
}
|
||||
|
||||
if self.logged_in() {
|
||||
let client_information_packet = self
|
||||
.local_player(&mut self.ecs.lock())
|
||||
.client_information
|
||||
.clone()
|
||||
.get();
|
||||
log::debug!(
|
||||
"Sending client information (already logged in): {:?}",
|
||||
client_information_packet
|
||||
client_information
|
||||
);
|
||||
self.write_packet(client_information_packet);
|
||||
self.write_packet(client_information.get());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a HashMap of all the players in the tab list.
|
||||
///
|
||||
/// Internally, this fetches the `players` field in [`LocalPlayer`].
|
||||
pub fn players(&mut self) -> HashMap<Uuid, PlayerInfo> {
|
||||
self.local_player(&mut self.ecs.lock()).players.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A bundle for the components that are present on a local player that received
|
||||
|
@ -488,49 +489,33 @@ pub struct JoinedClientBundle {
|
|||
pub game_profile: GameProfileComponent,
|
||||
pub physics_state: PhysicsState,
|
||||
pub local_player_events: LocalPlayerEvents,
|
||||
pub client_information: ClientInformation,
|
||||
pub tab_list: TabList,
|
||||
pub _local: Local,
|
||||
}
|
||||
|
||||
pub struct AzaleaPlugin;
|
||||
impl Plugin for AzaleaPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<StartWalkEvent>()
|
||||
.add_event::<StartSprintEvent>();
|
||||
// Minecraft ticks happen every 50ms
|
||||
app.insert_resource(FixedTime::new(Duration::from_millis(50)));
|
||||
|
||||
app.add_tick_system_set(
|
||||
SystemSet::new()
|
||||
.with_system(send_position)
|
||||
.with_system(update_in_loaded_chunk)
|
||||
.with_system(
|
||||
local_player_ai_step
|
||||
.before("ai_step")
|
||||
.after("sprint_listener"),
|
||||
),
|
||||
app.add_system(
|
||||
update_in_loaded_chunk
|
||||
.after(PhysicsSet)
|
||||
.after(handle_send_packet_event),
|
||||
);
|
||||
|
||||
// fire the Death event when the player dies.
|
||||
app.add_system(death_event.after("tick").after("packet"));
|
||||
|
||||
// walk and sprint event listeners
|
||||
app.add_system(walk_listener.label("walk_listener").before("travel"))
|
||||
.add_system(
|
||||
sprint_listener
|
||||
.label("sprint_listener")
|
||||
.before("travel")
|
||||
.before("walk_listener"),
|
||||
);
|
||||
app.add_system(death_event);
|
||||
|
||||
// add GameProfileComponent when we get an AddPlayerEvent
|
||||
app.add_system(
|
||||
retroactively_add_game_profile_component
|
||||
.after("tick")
|
||||
.after("packet"),
|
||||
);
|
||||
app.add_system(retroactively_add_game_profile_component.after(EntityUpdateSet::Index));
|
||||
|
||||
app.add_event::<SendPacketEvent>()
|
||||
.add_system(handle_send_packet_event.after("tick").after("packet"));
|
||||
.add_system(handle_send_packet_event);
|
||||
|
||||
app.init_resource::<WorldContainer>();
|
||||
app.init_resource::<InstanceContainer>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -549,6 +534,14 @@ pub fn init_ecs_app() -> App {
|
|||
// you might be able to just drop the lock or put it in its own scope to fix
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
app.edit_schedule(CoreSchedule::Main, |schedule| {
|
||||
schedule.set_build_settings(ScheduleBuildSettings {
|
||||
ambiguity_detection: LogLevel::Warn,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
app.add_plugins(DefaultPlugins);
|
||||
app
|
||||
}
|
||||
|
@ -557,17 +550,19 @@ pub fn init_ecs_app() -> App {
|
|||
/// first.
|
||||
#[doc(hidden)]
|
||||
pub fn start_ecs(
|
||||
app: App,
|
||||
mut app: App,
|
||||
run_schedule_receiver: mpsc::UnboundedReceiver<()>,
|
||||
run_schedule_sender: mpsc::UnboundedSender<()>,
|
||||
) -> Arc<Mutex<Ecs>> {
|
||||
) -> Arc<Mutex<World>> {
|
||||
app.setup();
|
||||
|
||||
// all resources should have been added by now so we can take the ecs from the
|
||||
// app
|
||||
let ecs = Arc::new(Mutex::new(app.world));
|
||||
|
||||
tokio::spawn(run_schedule_loop(
|
||||
ecs.clone(),
|
||||
app.schedule,
|
||||
app.outer_schedule_label,
|
||||
run_schedule_receiver,
|
||||
));
|
||||
tokio::spawn(tick_run_schedule_loop(run_schedule_sender));
|
||||
|
@ -576,14 +571,16 @@ pub fn start_ecs(
|
|||
}
|
||||
|
||||
async fn run_schedule_loop(
|
||||
ecs: Arc<Mutex<Ecs>>,
|
||||
mut schedule: Schedule,
|
||||
ecs: Arc<Mutex<World>>,
|
||||
outer_schedule_label: Box<dyn ScheduleLabel>,
|
||||
mut run_schedule_receiver: mpsc::UnboundedReceiver<()>,
|
||||
) {
|
||||
loop {
|
||||
// whenever we get an event from run_schedule_receiver, run the schedule
|
||||
run_schedule_receiver.recv().await;
|
||||
schedule.run(&mut ecs.lock());
|
||||
let mut ecs = ecs.lock();
|
||||
ecs.run_schedule_ref(&*outer_schedule_label);
|
||||
ecs.clear_trackers();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -611,14 +608,16 @@ pub struct DefaultPlugins;
|
|||
impl PluginGroup for DefaultPlugins {
|
||||
fn build(self) -> PluginGroupBuilder {
|
||||
PluginGroupBuilder::start::<Self>()
|
||||
.add(TickPlugin::default())
|
||||
.add(AzaleaPlugin)
|
||||
.add(LogPlugin::default())
|
||||
.add(TimePlugin::default())
|
||||
.add(PacketHandlerPlugin)
|
||||
.add(AzaleaPlugin)
|
||||
.add(EntityPlugin)
|
||||
.add(PhysicsPlugin)
|
||||
.add(EventPlugin)
|
||||
.add(TaskPoolPlugin::default())
|
||||
.add(ChatPlugin)
|
||||
.add(DisconnectPlugin)
|
||||
.add(PlayerMovePlugin)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
//! Disconnect a client from the server.
|
||||
|
||||
use azalea_ecs::{
|
||||
app::{App, CoreStage, Plugin},
|
||||
use bevy_app::{App, CoreSet, Plugin};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
event::{EventReader, EventWriter},
|
||||
query::Changed,
|
||||
schedule::IntoSystemDescriptor,
|
||||
schedule::IntoSystemConfigs,
|
||||
system::{Commands, Query},
|
||||
AppTickExt,
|
||||
};
|
||||
use derive_more::Deref;
|
||||
|
||||
|
@ -17,12 +16,15 @@ use crate::{client::JoinedClientBundle, LocalPlayer};
|
|||
pub struct DisconnectPlugin;
|
||||
impl Plugin for DisconnectPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<DisconnectEvent>()
|
||||
.add_system_to_stage(CoreStage::PostUpdate, handle_disconnect)
|
||||
.add_tick_system(
|
||||
update_read_packets_task_running_component.before(disconnect_on_read_packets_ended),
|
||||
app.add_event::<DisconnectEvent>().add_systems(
|
||||
(
|
||||
update_read_packets_task_running_component,
|
||||
disconnect_on_read_packets_ended,
|
||||
remove_components_from_disconnected_players,
|
||||
)
|
||||
.add_tick_system(disconnect_on_read_packets_ended);
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.chain(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +35,10 @@ pub struct DisconnectEvent {
|
|||
|
||||
/// System that removes the [`JoinedClientBundle`] from the entity when it
|
||||
/// receives a [`DisconnectEvent`].
|
||||
pub fn handle_disconnect(mut commands: Commands, mut events: EventReader<DisconnectEvent>) {
|
||||
pub fn remove_components_from_disconnected_players(
|
||||
mut commands: Commands,
|
||||
mut events: EventReader<DisconnectEvent>,
|
||||
) {
|
||||
for DisconnectEvent { entity } in events.iter() {
|
||||
commands.entity(*entity).remove::<JoinedClientBundle>();
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use azalea_ecs::{
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
ecs::Ecs,
|
||||
entity::Entity,
|
||||
query::{ROQueryItem, ReadOnlyWorldQuery, WorldQuery},
|
||||
world::World,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
|
@ -22,7 +22,7 @@ impl Client {
|
|||
/// .is_some();
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn query<'w, Q: WorldQuery>(&self, ecs: &'w mut Ecs) -> <Q as WorldQuery>::Item<'w> {
|
||||
pub fn query<'w, Q: WorldQuery>(&self, ecs: &'w mut World) -> <Q as WorldQuery>::Item<'w> {
|
||||
ecs.query::<Q>()
|
||||
.get_mut(ecs, self.entity)
|
||||
.expect("Our client is missing a required component.")
|
||||
|
@ -38,7 +38,7 @@ impl Client {
|
|||
/// Note that this will very likely change in the future.
|
||||
/// ```
|
||||
/// use azalea_client::{Client, GameProfileComponent};
|
||||
/// use azalea_ecs::query::With;
|
||||
/// use bevy_ecs::query::With;
|
||||
/// use azalea_world::entity::{Position, metadata::Player};
|
||||
///
|
||||
/// # fn example(mut bot: Client, sender_name: String) {
|
||||
|
@ -74,7 +74,7 @@ impl Client {
|
|||
}
|
||||
|
||||
pub trait EntityPredicate<Q: ReadOnlyWorldQuery, Filter: ReadOnlyWorldQuery> {
|
||||
fn find(&self, ecs_lock: Arc<Mutex<Ecs>>) -> Option<Entity>;
|
||||
fn find(&self, ecs_lock: Arc<Mutex<World>>) -> Option<Entity>;
|
||||
}
|
||||
impl<F, Q, Filter> EntityPredicate<(Q,), Filter> for F
|
||||
where
|
||||
|
@ -82,7 +82,7 @@ where
|
|||
Q: ReadOnlyWorldQuery,
|
||||
Filter: ReadOnlyWorldQuery,
|
||||
{
|
||||
fn find(&self, ecs_lock: Arc<Mutex<Ecs>>) -> Option<Entity> {
|
||||
fn find(&self, ecs_lock: Arc<Mutex<World>>) -> Option<Entity> {
|
||||
let mut ecs = ecs_lock.lock();
|
||||
let mut query = ecs.query_filtered::<(Entity, Q), Filter>();
|
||||
let entity = query.iter(&ecs).find(|(_, q)| (self)(q)).map(|(e, _)| e);
|
||||
|
|
|
@ -3,25 +3,19 @@
|
|||
|
||||
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 bevy_app::{App, CoreSchedule, IntoSystemAppConfig, Plugin};
|
||||
use bevy_ecs::{component::Component, event::EventReader, query::Added, system::Query};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
chat::{ChatPacket, ChatReceivedEvent},
|
||||
packet_handling::{
|
||||
AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketReceiver, RemovePlayerEvent,
|
||||
AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent,
|
||||
UpdatePlayerEvent,
|
||||
},
|
||||
PlayerInfo,
|
||||
|
@ -62,6 +56,23 @@ pub enum Event {
|
|||
Chat(ChatPacket),
|
||||
/// Happens 20 times per second, but only when the world is loaded.
|
||||
Tick,
|
||||
/// We received a packet from the server.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea_client::Event;
|
||||
/// # use azalea_protocol::packets::game::ClientboundGamePacket;
|
||||
/// # async fn example(event: Event) {
|
||||
/// # match event {
|
||||
/// Event::Packet(packet) => match *packet {
|
||||
/// ClientboundGamePacket::Login(_) => {
|
||||
/// println!("login packet");
|
||||
/// }
|
||||
/// _ => {}
|
||||
/// },
|
||||
/// # _ => {}
|
||||
/// # }
|
||||
/// # }
|
||||
/// ```
|
||||
Packet(Arc<ClientboundGamePacket>),
|
||||
/// A player joined the game (or more specifically, was added to the tab
|
||||
/// list).
|
||||
|
@ -98,7 +109,7 @@ impl Plugin for EventPlugin {
|
|||
.add_system(remove_player_listener)
|
||||
.add_system(death_listener)
|
||||
.add_system(keepalive_listener)
|
||||
.add_tick_system(tick_listener);
|
||||
.add_system(tick_listener.in_schedule(CoreSchedule::FixedUpdate));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,13 +144,14 @@ fn tick_listener(query: Query<&LocalPlayerEvents>) {
|
|||
}
|
||||
}
|
||||
|
||||
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 packet_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<PacketEvent>) {
|
||||
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::Packet(Arc::new(event.packet.clone())))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ pub fn minecraft_dir() -> Option<PathBuf> {
|
|||
pub fn home_env_var() -> &'static str {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"USERPROFILE"
|
||||
"APPDATA"
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
|
|
@ -26,8 +26,9 @@ mod player;
|
|||
pub mod task_pool;
|
||||
|
||||
pub use account::Account;
|
||||
pub use azalea_ecs as ecs;
|
||||
pub use client::{init_ecs_app, start_ecs, Client, ClientInformation, JoinError};
|
||||
pub use client::{
|
||||
init_ecs_app, start_ecs, Client, ClientInformation, JoinError, JoinedClientBundle, TabList,
|
||||
};
|
||||
pub use events::Event;
|
||||
pub use local_player::{GameProfileComponent, LocalPlayer};
|
||||
pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
use std::{collections::HashMap, io, sync::Arc};
|
||||
use std::{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::event::EventReader;
|
||||
use azalea_ecs::{query::Added, system::Query};
|
||||
use azalea_protocol::packets::game::ServerboundGamePacket;
|
||||
use azalea_world::{
|
||||
entity::{self, Dead},
|
||||
PartialWorld, World,
|
||||
Instance, PartialInstance,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
component::Component, entity::Entity, event::EventReader, query::Added, system::Query,
|
||||
};
|
||||
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,
|
||||
ClientInformation, WalkDirection,
|
||||
};
|
||||
|
||||
/// This is a component for our local player entities that are probably in a
|
||||
|
@ -34,17 +32,14 @@ use crate::{
|
|||
#[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>>,
|
||||
|
||||
/// The partial instance is the world this client currently has loaded. It
|
||||
/// has a limited render distance.
|
||||
pub partial_instance: Arc<RwLock<PartialInstance>>,
|
||||
/// The world is the combined [`PartialInstance`]s of all clients in the
|
||||
/// same world. (Only relevant if you're using a shared world, i.e. a
|
||||
/// swarm)
|
||||
pub world: Arc<RwLock<Instance>>,
|
||||
|
||||
/// A task that reads packets from the server. The client is disconnected
|
||||
/// when this task ends.
|
||||
|
@ -88,7 +83,7 @@ impl LocalPlayer {
|
|||
pub fn new(
|
||||
entity: Entity,
|
||||
packet_writer: mpsc::UnboundedSender<ServerboundGamePacket>,
|
||||
world: Arc<RwLock<World>>,
|
||||
world: Arc<RwLock<Instance>>,
|
||||
read_packets_task: JoinHandle<()>,
|
||||
write_packets_task: JoinHandle<()>,
|
||||
) -> Self {
|
||||
|
@ -97,11 +92,8 @@ impl LocalPlayer {
|
|||
LocalPlayer {
|
||||
packet_writer,
|
||||
|
||||
client_information: ClientInformation::default(),
|
||||
players: HashMap::new(),
|
||||
|
||||
world,
|
||||
partial_world: Arc::new(RwLock::new(PartialWorld::new(
|
||||
partial_instance: Arc::new(RwLock::new(PartialInstance::new(
|
||||
client_information.view_distance.into(),
|
||||
Some(entity),
|
||||
))),
|
||||
|
@ -129,7 +121,7 @@ impl Drop for LocalPlayer {
|
|||
|
||||
/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s.
|
||||
pub fn update_in_loaded_chunk(
|
||||
mut commands: azalea_ecs::system::Commands,
|
||||
mut commands: bevy_ecs::system::Commands,
|
||||
query: Query<(Entity, &LocalPlayer, &entity::Position)>,
|
||||
) {
|
||||
for (entity, local_player, position) in &query {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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 crate::local_player::{
|
||||
update_in_loaded_chunk, LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState,
|
||||
};
|
||||
use azalea_physics::{force_jump_listener, PhysicsSet};
|
||||
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
|
||||
use azalea_protocol::packets::game::{
|
||||
serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket,
|
||||
|
@ -13,6 +14,14 @@ use azalea_world::{
|
|||
entity::{self, metadata::Sprinting, Attributes, Jumping, MinecraftEntityId},
|
||||
MoveEntityError,
|
||||
};
|
||||
use bevy_app::{App, CoreSchedule, IntoSystemAppConfigs, Plugin};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
event::EventReader,
|
||||
query::With,
|
||||
schedule::{IntoSystemConfig, IntoSystemConfigs},
|
||||
system::Query,
|
||||
};
|
||||
use std::backtrace::Backtrace;
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -34,6 +43,28 @@ impl From<MoveEntityError> for MovePlayerError {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PlayerMovePlugin;
|
||||
|
||||
impl Plugin for PlayerMovePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<StartWalkEvent>()
|
||||
.add_event::<StartSprintEvent>()
|
||||
.add_systems(
|
||||
(sprint_listener, walk_listener)
|
||||
.chain()
|
||||
.before(force_jump_listener),
|
||||
)
|
||||
.add_systems(
|
||||
(
|
||||
local_player_ai_step.in_set(PhysicsSet),
|
||||
send_position.after(update_in_loaded_chunk),
|
||||
)
|
||||
.chain()
|
||||
.in_schedule(CoreSchedule::FixedUpdate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Set whether we're jumping. This acts as if you held space in
|
||||
/// vanilla. If you want to jump once, use the `jump` function.
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,11 @@
|
|||
use azalea_auth::game_profile::GameProfile;
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_core::GameType;
|
||||
use azalea_ecs::{
|
||||
use azalea_world::entity::EntityInfos;
|
||||
use bevy_ecs::{
|
||||
event::EventReader,
|
||||
system::{Commands, Res},
|
||||
};
|
||||
use azalea_world::entity::EntityInfos;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{packet_handling::AddPlayerEvent, GameProfileComponent};
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
//! Borrowed from `bevy_core`.
|
||||
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
schedule::IntoSystemDescriptor,
|
||||
system::Resource,
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bevy_app::{App, CoreSet, Plugin};
|
||||
use bevy_ecs::{
|
||||
schedule::IntoSystemConfig,
|
||||
system::{NonSend, Resource},
|
||||
};
|
||||
use bevy_tasks::{
|
||||
tick_global_task_pools_on_main_thread, AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool,
|
||||
TaskPoolBuilder,
|
||||
};
|
||||
use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder};
|
||||
|
||||
/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`,
|
||||
/// `IoTaskPool`.
|
||||
|
@ -22,13 +27,16 @@ impl Plugin for TaskPoolPlugin {
|
|||
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(),
|
||||
);
|
||||
app.add_system(tick_global_task_pools.in_base_set(CoreSet::Last));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NonSendMarker(PhantomData<*mut ()>);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn tick_global_task_pools(_main_thread_marker: Option<NonSend<NonSendMarker>>) {
|
||||
tick_global_task_pools_on_main_thread();
|
||||
}
|
||||
|
||||
/// Helper for configuring and creating the default task pools. For end-users
|
||||
/// who want full control, set up [`TaskPoolPlugin`](TaskPoolPlugin)
|
||||
#[derive(Clone, Resource)]
|
||||
|
|
|
@ -9,11 +9,13 @@ version = "0.6.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.6.0" }
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.6.0" }
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.6.0" }
|
||||
bevy_ecs = {version = "0.9.1", default-features = false, optional = true}
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
|
||||
azalea-chat = { path = "../azalea-chat", version = "^0.6.0" }
|
||||
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0" }
|
||||
bevy_ecs = { version = "0.10.0", default-features = false, optional = true }
|
||||
serde = {version = "^1.0.152", optional = true}
|
||||
uuid = "^1.1.2"
|
||||
|
||||
[features]
|
||||
bevy_ecs = ["dep:bevy_ecs"]
|
||||
serde = ["dep:serde"]
|
|
@ -1,5 +1,6 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![feature(int_roundings)]
|
||||
#![feature(const_for)]
|
||||
#![allow(incomplete_features)]
|
||||
#![feature(generic_const_exprs)]
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ macro_rules! vec3_impl {
|
|||
Self { x, y, z }
|
||||
}
|
||||
|
||||
/// Get the distance of this vector to the origin by doing `x^2 + y^2 +
|
||||
/// z^2`.
|
||||
pub fn length_sqr(&self) -> $type {
|
||||
self.x * self.x + self.y * self.y + self.z * self.z
|
||||
}
|
||||
|
@ -139,6 +141,11 @@ impl BlockPos {
|
|||
z: self.z as f64 + 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the distance of this vector from the origin by doing `x + y + z`.
|
||||
pub fn length_manhattan(&self) -> u32 {
|
||||
(self.x.abs() + self.y.abs() + self.z.abs()) as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Chunk coordinates are used to represent where a chunk is in the world. You
|
||||
|
@ -148,12 +155,21 @@ pub struct ChunkPos {
|
|||
pub x: i32,
|
||||
pub z: i32,
|
||||
}
|
||||
|
||||
impl ChunkPos {
|
||||
pub fn new(x: i32, z: i32) -> Self {
|
||||
ChunkPos { x, z }
|
||||
}
|
||||
}
|
||||
impl Add<ChunkPos> for ChunkPos {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
x: self.x + rhs.x,
|
||||
z: self.z + rhs.z,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The coordinates of a chunk section in the world.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
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.
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Hash, Clone, PartialEq, Eq)]
|
||||
pub struct ResourceLocation {
|
||||
|
@ -16,7 +16,7 @@ static DEFAULT_NAMESPACE: &str = "minecraft";
|
|||
// static REALMS_NAMESPACE: &str = "realms";
|
||||
|
||||
impl ResourceLocation {
|
||||
pub fn new(resource_string: &str) -> Result<ResourceLocation, BufReadError> {
|
||||
pub fn new(resource_string: &str) -> ResourceLocation {
|
||||
let sep_byte_position_option = resource_string.chars().position(|c| c == ':');
|
||||
let (namespace, path) = if let Some(sep_byte_position) = sep_byte_position_option {
|
||||
if sep_byte_position == 0 {
|
||||
|
@ -30,10 +30,10 @@ impl ResourceLocation {
|
|||
} else {
|
||||
(DEFAULT_NAMESPACE, resource_string)
|
||||
};
|
||||
Ok(ResourceLocation {
|
||||
ResourceLocation {
|
||||
namespace: namespace.to_string(),
|
||||
path: path.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ impl std::fmt::Debug for ResourceLocation {
|
|||
impl McBufReadable for ResourceLocation {
|
||||
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
|
||||
let location_string = String::read_from(buf)?;
|
||||
ResourceLocation::new(&location_string)
|
||||
Ok(ResourceLocation::new(&location_string))
|
||||
}
|
||||
}
|
||||
impl McBufWritable for ResourceLocation {
|
||||
|
@ -60,31 +60,59 @@ impl McBufWritable for ResourceLocation {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for ResourceLocation {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> Deserialize<'de> for ResourceLocation {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s.contains(':') {
|
||||
Ok(ResourceLocation::new(&s))
|
||||
} else {
|
||||
Err(de::Error::invalid_value(
|
||||
de::Unexpected::Str(&s),
|
||||
&"a valid ResourceLocation",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_resource_location() {
|
||||
let r = ResourceLocation::new("abcdef:ghijkl").unwrap();
|
||||
let r = ResourceLocation::new("abcdef:ghijkl");
|
||||
assert_eq!(r.namespace, "abcdef");
|
||||
assert_eq!(r.path, "ghijkl");
|
||||
}
|
||||
#[test]
|
||||
fn no_namespace() {
|
||||
let r = ResourceLocation::new("azalea").unwrap();
|
||||
let r = ResourceLocation::new("azalea");
|
||||
assert_eq!(r.namespace, "minecraft");
|
||||
assert_eq!(r.path, "azalea");
|
||||
}
|
||||
#[test]
|
||||
fn colon_start() {
|
||||
let r = ResourceLocation::new(":azalea").unwrap();
|
||||
let r = ResourceLocation::new(":azalea");
|
||||
assert_eq!(r.namespace, "minecraft");
|
||||
assert_eq!(r.path, "azalea");
|
||||
}
|
||||
#[test]
|
||||
fn colon_end() {
|
||||
let r = ResourceLocation::new("azalea:").unwrap();
|
||||
let r = ResourceLocation::new("azalea:");
|
||||
assert_eq!(r.namespace, "azalea");
|
||||
assert_eq!(r.path, "");
|
||||
}
|
||||
|
@ -93,7 +121,6 @@ mod tests {
|
|||
fn mcbuf_resource_location() {
|
||||
let mut buf = Vec::new();
|
||||
ResourceLocation::new("minecraft:dirt")
|
||||
.unwrap()
|
||||
.write_into(&mut buf)
|
||||
.unwrap();
|
||||
|
||||
|
@ -101,7 +128,7 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
ResourceLocation::read_from(&mut buf).unwrap(),
|
||||
ResourceLocation::new("minecraft:dirt").unwrap()
|
||||
ResourceLocation::new("minecraft:dirt")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ sha-1 = "^0.10.0"
|
|||
uuid = "^1.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = {version = "^0.3.5", features = ["html_reports"]}
|
||||
criterion = {version = "^0.4.0", features = ["html_reports"]}
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
[package]
|
||||
description = "ECS stuff used in Azalea"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-ecs"
|
||||
version = "0.6.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.6.0"}
|
||||
bevy_app = "0.9.1"
|
||||
bevy_ecs = {version = "0.9.1", default-features = false}
|
||||
tokio = {version = "1.25.0", features = ["time"]}
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
description = "Azalea ECS Macros"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "azalea-ecs-macros"
|
||||
version = "0.6.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = "1.0"
|
||||
toml = "0.7.0"
|
|
@ -1,125 +0,0 @@
|
|||
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 }
|
||||
}
|
|
@ -1,466 +0,0 @@
|
|||
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 }
|
||||
}
|
|
@ -1,523 +0,0 @@
|
|||
//! 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)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
#![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} = ...`"
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
#![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()
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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",
|
||||
)),
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
#![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
|
||||
}
|
||||
}
|
|
@ -10,6 +10,6 @@ version = "0.6.0"
|
|||
|
||||
[dependencies]
|
||||
once_cell = "1.16.0"
|
||||
serde = "1.0.137"
|
||||
serde_json = "1.0.81"
|
||||
serde = "^1.0.152"
|
||||
serde_json = "^1.0.93"
|
||||
# tokio = {version = "^1.21.2", features = ["fs"]}
|
||||
|
|
|
@ -9,17 +9,21 @@ repository = "https://github.com/mat-1/azalea/tree/main/azalea-nbt"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ahash = { version = "^0.8.0", features = ["serde"]}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.6.0" }
|
||||
ahash = { version = "^0.8.3" }
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
|
||||
byteorder = "^1.4.3"
|
||||
flate2 = "^1.0.23"
|
||||
flate2 = "^1.0.25"
|
||||
log = "0.4.17"
|
||||
num-derive = "^0.3.3"
|
||||
num-traits = "^0.2.14"
|
||||
serde = {version = "^1.0.148", features = ["derive"]}
|
||||
num-traits = "^0.2.15"
|
||||
serde = { version = "1.0.152", features = ["derive"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = {version = "^0.3.5", features = ["html_reports"]}
|
||||
criterion = {version = "^0.4.0", features = ["html_reports"]}
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serde = ["dep:serde", "ahash/serde"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
@ -7,11 +7,9 @@ A fast NBT serializer and deserializer.
|
|||
```
|
||||
use ahash::AHashMap;
|
||||
use azalea_nbt::Tag;
|
||||
use std::{io::{Cursor, Read}, fs::File};
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut file = File::open("tests/hello_world.nbt").unwrap();
|
||||
let mut buf = vec![];
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
let buf = include_bytes!("../tests/hello_world.nbt");
|
||||
let tag = Tag::read(&mut Cursor::new(&buf[..])).unwrap();
|
||||
assert_eq!(
|
||||
tag,
|
||||
|
|
|
@ -1,45 +1,38 @@
|
|||
use ahash::AHashMap;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An NBT value.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
#[repr(u8)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))]
|
||||
pub enum Tag {
|
||||
#[default]
|
||||
End, // 0
|
||||
Byte(i8), // 1
|
||||
Short(i16), // 2
|
||||
Int(i32), // 3
|
||||
Long(i64), // 4
|
||||
Float(f32), // 5
|
||||
Double(f64), // 6
|
||||
ByteArray(Vec<u8>), // 7
|
||||
String(String), // 8
|
||||
List(Vec<Tag>), // 9
|
||||
Compound(AHashMap<String, Tag>), // 10
|
||||
IntArray(Vec<i32>), // 11
|
||||
LongArray(Vec<i64>), // 12
|
||||
End = 0,
|
||||
Byte(i8) = 1,
|
||||
Short(i16) = 2,
|
||||
Int(i32) = 3,
|
||||
Long(i64) = 4,
|
||||
Float(f32) = 5,
|
||||
Double(f64) = 6,
|
||||
ByteArray(Vec<u8>) = 7,
|
||||
String(String) = 8,
|
||||
List(Vec<Tag>) = 9,
|
||||
Compound(AHashMap<String, Tag>) = 10,
|
||||
IntArray(Vec<i32>) = 11,
|
||||
LongArray(Vec<i64>) = 12,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// Get the numerical ID of the tag type.
|
||||
#[inline]
|
||||
pub fn id(&self) -> u8 {
|
||||
match self {
|
||||
Tag::End => 0,
|
||||
Tag::Byte(_) => 1,
|
||||
Tag::Short(_) => 2,
|
||||
Tag::Int(_) => 3,
|
||||
Tag::Long(_) => 4,
|
||||
Tag::Float(_) => 5,
|
||||
Tag::Double(_) => 6,
|
||||
Tag::ByteArray(_) => 7,
|
||||
Tag::String(_) => 8,
|
||||
Tag::List(_) => 9,
|
||||
Tag::Compound(_) => 10,
|
||||
Tag::IntArray(_) => 11,
|
||||
Tag::LongArray(_) => 12,
|
||||
}
|
||||
// SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)`
|
||||
// `union` between `repr(C)` structs, each of which has the `u8`
|
||||
// discriminant as its first field, so we can read the discriminant
|
||||
// without offsetting the pointer.
|
||||
unsafe { *<*const _>::from(self).cast::<u8>() }
|
||||
}
|
||||
|
||||
/// If the type is a byte, return the [`i8`].
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
use ahash::AHashMap;
|
||||
use azalea_nbt::Tag;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn test_decode_hello_world() {
|
||||
// read hello_world.nbt
|
||||
let mut file = File::open("tests/hello_world.nbt").unwrap();
|
||||
let mut buf = vec![];
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
let buf = include_bytes!("hello_world.nbt").to_vec();
|
||||
let tag = Tag::read(&mut Cursor::new(&buf[..])).unwrap();
|
||||
assert_eq!(
|
||||
tag,
|
||||
|
@ -26,9 +21,7 @@ fn test_decode_hello_world() {
|
|||
|
||||
#[test]
|
||||
fn test_roundtrip_hello_world() {
|
||||
let mut file = File::open("tests/hello_world.nbt").unwrap();
|
||||
let mut original = Vec::new();
|
||||
file.read_to_end(&mut original).unwrap();
|
||||
let original = include_bytes!("hello_world.nbt").to_vec();
|
||||
|
||||
let mut original_stream = Cursor::new(&original[..]);
|
||||
let tag = Tag::read(&mut original_stream).unwrap();
|
||||
|
@ -43,9 +36,7 @@ fn test_roundtrip_hello_world() {
|
|||
#[test]
|
||||
fn test_bigtest() {
|
||||
// read bigtest.nbt
|
||||
let mut file = File::open("tests/bigtest.nbt").unwrap();
|
||||
let mut original = Vec::new();
|
||||
file.read_to_end(&mut original).unwrap();
|
||||
let original = include_bytes!("bigtest.nbt").to_vec();
|
||||
|
||||
let mut original_stream = Cursor::new(original);
|
||||
let original_tag = Tag::read_gzip(&mut original_stream).unwrap();
|
||||
|
@ -81,9 +72,7 @@ fn test_stringtest() {
|
|||
Tag::String("😁".to_string()),
|
||||
])
|
||||
)]));
|
||||
let mut file = std::fs::File::open("tests/stringtest.nbt").unwrap();
|
||||
let mut original = Vec::new();
|
||||
file.read_to_end(&mut original).unwrap();
|
||||
let original = include_bytes!("stringtest.nbt").to_vec();
|
||||
|
||||
let mut original_stream = Cursor::new(original);
|
||||
let original_tag = Tag::read_gzip(&mut original_stream).unwrap();
|
||||
|
@ -93,9 +82,7 @@ fn test_stringtest() {
|
|||
|
||||
#[test]
|
||||
fn test_complex_player() {
|
||||
let mut file = File::open("tests/complex_player.dat").unwrap();
|
||||
let mut original = Vec::new();
|
||||
file.read_to_end(&mut original).unwrap();
|
||||
let original = include_bytes!("complex_player.dat").to_vec();
|
||||
|
||||
let mut original_stream = Cursor::new(original);
|
||||
let original_tag = Tag::read_gzip(&mut original_stream).unwrap();
|
||||
|
@ -110,9 +97,7 @@ fn test_complex_player() {
|
|||
|
||||
#[test]
|
||||
fn test_simple_player() {
|
||||
let mut file = File::open("tests/simple_player.dat").unwrap();
|
||||
let mut original = Vec::new();
|
||||
file.read_to_end(&mut original).unwrap();
|
||||
let original = include_bytes!("simple_player.dat").to_vec();
|
||||
|
||||
let mut original_stream = Cursor::new(original);
|
||||
let original_tag = Tag::read_gzip(&mut original_stream).unwrap();
|
||||
|
|
|
@ -9,13 +9,15 @@ version = "0.6.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.6.0"}
|
||||
azalea-core = {path = "../azalea-core", version = "^0.6.0"}
|
||||
azalea-ecs = {version = "0.6.0", path = "../azalea-ecs"}
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.6.0"}
|
||||
azalea-world = {path = "../azalea-world", version = "^0.6.0"}
|
||||
azalea-block = { path = "../azalea-block", version = "^0.6.0" }
|
||||
azalea-core = { path = "../azalea-core", version = "^0.6.0" }
|
||||
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
|
||||
azalea-world = { path = "../azalea-world", version = "^0.6.0" }
|
||||
bevy_app = "0.10.0"
|
||||
bevy_ecs = "0.10.0"
|
||||
once_cell = "1.16.0"
|
||||
parking_lot = "^0.12.1"
|
||||
|
||||
[dev-dependencies]
|
||||
bevy_time = "0.10.0"
|
||||
uuid = "^1.1.2"
|
||||
|
|
|
@ -7,7 +7,7 @@ mod world_collisions;
|
|||
use azalea_core::{Axis, Vec3, AABB, EPSILON};
|
||||
use azalea_world::{
|
||||
entity::{self},
|
||||
MoveEntityError, World,
|
||||
Instance, MoveEntityError,
|
||||
};
|
||||
pub use blocks::BlockWithShape;
|
||||
pub use discrete_voxel_shape::*;
|
||||
|
@ -52,7 +52,7 @@ pub enum MoverType {
|
|||
|
||||
// return var4;
|
||||
// }
|
||||
fn collide(movement: &Vec3, world: &World, physics: &entity::Physics) -> Vec3 {
|
||||
fn collide(movement: &Vec3, world: &Instance, physics: &entity::Physics) -> Vec3 {
|
||||
let entity_bounding_box = physics.bounding_box;
|
||||
// TODO: get_entity_collisions
|
||||
// let entity_collisions = world.get_entity_collisions(self,
|
||||
|
@ -73,7 +73,7 @@ fn collide(movement: &Vec3, world: &World, physics: &entity::Physics) -> Vec3 {
|
|||
pub fn move_colliding(
|
||||
_mover_type: &MoverType,
|
||||
movement: &Vec3,
|
||||
world: &World,
|
||||
world: &Instance,
|
||||
position: &mut entity::Position,
|
||||
physics: &mut entity::Physics,
|
||||
) -> Result<(), MoveEntityError> {
|
||||
|
@ -186,7 +186,7 @@ pub fn move_colliding(
|
|||
fn collide_bounding_box(
|
||||
movement: &Vec3,
|
||||
entity_bounding_box: &AABB,
|
||||
world: &World,
|
||||
world: &Instance,
|
||||
entity_collisions: Vec<VoxelShape>,
|
||||
) -> Vec3 {
|
||||
let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
|
||||
|
|
|
@ -2,16 +2,16 @@ use super::Shapes;
|
|||
use crate::collision::{BlockWithShape, VoxelShape, AABB};
|
||||
use azalea_block::BlockState;
|
||||
use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON};
|
||||
use azalea_world::{Chunk, World};
|
||||
use azalea_world::{Chunk, Instance};
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn get_block_collisions(world: &World, aabb: AABB) -> BlockCollisions<'_> {
|
||||
pub fn get_block_collisions(world: &Instance, aabb: AABB) -> BlockCollisions<'_> {
|
||||
BlockCollisions::new(world, aabb)
|
||||
}
|
||||
|
||||
pub struct BlockCollisions<'a> {
|
||||
pub world: &'a World,
|
||||
pub world: &'a Instance,
|
||||
pub aabb: AABB,
|
||||
pub entity_shape: VoxelShape,
|
||||
pub cursor: Cursor3d,
|
||||
|
@ -19,7 +19,7 @@ pub struct BlockCollisions<'a> {
|
|||
}
|
||||
|
||||
impl<'a> BlockCollisions<'a> {
|
||||
pub fn new(world: &'a World, aabb: AABB) -> Self {
|
||||
pub fn new(world: &'a Instance, aabb: AABB) -> Self {
|
||||
let origin_x = (aabb.min_x - EPSILON).floor() as i32 - 1;
|
||||
let origin_y = (aabb.min_y - EPSILON).floor() as i32 - 1;
|
||||
let origin_z = (aabb.min_z - EPSILON).floor() as i32 - 1;
|
||||
|
|
|
@ -5,42 +5,37 @@ pub mod collision;
|
|||
|
||||
use azalea_block::{Block, BlockState};
|
||||
use azalea_core::{BlockPos, Vec3};
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
entity::Entity,
|
||||
event::{EventReader, EventWriter},
|
||||
query::With,
|
||||
schedule::{IntoSystemDescriptor, SystemSet},
|
||||
system::{Query, Res},
|
||||
AppTickExt,
|
||||
};
|
||||
use azalea_world::{
|
||||
entity::{
|
||||
metadata::Sprinting, move_relative, Attributes, Jumping, Local, Physics, Position,
|
||||
WorldName,
|
||||
},
|
||||
World, WorldContainer,
|
||||
Instance, InstanceContainer,
|
||||
};
|
||||
use bevy_app::{App, CoreSchedule, IntoSystemAppConfigs, Plugin};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
event::{EventReader, EventWriter},
|
||||
query::With,
|
||||
schedule::{IntoSystemConfig, IntoSystemConfigs, SystemSet},
|
||||
system::{Query, Res},
|
||||
};
|
||||
use collision::{move_colliding, MoverType};
|
||||
|
||||
/// A Bevy [`SystemSet`] for running physics that makes entities do things.
|
||||
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
||||
pub struct PhysicsSet;
|
||||
|
||||
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"),
|
||||
),
|
||||
.add_system(force_jump_listener.before(azalea_world::entity::update_bounding_box))
|
||||
.add_systems(
|
||||
(ai_step, travel)
|
||||
.chain()
|
||||
.in_set(PhysicsSet)
|
||||
.in_schedule(CoreSchedule::FixedUpdate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +44,7 @@ impl Plugin for PhysicsPlugin {
|
|||
/// gravity, collisions, and some other stuff.
|
||||
fn travel(
|
||||
mut query: Query<(&mut Physics, &mut Position, &Attributes, &WorldName), With<Local>>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
) {
|
||||
for (mut physics, mut position, attributes, world_name) in &mut query {
|
||||
let world_lock = world_container
|
||||
|
@ -162,9 +157,9 @@ pub fn ai_step(
|
|||
/// Jump even if we aren't on the ground.
|
||||
pub struct ForceJumpEvent(pub Entity);
|
||||
|
||||
fn force_jump_listener(
|
||||
pub fn force_jump_listener(
|
||||
mut query: Query<(&mut Physics, &Position, &Sprinting, &WorldName)>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
mut events: EventReader<ForceJumpEvent>,
|
||||
) {
|
||||
for event in events.iter() {
|
||||
|
@ -207,7 +202,7 @@ fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
|
|||
|
||||
fn handle_relative_friction_and_calculate_movement(
|
||||
block_friction: f32,
|
||||
world: &World,
|
||||
world: &Instance,
|
||||
physics: &mut Physics,
|
||||
position: &mut Position,
|
||||
attributes: &Attributes,
|
||||
|
@ -257,7 +252,7 @@ fn get_friction_influenced_speed(physics: &Physics, attributes: &Attributes, fri
|
|||
|
||||
/// Returns the what the entity's jump should be multiplied by based on the
|
||||
/// block they're standing on.
|
||||
fn block_jump_factor(world: &World, position: &Position) -> f32 {
|
||||
fn block_jump_factor(world: &Instance, position: &Position) -> f32 {
|
||||
let block_at_pos = world.chunks.get_block_state(&position.into());
|
||||
let block_below = world
|
||||
.chunks
|
||||
|
@ -285,7 +280,7 @@ fn block_jump_factor(world: &World, position: &Position) -> f32 {
|
|||
// public double getJumpBoostPower() {
|
||||
// return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F *
|
||||
// (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; }
|
||||
fn jump_power(world: &World, position: &Position) -> f32 {
|
||||
fn jump_power(world: &Instance, position: &Position) -> f32 {
|
||||
0.42 * block_jump_factor(world, position)
|
||||
}
|
||||
|
||||
|
@ -309,30 +304,29 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
use azalea_core::{ChunkPos, ResourceLocation};
|
||||
use azalea_ecs::{app::App, TickPlugin};
|
||||
use azalea_world::{
|
||||
entity::{EntityBundle, EntityPlugin, MinecraftEntityId},
|
||||
Chunk, PartialWorld,
|
||||
Chunk, PartialInstance,
|
||||
};
|
||||
use bevy_app::App;
|
||||
use bevy_time::fixed_timestep::FixedTime;
|
||||
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.add_plugin(PhysicsPlugin)
|
||||
.add_plugin(EntityPlugin)
|
||||
.insert_resource(FixedTime::new(Duration::from_millis(50)))
|
||||
.init_resource::<InstanceContainer>();
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gravity() {
|
||||
let mut app = make_test_app();
|
||||
let _world_lock = app.world.resource_mut::<WorldContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
let _world_lock = app.world.resource_mut::<InstanceContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
384,
|
||||
-64,
|
||||
);
|
||||
|
@ -348,7 +342,7 @@ mod tests {
|
|||
z: 0.,
|
||||
},
|
||||
azalea_registry::EntityKind::Zombie,
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
),
|
||||
MinecraftEntityId(0),
|
||||
Local,
|
||||
|
@ -359,6 +353,7 @@ mod tests {
|
|||
// y should start at 70
|
||||
assert_eq!(entity_pos.y, 70.);
|
||||
}
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
{
|
||||
let entity_pos = *app.world.get::<Position>(entity).unwrap();
|
||||
|
@ -367,6 +362,7 @@ mod tests {
|
|||
let entity_physics = app.world.get::<Physics>(entity).unwrap().clone();
|
||||
assert!(entity_physics.delta.y < 0.);
|
||||
}
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
{
|
||||
let entity_pos = *app.world.get::<Position>(entity).unwrap();
|
||||
|
@ -381,12 +377,12 @@ mod tests {
|
|||
#[test]
|
||||
fn test_collision() {
|
||||
let mut app = make_test_app();
|
||||
let world_lock = app.world.resource_mut::<WorldContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
let world_lock = app.world.resource_mut::<InstanceContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
384,
|
||||
-64,
|
||||
);
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
|
@ -404,7 +400,7 @@ mod tests {
|
|||
z: 0.5,
|
||||
},
|
||||
azalea_registry::EntityKind::Player,
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
),
|
||||
MinecraftEntityId(0),
|
||||
Local,
|
||||
|
@ -419,6 +415,7 @@ 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"
|
||||
);
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
{
|
||||
let entity_pos = *app.world.get::<Position>(entity).unwrap();
|
||||
|
@ -427,6 +424,7 @@ mod tests {
|
|||
let entity_physics = app.world.get::<Physics>(entity).unwrap().clone();
|
||||
assert!(entity_physics.delta.y < 0.);
|
||||
}
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
{
|
||||
let entity_pos = *app.world.get::<Position>(entity).unwrap();
|
||||
|
@ -438,12 +436,12 @@ mod tests {
|
|||
#[test]
|
||||
fn test_slab_collision() {
|
||||
let mut app = make_test_app();
|
||||
let world_lock = app.world.resource_mut::<WorldContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
let world_lock = app.world.resource_mut::<InstanceContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
384,
|
||||
-64,
|
||||
);
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
|
@ -461,7 +459,7 @@ mod tests {
|
|||
z: 0.5,
|
||||
},
|
||||
azalea_registry::EntityKind::Player,
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
),
|
||||
MinecraftEntityId(0),
|
||||
Local,
|
||||
|
@ -469,8 +467,8 @@ mod tests {
|
|||
.id();
|
||||
let block_state = partial_world.chunks.set_block_state(
|
||||
&BlockPos { x: 0, y: 69, z: 0 },
|
||||
azalea_block::StoneSlabBlock {
|
||||
kind: azalea_block::Type::Bottom,
|
||||
azalea_block::blocks::StoneSlab {
|
||||
kind: azalea_block::properties::Type::Bottom,
|
||||
waterlogged: false,
|
||||
}
|
||||
.into(),
|
||||
|
@ -482,6 +480,7 @@ mod tests {
|
|||
);
|
||||
// do a few steps so we fall on the slab
|
||||
for _ in 0..20 {
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
}
|
||||
let entity_pos = app.world.get::<Position>(entity).unwrap();
|
||||
|
@ -491,12 +490,12 @@ mod tests {
|
|||
#[test]
|
||||
fn test_top_slab_collision() {
|
||||
let mut app = make_test_app();
|
||||
let world_lock = app.world.resource_mut::<WorldContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
let world_lock = app.world.resource_mut::<InstanceContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
384,
|
||||
-64,
|
||||
);
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
|
@ -514,7 +513,7 @@ mod tests {
|
|||
z: 0.5,
|
||||
},
|
||||
azalea_registry::EntityKind::Player,
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
),
|
||||
MinecraftEntityId(0),
|
||||
Local,
|
||||
|
@ -522,8 +521,8 @@ mod tests {
|
|||
.id();
|
||||
let block_state = world_lock.write().chunks.set_block_state(
|
||||
&BlockPos { x: 0, y: 69, z: 0 },
|
||||
azalea_block::StoneSlabBlock {
|
||||
kind: azalea_block::Type::Top,
|
||||
azalea_block::blocks::StoneSlab {
|
||||
kind: azalea_block::properties::Type::Top,
|
||||
waterlogged: false,
|
||||
}
|
||||
.into(),
|
||||
|
@ -534,6 +533,7 @@ mod tests {
|
|||
);
|
||||
// do a few steps so we fall on the slab
|
||||
for _ in 0..20 {
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
}
|
||||
let entity_pos = app.world.get::<Position>(entity).unwrap();
|
||||
|
@ -543,12 +543,12 @@ mod tests {
|
|||
#[test]
|
||||
fn test_weird_wall_collision() {
|
||||
let mut app = make_test_app();
|
||||
let world_lock = app.world.resource_mut::<WorldContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
let world_lock = app.world.resource_mut::<InstanceContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
384,
|
||||
-64,
|
||||
);
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
|
@ -566,7 +566,7 @@ mod tests {
|
|||
z: 0.5,
|
||||
},
|
||||
azalea_registry::EntityKind::Player,
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
),
|
||||
MinecraftEntityId(0),
|
||||
Local,
|
||||
|
@ -574,11 +574,11 @@ mod tests {
|
|||
.id();
|
||||
let block_state = world_lock.write().chunks.set_block_state(
|
||||
&BlockPos { x: 0, y: 69, z: 0 },
|
||||
azalea_block::CobblestoneWallBlock {
|
||||
east: azalea_block::EastWall::Low,
|
||||
north: azalea_block::NorthWall::Low,
|
||||
south: azalea_block::SouthWall::Low,
|
||||
west: azalea_block::WestWall::Low,
|
||||
azalea_block::blocks::CobblestoneWall {
|
||||
east: azalea_block::properties::EastWall::Low,
|
||||
north: azalea_block::properties::NorthWall::Low,
|
||||
south: azalea_block::properties::SouthWall::Low,
|
||||
west: azalea_block::properties::WestWall::Low,
|
||||
up: false,
|
||||
waterlogged: false,
|
||||
}
|
||||
|
@ -590,6 +590,7 @@ mod tests {
|
|||
);
|
||||
// do a few steps so we fall on the wall
|
||||
for _ in 0..20 {
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
}
|
||||
|
||||
|
@ -600,12 +601,12 @@ mod tests {
|
|||
#[test]
|
||||
fn test_negative_coordinates_weird_wall_collision() {
|
||||
let mut app = make_test_app();
|
||||
let world_lock = app.world.resource_mut::<WorldContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
let world_lock = app.world.resource_mut::<InstanceContainer>().insert(
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
384,
|
||||
-64,
|
||||
);
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: -1, z: -1 },
|
||||
|
@ -623,7 +624,7 @@ mod tests {
|
|||
z: -7.5,
|
||||
},
|
||||
azalea_registry::EntityKind::Player,
|
||||
ResourceLocation::new("minecraft:overworld").unwrap(),
|
||||
ResourceLocation::new("minecraft:overworld"),
|
||||
),
|
||||
MinecraftEntityId(0),
|
||||
Local,
|
||||
|
@ -635,11 +636,11 @@ mod tests {
|
|||
y: 69,
|
||||
z: -8,
|
||||
},
|
||||
azalea_block::CobblestoneWallBlock {
|
||||
east: azalea_block::EastWall::Low,
|
||||
north: azalea_block::NorthWall::Low,
|
||||
south: azalea_block::SouthWall::Low,
|
||||
west: azalea_block::WestWall::Low,
|
||||
azalea_block::blocks::CobblestoneWall {
|
||||
east: azalea_block::properties::EastWall::Low,
|
||||
north: azalea_block::properties::NorthWall::Low,
|
||||
south: azalea_block::properties::SouthWall::Low,
|
||||
west: azalea_block::properties::WestWall::Low,
|
||||
up: false,
|
||||
waterlogged: false,
|
||||
}
|
||||
|
@ -651,6 +652,7 @@ mod tests {
|
|||
);
|
||||
// do a few steps so we fall on the wall
|
||||
for _ in 0..20 {
|
||||
app.world.run_schedule(CoreSchedule::FixedUpdate);
|
||||
app.update();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,38 +9,50 @@ version = "0.6.0"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-compression = {version = "^0.3.8", features = ["tokio", "zlib"], optional = true}
|
||||
async-compression = { version = "^0.3.8", features = [
|
||||
"tokio",
|
||||
"zlib",
|
||||
], optional = true }
|
||||
async-recursion = "1.0.0"
|
||||
azalea-auth = {path = "../azalea-auth", version = "^0.6.0" }
|
||||
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.6.0" }
|
||||
azalea-brigadier = {path = "../azalea-brigadier", version = "^0.6.0", features = ["azalea-buf"]}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.6.0" }
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.6.0" }
|
||||
azalea-core = {path = "../azalea-core", optional = true, version = "^0.6.0" }
|
||||
azalea-crypto = {path = "../azalea-crypto", version = "^0.6.0" }
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.6.0" }
|
||||
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.6.0" }
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.6.0" }
|
||||
azalea-world = {path = "../azalea-world", version = "^0.6.0" }
|
||||
bevy_ecs = { version = "0.9.1", default-features = false }
|
||||
azalea-auth = { path = "../azalea-auth", version = "^0.6.0" }
|
||||
azalea-block = { path = "../azalea-block", default-features = false, version = "^0.6.0" }
|
||||
azalea-brigadier = { path = "../azalea-brigadier", version = "^0.6.0", features = [
|
||||
"azalea-buf",
|
||||
] }
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
|
||||
azalea-chat = { path = "../azalea-chat", version = "^0.6.0" }
|
||||
azalea-core = { path = "../azalea-core", optional = true, version = "^0.6.0", features = [
|
||||
"serde",
|
||||
] }
|
||||
azalea-crypto = { path = "../azalea-crypto", version = "^0.6.0" }
|
||||
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0", features = [
|
||||
"serde",
|
||||
] }
|
||||
azalea-protocol-macros = { path = "./azalea-protocol-macros", version = "^0.6.0" }
|
||||
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
|
||||
azalea-world = { path = "../azalea-world", version = "^0.6.0" }
|
||||
bevy_ecs = { version = "0.10.0", default-features = false }
|
||||
byteorder = "^1.4.3"
|
||||
bytes = "^1.1.0"
|
||||
flate2 = "1.0.23"
|
||||
flate2 = "1.0.25"
|
||||
futures = "0.3.24"
|
||||
futures-util = "0.3.24"
|
||||
log = "0.4.17"
|
||||
serde = {version = "1.0.130", features = ["serde_derive"]}
|
||||
serde_json = "^1.0.72"
|
||||
serde = { version = "1.0.152", features = ["serde_derive"] }
|
||||
serde_json = "^1.0.93"
|
||||
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"]}
|
||||
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"
|
||||
|
||||
[features]
|
||||
connecting = []
|
||||
default = ["packets"]
|
||||
packets = ["connecting", "dep:async-compression", "dep:azalea-core"]
|
||||
strict_registry = ["packets"]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "^1.0.65"
|
||||
|
|
|
@ -79,10 +79,7 @@ impl Display for ServerAddress {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Cursor,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::{
|
||||
packets::{
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
use azalea_protocol_macros::ClientboundGamePacket;
|
||||
use azalea_buf::McBuf;
|
||||
|
||||
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
|
||||
pub struct ClientboundChunksBiomesPacket {
|
||||
pub chunk_biome_data: todo!(),
|
||||
}
|
|
@ -596,7 +596,7 @@ mod tests {
|
|||
node_type: NodeType::Argument {
|
||||
name: "position".to_string(),
|
||||
parser: BrigadierParser::Vec3,
|
||||
suggestions_type: Some(ResourceLocation::new("minecraft:test_suggestion").unwrap()),
|
||||
suggestions_type: Some(ResourceLocation::new("minecraft:test_suggestion")),
|
||||
},
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
use self::registry::RegistryHolder;
|
||||
use azalea_buf::McBuf;
|
||||
use azalea_core::{GameType, GlobalPos, OptionalGameType, ResourceLocation};
|
||||
use azalea_protocol_macros::ClientboundGamePacket;
|
||||
|
||||
/// The first packet sent by the server to the client after login.
|
||||
///
|
||||
/// This packet contains information about the state of the player, the
|
||||
/// world, and the registry.
|
||||
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
|
||||
pub struct ClientboundLoginPacket {
|
||||
pub player_id: u32,
|
||||
|
@ -9,7 +14,7 @@ pub struct ClientboundLoginPacket {
|
|||
pub game_type: GameType,
|
||||
pub previous_game_type: OptionalGameType,
|
||||
pub levels: Vec<ResourceLocation>,
|
||||
pub registry_holder: azalea_nbt::Tag,
|
||||
pub registry_holder: RegistryHolder,
|
||||
pub dimension_type: ResourceLocation,
|
||||
pub dimension: ResourceLocation,
|
||||
pub seed: i64,
|
||||
|
@ -25,3 +30,443 @@ pub struct ClientboundLoginPacket {
|
|||
pub is_flat: bool,
|
||||
pub last_death_location: Option<GlobalPos>,
|
||||
}
|
||||
|
||||
pub mod registry {
|
||||
//! [ClientboundLoginPacket](super::ClientboundLoginPacket) Registry
|
||||
//! Structures
|
||||
//!
|
||||
//! This module contains the structures used to represent the registry
|
||||
//! sent to the client upon login. This contains a lot of information about
|
||||
//! the game, including the types of chat messages, dimensions, and
|
||||
//! biomes.
|
||||
|
||||
use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
|
||||
use azalea_core::ResourceLocation;
|
||||
use azalea_nbt::Tag;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
|
||||
/// The base of the registry.
|
||||
///
|
||||
/// This is the registry that is sent to the client upon login.
|
||||
///
|
||||
/// As a tag, it is a compound tag that only contains a single compound tag.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct RegistryHolder {
|
||||
#[serde(rename = "")]
|
||||
pub root: RegistryRoot,
|
||||
}
|
||||
|
||||
impl TryFrom<Tag> for RegistryHolder {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(value: Tag) -> Result<Self, Self::Error> {
|
||||
serde_json::from_value(serde_json::to_value(value)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<Tag> for RegistryHolder {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_into(self) -> Result<Tag, Self::Error> {
|
||||
serde_json::from_value(serde_json::to_value(self)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl McBufReadable for RegistryHolder {
|
||||
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
|
||||
RegistryHolder::try_from(Tag::read_from(buf)?)
|
||||
.map_err(|e| BufReadError::Deserialization { source: e })
|
||||
}
|
||||
}
|
||||
|
||||
impl McBufWritable for RegistryHolder {
|
||||
fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> {
|
||||
TryInto::<Tag>::try_into(self.clone())?.write_into(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// The main part of the registry.
|
||||
///
|
||||
/// The only field of [`RegistryHolder`].
|
||||
/// Contains information from the server about chat, dimensions,
|
||||
/// and world generation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct RegistryRoot {
|
||||
#[cfg(feature = "strict_registry")]
|
||||
#[serde(rename = "minecraft:chat_type")]
|
||||
pub chat_type: RegistryType<ChatTypeElement>,
|
||||
|
||||
#[cfg(not(feature = "strict_registry"))]
|
||||
#[serde(rename = "minecraft:chat_type")]
|
||||
pub chat_type: Tag,
|
||||
|
||||
#[serde(rename = "minecraft:dimension_type")]
|
||||
pub dimension_type: RegistryType<DimensionTypeElement>,
|
||||
|
||||
#[cfg(feature = "strict_registry")]
|
||||
#[serde(rename = "minecraft:worldgen/biome")]
|
||||
pub world_type: RegistryType<WorldTypeElement>,
|
||||
|
||||
#[cfg(not(feature = "strict_registry"))]
|
||||
#[serde(rename = "minecraft:worldgen/biome")]
|
||||
pub world_type: Tag,
|
||||
}
|
||||
|
||||
/// A collection of values for a certain type of registry data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct RegistryType<T> {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: ResourceLocation,
|
||||
pub value: Vec<TypeValue<T>>,
|
||||
}
|
||||
|
||||
/// A value for a certain type of registry data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct TypeValue<T> {
|
||||
pub id: u32,
|
||||
pub name: ResourceLocation,
|
||||
pub element: T,
|
||||
}
|
||||
|
||||
/// Data about a kind of chat message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct ChatTypeElement {
|
||||
pub chat: ChatTypeData,
|
||||
pub narration: ChatTypeData,
|
||||
}
|
||||
|
||||
/// Data about a chat message.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct ChatTypeData {
|
||||
pub translation_key: String,
|
||||
pub parameters: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<ChatTypeStyle>,
|
||||
}
|
||||
|
||||
/// The style of a chat message.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct ChatTypeStyle {
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "Convert")]
|
||||
pub bold: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "Convert")]
|
||||
pub italic: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "Convert")]
|
||||
pub underlined: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "Convert")]
|
||||
pub strikethrough: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "Convert")]
|
||||
pub obfuscated: Option<bool>,
|
||||
}
|
||||
|
||||
/// Dimension attributes.
|
||||
#[cfg(feature = "strict_registry")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DimensionTypeElement {
|
||||
pub ambient_light: f32,
|
||||
#[serde(with = "Convert")]
|
||||
pub bed_works: bool,
|
||||
pub coordinate_scale: f32,
|
||||
pub effects: ResourceLocation,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fixed_time: Option<u32>,
|
||||
#[serde(with = "Convert")]
|
||||
pub has_ceiling: bool,
|
||||
#[serde(with = "Convert")]
|
||||
pub has_raids: bool,
|
||||
#[serde(with = "Convert")]
|
||||
pub has_skylight: bool,
|
||||
pub height: u32,
|
||||
pub infiniburn: ResourceLocation,
|
||||
pub logical_height: u32,
|
||||
pub min_y: i32,
|
||||
pub monster_spawn_block_light_limit: u32,
|
||||
pub monster_spawn_light_level: MonsterSpawnLightLevel,
|
||||
#[serde(with = "Convert")]
|
||||
pub natural: bool,
|
||||
#[serde(with = "Convert")]
|
||||
pub piglin_safe: bool,
|
||||
#[serde(with = "Convert")]
|
||||
pub respawn_anchor_works: bool,
|
||||
#[serde(with = "Convert")]
|
||||
pub ultrawarm: bool,
|
||||
}
|
||||
|
||||
/// Dimension attributes.
|
||||
#[cfg(not(feature = "strict_registry"))]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DimensionTypeElement {
|
||||
pub height: u32,
|
||||
pub min_y: i32,
|
||||
}
|
||||
|
||||
/// The light level at which monsters can spawn.
|
||||
///
|
||||
/// This can be either a single minimum value, or a formula with a min and
|
||||
/// max.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub enum MonsterSpawnLightLevel {
|
||||
/// A simple minimum value.
|
||||
Simple(u32),
|
||||
/// A complex value with a type, minimum, and maximum.
|
||||
/// Vanilla minecraft only uses one type, "minecraft:uniform".
|
||||
Complex {
|
||||
#[serde(rename = "type")]
|
||||
kind: ResourceLocation,
|
||||
value: MonsterSpawnLightLevelValues,
|
||||
},
|
||||
}
|
||||
|
||||
/// The min and max light levels at which monsters can spawn.
|
||||
///
|
||||
/// Values are inclusive.
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct MonsterSpawnLightLevelValues {
|
||||
#[serde(rename = "min_inclusive")]
|
||||
pub min: u32,
|
||||
#[serde(rename = "max_inclusive")]
|
||||
pub max: u32,
|
||||
}
|
||||
|
||||
/// Biome attributes.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct WorldTypeElement {
|
||||
pub temperature: f32,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature_modifier: Option<String>,
|
||||
pub downfall: f32,
|
||||
pub precipitation: BiomePrecipitation,
|
||||
pub effects: BiomeEffects,
|
||||
}
|
||||
|
||||
/// The precipitation of a biome.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub enum BiomePrecipitation {
|
||||
#[serde(rename = "none")]
|
||||
None,
|
||||
#[serde(rename = "rain")]
|
||||
Rain,
|
||||
#[serde(rename = "snow")]
|
||||
Snow,
|
||||
}
|
||||
|
||||
/// The effects of a biome.
|
||||
///
|
||||
/// This includes the sky, fog, water, and grass color,
|
||||
/// as well as music and other sound effects.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct BiomeEffects {
|
||||
pub sky_color: u32,
|
||||
pub fog_color: u32,
|
||||
pub water_color: u32,
|
||||
pub water_fog_color: u32,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub foliage_color: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grass_color: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grass_color_modifier: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub music: Option<BiomeMusic>,
|
||||
pub mood_sound: BiomeMoodSound,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub additions_sound: Option<AdditionsSound>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ambient_sound: Option<SoundId>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub particle: Option<BiomeParticle>,
|
||||
}
|
||||
|
||||
/// The music of the biome.
|
||||
///
|
||||
/// Some biomes have unique music that only play when inside them.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct BiomeMusic {
|
||||
#[serde(with = "Convert")]
|
||||
pub replace_current_music: bool,
|
||||
pub max_delay: u32,
|
||||
pub min_delay: u32,
|
||||
pub sound: SoundId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct BiomeMoodSound {
|
||||
pub tick_delay: u32,
|
||||
pub block_search_extent: u32,
|
||||
pub offset: f32,
|
||||
pub sound: SoundId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct AdditionsSound {
|
||||
pub tick_chance: f32,
|
||||
pub sound: SoundId,
|
||||
}
|
||||
|
||||
/// The ID of a sound.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct SoundId {
|
||||
pub sound_id: azalea_registry::SoundEvent,
|
||||
}
|
||||
|
||||
/// Biome particles.
|
||||
///
|
||||
/// Some biomes have particles that spawn in the air.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))]
|
||||
pub struct BiomeParticle {
|
||||
pub probability: f32,
|
||||
pub options: HashMap<String, String>,
|
||||
}
|
||||
|
||||
// Using a trait because you can't implement methods for
|
||||
// types you don't own, in this case Option<bool> and bool.
|
||||
trait Convert: Sized {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer;
|
||||
|
||||
fn deserialize<'de, D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>;
|
||||
}
|
||||
|
||||
// Convert between bool and u8
|
||||
impl Convert for bool {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_u8(if *self { 1 } else { 0 })
|
||||
}
|
||||
|
||||
fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
convert::<D>(u8::deserialize(deserializer)?)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert between Option<bool> and u8
|
||||
impl Convert for Option<bool> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Some(value) = self {
|
||||
Convert::serialize(value, serializer)
|
||||
} else {
|
||||
serializer.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
if let Some(value) = Option::<u8>::deserialize(deserializer)? {
|
||||
Ok(Some(convert::<D>(value)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deserializing logic here to deduplicate code
|
||||
fn convert<'de, D>(value: u8) -> Result<bool, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match value {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
other => Err(de::Error::invalid_value(
|
||||
de::Unexpected::Unsigned(other as u64),
|
||||
&"zero or one",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::registry::{DimensionTypeElement, RegistryHolder, RegistryRoot, RegistryType};
|
||||
use azalea_core::ResourceLocation;
|
||||
use azalea_nbt::Tag;
|
||||
|
||||
#[test]
|
||||
fn test_convert() {
|
||||
// Do NOT use Tag::End, they should be Tag::Compound.
|
||||
// This is just for testing.
|
||||
let registry = RegistryHolder {
|
||||
root: RegistryRoot {
|
||||
chat_type: Tag::End,
|
||||
dimension_type: RegistryType::<DimensionTypeElement> {
|
||||
kind: ResourceLocation::new("minecraft:dimension_type"),
|
||||
value: Vec::new(),
|
||||
},
|
||||
world_type: Tag::End,
|
||||
},
|
||||
};
|
||||
|
||||
let tag: Tag = registry.try_into().unwrap();
|
||||
let root = tag
|
||||
.as_compound()
|
||||
.unwrap()
|
||||
.get("")
|
||||
.unwrap()
|
||||
.as_compound()
|
||||
.unwrap();
|
||||
|
||||
let dimension = root
|
||||
.get("minecraft:dimension_type")
|
||||
.unwrap()
|
||||
.as_compound()
|
||||
.unwrap();
|
||||
let dimension_type = dimension.get("type").unwrap().as_string().unwrap();
|
||||
assert!(dimension_type == "minecraft:dimension_type");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ mod tests {
|
|||
let packet = ClientboundUpdateAdvancementsPacket {
|
||||
reset: true,
|
||||
added: [(
|
||||
ResourceLocation::new("minecraft:test").unwrap(),
|
||||
ResourceLocation::new("minecraft:test"),
|
||||
Advancement {
|
||||
parent_id: None,
|
||||
display: Some(DisplayInfo {
|
||||
|
@ -144,11 +144,11 @@ mod tests {
|
|||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
removed: vec![ResourceLocation::new("minecraft:test2").unwrap()],
|
||||
removed: vec![ResourceLocation::new("minecraft:test2")],
|
||||
progress: [(
|
||||
ResourceLocation::new("minecraft:test3").unwrap(),
|
||||
ResourceLocation::new("minecraft:test3"),
|
||||
[(
|
||||
ResourceLocation::new("minecraft:test4").unwrap(),
|
||||
ResourceLocation::new("minecraft:test4"),
|
||||
CriterionProgress {
|
||||
date: Some(123456789),
|
||||
},
|
||||
|
@ -170,12 +170,12 @@ mod tests {
|
|||
|
||||
let advancement = packet
|
||||
.added
|
||||
.get(&ResourceLocation::new("minecraft:test").unwrap())
|
||||
.get(&ResourceLocation::new("minecraft:test"))
|
||||
.unwrap()
|
||||
.clone();
|
||||
let read_advancement = read_packet
|
||||
.added
|
||||
.get(&ResourceLocation::new("minecraft:test").unwrap())
|
||||
.get(&ResourceLocation::new("minecraft:test"))
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(advancement.parent_id, read_advancement.parent_id);
|
||||
|
|
|
@ -124,142 +124,106 @@ impl McBufWritable for Recipe {
|
|||
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
||||
match &self.data {
|
||||
RecipeData::CraftingShapeless(recipe) => {
|
||||
ResourceLocation::new("minecraft:crafting_shapeless")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_shapeless").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingShaped(recipe) => {
|
||||
ResourceLocation::new("minecraft:crafting_shaped")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_shaped").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialArmorDye => {
|
||||
ResourceLocation::new("minecraft:crafting_special_armordye")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_special_armordye").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialBookCloning => {
|
||||
ResourceLocation::new("minecraft:crafting_special_bookcloning")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_special_bookcloning").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialMapCloning => {
|
||||
ResourceLocation::new("minecraft:crafting_special_mapcloning")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_special_mapcloning").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialMapExtending => {
|
||||
ResourceLocation::new("minecraft:crafting_special_mapextending")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_special_mapextending").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialFireworkRocket => {
|
||||
ResourceLocation::new("minecraft:crafting_special_firework_rocket")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialFireworkStar => {
|
||||
ResourceLocation::new("minecraft:crafting_special_firework_star")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialFireworkStarFade => {
|
||||
ResourceLocation::new("minecraft:crafting_special_firework_star_fade")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialRepairItem => {
|
||||
ResourceLocation::new("minecraft:crafting_special_repairitem")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_special_repairitem").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialTippedArrow => {
|
||||
ResourceLocation::new("minecraft:crafting_special_tippedarrow")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:crafting_special_tippedarrow").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialBannerDuplicate => {
|
||||
ResourceLocation::new("minecraft:crafting_special_bannerduplicate")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialBannerAddPattern => {
|
||||
ResourceLocation::new("minecraft:crafting_special_banneraddpattern")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialShieldDecoration => {
|
||||
ResourceLocation::new("minecraft:crafting_special_shielddecoration")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialShulkerBoxColoring => {
|
||||
ResourceLocation::new("minecraft:crafting_special_shulkerboxcoloring")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CraftingSpecialSuspiciousStew => {
|
||||
ResourceLocation::new("minecraft:crafting_special_suspiciousstew")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
}
|
||||
RecipeData::Smelting(recipe) => {
|
||||
ResourceLocation::new("minecraft:smelting")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:smelting").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::Blasting(recipe) => {
|
||||
ResourceLocation::new("minecraft:blasting")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:blasting").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::Smoking(recipe) => {
|
||||
ResourceLocation::new("minecraft:smoking")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:smoking").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::CampfireCooking(recipe) => {
|
||||
ResourceLocation::new("minecraft:campfire_cooking")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:campfire_cooking").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::Stonecutting(recipe) => {
|
||||
ResourceLocation::new("minecraft:stonecutting")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:stonecutting").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
RecipeData::Smithing(recipe) => {
|
||||
ResourceLocation::new("minecraft:smithing")
|
||||
.unwrap()
|
||||
.write_into(buf)?;
|
||||
ResourceLocation::new("minecraft:smithing").write_into(buf)?;
|
||||
self.identifier.write_into(buf)?;
|
||||
recipe.write_into(buf)?;
|
||||
}
|
||||
|
@ -275,78 +239,60 @@ impl McBufReadable for Recipe {
|
|||
|
||||
// rust doesn't let us match ResourceLocation so we have to do a big
|
||||
// if-else chain :(
|
||||
let data = if recipe_type == ResourceLocation::new("minecraft:crafting_shapeless").unwrap()
|
||||
{
|
||||
let data = if recipe_type == ResourceLocation::new("minecraft:crafting_shapeless") {
|
||||
RecipeData::CraftingShapeless(ShapelessRecipe::read_from(buf)?)
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_shaped").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_shaped") {
|
||||
RecipeData::CraftingShaped(ShapedRecipe::read_from(buf)?)
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_armordye").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_armordye") {
|
||||
RecipeData::CraftingSpecialArmorDye
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_bookcloning").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_bookcloning") {
|
||||
RecipeData::CraftingSpecialBookCloning
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_mapcloning").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_mapcloning") {
|
||||
RecipeData::CraftingSpecialMapCloning
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_mapextending").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_mapextending") {
|
||||
RecipeData::CraftingSpecialMapExtending
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_firework_rocket").unwrap()
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_firework_rocket")
|
||||
{
|
||||
RecipeData::CraftingSpecialFireworkRocket
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_firework_star").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_firework_star") {
|
||||
RecipeData::CraftingSpecialFireworkStar
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_firework_star_fade").unwrap()
|
||||
== ResourceLocation::new("minecraft:crafting_special_firework_star_fade")
|
||||
{
|
||||
RecipeData::CraftingSpecialFireworkStarFade
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_repairitem").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_repairitem") {
|
||||
RecipeData::CraftingSpecialRepairItem
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_tippedarrow").unwrap()
|
||||
{
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_tippedarrow") {
|
||||
RecipeData::CraftingSpecialTippedArrow
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_bannerduplicate").unwrap()
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_bannerduplicate")
|
||||
{
|
||||
RecipeData::CraftingSpecialBannerDuplicate
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_banneraddpattern").unwrap()
|
||||
== ResourceLocation::new("minecraft:crafting_special_banneraddpattern")
|
||||
{
|
||||
RecipeData::CraftingSpecialBannerAddPattern
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_shielddecoration").unwrap()
|
||||
== ResourceLocation::new("minecraft:crafting_special_shielddecoration")
|
||||
{
|
||||
RecipeData::CraftingSpecialShieldDecoration
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_shulkerboxcoloring").unwrap()
|
||||
== ResourceLocation::new("minecraft:crafting_special_shulkerboxcoloring")
|
||||
{
|
||||
RecipeData::CraftingSpecialShulkerBoxColoring
|
||||
} else if recipe_type
|
||||
== ResourceLocation::new("minecraft:crafting_special_suspiciousstew").unwrap()
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:crafting_special_suspiciousstew")
|
||||
{
|
||||
RecipeData::CraftingSpecialSuspiciousStew
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:smelting").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:smelting") {
|
||||
RecipeData::Smelting(CookingRecipe::read_from(buf)?)
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:blasting").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:blasting") {
|
||||
RecipeData::Blasting(CookingRecipe::read_from(buf)?)
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:smoking").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:smoking") {
|
||||
RecipeData::Smoking(CookingRecipe::read_from(buf)?)
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:campfire_cooking").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:campfire_cooking") {
|
||||
RecipeData::CampfireCooking(CookingRecipe::read_from(buf)?)
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:stonecutting").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:stonecutting") {
|
||||
RecipeData::Stonecutting(StoneCuttingRecipe::read_from(buf)?)
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:smithing").unwrap() {
|
||||
} else if recipe_type == ResourceLocation::new("minecraft:smithing") {
|
||||
RecipeData::Smithing(SmithingRecipe::read_from(buf)?)
|
||||
} else {
|
||||
return Err(BufReadError::UnexpectedStringEnumVariant {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use azalea_buf::{McBuf, McBufReadable, McBufWritable};
|
||||
use azalea_core::FixedBitSet;
|
||||
use azalea_protocol_macros::ServerboundGamePacket;
|
||||
use bevy_ecs::component::Component;
|
||||
|
||||
#[derive(Clone, Debug, McBuf, ServerboundGamePacket, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, McBuf, ServerboundGamePacket, PartialEq, Eq, Component)]
|
||||
pub struct ServerboundClientInformationPacket {
|
||||
/// The locale of the client.
|
||||
pub language: String,
|
||||
|
|
|
@ -50,6 +50,15 @@ pub async fn resolve_address(address: &ServerAddress) -> Result<SocketAddr, Reso
|
|||
port: redirect_srv.port(),
|
||||
};
|
||||
|
||||
if redirect_address.host == address.host {
|
||||
let lookup_ip_result = resolver.lookup_ip(redirect_address.host).await;
|
||||
let lookup_ip = lookup_ip_result.map_err(|_| ResolverError::NoIp)?;
|
||||
return Ok(SocketAddr::new(
|
||||
lookup_ip.iter().next().unwrap(),
|
||||
redirect_address.port,
|
||||
))
|
||||
}
|
||||
|
||||
// debug!("redirecting to {:?}", redirect_address);
|
||||
|
||||
return resolve_address(&redirect_address).await;
|
||||
|
|
|
@ -9,6 +9,11 @@ version = "0.6.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.6.0" }
|
||||
azalea-registry-macros = {path = "./azalea-registry-macros", version = "^0.6.0" }
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
|
||||
azalea-registry-macros = { path = "./azalea-registry-macros", version = "^0.6.0" }
|
||||
enum-as-inner = "0.5.1"
|
||||
serde = { version = "1.0.155", optional = true }
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "azalea-registry-macros/serde"]
|
||||
default = ["serde"]
|
||||
|
|
|
@ -15,3 +15,6 @@ proc-macro = true
|
|||
proc-macro2 = "1.0.39"
|
||||
quote = "1.0.18"
|
||||
syn = "1.0.95"
|
||||
|
||||
[features]
|
||||
serde = []
|
||||
|
|
|
@ -124,12 +124,16 @@ pub fn registry(input: TokenStream) -> TokenStream {
|
|||
|
||||
// Display that uses registry ids
|
||||
let mut display_items = quote! {};
|
||||
let mut from_str_items = quote! {};
|
||||
for item in &input.items {
|
||||
let name = &item.name;
|
||||
let id = &item.id;
|
||||
display_items.extend(quote! {
|
||||
Self::#name => write!(f, #id),
|
||||
});
|
||||
from_str_items.extend(quote! {
|
||||
#id => Ok(Self::#name),
|
||||
});
|
||||
}
|
||||
generated.extend(quote! {
|
||||
impl std::fmt::Display for #name {
|
||||
|
@ -139,7 +143,40 @@ pub fn registry(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
}
|
||||
impl std::str::FromStr for #name {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
#from_str_items
|
||||
_ => Err(format!("{s:?} is not a valid {name}", s = s, name = stringify!(#name))),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
{
|
||||
generated.extend(quote! {
|
||||
impl serde::Serialize for #name {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for #name {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generated.into()
|
||||
}
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
// auto-generated (so you can add doc comments to the registry enums if you
|
||||
// want)
|
||||
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable};
|
||||
use azalea_registry_macros::registry;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
pub trait Registry
|
||||
where
|
||||
|
|
|
@ -9,14 +9,17 @@ version = "0.6.0"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.6.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.6.0"}
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.6.0"}
|
||||
azalea-core = {path = "../azalea-core", version = "^0.6.0", features = ["bevy_ecs"]}
|
||||
azalea-ecs = {version = "0.6.0", path = "../azalea-ecs"}
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.6.0"}
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.6.0"}
|
||||
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
|
||||
azalea-block = { path = "../azalea-block", default-features = false, version = "^0.6.0" }
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
|
||||
azalea-chat = { path = "../azalea-chat", version = "^0.6.0" }
|
||||
azalea-core = { path = "../azalea-core", version = "^0.6.0", features = [
|
||||
"bevy_ecs",
|
||||
] }
|
||||
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0" }
|
||||
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
|
||||
bevy_app = "0.10.0"
|
||||
bevy_ecs = "0.10.0"
|
||||
derive_more = { version = "0.99.17", features = ["deref", "deref_mut"] }
|
||||
enum-as-inner = "0.5.1"
|
||||
log = "0.4.17"
|
||||
nohash-hasher = "0.2.0"
|
||||
|
|
|
@ -158,13 +158,13 @@ impl BitStorage {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
/// Get the data at the given index.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the given index is greater than or equal to
|
||||
/// the size of this storage.
|
||||
pub fn get(&self, index: usize) -> u64 {
|
||||
// Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)var1);
|
||||
// int var2 = this.cellIndex(var1);
|
||||
// long var3 = this.data[var2];
|
||||
// int var5 = (var1 - var2 * this.valuesPerLong) * this.bits;
|
||||
// return (int)(var3 >> var5 & this.mask);
|
||||
|
||||
assert!(
|
||||
index < self.size,
|
||||
"Index {} out of bounds (must be less than {})",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use azalea_core::ResourceLocation;
|
||||
use azalea_ecs::system::Resource;
|
||||
use bevy_ecs::system::Resource;
|
||||
use log::error;
|
||||
use nohash_hasher::IntMap;
|
||||
use parking_lot::RwLock;
|
||||
|
@ -8,12 +8,12 @@ use std::{
|
|||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use crate::{ChunkStorage, World};
|
||||
use crate::{ChunkStorage, Instance};
|
||||
|
||||
/// A container of [`World`]s. Worlds are stored as a Weak pointer here, so
|
||||
/// if no clients are using a world it will be forgotten.
|
||||
/// A container of [`Instance`]s (aka worlds). Instances are stored as a Weak
|
||||
/// pointer here, so if no clients are using an instance it will be forgotten.
|
||||
#[derive(Default, Resource)]
|
||||
pub struct WorldContainer {
|
||||
pub struct InstanceContainer {
|
||||
// We just refer to the chunks here and don't include entities because there's not that many
|
||||
// cases where we'd want to get every entity in the world (just getting the entities in chunks
|
||||
// should work fine).
|
||||
|
@ -26,18 +26,18 @@ pub struct WorldContainer {
|
|||
// telling them apart. We hope most servers are nice and don't do that though. It's only an
|
||||
// issue when there's multiple clients with the same WorldContainer in different worlds
|
||||
// anyways.
|
||||
pub worlds: HashMap<ResourceLocation, Weak<RwLock<World>>>,
|
||||
pub worlds: HashMap<ResourceLocation, Weak<RwLock<Instance>>>,
|
||||
}
|
||||
|
||||
impl WorldContainer {
|
||||
impl InstanceContainer {
|
||||
pub fn new() -> Self {
|
||||
WorldContainer {
|
||||
InstanceContainer {
|
||||
worlds: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a world from the container.
|
||||
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<RwLock<World>>> {
|
||||
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<RwLock<Instance>>> {
|
||||
self.worlds.get(name).and_then(|world| world.upgrade())
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ impl WorldContainer {
|
|||
name: ResourceLocation,
|
||||
height: u32,
|
||||
min_y: i32,
|
||||
) -> Arc<RwLock<World>> {
|
||||
) -> Arc<RwLock<Instance>> {
|
||||
if let Some(existing_lock) = self.worlds.get(&name).and_then(|world| world.upgrade()) {
|
||||
let existing = existing_lock.read();
|
||||
if existing.chunks.height != height {
|
||||
|
@ -66,7 +66,7 @@ impl WorldContainer {
|
|||
}
|
||||
existing_lock.clone()
|
||||
} else {
|
||||
let world = Arc::new(RwLock::new(World {
|
||||
let world = Arc::new(RwLock::new(Instance {
|
||||
chunks: ChunkStorage::new(height, min_y),
|
||||
entities_by_chunk: HashMap::new(),
|
||||
entity_by_id: IntMap::default(),
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
|
||||
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
|
||||
use azalea_ecs::component::Component;
|
||||
use bevy_ecs::component::Component;
|
||||
use thiserror::Error;
|
||||
use uuid::{uuid, Uuid};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use azalea_buf::{
|
|||
};
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot};
|
||||
use azalea_ecs::component::Component;
|
||||
use bevy_ecs::component::Component;
|
||||
use derive_more::Deref;
|
||||
use enum_as_inner::EnumAsInner;
|
||||
use nohash_hasher::IntSet;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use azalea_core::{Vec3, AABB};
|
||||
use azalea_ecs::{query::Changed, system::Query};
|
||||
use bevy_ecs::{query::Changed, system::Query};
|
||||
|
||||
use super::{Physics, Position};
|
||||
|
||||
|
|
|
@ -6,18 +6,17 @@ use crate::{
|
|||
entity::{
|
||||
self, add_dead, update_bounding_box, EntityUuid, MinecraftEntityId, Position, WorldName,
|
||||
},
|
||||
update_entity_by_id_index, update_uuid_index, PartialWorld, WorldContainer,
|
||||
update_entity_by_id_index, update_uuid_index, InstanceContainer, PartialInstance,
|
||||
};
|
||||
use azalea_core::ChunkPos;
|
||||
use azalea_ecs::{
|
||||
app::{App, CoreStage, Plugin},
|
||||
use bevy_app::{App, CoreSet, Plugin};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
ecs::Ecs,
|
||||
ecs::EntityMut,
|
||||
entity::Entity,
|
||||
query::{Added, Changed, With, Without},
|
||||
schedule::{IntoSystemDescriptor, SystemSet},
|
||||
system::{Command, Commands, Query, Res, ResMut, Resource},
|
||||
schedule::{IntoSystemConfig, IntoSystemConfigs, SystemSet},
|
||||
system::{Commands, EntityCommand, Query, Res, ResMut, Resource},
|
||||
world::{EntityMut, World},
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use log::{debug, warn};
|
||||
|
@ -32,56 +31,62 @@ use uuid::Uuid;
|
|||
|
||||
use super::Local;
|
||||
|
||||
/// A Bevy [`SystemSet`] for various types of entity updates.
|
||||
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
|
||||
pub enum EntityUpdateSet {
|
||||
/// Remove ECS entities that refer to an entity that was already in the ECS
|
||||
/// before.
|
||||
Deduplicate,
|
||||
/// Create search indexes for entities.
|
||||
Index,
|
||||
/// Remove despawned entities from search indexes.
|
||||
Deindex,
|
||||
}
|
||||
|
||||
/// Plugin handling some basic entity functionality.
|
||||
pub struct EntityPlugin;
|
||||
impl Plugin for EntityPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_system_set(
|
||||
SystemSet::new()
|
||||
.after("tick")
|
||||
.after("packet")
|
||||
.with_system(update_entity_chunk_positions)
|
||||
.with_system(remove_despawned_entities_from_indexes)
|
||||
.with_system(update_bounding_box)
|
||||
.with_system(add_dead)
|
||||
.with_system(
|
||||
add_updates_received
|
||||
.after("deduplicate_entities")
|
||||
.after("deduplicate_local_entities")
|
||||
.label("add_updates_received"),
|
||||
)
|
||||
.with_system(
|
||||
update_uuid_index
|
||||
.label("update_uuid_index")
|
||||
.after("deduplicate_local_entities")
|
||||
.after("deduplicate_entities"),
|
||||
)
|
||||
.with_system(debug_detect_updates_received_on_local_entities)
|
||||
.with_system(
|
||||
update_entity_by_id_index
|
||||
.label("update_entity_by_id_index")
|
||||
.after("deduplicate_entities"),
|
||||
)
|
||||
.with_system(debug_new_entity),
|
||||
// entities get added pre-update
|
||||
// added to indexes during update (done by this plugin)
|
||||
// modified during update
|
||||
// despawned post-update (done by this plugin)
|
||||
app.add_system(
|
||||
remove_despawned_entities_from_indexes
|
||||
.in_base_set(CoreSet::PreUpdate)
|
||||
.in_set(EntityUpdateSet::Deindex),
|
||||
)
|
||||
.add_system_set_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
SystemSet::new()
|
||||
.with_system(deduplicate_entities.label("deduplicate_entities"))
|
||||
.with_system(
|
||||
deduplicate_local_entities
|
||||
.label("deduplicate_local_entities")
|
||||
.before("update_uuid_index")
|
||||
.before("update_entity_by_id_index"),
|
||||
),
|
||||
.add_systems(
|
||||
(deduplicate_entities, deduplicate_local_entities)
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.in_set(EntityUpdateSet::Deduplicate),
|
||||
)
|
||||
.add_systems(
|
||||
(
|
||||
update_entity_chunk_positions,
|
||||
update_uuid_index,
|
||||
update_entity_by_id_index,
|
||||
)
|
||||
.in_set(EntityUpdateSet::Index),
|
||||
)
|
||||
.add_systems((
|
||||
add_updates_received,
|
||||
debug_new_entity,
|
||||
debug_detect_updates_received_on_local_entities,
|
||||
add_dead,
|
||||
update_bounding_box,
|
||||
))
|
||||
.init_resource::<EntityInfos>();
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_new_entity(query: Query<Entity, Added<MinecraftEntityId>>) {
|
||||
for entity in query.iter() {
|
||||
debug!("new entity: {:?}", entity);
|
||||
fn debug_new_entity(query: Query<(Entity, Option<&Local>), Added<MinecraftEntityId>>) {
|
||||
for (entity, local) in query.iter() {
|
||||
if local.is_some() {
|
||||
debug!("new local entity: {:?}", entity);
|
||||
} else {
|
||||
debug!("new entity: {:?}", entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,9 +134,9 @@ impl PartialEntityInfos {
|
|||
}
|
||||
}
|
||||
|
||||
/// A [`Command`] that applies a "relative update" to an entity, which means
|
||||
/// this update won't be run multiple times by different clients in the same
|
||||
/// world.
|
||||
/// An [`EntityCommand`] that applies a "relative update" to an entity, which
|
||||
/// means this update won't be run multiple times by different clients in the
|
||||
/// same world.
|
||||
///
|
||||
/// This is used to avoid a bug where when there's multiple clients in the same
|
||||
/// world and an entity sends a relative move packet to all clients, its
|
||||
|
@ -141,26 +146,24 @@ impl PartialEntityInfos {
|
|||
/// other clients within render distance will get too. You usually don't need
|
||||
/// this when the change isn't relative either.
|
||||
pub struct RelativeEntityUpdate {
|
||||
pub entity: Entity,
|
||||
pub partial_world: Arc<RwLock<PartialWorld>>,
|
||||
pub partial_world: Arc<RwLock<PartialInstance>>,
|
||||
// a function that takes the entity and updates it
|
||||
pub update: Box<dyn FnOnce(&mut EntityMut) + Send + Sync>,
|
||||
}
|
||||
impl Command for RelativeEntityUpdate {
|
||||
fn write(self, world: &mut Ecs) {
|
||||
impl EntityCommand for RelativeEntityUpdate {
|
||||
fn write(self, entity: Entity, world: &mut World) {
|
||||
let partial_entity_infos = &mut self.partial_world.write().entity_infos;
|
||||
|
||||
let mut entity = world.entity_mut(self.entity);
|
||||
let mut entity_mut = world.entity_mut(entity);
|
||||
|
||||
if Some(self.entity) == partial_entity_infos.owner_entity {
|
||||
if Some(entity) == partial_entity_infos.owner_entity {
|
||||
// if the entity owns this partial world, it's always allowed to update itself
|
||||
(self.update)(&mut entity);
|
||||
(self.update)(&mut entity_mut);
|
||||
return;
|
||||
};
|
||||
|
||||
let entity_id = *entity.get::<MinecraftEntityId>().unwrap();
|
||||
|
||||
let Some(updates_received) = entity.get_mut::<UpdatesReceived>() else {
|
||||
let entity_id = *entity_mut.get::<MinecraftEntityId>().unwrap();
|
||||
let Some(updates_received) = entity_mut.get_mut::<UpdatesReceived>() else {
|
||||
// a client tried to update another client, which isn't allowed
|
||||
return;
|
||||
};
|
||||
|
@ -177,9 +180,9 @@ impl Command for RelativeEntityUpdate {
|
|||
.updates_received
|
||||
.insert(entity_id, new_updates_received);
|
||||
|
||||
**entity.get_mut::<UpdatesReceived>().unwrap() = new_updates_received;
|
||||
**entity_mut.get_mut::<UpdatesReceived>().unwrap() = new_updates_received;
|
||||
|
||||
let mut entity = world.entity_mut(self.entity);
|
||||
let mut entity = world.entity_mut(entity);
|
||||
(self.update)(&mut entity);
|
||||
}
|
||||
}
|
||||
|
@ -215,7 +218,7 @@ fn update_entity_chunk_positions(
|
|||
),
|
||||
Changed<entity::Position>,
|
||||
>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
) {
|
||||
for (entity, pos, last_pos, world_name) in query.iter_mut() {
|
||||
let world_lock = world_container.get(world_name).unwrap();
|
||||
|
@ -282,7 +285,7 @@ fn debug_detect_updates_received_on_local_entities(
|
|||
fn remove_despawned_entities_from_indexes(
|
||||
mut commands: Commands,
|
||||
mut entity_infos: ResMut<EntityInfos>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
query: Query<(Entity, &EntityUuid, &Position, &WorldName, &LoadedBy), Changed<LoadedBy>>,
|
||||
) {
|
||||
for (entity, uuid, position, world_name, loaded_by) in &query {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,7 +12,7 @@ use self::{attributes::AttributeInstance, metadata::Health};
|
|||
pub use attributes::Attributes;
|
||||
use azalea_block::BlockState;
|
||||
use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB};
|
||||
use azalea_ecs::{
|
||||
use bevy_ecs::{
|
||||
bundle::Bundle,
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
|
@ -22,7 +22,9 @@ use azalea_ecs::{
|
|||
pub use data::*;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub use dimensions::{update_bounding_box, EntityDimensions};
|
||||
pub use info::{EntityInfos, EntityPlugin, LoadedBy, PartialEntityInfos, RelativeEntityUpdate};
|
||||
pub use info::{
|
||||
EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, PartialEntityInfos, RelativeEntityUpdate,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -240,7 +242,7 @@ pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<
|
|||
/// Most of the time, you should be using `azalea_registry::EntityKind`
|
||||
/// instead.
|
||||
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref)]
|
||||
pub struct EntityKind(azalea_registry::EntityKind);
|
||||
pub struct EntityKind(pub azalea_registry::EntityKind);
|
||||
|
||||
/// A bundle of components that every entity has. This doesn't contain metadata,
|
||||
/// that has to be added separately.
|
||||
|
|
247
azalea-world/src/iterators.rs
Normal file
247
azalea-world/src/iterators.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
//! Iterators for iterating over Minecraft blocks and chunks, based on
|
||||
//! [prismarine-world's iterators](https://github.com/PrismarineJS/prismarine-world/blob/master/src/iterators.js).
|
||||
|
||||
use azalea_core::{BlockPos, ChunkPos};
|
||||
|
||||
/// An octahedron iterator, useful for iterating over blocks in a world.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea_core::BlockPos;
|
||||
/// # use azalea_world::iterators::BlockIterator;
|
||||
///
|
||||
/// let mut iter = BlockIterator::new(BlockPos::default(), 4);
|
||||
/// for block_pos in iter {
|
||||
/// println!("{:?}", block_pos);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct BlockIterator {
|
||||
start: BlockPos,
|
||||
max_distance: u32,
|
||||
|
||||
pos: BlockPos,
|
||||
apothem: u32,
|
||||
left: i32,
|
||||
right: i32,
|
||||
}
|
||||
impl BlockIterator {
|
||||
pub fn new(start: BlockPos, max_distance: u32) -> Self {
|
||||
Self {
|
||||
start,
|
||||
max_distance,
|
||||
|
||||
pos: BlockPos {
|
||||
x: -1,
|
||||
y: -1,
|
||||
z: -1,
|
||||
},
|
||||
apothem: 1,
|
||||
left: 1,
|
||||
right: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for BlockIterator {
|
||||
type Item = BlockPos;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.apothem > self.max_distance {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.right -= 1;
|
||||
if self.right < 0 {
|
||||
self.left -= 1;
|
||||
if self.left < 0 {
|
||||
self.pos.z += 2;
|
||||
if self.pos.z > 1 {
|
||||
self.pos.y += 2;
|
||||
if self.pos.y > 1 {
|
||||
self.pos.x += 2;
|
||||
if self.pos.x > 1 {
|
||||
self.apothem += 1;
|
||||
self.pos.x = -1;
|
||||
}
|
||||
self.pos.y = -1;
|
||||
}
|
||||
self.pos.z = -1;
|
||||
}
|
||||
self.left = self.apothem as i32;
|
||||
}
|
||||
self.right = self.left;
|
||||
}
|
||||
let x = self.pos.x * self.right;
|
||||
let y = self.pos.y * ((self.apothem as i32) - self.left);
|
||||
let z = self.pos.z * ((self.apothem as i32) - (i32::abs(x) + i32::abs(y)));
|
||||
Some(BlockPos { x, y, z } + self.start)
|
||||
}
|
||||
}
|
||||
|
||||
/// A spiral iterator, useful for iterating over chunks in a world. Use
|
||||
/// `ChunkIterator` to sort by x+y+z (Manhattan) distance.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea_core::ChunkPos;
|
||||
/// # use azalea_world::iterators::SquareChunkIterator;
|
||||
///
|
||||
/// let mut iter = SquareChunkIterator::new(ChunkPos::default(), 4);
|
||||
/// for chunk_pos in iter {
|
||||
/// println!("{:?}", chunk_pos);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct SquareChunkIterator {
|
||||
start: ChunkPos,
|
||||
number_of_points: u32,
|
||||
|
||||
dir: ChunkPos,
|
||||
|
||||
segment_len: u32,
|
||||
pos: ChunkPos,
|
||||
segment_passed: u32,
|
||||
current_iter: u32,
|
||||
}
|
||||
impl SquareChunkIterator {
|
||||
pub fn new(start: ChunkPos, max_distance: u32) -> Self {
|
||||
Self {
|
||||
start,
|
||||
number_of_points: u32::pow(max_distance * 2 - 1, 2),
|
||||
|
||||
dir: ChunkPos { x: 1, z: 0 },
|
||||
|
||||
segment_len: 1,
|
||||
pos: ChunkPos::default(),
|
||||
segment_passed: 0,
|
||||
current_iter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the distance that this iterator won't go past.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea_core::ChunkPos;
|
||||
/// # use azalea_world::iterators::SquareChunkIterator;
|
||||
///
|
||||
/// let mut iter = SquareChunkIterator::new(ChunkPos::default(), 2);
|
||||
/// while let Some(chunk_pos) = iter.next() {
|
||||
/// println!("{:?}", chunk_pos);
|
||||
/// }
|
||||
/// iter.set_max_distance(4);
|
||||
/// while let Some(chunk_pos) = iter.next() {
|
||||
/// println!("{:?}", chunk_pos);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn set_max_distance(&mut self, max_distance: u32) {
|
||||
self.number_of_points = u32::pow(max_distance * 2 - 1, 2);
|
||||
}
|
||||
}
|
||||
impl Iterator for SquareChunkIterator {
|
||||
type Item = ChunkPos;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_iter > self.number_of_points {
|
||||
return None;
|
||||
}
|
||||
|
||||
let output = self.start + self.dir;
|
||||
|
||||
// make a step, add the direction to the current position
|
||||
self.pos.x += self.dir.x;
|
||||
self.pos.z += self.dir.z;
|
||||
self.segment_passed += 1;
|
||||
|
||||
if self.segment_passed == self.segment_len {
|
||||
// done with current segment
|
||||
self.segment_passed = 0;
|
||||
|
||||
// rotate directions
|
||||
(self.dir.x, self.dir.z) = (-self.dir.z, self.dir.x);
|
||||
|
||||
// increase segment length if necessary
|
||||
if self.dir.z == 0 {
|
||||
self.segment_len += 1;
|
||||
}
|
||||
}
|
||||
self.current_iter += 1;
|
||||
Some(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// A diagonal spiral iterator, useful for iterating over chunks in a world.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea_core::ChunkPos;
|
||||
/// # use azalea_world::iterators::ChunkIterator;
|
||||
///
|
||||
/// let mut iter = ChunkIterator::new(ChunkPos::default(), 4);
|
||||
/// for chunk_pos in iter {
|
||||
/// println!("{:?}", chunk_pos);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct ChunkIterator {
|
||||
pub max_distance: u32,
|
||||
pub start: ChunkPos,
|
||||
pub pos: ChunkPos,
|
||||
pub layer: i32,
|
||||
pub leg: i32,
|
||||
}
|
||||
impl ChunkIterator {
|
||||
pub fn new(start: ChunkPos, max_distance: u32) -> Self {
|
||||
Self {
|
||||
max_distance,
|
||||
start,
|
||||
pos: ChunkPos { x: 2, z: -1 },
|
||||
layer: 1,
|
||||
leg: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Iterator for ChunkIterator {
|
||||
type Item = ChunkPos;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.leg {
|
||||
-1 => {
|
||||
self.leg = 0;
|
||||
return Some(self.start);
|
||||
}
|
||||
0 => {
|
||||
if self.max_distance == 1 {
|
||||
return None;
|
||||
}
|
||||
self.pos.x -= 1;
|
||||
self.pos.z += 1;
|
||||
if self.pos.x == 0 {
|
||||
self.leg = 1;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
self.pos.x -= 1;
|
||||
self.pos.z -= 1;
|
||||
if self.pos.z == 0 {
|
||||
self.leg = 2;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
self.pos.x += 1;
|
||||
self.pos.z -= 1;
|
||||
if self.pos.x == 0 {
|
||||
self.leg = 3;
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
self.pos.x += 1;
|
||||
self.pos.z += 1;
|
||||
if self.pos.z == 0 {
|
||||
self.pos.x += 1;
|
||||
self.leg = 0;
|
||||
self.layer += 1;
|
||||
if self.layer == self.max_distance as i32 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Some(self.start + self.pos)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ mod bit_storage;
|
|||
mod chunk_storage;
|
||||
mod container;
|
||||
pub mod entity;
|
||||
pub mod iterators;
|
||||
pub mod palette;
|
||||
mod world;
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@ pub enum PalettedContainerType {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct PalettedContainer {
|
||||
pub bits_per_entry: u8,
|
||||
/// This is usually a list of unique values that appear in the container so
|
||||
/// they can be indexed by the bit storage.
|
||||
///
|
||||
/// Sometimes it doesn't contain anything if there's too many unique items
|
||||
/// in the bit storage, though.
|
||||
pub palette: Palette,
|
||||
/// Compacted list of indices pointing to entry IDs in the Palette.
|
||||
pub storage: BitStorage,
|
||||
|
@ -37,7 +42,7 @@ impl PalettedContainer {
|
|||
container_type: &'static PalettedContainerType,
|
||||
) -> Result<Self, BufReadError> {
|
||||
let bits_per_entry = u8::read_from(buf)?;
|
||||
let palette_type = PaletteType::from_bits_and_type(bits_per_entry, container_type);
|
||||
let palette_type = PaletteKind::from_bits_and_type(bits_per_entry, container_type);
|
||||
let palette = palette_type.read(buf)?;
|
||||
let size = container_type.size();
|
||||
|
||||
|
@ -57,15 +62,33 @@ impl PalettedContainer {
|
|||
}
|
||||
|
||||
/// Calculates the index of the given coordinates.
|
||||
pub fn get_index(&self, x: usize, y: usize, z: usize) -> usize {
|
||||
pub fn index_from_coords(&self, x: usize, y: usize, z: usize) -> usize {
|
||||
let size_bits = self.container_type.size_bits();
|
||||
|
||||
(((y << size_bits) | z) << size_bits) | x
|
||||
}
|
||||
|
||||
pub fn coords_from_index(&self, index: usize) -> (usize, usize, usize) {
|
||||
let size_bits = self.container_type.size_bits();
|
||||
let mask = (1 << size_bits) - 1;
|
||||
(
|
||||
index & mask,
|
||||
(index >> size_bits >> size_bits) & mask,
|
||||
(index >> size_bits) & mask,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the value at the given index.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if the index is greater than or equal to the number
|
||||
/// of things in the storage. (So for block states, it must be less than
|
||||
/// 4096).
|
||||
pub fn get_at_index(&self, index: usize) -> u32 {
|
||||
// first get the pallete id
|
||||
let paletted_value = self.storage.get(index);
|
||||
// and then get the value from that id
|
||||
self.palette.value_for(paletted_value as usize)
|
||||
}
|
||||
|
||||
|
@ -73,14 +96,14 @@ impl PalettedContainer {
|
|||
pub fn get(&self, x: usize, y: usize, z: usize) -> u32 {
|
||||
// let paletted_value = self.storage.get(self.get_index(x, y, z));
|
||||
// self.palette.value_for(paletted_value as usize)
|
||||
self.get_at_index(self.get_index(x, y, z))
|
||||
self.get_at_index(self.index_from_coords(x, y, z))
|
||||
}
|
||||
|
||||
/// Sets the id at the given coordinates and return the previous id
|
||||
pub fn get_and_set(&mut self, x: usize, y: usize, z: usize, value: u32) -> u32 {
|
||||
let paletted_value = self.id_for(value);
|
||||
self.storage
|
||||
.get_and_set(self.get_index(x, y, z), paletted_value as u64) as u32
|
||||
.get_and_set(self.index_from_coords(x, y, z), paletted_value as u64) as u32
|
||||
}
|
||||
|
||||
/// Sets the id at the given index and return the previous id. You probably
|
||||
|
@ -92,12 +115,12 @@ impl PalettedContainer {
|
|||
|
||||
/// Sets the id at the given coordinates and return the previous id
|
||||
pub fn set(&mut self, x: usize, y: usize, z: usize, value: u32) {
|
||||
self.set_at_index(self.get_index(x, y, z), value);
|
||||
self.set_at_index(self.index_from_coords(x, y, z), value);
|
||||
}
|
||||
|
||||
fn create_or_reuse_data(&self, bits_per_entry: u8) -> PalettedContainer {
|
||||
let new_palette_type =
|
||||
PaletteType::from_bits_and_type(bits_per_entry, &self.container_type);
|
||||
PaletteKind::from_bits_and_type(bits_per_entry, &self.container_type);
|
||||
// note for whoever is trying to optimize this: vanilla has this
|
||||
// but it causes a stack overflow since it's not changing the bits per entry
|
||||
// i don't know how to fix this properly so glhf
|
||||
|
@ -188,13 +211,14 @@ impl McBufWritable for PalettedContainer {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PaletteType {
|
||||
pub enum PaletteKind {
|
||||
SingleValue,
|
||||
Linear,
|
||||
Hashmap,
|
||||
Global,
|
||||
}
|
||||
|
||||
/// A representation of the different types of chunk palettes Minecraft uses.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Palette {
|
||||
/// ID of the corresponding entry in its global palette
|
||||
|
@ -211,13 +235,7 @@ impl Palette {
|
|||
match self {
|
||||
Palette::SingleValue(v) => *v,
|
||||
Palette::Linear(v) => v[id],
|
||||
Palette::Hashmap(v) => {
|
||||
if id >= v.len() {
|
||||
0
|
||||
} else {
|
||||
v[id]
|
||||
}
|
||||
}
|
||||
Palette::Hashmap(v) => v.get(id).copied().unwrap_or_default(),
|
||||
Palette::Global => id as u32,
|
||||
}
|
||||
}
|
||||
|
@ -241,49 +259,49 @@ impl McBufWritable for Palette {
|
|||
}
|
||||
}
|
||||
|
||||
impl PaletteType {
|
||||
impl PaletteKind {
|
||||
pub fn from_bits_and_type(bits_per_entry: u8, container_type: &PalettedContainerType) -> Self {
|
||||
match container_type {
|
||||
PalettedContainerType::BlockStates => match bits_per_entry {
|
||||
0 => PaletteType::SingleValue,
|
||||
1..=4 => PaletteType::Linear,
|
||||
5..=8 => PaletteType::Hashmap,
|
||||
_ => PaletteType::Global,
|
||||
0 => PaletteKind::SingleValue,
|
||||
1..=4 => PaletteKind::Linear,
|
||||
5..=8 => PaletteKind::Hashmap,
|
||||
_ => PaletteKind::Global,
|
||||
},
|
||||
PalettedContainerType::Biomes => match bits_per_entry {
|
||||
0 => PaletteType::SingleValue,
|
||||
1..=3 => PaletteType::Linear,
|
||||
_ => PaletteType::Global,
|
||||
0 => PaletteKind::SingleValue,
|
||||
1..=3 => PaletteKind::Linear,
|
||||
_ => PaletteKind::Global,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, buf: &mut Cursor<&[u8]>) -> Result<Palette, BufReadError> {
|
||||
Ok(match self {
|
||||
PaletteType::SingleValue => Palette::SingleValue(u32::var_read_from(buf)?),
|
||||
PaletteType::Linear => Palette::Linear(Vec::<u32>::var_read_from(buf)?),
|
||||
PaletteType::Hashmap => Palette::Hashmap(Vec::<u32>::var_read_from(buf)?),
|
||||
PaletteType::Global => Palette::Global,
|
||||
PaletteKind::SingleValue => Palette::SingleValue(u32::var_read_from(buf)?),
|
||||
PaletteKind::Linear => Palette::Linear(Vec::<u32>::var_read_from(buf)?),
|
||||
PaletteKind::Hashmap => Palette::Hashmap(Vec::<u32>::var_read_from(buf)?),
|
||||
PaletteKind::Global => Palette::Global,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_empty_palette(&self) -> Palette {
|
||||
match self {
|
||||
PaletteType::SingleValue => Palette::SingleValue(0),
|
||||
PaletteType::Linear => Palette::Linear(Vec::new()),
|
||||
PaletteType::Hashmap => Palette::Hashmap(Vec::new()),
|
||||
PaletteType::Global => Palette::Global,
|
||||
PaletteKind::SingleValue => Palette::SingleValue(0),
|
||||
PaletteKind::Linear => Palette::Linear(Vec::new()),
|
||||
PaletteKind::Hashmap => Palette::Hashmap(Vec::new()),
|
||||
PaletteKind::Global => Palette::Global,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Palette> for PaletteType {
|
||||
impl From<&Palette> for PaletteKind {
|
||||
fn from(palette: &Palette) -> Self {
|
||||
match palette {
|
||||
Palette::SingleValue(_) => PaletteType::SingleValue,
|
||||
Palette::Linear(_) => PaletteType::Linear,
|
||||
Palette::Hashmap(_) => PaletteType::Hashmap,
|
||||
Palette::Global => PaletteType::Global,
|
||||
Palette::SingleValue(_) => PaletteKind::SingleValue,
|
||||
Palette::Linear(_) => PaletteKind::Linear,
|
||||
Palette::Hashmap(_) => PaletteKind::Hashmap,
|
||||
Palette::Global => PaletteKind::Global,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,14 +331,14 @@ mod tests {
|
|||
assert_eq!(palette_container.bits_per_entry, 0);
|
||||
assert_eq!(palette_container.get_at_index(0), 0);
|
||||
assert_eq!(
|
||||
PaletteType::from(&palette_container.palette),
|
||||
PaletteType::SingleValue
|
||||
PaletteKind::from(&palette_container.palette),
|
||||
PaletteKind::SingleValue
|
||||
);
|
||||
palette_container.set_at_index(0, 1);
|
||||
assert_eq!(palette_container.get_at_index(0), 1);
|
||||
assert_eq!(
|
||||
PaletteType::from(&palette_container.palette),
|
||||
PaletteType::Linear
|
||||
PaletteKind::from(&palette_container.palette),
|
||||
PaletteKind::Linear
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -359,4 +377,22 @@ mod tests {
|
|||
palette_container.set_at_index(16, 16); // 5 bits
|
||||
assert_eq!(palette_container.bits_per_entry, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coords_from_index() {
|
||||
let palette_container =
|
||||
PalettedContainer::new(&PalettedContainerType::BlockStates).unwrap();
|
||||
|
||||
for x in 0..15 {
|
||||
for y in 0..15 {
|
||||
for z in 0..15 {
|
||||
assert_eq!(
|
||||
palette_container
|
||||
.coords_from_index(palette_container.index_from_coords(x, y, z)),
|
||||
(x, y, z)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,13 @@ use crate::{
|
|||
entity::{
|
||||
EntityInfos, EntityUuid, LoadedBy, Local, MinecraftEntityId, PartialEntityInfos, WorldName,
|
||||
},
|
||||
ChunkStorage, PartialChunkStorage, WorldContainer,
|
||||
iterators::ChunkIterator,
|
||||
palette::Palette,
|
||||
ChunkStorage, InstanceContainer, PartialChunkStorage,
|
||||
};
|
||||
use azalea_core::ChunkPos;
|
||||
use azalea_ecs::{
|
||||
use azalea_block::{BlockState, BlockStates};
|
||||
use azalea_core::{BlockPos, ChunkPos};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
query::{Changed, With, Without},
|
||||
system::{Commands, Query, Res, ResMut},
|
||||
|
@ -18,24 +21,24 @@ use std::{
|
|||
fmt::Debug,
|
||||
};
|
||||
|
||||
/// PartialWorlds are usually owned by clients, and hold strong references to
|
||||
/// chunks and entities in [`World`]s.
|
||||
/// PartialInstances are usually owned by clients, and hold strong references to
|
||||
/// chunks and entities in [`Instance`]s.
|
||||
///
|
||||
/// Basically, they hold the chunks and entities that are within render
|
||||
/// distance but can still access chunks and entities owned by other
|
||||
/// `PartialWorld`s that have the same `World`.
|
||||
/// `PartialInstance`s that have the same `Instance`.
|
||||
///
|
||||
/// This is primarily useful for having multiple clients in the same world.
|
||||
pub struct PartialWorld {
|
||||
/// This is primarily useful for having multiple clients in the same Instance.
|
||||
pub struct PartialInstance {
|
||||
pub chunks: PartialChunkStorage,
|
||||
/// Some metadata about entities, like what entities are in certain chunks.
|
||||
/// This does not contain the entity data itself, that's in the ECS.
|
||||
pub entity_infos: PartialEntityInfos,
|
||||
}
|
||||
|
||||
impl PartialWorld {
|
||||
impl PartialInstance {
|
||||
pub fn new(chunk_radius: u32, owner_entity: Option<Entity>) -> Self {
|
||||
PartialWorld {
|
||||
PartialInstance {
|
||||
chunks: PartialChunkStorage::new(chunk_radius),
|
||||
entity_infos: PartialEntityInfos::new(owner_entity),
|
||||
}
|
||||
|
@ -56,7 +59,7 @@ pub fn deduplicate_entities(
|
|||
(Changed<MinecraftEntityId>, Without<Local>),
|
||||
>,
|
||||
mut loaded_by_query: Query<&mut LoadedBy>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
) {
|
||||
// if this entity already exists, remove it
|
||||
for (new_entity, id, world_name) in query.iter_mut() {
|
||||
|
@ -82,8 +85,8 @@ pub fn deduplicate_entities(
|
|||
}
|
||||
commands.entity(new_entity).despawn();
|
||||
info!(
|
||||
"Entity with id {id:?} / {new_entity:?} already existed in the world, merging it with {old_entity:?}"
|
||||
);
|
||||
"Entity with id {id:?} / {new_entity:?} already existed in the world, merging it with {old_entity:?}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
@ -101,7 +104,7 @@ pub fn deduplicate_local_entities(
|
|||
(Entity, &MinecraftEntityId, &WorldName),
|
||||
(Changed<MinecraftEntityId>, With<Local>),
|
||||
>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
) {
|
||||
// if this entity already exists, remove the old one
|
||||
for (new_entity, id, world_name) in query.iter_mut() {
|
||||
|
@ -127,15 +130,20 @@ pub fn deduplicate_local_entities(
|
|||
|
||||
pub fn update_uuid_index(
|
||||
mut entity_infos: ResMut<EntityInfos>,
|
||||
query: Query<(Entity, &EntityUuid), Changed<EntityUuid>>,
|
||||
query: Query<(Entity, &EntityUuid, Option<&Local>), Changed<EntityUuid>>,
|
||||
) {
|
||||
for (entity, &uuid) in query.iter() {
|
||||
for (entity, &uuid, local) in query.iter() {
|
||||
// only add it if it doesn't already exist in
|
||||
// entity_infos.entity_by_uuid
|
||||
// if entity_infos.entity_by_uuid.contains_key(&uuid) {
|
||||
// warn!("Entity with UUID {uuid:?} already existed in the world, not adding
|
||||
// to index (ecs id: {entity:?})", uuid=*uuid); continue;
|
||||
// }
|
||||
if local.is_none() {
|
||||
if let Some(old_entity) = entity_infos.entity_by_uuid.get(&uuid) {
|
||||
debug!(
|
||||
"Entity with UUID {uuid:?} already existed in the world, not adding to
|
||||
index (old ecs id: {old_entity:?} / new ecs id: {entity:?})"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entity_infos.entity_by_uuid.insert(*uuid, entity);
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +175,7 @@ pub fn update_uuid_index(
|
|||
/// A world where the chunks are stored as weak pointers. This is used for
|
||||
/// shared worlds.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct World {
|
||||
pub struct Instance {
|
||||
pub chunks: ChunkStorage,
|
||||
|
||||
/// An index of all the entities we know are in the chunks of the world
|
||||
|
@ -177,14 +185,84 @@ pub struct World {
|
|||
pub entity_by_id: IntMap<MinecraftEntityId, Entity>,
|
||||
}
|
||||
|
||||
impl World {
|
||||
impl Instance {
|
||||
/// Get an ECS [`Entity`] from a Minecraft entity ID.
|
||||
pub fn entity_by_id(&self, entity_id: &MinecraftEntityId) -> Option<Entity> {
|
||||
self.entity_by_id.get(entity_id).copied()
|
||||
}
|
||||
|
||||
/// Find the coordinates of a block in the world.
|
||||
///
|
||||
/// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2`, for
|
||||
/// optimization purposes.
|
||||
pub fn find_block(
|
||||
&self,
|
||||
nearest_to: impl Into<BlockPos>,
|
||||
block_states: &BlockStates,
|
||||
) -> Option<BlockPos> {
|
||||
// iterate over every chunk in a 3d spiral pattern
|
||||
// and then check the palette for the block state
|
||||
|
||||
let nearest_to: BlockPos = nearest_to.into();
|
||||
let start_chunk: ChunkPos = (&nearest_to).into();
|
||||
let iter = ChunkIterator::new(start_chunk, 32);
|
||||
|
||||
for chunk_pos in iter {
|
||||
let chunk = self.chunks.get(&chunk_pos).unwrap();
|
||||
|
||||
let mut nearest_found_pos: Option<BlockPos> = None;
|
||||
let mut nearest_found_distance = 0;
|
||||
|
||||
for (section_index, section) in chunk.read().sections.iter().enumerate() {
|
||||
let maybe_has_block = match §ion.states.palette {
|
||||
Palette::SingleValue(id) => block_states.contains(&BlockState { id: *id }),
|
||||
Palette::Linear(ids) => ids
|
||||
.iter()
|
||||
.any(|&id| block_states.contains(&BlockState { id })),
|
||||
Palette::Hashmap(ids) => ids
|
||||
.iter()
|
||||
.any(|&id| block_states.contains(&BlockState { id })),
|
||||
Palette::Global => true,
|
||||
};
|
||||
if !maybe_has_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..4096 {
|
||||
let block_state = section.states.get_at_index(i);
|
||||
let block_state = BlockState { id: block_state };
|
||||
|
||||
if block_states.contains(&block_state) {
|
||||
let (section_x, section_y, section_z) = section.states.coords_from_index(i);
|
||||
let (x, y, z) = (
|
||||
chunk_pos.x * 16 + (section_x as i32),
|
||||
self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
|
||||
chunk_pos.z * 16 + (section_z as i32),
|
||||
);
|
||||
let this_block_pos = BlockPos { x, y, z };
|
||||
let this_block_distance = (nearest_to - this_block_pos).length_manhattan();
|
||||
// only update if it's closer
|
||||
if nearest_found_pos.is_none()
|
||||
|| this_block_distance < nearest_found_distance
|
||||
{
|
||||
nearest_found_pos = Some(this_block_pos);
|
||||
nearest_found_distance = this_block_distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we found the position, return it
|
||||
if nearest_found_pos.is_some() {
|
||||
return nearest_found_pos;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PartialWorld {
|
||||
impl Debug for PartialInstance {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("World")
|
||||
.field("chunk_storage", &self.chunks)
|
||||
|
@ -193,8 +271,8 @@ impl Debug for PartialWorld {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for PartialWorld {
|
||||
/// Creates a completely self-contained `PartialWorld`. This is only for
|
||||
impl Default for PartialInstance {
|
||||
/// Creates a completely self-contained `PartialInstance`. This is only for
|
||||
/// testing and shouldn't be used in actual code!
|
||||
fn default() -> Self {
|
||||
let chunk_storage = PartialChunkStorage::default();
|
||||
|
@ -208,24 +286,30 @@ impl Default for PartialWorld {
|
|||
|
||||
/// System to keep the entity_by_id index up-to-date.
|
||||
pub fn update_entity_by_id_index(
|
||||
mut query: Query<(Entity, &MinecraftEntityId, &WorldName), Changed<MinecraftEntityId>>,
|
||||
world_container: Res<WorldContainer>,
|
||||
mut query: Query<
|
||||
(Entity, &MinecraftEntityId, &WorldName, Option<&Local>),
|
||||
Changed<MinecraftEntityId>,
|
||||
>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
) {
|
||||
for (entity, id, world_name) in query.iter_mut() {
|
||||
for (entity, id, world_name, local) in query.iter_mut() {
|
||||
let world_lock = world_container.get(world_name).unwrap();
|
||||
let mut world = world_lock.write();
|
||||
// if let Some(old_entity) = world.entity_by_id.get(id) {
|
||||
// warn!(
|
||||
// "Entity with ID {id:?} already existed in the world, not adding to
|
||||
// index (old ecs id: {old_entity:?} / new ecs id: {entity:?})" );
|
||||
// continue;
|
||||
// }
|
||||
if local.is_none() {
|
||||
if let Some(old_entity) = world.entity_by_id.get(id) {
|
||||
debug!(
|
||||
"Entity with ID {id:?} already existed in the world, not adding to
|
||||
index (old ecs id: {old_entity:?} / new ecs id: {entity:?})"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
world.entity_by_id.insert(*id, entity);
|
||||
debug!("Added {entity:?} to {world_name:?} with {id:?}.");
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChunkStorage> for World {
|
||||
impl From<ChunkStorage> for Instance {
|
||||
/// Make an empty world from this `ChunkStorage`. This is meant to be a
|
||||
/// convenience function for tests.
|
||||
fn from(chunks: ChunkStorage) -> Self {
|
||||
|
|
|
@ -8,33 +8,31 @@ version = "0.6.0"
|
|||
|
||||
[package.metadata.release]
|
||||
pre-release-replacements = [
|
||||
{file = "README.md", search = "`azalea = \"[a-z0-9\\.-]+\"`", replace = "`azalea = \"{{version}}\"`"},
|
||||
{ file = "README.md", search = "`azalea = \"[a-z0-9\\.-]+\"`", replace = "`azalea = \"{{version}}\"`" },
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "^1.0.65"
|
||||
async-trait = "0.1.58"
|
||||
azalea-block = {version = "0.6.0", path = "../azalea-block"}
|
||||
azalea-chat = {version = "0.6.0", path = "../azalea-chat"}
|
||||
azalea-client = {version = "0.6.0", path = "../azalea-client"}
|
||||
azalea-core = {version = "0.6.0", path = "../azalea-core"}
|
||||
azalea-ecs = {version = "0.6.0", path = "../azalea-ecs"}
|
||||
azalea-physics = {version = "0.6.0", path = "../azalea-physics"}
|
||||
azalea-protocol = {version = "0.6.0", path = "../azalea-protocol"}
|
||||
azalea-registry = {version = "0.6.0", path = "../azalea-registry"}
|
||||
azalea-world = {version = "0.6.0", path = "../azalea-world"}
|
||||
bevy_tasks = "0.9.1"
|
||||
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
|
||||
azalea-block = { version = "0.6.0", path = "../azalea-block" }
|
||||
azalea-chat = { version = "0.6.0", path = "../azalea-chat" }
|
||||
azalea-client = { version = "0.6.0", path = "../azalea-client" }
|
||||
azalea-core = { version = "0.6.0", path = "../azalea-core" }
|
||||
azalea-physics = { version = "0.6.0", path = "../azalea-physics" }
|
||||
azalea-protocol = { version = "0.6.0", path = "../azalea-protocol" }
|
||||
azalea-registry = { version = "0.6.0", path = "../azalea-registry" }
|
||||
azalea-world = { version = "0.6.0", path = "../azalea-world" }
|
||||
bevy_app = "0.10.0"
|
||||
bevy_ecs = "0.10.0"
|
||||
bevy_tasks = "0.10.0"
|
||||
derive_more = { version = "0.99.17", features = ["deref", "deref_mut"] }
|
||||
futures = "0.3.25"
|
||||
futures-lite = "1.12.0"
|
||||
log = "0.4.17"
|
||||
nohash-hasher = "0.2.0"
|
||||
num-traits = "0.2.15"
|
||||
parking_lot = {version = "^0.12.1", features = ["deadlock_detection"]}
|
||||
parking_lot = { version = "^0.12.1", features = ["deadlock_detection"] }
|
||||
priority-queue = "1.3.0"
|
||||
thiserror = "^1.0.37"
|
||||
tokio = "^1.24.2"
|
||||
uuid = "1.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "^0.10.0"
|
||||
|
|
|
@ -83,4 +83,25 @@ Also note that just because something is an entity in the ECS doesn't mean that
|
|||
|
||||
See the [Bevy Cheatbook](https://bevy-cheatbook.github.io/programming/ecs-intro.html) to learn more about Bevy ECS (and the ECS paradigm in general).
|
||||
|
||||
# Debugging
|
||||
|
||||
Azalea uses several relatively complex features of Rust, which may make debugging certain issues more tricky if you're not familiar with them.
|
||||
|
||||
## Logging
|
||||
|
||||
One of the most useful tools for debugging issues is logging. The default log level is `info`, but you can make it show more or less information by changing the log level. Enabling logging is done with `RUST_LOG=debug cargo run` on Linux/bash or `set RUST_LOG=debug && cargo run` on Windows. The log levels are `trace`, `debug`, `info`, `warn`, and `error`, in ascending priority.
|
||||
|
||||
If it's a crash/panic and you believe it has to do with parsing a packet, you might want to set the level to `trace` since that'll make it show the first few hundred bytes of every packet received. This may produce a lot of logs, so pipe it into a file with `&> azalea.log` (on Linux).
|
||||
|
||||
Note: If you get a `SetLoggerError`, it's because you have multiple loggers. Azalea comes with a logger by default, see [`bevy_log`] for more information.
|
||||
|
||||
## Deadlocks
|
||||
|
||||
If your code is simply hanging, it might be a deadlock. Copy the deadlock block in [`azalea/examples/testbot.rs`](https://github.com/mat-1/azalea/blob/main/azalea/examples/testbot.rs) to the beginning of your code and it'll print a long backtrace if a deadlock is detected.
|
||||
|
||||
## Backtraces
|
||||
|
||||
Backtraces are also useful, though they're sometimes hard to read and don't always contain the actual location of the error. Run your code with `RUST_BACKTRACE=1` to enable full backtraces. If it's very long, often searching for the keyword "azalea" will help you filter out unrelated things and find the actual source of the issue.
|
||||
|
||||
[`azalea_client`]: https://docs.rs/azalea-client
|
||||
[`bevy_log`]: https://docs.rs/bevy_log
|
||||
|
|
|
@ -4,8 +4,6 @@ use azalea::prelude::*;
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let account = Account::offline("bot");
|
||||
// or let account = Account::microsoft("email").await;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use azalea::pathfinder::BlockPosGoal;
|
|||
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
|
||||
use azalea::{Account, Client, Event};
|
||||
use azalea_protocol::packets::game::serverbound_client_command_packet::ServerboundClientCommandPacket;
|
||||
use azalea_protocol::packets::game::ClientboundGamePacket;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default, Clone, Component)]
|
||||
|
@ -19,13 +20,10 @@ struct SwarmState {}
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
{
|
||||
use parking_lot::deadlock;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// Create a background thread which checks for deadlocks every 10s
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
|
@ -48,7 +46,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
let mut accounts = Vec::new();
|
||||
let mut states = Vec::new();
|
||||
|
||||
for i in 0..1 {
|
||||
for i in 0..5 {
|
||||
accounts.push(Account::offline(&format!("bot{i}")));
|
||||
states.push(State::default());
|
||||
}
|
||||
|
@ -142,6 +140,25 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
|
|||
"lag" => {
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
"findblock" => {
|
||||
let target_pos = bot.world().read().find_block(
|
||||
bot.component::<Position>(),
|
||||
&azalea_registry::Block::DiamondBlock.into(),
|
||||
);
|
||||
bot.chat(&format!("target_pos: {target_pos:?}",));
|
||||
}
|
||||
"gotoblock" => {
|
||||
let target_pos = bot.world().read().find_block(
|
||||
bot.component::<Position>(),
|
||||
&azalea_registry::Block::DiamondBlock.into(),
|
||||
);
|
||||
if let Some(target_pos) = target_pos {
|
||||
// +1 to stand on top of the block
|
||||
bot.goto(BlockPosGoal::from(target_pos.up(1)));
|
||||
} else {
|
||||
bot.chat("no diamond block found");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -151,6 +168,12 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
|
|||
action: azalea_protocol::packets::game::serverbound_client_command_packet::Action::PerformRespawn,
|
||||
}.get());
|
||||
}
|
||||
Event::Packet(packet) => match *packet {
|
||||
ClientboundGamePacket::Login(_) => {
|
||||
println!("login packet");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -166,7 +189,9 @@ async fn swarm_handle(
|
|||
SwarmEvent::Disconnect(account) => {
|
||||
println!("bot got kicked! {}", account.username);
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
swarm.add(account, State::default()).await?;
|
||||
swarm
|
||||
.add_with_exponential_backoff(account, State::default())
|
||||
.await;
|
||||
}
|
||||
SwarmEvent::Chat(m) => {
|
||||
println!("swarm chat message: {}", m.message().to_ansi());
|
||||
|
|
1
azalea/examples/todo/README.md
Normal file
1
azalea/examples/todo/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
These examples don't work yet and were only written to help design APIs. They will work in the future (probably with minor changes).
|
5
azalea/examples/craft_dig_straight_down.rs → azalea/examples/todo/craft_dig_straight_down.rs
Executable file → Normal file
5
azalea/examples/craft_dig_straight_down.rs → azalea/examples/todo/craft_dig_straight_down.rs
Executable file → Normal file
|
@ -38,7 +38,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
|||
bot.goto(pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0)))
|
||||
.await;
|
||||
let chest = bot
|
||||
.open_container(&bot.world().find_one_block(|b| b.id == "minecraft:chest"))
|
||||
.open_container(&bot.world().find_block(azalea_registry::Block::Chest))
|
||||
.await
|
||||
.unwrap();
|
||||
bot.take_amount(&chest, 5, |i| i.id == "#minecraft:planks")
|
||||
|
@ -47,8 +47,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
|||
|
||||
let crafting_table = bot
|
||||
.open_crafting_table(
|
||||
&bot.world
|
||||
.find_one_block(|b| b.id == "minecraft:crafting_table"),
|
||||
&bot.world.find_block(azalea_registry::Block::CraftingTable),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
2
azalea/examples/pvp.rs → azalea/examples/todo/pvp.rs
Executable file → Normal file
2
azalea/examples/pvp.rs → azalea/examples/todo/pvp.rs
Executable file → Normal file
|
@ -1,9 +1,9 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use azalea::ecs::query::With;
|
||||
use azalea::entity::metadata::Player;
|
||||
use azalea::{pathfinder, Account, Client, Event, GameProfileComponent};
|
||||
use azalea::{prelude::*, swarm::prelude::*};
|
||||
use azalea_ecs::query::With;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
|
@ -1,14 +1,14 @@
|
|||
use azalea_core::Vec3;
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin, PluginGroup, PluginGroupBuilder},
|
||||
use crate::app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
|
||||
use crate::ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
event::EventReader,
|
||||
query::{With, Without},
|
||||
schedule::IntoSystemDescriptor,
|
||||
schedule::IntoSystemConfig,
|
||||
system::{Commands, Query},
|
||||
AppTickExt,
|
||||
};
|
||||
use azalea_core::Vec3;
|
||||
use azalea_physics::{force_jump_listener, PhysicsSet};
|
||||
use azalea_world::entity::{metadata::Player, set_rotation, Jumping, Local, Physics, Position};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
|
@ -20,10 +20,14 @@ impl Plugin for BotPlugin {
|
|||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<LookAtEvent>()
|
||||
.add_event::<JumpEvent>()
|
||||
.add_system(insert_bot.before("deduplicate_entities"))
|
||||
.add_system(look_at_listener)
|
||||
.add_system(jump_listener.label("jump_listener").before("ai_step"))
|
||||
.add_tick_system(stop_jumping.after("ai_step"));
|
||||
.add_systems((
|
||||
insert_bot,
|
||||
look_at_listener.before(force_jump_listener),
|
||||
jump_listener,
|
||||
stop_jumping
|
||||
.in_schedule(CoreSchedule::FixedUpdate)
|
||||
.after(PhysicsSet),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,18 +6,15 @@ pub mod pathfinder;
|
|||
pub mod prelude;
|
||||
pub mod swarm;
|
||||
|
||||
use app::{App, Plugin, PluginGroup};
|
||||
pub use azalea_block as blocks;
|
||||
pub use azalea_client::*;
|
||||
pub use azalea_core::{BlockPos, Vec3};
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
component::Component,
|
||||
};
|
||||
pub use azalea_protocol as protocol;
|
||||
pub use azalea_registry::EntityKind;
|
||||
pub use azalea_world::{entity, World};
|
||||
pub use azalea_world::{entity, Instance};
|
||||
use bot::DefaultBotPlugins;
|
||||
use ecs::app::PluginGroup;
|
||||
use ecs::component::Component;
|
||||
use futures::Future;
|
||||
use protocol::{
|
||||
resolver::{self, ResolverError},
|
||||
|
@ -26,7 +23,10 @@ use protocol::{
|
|||
use thiserror::Error;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub type HandleFn<Fut, S> = fn(Client, Event, S) -> Fut;
|
||||
pub use bevy_app as app;
|
||||
pub use bevy_ecs as ecs;
|
||||
|
||||
pub type HandleFn<Fut, S> = fn(Client, azalea_client::Event, S) -> Fut;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum StartError {
|
||||
|
@ -106,6 +106,12 @@ where
|
|||
self.handler = Some(handler);
|
||||
self
|
||||
}
|
||||
/// Set the client state instead of initializing defaults.
|
||||
#[must_use]
|
||||
pub fn set_state(mut self, state: S) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
/// Add a plugin to the client.
|
||||
#[must_use]
|
||||
pub fn add_plugin<T: Plugin>(mut self, plugin: T) -> Self {
|
||||
|
@ -136,6 +142,7 @@ where
|
|||
|
||||
// An event that causes the schedule to run. This is only used internally.
|
||||
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
|
||||
|
||||
let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone());
|
||||
|
||||
let (bot, mut rx) = Client::start_client(
|
||||
|
|
|
@ -4,23 +4,23 @@ mod mtdstarlite;
|
|||
use crate::bot::{JumpEvent, LookAtEvent};
|
||||
use crate::{SprintDirection, WalkDirection};
|
||||
|
||||
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
||||
use azalea_core::{BlockPos, CardinalDirection};
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
use crate::app::{App, CoreSchedule, IntoSystemAppConfig, Plugin};
|
||||
use crate::ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
event::{EventReader, EventWriter},
|
||||
query::{With, Without},
|
||||
schedule::IntoSystemDescriptor,
|
||||
schedule::IntoSystemConfig,
|
||||
system::{Commands, Query, Res},
|
||||
AppTickExt,
|
||||
};
|
||||
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
||||
use azalea_core::{BlockPos, CardinalDirection};
|
||||
use azalea_physics::PhysicsSet;
|
||||
use azalea_world::entity::metadata::Player;
|
||||
use azalea_world::entity::Local;
|
||||
use azalea_world::{
|
||||
entity::{Physics, Position, WorldName},
|
||||
WorldContainer,
|
||||
InstanceContainer,
|
||||
};
|
||||
use bevy_tasks::{AsyncComputeTaskPool, Task};
|
||||
use futures_lite::future;
|
||||
|
@ -36,10 +36,16 @@ impl Plugin for PathfinderPlugin {
|
|||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<GotoEvent>()
|
||||
.add_event::<PathFoundEvent>()
|
||||
.add_tick_system(tick_execute_path.before("walk_listener"))
|
||||
.add_system(
|
||||
// Adding `.in_schedule(CoreSchedule::FixedUpdate)` makes a system run every
|
||||
// Minecraft tick (every 50 milliseconds).
|
||||
tick_execute_path
|
||||
.in_schedule(CoreSchedule::FixedUpdate)
|
||||
.before(PhysicsSet),
|
||||
)
|
||||
.add_system(goto_listener)
|
||||
.add_system(add_default_pathfinder.after("deduplicate_entities"))
|
||||
.add_system(handle_tasks)
|
||||
.add_system(add_default_pathfinder)
|
||||
.add_system(handle_tasks.before(path_found_listener))
|
||||
.add_system(path_found_listener);
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +93,7 @@ fn goto_listener(
|
|||
mut commands: Commands,
|
||||
mut events: EventReader<GotoEvent>,
|
||||
mut query: Query<(&Position, &WorldName)>,
|
||||
world_container: Res<WorldContainer>,
|
||||
world_container: Res<InstanceContainer>,
|
||||
) {
|
||||
let thread_pool = AsyncComputeTaskPool::get();
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use super::{Node, VerticalVel};
|
||||
use azalea_core::{BlockPos, CardinalDirection};
|
||||
use azalea_physics::collision::{self, BlockWithShape};
|
||||
use azalea_world::World;
|
||||
use azalea_world::Instance;
|
||||
|
||||
/// whether this block is passable
|
||||
fn is_block_passable(pos: &BlockPos, world: &World) -> bool {
|
||||
fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
|
||||
if let Some(block) = world.chunks.get_block_state(pos) {
|
||||
block.shape() == &collision::empty_shape()
|
||||
} else {
|
||||
|
@ -13,7 +13,7 @@ fn is_block_passable(pos: &BlockPos, world: &World) -> bool {
|
|||
}
|
||||
|
||||
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
||||
fn is_block_solid(pos: &BlockPos, world: &World) -> bool {
|
||||
fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
|
||||
if let Some(block) = world.chunks.get_block_state(pos) {
|
||||
block.shape() == &collision::block_shape()
|
||||
} else {
|
||||
|
@ -22,14 +22,14 @@ fn is_block_solid(pos: &BlockPos, world: &World) -> bool {
|
|||
}
|
||||
|
||||
/// Whether this block and the block above are passable
|
||||
fn is_passable(pos: &BlockPos, world: &World) -> bool {
|
||||
fn is_passable(pos: &BlockPos, world: &Instance) -> bool {
|
||||
is_block_passable(pos, world) && is_block_passable(&pos.up(1), world)
|
||||
}
|
||||
|
||||
/// Whether we can stand in this position. Checks if the block below is solid,
|
||||
/// and that the two blocks above that are passable.
|
||||
|
||||
fn is_standable(pos: &BlockPos, world: &World) -> bool {
|
||||
fn is_standable(pos: &BlockPos, world: &Instance) -> bool {
|
||||
is_block_solid(&pos.down(1), world) && is_passable(pos, world)
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ const JUMP_COST: f32 = 0.5;
|
|||
const WALK_ONE_BLOCK_COST: f32 = 1.0;
|
||||
|
||||
pub trait Move: Send + Sync {
|
||||
fn cost(&self, world: &World, node: &Node) -> f32;
|
||||
fn cost(&self, world: &Instance, node: &Node) -> f32;
|
||||
/// Returns by how much the entity's position should be changed when this
|
||||
/// move is executed.
|
||||
fn offset(&self) -> BlockPos;
|
||||
|
@ -51,7 +51,7 @@ pub trait Move: Send + Sync {
|
|||
|
||||
pub struct ForwardMove(pub CardinalDirection);
|
||||
impl Move for ForwardMove {
|
||||
fn cost(&self, world: &World, node: &Node) -> f32 {
|
||||
fn cost(&self, world: &Instance, node: &Node) -> f32 {
|
||||
if is_standable(&(node.pos + self.offset()), world)
|
||||
&& node.vertical_vel == VerticalVel::None
|
||||
{
|
||||
|
@ -67,7 +67,7 @@ impl Move for ForwardMove {
|
|||
|
||||
pub struct AscendMove(pub CardinalDirection);
|
||||
impl Move for AscendMove {
|
||||
fn cost(&self, world: &World, node: &Node) -> f32 {
|
||||
fn cost(&self, world: &Instance, node: &Node) -> f32 {
|
||||
if node.vertical_vel == VerticalVel::None
|
||||
&& is_block_passable(&node.pos.up(2), world)
|
||||
&& is_standable(&(node.pos + self.offset()), world)
|
||||
|
@ -89,7 +89,7 @@ impl Move for AscendMove {
|
|||
}
|
||||
pub struct DescendMove(pub CardinalDirection);
|
||||
impl Move for DescendMove {
|
||||
fn cost(&self, world: &World, node: &Node) -> f32 {
|
||||
fn cost(&self, world: &Instance, node: &Node) -> f32 {
|
||||
// check whether 3 blocks vertically forward are passable
|
||||
if node.vertical_vel == VerticalVel::None
|
||||
&& is_standable(&(node.pos + self.offset()), world)
|
||||
|
@ -112,7 +112,7 @@ impl Move for DescendMove {
|
|||
}
|
||||
pub struct DiagonalMove(pub CardinalDirection);
|
||||
impl Move for DiagonalMove {
|
||||
fn cost(&self, world: &World, node: &Node) -> f32 {
|
||||
fn cost(&self, world: &Instance, node: &Node) -> f32 {
|
||||
if node.vertical_vel != VerticalVel::None {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
|
@ -151,11 +151,11 @@ mod tests {
|
|||
use super::*;
|
||||
use azalea_block::BlockState;
|
||||
use azalea_core::ChunkPos;
|
||||
use azalea_world::{Chunk, ChunkStorage, PartialWorld};
|
||||
use azalea_world::{Chunk, ChunkStorage, PartialInstance};
|
||||
|
||||
#[test]
|
||||
fn test_is_passable() {
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut chunk_storage = ChunkStorage::default();
|
||||
|
||||
partial_world.chunks.set(
|
||||
|
@ -181,7 +181,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_is_solid() {
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut chunk_storage = ChunkStorage::default();
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
|
@ -206,7 +206,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_is_standable() {
|
||||
let mut partial_world = PartialWorld::default();
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut chunk_storage = ChunkStorage::default();
|
||||
partial_world.chunks.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
|
|
|
@ -3,4 +3,6 @@
|
|||
|
||||
pub use crate::{bot::BotClientExt, pathfinder::PathfinderClientExt, ClientBuilder};
|
||||
pub use azalea_client::{Account, Client, Event};
|
||||
pub use azalea_ecs::{component::Component, system::Resource};
|
||||
// this is necessary to make the macros that reference bevy_ecs work
|
||||
pub use crate::ecs as bevy_ecs;
|
||||
pub use crate::ecs::{component::Component, system::Resource};
|
||||
|
|
|
@ -13,14 +13,14 @@
|
|||
// in Swarm that's set to the smallest index of all the bots, and we remove all
|
||||
// messages from the queue that are before that index.
|
||||
|
||||
use azalea_client::chat::{ChatPacket, ChatReceivedEvent};
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
use crate::ecs::{
|
||||
component::Component,
|
||||
event::{EventReader, EventWriter},
|
||||
schedule::IntoSystemDescriptor,
|
||||
schedule::IntoSystemConfigs,
|
||||
system::{Commands, Query, Res, ResMut, Resource},
|
||||
};
|
||||
use azalea_client::chat::{ChatPacket, ChatReceivedEvent};
|
||||
use bevy_app::{App, Plugin};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::{Swarm, SwarmEvent};
|
||||
|
@ -30,8 +30,7 @@ pub struct SwarmChatPlugin;
|
|||
impl Plugin for SwarmChatPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<NewChatMessageEvent>()
|
||||
.add_system(chat_listener.label("chat_listener"))
|
||||
.add_system(update_min_index_and_shrink_queue.after("chat_listener"))
|
||||
.add_systems((chat_listener, update_min_index_and_shrink_queue).chain())
|
||||
.insert_resource(GlobalChatState {
|
||||
chat_queue: VecDeque::new(),
|
||||
chat_min_index: 0,
|
||||
|
@ -151,7 +150,7 @@ fn update_min_index_and_shrink_queue(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use azalea_ecs::{ecs::Ecs, event::Events, system::SystemState};
|
||||
use bevy_ecs::{event::Events, prelude::World, system::SystemState};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -161,8 +160,7 @@ mod tests {
|
|||
// event mangement in drain_events
|
||||
app.init_resource::<Events<ChatReceivedEvent>>()
|
||||
.init_resource::<Events<NewChatMessageEvent>>()
|
||||
.add_system(chat_listener.label("chat_listener"))
|
||||
.add_system(update_min_index_and_shrink_queue.after("chat_listener"))
|
||||
.add_systems((chat_listener, update_min_index_and_shrink_queue).chain())
|
||||
.insert_resource(GlobalChatState {
|
||||
chat_queue: VecDeque::new(),
|
||||
chat_min_index: 0,
|
||||
|
@ -170,7 +168,7 @@ mod tests {
|
|||
app
|
||||
}
|
||||
|
||||
fn drain_events(ecs: &mut Ecs) -> Vec<ChatPacket> {
|
||||
fn drain_events(ecs: &mut World) -> Vec<ChatPacket> {
|
||||
let mut system_state: SystemState<ResMut<Events<NewChatMessageEvent>>> =
|
||||
SystemState::new(ecs);
|
||||
let mut events = system_state.get_mut(ecs);
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
use azalea_client::LocalPlayer;
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin},
|
||||
event::EventWriter,
|
||||
query::With,
|
||||
system::{Query, ResMut, Resource},
|
||||
};
|
||||
use azalea_world::entity::MinecraftEntityId;
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::prelude::*;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
pub struct SwarmPlugin;
|
||||
|
|
|
@ -6,19 +6,14 @@ pub mod prelude;
|
|||
|
||||
use crate::{bot::DefaultBotPlugins, HandleFn};
|
||||
use azalea_client::{chat::ChatPacket, init_ecs_app, start_ecs, Account, Client, Event, JoinError};
|
||||
use azalea_ecs::{
|
||||
app::{App, Plugin, PluginGroup, PluginGroupBuilder},
|
||||
component::Component,
|
||||
ecs::Ecs,
|
||||
entity::Entity,
|
||||
system::Resource,
|
||||
};
|
||||
use azalea_protocol::{
|
||||
connect::ConnectionError,
|
||||
resolver::{self, ResolverError},
|
||||
ServerAddress,
|
||||
};
|
||||
use azalea_world::WorldContainer;
|
||||
use azalea_world::InstanceContainer;
|
||||
use bevy_app::{App, Plugin, PluginGroup, PluginGroupBuilder};
|
||||
use bevy_ecs::{component::Component, entity::Entity, system::Resource, world::World};
|
||||
use futures::future::join_all;
|
||||
use log::error;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
@ -29,20 +24,20 @@ use tokio::sync::mpsc;
|
|||
/// A swarm is a way to conveniently control many bots at once, while also
|
||||
/// being able to control bots at an individual level when desired.
|
||||
///
|
||||
/// Swarms are created from [`azalea::swarm::SwarmBuilder`].
|
||||
/// Swarms are created from [`SwarmBuilder`].
|
||||
///
|
||||
/// The `S` type parameter is the type of the state for individual bots.
|
||||
/// It's used to make the [`Swarm::add`] function work.
|
||||
#[derive(Clone, Resource)]
|
||||
pub struct Swarm {
|
||||
pub ecs_lock: Arc<Mutex<Ecs>>,
|
||||
pub ecs_lock: Arc<Mutex<World>>,
|
||||
|
||||
bots: Arc<Mutex<HashMap<Entity, Client>>>,
|
||||
|
||||
// bot_datas: Arc<Mutex<Vec<(Client, S)>>>,
|
||||
resolved_address: SocketAddr,
|
||||
address: ServerAddress,
|
||||
pub world_container: Arc<RwLock<WorldContainer>>,
|
||||
pub world_container: Arc<RwLock<InstanceContainer>>,
|
||||
|
||||
bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>,
|
||||
swarm_tx: mpsc::UnboundedSender<SwarmEvent>,
|
||||
|
@ -199,6 +194,12 @@ where
|
|||
self.swarm_handler = Some(handler);
|
||||
self
|
||||
}
|
||||
/// Set the swarm state instead of initializing defaults.
|
||||
#[must_use]
|
||||
pub fn set_swarm_state(mut self, swarm_state: SS) -> Self {
|
||||
self.swarm_state = swarm_state;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a plugin to the swarm.
|
||||
#[must_use]
|
||||
|
@ -247,7 +248,7 @@ where
|
|||
// resolve the address
|
||||
let resolved_address = resolver::resolve_address(&address).await?;
|
||||
|
||||
let world_container = Arc::new(RwLock::new(WorldContainer::default()));
|
||||
let world_container = Arc::new(RwLock::new(InstanceContainer::default()));
|
||||
|
||||
// we can't modify the swarm plugins after this
|
||||
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
|
||||
|
|
|
@ -45,7 +45,7 @@ use super::{EntityDataItem, EntityDataValue, OptionalUnsignedInt, Pose, Rotation
|
|||
use azalea_block::BlockState;
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_core::{BlockPos, Direction, Particle, Slot};
|
||||
use azalea_ecs::{bundle::Bundle, component::Component};
|
||||
use bevy_ecs::{bundle::Bundle, component::Component};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
@ -182,7 +182,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
|
|||
|
||||
# impl Allay {
|
||||
# pub fn apply_metadata(
|
||||
# entity: &mut azalea_ecs::system::EntityCommands,
|
||||
# entity: &mut bevy_ecs::system::EntityCommands,
|
||||
# d: EntityDataItem,
|
||||
# ) -> Result<(), UpdateMetadataError> {
|
||||
# match d.index {
|
||||
|
@ -195,7 +195,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
|
|||
# }
|
||||
code.append(f'impl {struct_name} {{')
|
||||
code.append(
|
||||
f' pub fn apply_metadata(entity: &mut azalea_ecs::system::EntityCommands, d: EntityDataItem) -> Result<(), UpdateMetadataError> {{')
|
||||
f' pub fn apply_metadata(entity: &mut bevy_ecs::system::EntityCommands, d: EntityDataItem) -> Result<(), UpdateMetadataError> {{')
|
||||
code.append(f' match d.index {{')
|
||||
|
||||
parent_last_index = -1
|
||||
|
@ -399,7 +399,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
|
|||
|
||||
# and now make the main apply_metadata
|
||||
# pub fn apply_metadata(
|
||||
# entity: &mut azalea_ecs::system::EntityCommands,
|
||||
# entity: &mut bevy_ecs::system::EntityCommands,
|
||||
# items: Vec<EntityDataItem>,
|
||||
# ) -> Result<(), UpdateMetadataError> {
|
||||
# if entity.contains::<Allay>() {
|
||||
|
@ -413,7 +413,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
|
|||
# }
|
||||
code.append(
|
||||
f'''pub fn apply_metadata(
|
||||
entity: &mut azalea_ecs::system::EntityCommands,
|
||||
entity: &mut bevy_ecs::system::EntityCommands,
|
||||
entity_kind: azalea_registry::EntityKind,
|
||||
items: Vec<EntityDataItem>,
|
||||
) -> Result<(), UpdateMetadataError> {{
|
||||
|
@ -435,7 +435,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
|
|||
code.append('}')
|
||||
code.append('')
|
||||
|
||||
# pub fn apply_default_metadata(entity: &mut azalea_ecs::system::EntityCommands, kind: azalea_registry::EntityKind) {
|
||||
# pub fn apply_default_metadata(entity: &mut bevy_ecs::system::EntityCommands, kind: azalea_registry::EntityKind) {
|
||||
# match kind {
|
||||
# azalea_registry::EntityKind::AreaEffectCloud => {
|
||||
# entity.insert(AreaEffectCloudMetadataBundle::default());
|
||||
|
@ -443,7 +443,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
|
|||
# }
|
||||
# }
|
||||
code.append(
|
||||
'pub fn apply_default_metadata(entity: &mut azalea_ecs::system::EntityCommands, kind: azalea_registry::EntityKind) {')
|
||||
'pub fn apply_default_metadata(entity: &mut bevy_ecs::system::EntityCommands, kind: azalea_registry::EntityKind) {')
|
||||
code.append(' match kind {')
|
||||
for entity_id in burger_entity_data:
|
||||
if entity_id.startswith('~'):
|
||||
|
|
Loading…
Add table
Reference in a new issue