* more mining stuff

* initialize azalea-tags crate

* more mining stuff 2

* mining in ecs

* well technically mining works but

no codegen for how long it takes to mine each block yet

* rename downloads to __cache__

it was bothering me since it's not *just* downloads

* codegen block behavior

* fix not sending packet to finish breaking block

* mining animation 🎉

* clippy

* cleanup, move Client::mine into a client extension

* add azalea/src/mining.rs

---------

Co-authored-by: mat <git@matdoes.dev>
This commit is contained in:
mat 2023-07-14 22:20:40 -05:00 committed by GitHub
parent d1afd02aa8
commit 7405427199
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 7523 additions and 1603 deletions

129
Cargo.lock generated
View file

@ -87,9 +87,9 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "async-channel"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
@ -98,9 +98,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0122885821398cc923ece939e24d1056a2384ee719432397fa9db87230ff11"
checksum = "62b74f44609f0f91493e3082d3734d98497e094777144380ea4db9f9905dd5b6"
dependencies = [
"flate2",
"futures-core",
@ -178,6 +178,7 @@ dependencies = [
"azalea-chat",
"azalea-client",
"azalea-core",
"azalea-entity",
"azalea-inventory",
"azalea-physics",
"azalea-protocol",
@ -293,6 +294,7 @@ dependencies = [
"azalea-chat",
"azalea-core",
"azalea-crypto",
"azalea-entity",
"azalea-inventory",
"azalea-nbt",
"azalea-physics",
@ -349,6 +351,29 @@ dependencies = [
"uuid",
]
[[package]]
name = "azalea-entity"
version = "0.1.0"
dependencies = [
"azalea-block",
"azalea-buf",
"azalea-chat",
"azalea-core",
"azalea-inventory",
"azalea-nbt",
"azalea-registry",
"azalea-world",
"bevy_app",
"bevy_ecs",
"derive_more",
"enum-as-inner 0.6.0",
"log",
"nohash-hasher",
"parking_lot",
"thiserror",
"uuid",
]
[[package]]
name = "azalea-inventory"
version = "0.7.0"
@ -401,6 +426,7 @@ version = "0.7.0"
dependencies = [
"azalea-block",
"azalea-core",
"azalea-entity",
"azalea-inventory",
"azalea-registry",
"azalea-world",
@ -426,6 +452,7 @@ dependencies = [
"azalea-chat",
"azalea-core",
"azalea-crypto",
"azalea-entity",
"azalea-inventory",
"azalea-nbt",
"azalea-protocol-macros",
@ -465,6 +492,7 @@ version = "0.7.0"
dependencies = [
"azalea-buf",
"azalea-registry-macros",
"once_cell",
"serde",
]
@ -933,9 +961,9 @@ dependencies = [
[[package]]
name = "const-oid"
version = "0.9.2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747"
[[package]]
name = "convert_case"
@ -945,9 +973,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cpufeatures"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
@ -1058,9 +1086,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "der"
version = "0.7.6"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17"
checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946"
dependencies = [
"const-oid",
"pem-rfc7468",
@ -1151,15 +1179,15 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.3.25"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569"
checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3"
dependencies = [
"serde",
]
@ -1531,10 +1559,11 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7"
checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97"
dependencies = [
"futures-util",
"http",
"hyper",
"rustls",
@ -1612,9 +1641,9 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-terminal"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix",
@ -1632,9 +1661,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.6"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "js-sys"
@ -1783,9 +1812,9 @@ dependencies = [
[[package]]
name = "num"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606"
checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af"
dependencies = [
"num-bigint",
"num-complex",
@ -1808,9 +1837,9 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
version = "0.8.2"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [
"byteorder",
"lazy_static",
@ -2153,7 +2182,7 @@ dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.2",
"regex-syntax 0.7.3",
"regex-syntax 0.7.4",
]
[[package]]
@ -2173,7 +2202,7 @@ checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.3",
"regex-syntax 0.7.4",
]
[[package]]
@ -2184,9 +2213,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "reqwest"
@ -2299,9 +2328,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.1"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc6396159432b5c8490d4e301d8c705f61860b8b6c863bf79942ce5401968f3"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
dependencies = [
"bitflags 2.3.3",
"errno",
@ -2312,9 +2341,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.2"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
dependencies = [
"log",
"ring",
@ -2333,9 +2362,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.100.1"
version = "0.101.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
dependencies = [
"ring",
"untrusted",
@ -2343,15 +2372,15 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
[[package]]
name = "ryu"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
[[package]]
name = "same-file"
@ -2386,27 +2415,27 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
version = "1.0.170"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56657f512baabca8f840542f9ca8152aecf182c473c26e46e58d6aab4f6e439"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.10"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c5113243e4a3a1c96587342d067f3e6b0f50790b6cf40d2868eb647a3eef0e"
checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.170"
version = "1.0.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77d477848e6b23adba0db397777d5aad864555bc17fd9c89abb3b8009788b7b8"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
dependencies = [
"proc-macro2",
"quote",
@ -2415,9 +2444,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.100"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed"
dependencies = [
"itoa",
"ryu",
@ -2723,9 +2752,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
[[package]]
name = "toml_edit"
version = "0.19.11"
version = "0.19.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78"
dependencies = [
"indexmap 2.0.0",
"toml_datetime",
@ -3162,9 +3191,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529"
dependencies = [
"memchr",
]

View file

@ -16,6 +16,7 @@ members = [
"azalea-physics",
"azalea-registry",
"azalea-inventory",
"azalea-entity",
]
resolver = "2"

View file

@ -312,6 +312,9 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut from_registry_block_to_blockstate_match = quote! {};
let mut from_registry_block_to_blockstates_match = quote! {};
// a list of block state ids that are waterlogged
let mut waterlogged_state_ids: Vec<u32> = Vec::new();
for block in &input.block_definitions.blocks {
let block_property_names = &block
.properties_and_defaults
@ -448,15 +451,21 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
is_default = false;
}
let property_type = if property.is_enum {
let property_value = if property.is_enum {
quote! {properties::#property_struct_name_ident::#variant}
} else {
quote! {#variant}
};
from_block_to_state_combination_match_inner.extend(quote! {
#property_name: #property_type,
#property_name: #property_value,
});
// if "waterlogged" is a property and it's true for this state then add it to
// waterlogged_state_ids
if property_name == "waterlogged" && property_value.to_string() == "true" {
waterlogged_state_ids.push(state_id)
}
}
from_block_to_state_match_inner.extend(quote! {
@ -473,7 +482,16 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
}
let Some(default_state_id) = default_state_id else {
let defaults = properties_with_name.iter().map(|p| if let TokenTree::Ident(i) = p.default.clone().into_iter().last().unwrap() { i.to_string() } else { panic!() }).collect::<Vec<_>>();
let defaults = properties_with_name
.iter()
.map(|p| {
if let TokenTree::Ident(i) = p.default.clone().into_iter().last().unwrap() {
i.to_string()
} else {
panic!()
}
})
.collect::<Vec<_>>();
panic!("Couldn't get default state id for {block_name_pascal_case}, combinations={block_properties_vec:?}, defaults={defaults:?}")
};
@ -570,6 +588,9 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
fn as_block_state(&self) -> BlockState {
#from_block_to_state_match
}
fn as_registry_block(&self) -> azalea_registry::Block {
azalea_registry::Block::#block_name_pascal_case
}
}
impl From<#block_struct_name> for BlockState {
@ -590,6 +611,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
block_structs.extend(block_struct);
}
let waterlogged_state_ids_match = quote! { #(#waterlogged_state_ids)|* };
let last_state_id = state_id - 1;
let mut generated = quote! {
impl BlockState {
@ -598,6 +621,12 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
pub fn max_state() -> u32 {
#last_state_id
}
/// Whether the given block state is waterlogged. This checks for
/// the `waterlogged` field, so it'll return false for water.
pub fn waterlogged(&self) -> bool {
matches!(self.id, #waterlogged_state_ids_match)
}
}
pub mod properties {

View file

@ -1,35 +1,55 @@
pub struct BlockBehavior {
pub has_collision: bool,
pub friction: f32,
pub jump_factor: f32,
pub destroy_time: f32,
pub explosion_resistance: f32,
pub requires_correct_tool_for_drops: bool,
}
impl Default for BlockBehavior {
fn default() -> Self {
Self {
has_collision: true,
friction: 0.6,
jump_factor: 1.0,
destroy_time: 0.,
explosion_resistance: 0.,
requires_correct_tool_for_drops: false,
}
}
}
impl BlockBehavior {
#[inline]
pub fn no_collision(mut self) -> Self {
self.has_collision = false;
self
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn friction(mut self, friction: f32) -> Self {
self.friction = friction;
self
}
#[inline]
pub fn jump_factor(mut self, jump_factor: f32) -> Self {
self.jump_factor = jump_factor;
self
}
pub fn destroy_time(mut self, destroy_time: f32) -> Self {
self.destroy_time = destroy_time;
self
}
pub fn explosion_resistance(mut self, explosion_resistance: f32) -> Self {
self.explosion_resistance = f32::max(0., explosion_resistance);
self
}
pub fn strength(self, destroy_time: f32, explosion_resistance: f32) -> Self {
self.destroy_time(destroy_time)
.explosion_resistance(explosion_resistance)
}
pub fn requires_correct_tool_for_drops(mut self) -> Self {
self.requires_correct_tool_for_drops = true;
self
}
}

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,9 @@ pub trait Block: Debug + Any {
/// Convert the block to a block state. This is lossless, as the block
/// contains all the state data.
fn as_block_state(&self) -> BlockState;
/// Convert the block to an [`azalea_registry::Block`]. This is lossy, as
/// `azalea_registry::Block` doesn't contain any state data.
fn as_registry_block(&self) -> azalea_registry::Block;
}
impl dyn Block {
pub fn downcast_ref<T: Block>(&self) -> Option<&T> {
@ -45,19 +48,14 @@ pub struct BlockState {
impl BlockState {
pub const AIR: BlockState = BlockState { id: 0 };
/// Transmutes a u32 to a block state.
///
/// # Safety
/// The `state_id` should be a valid block state.
#[inline]
pub unsafe fn from_u32_unchecked(state_id: u32) -> Self {
BlockState { id: state_id }
}
#[inline]
pub fn is_valid_state(state_id: u32) -> bool {
state_id <= Self::max_state()
}
pub fn is_air(&self) -> bool {
self == &Self::AIR
}
}
impl TryFrom<u32> for BlockState {
@ -66,7 +64,7 @@ impl TryFrom<u32> for BlockState {
/// Safely converts a state id to a block state.
fn try_from(state_id: u32) -> Result<Self, Self::Error> {
if Self::is_valid_state(state_id) {
Ok(unsafe { Self::from_u32_unchecked(state_id) })
Ok(BlockState { id: state_id })
} else {
Err(())
}
@ -98,6 +96,68 @@ impl std::fmt::Debug for BlockState {
}
}
#[derive(Clone, Debug)]
pub struct FluidState {
pub fluid: azalea_registry::Fluid,
pub height: u8,
}
impl Default for FluidState {
fn default() -> Self {
Self {
fluid: azalea_registry::Fluid::Empty,
height: 0,
}
}
}
impl From<BlockState> for FluidState {
fn from(state: BlockState) -> Self {
if state.waterlogged() {
Self {
fluid: azalea_registry::Fluid::Water,
height: 15,
}
} else {
let block = Box::<dyn Block>::from(state);
if let Some(water) = block.downcast_ref::<crate::blocks::Water>() {
Self {
fluid: azalea_registry::Fluid::Water,
height: water.level as u8,
}
} else if let Some(lava) = block.downcast_ref::<crate::blocks::Lava>() {
Self {
fluid: azalea_registry::Fluid::Lava,
height: lava.level as u8,
}
} else {
Self {
fluid: azalea_registry::Fluid::Empty,
height: 0,
}
}
}
}
}
impl From<FluidState> for BlockState {
fn from(state: FluidState) -> Self {
match state.fluid {
azalea_registry::Fluid::Empty => BlockState::AIR,
azalea_registry::Fluid::Water | azalea_registry::Fluid::FlowingWater => {
BlockState::from(crate::blocks::Water {
level: crate::properties::WaterLevel::from(state.height as u32),
})
}
azalea_registry::Fluid::Lava | azalea_registry::Fluid::FlowingLava => {
BlockState::from(crate::blocks::Lava {
level: crate::properties::LavaLevel::from(state.height as u32),
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -96,17 +96,10 @@ fn read_utf_with_len(buf: &mut Cursor<&[u8]>, max_length: u32) -> Result<String,
/// number of bytes read
// pub async fn read_varint_async(
// reader: &mut (dyn AsyncRead + Unpin + Send),
// ) -> Result<i32, BufReadError> {
// let mut buffer = [0];
// let mut ans = 0;
// for i in 0..5 {
// reader.read_exact(&mut buffer).await?;
// ans |= ((buffer[0] & 0b0111_1111) as i32) << (7 * i);
// if buffer[0] & 0b1000_0000 == 0 {
// break;
// }
// }
// Ok(ans)
// ) -> Result<i32, BufReadError> { let mut buffer = [0]; let mut ans = 0; for i
// in 0..5 { reader.read_exact(&mut buffer).await?; ans |= ((buffer[0] &
// 0b0111_1111) as i32) << (7 * i); if buffer[0] & 0b1000_0000 == 0 { break; }
// } Ok(ans)
// }
pub trait McBufReadable

View file

@ -38,6 +38,7 @@ regex = "1.9.1"
thiserror = "^1.0.43"
tokio = { version = "^1.29.1", features = ["sync"] }
uuid = "^1.4.0"
azalea-entity = { version = "0.1.0", path = "../azalea-entity" }
[features]
default = ["log"]

View file

@ -187,7 +187,10 @@ impl Account {
*access_token_mutex.lock() = new_access_token;
let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } =
new_account.account_opts else { unreachable!() };
new_account.account_opts
else {
unreachable!()
};
*msa.lock() = new_msa.lock().clone();
Ok(())

View file

@ -8,6 +8,7 @@ use crate::{
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
},
mining::{self, MinePlugin},
movement::{LastSentLookDirection, PlayerMovePlugin},
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
player::retroactively_add_game_profile_component,
@ -19,6 +20,7 @@ use crate::{
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
use azalea_core::Vec3;
use azalea_entity::{EntityPlugin, EntityUpdateSet, Local, Position};
use azalea_physics::{PhysicsPlugin, PhysicsSet};
use azalea_protocol::{
connect::{Connection, ConnectionError},
@ -41,10 +43,7 @@ use azalea_protocol::{
},
resolver, ServerAddress,
};
use azalea_world::{
entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName},
Instance, InstanceContainer, PartialInstance,
};
use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance};
use bevy_app::{App, FixedUpdate, Main, Plugin, PluginGroup, PluginGroupBuilder, Update};
use bevy_ecs::{
bundle::Bundle,
@ -132,6 +131,10 @@ impl From<ClientboundPlayerAbilitiesPacket> for PlayerAbilities {
}
}
/// Level must be 0..=4
#[derive(Component, Clone, Default, Deref, DerefMut)]
pub struct PermissionLevel(pub u8);
/// A component that contains a map of player UUIDs to their information in the
/// tab list.
///
@ -301,6 +304,8 @@ impl Client {
current_sequence_number: CurrentSequenceNumber::default(),
last_sent_direction: LastSentLookDirection::default(),
abilities: PlayerAbilities::default(),
permission_level: PermissionLevel::default(),
mining: mining::MineBundle::default(),
_local: Local,
});
@ -466,9 +471,9 @@ impl Client {
/// # Examples
///
/// ```
/// # use azalea_world::entity::WorldName;
/// # use azalea_world::InstanceName;
/// # fn example(client: &azalea_client::Client) {
/// let world_name = client.component::<WorldName>();
/// let world_name = client.component::<InstanceName>();
/// # }
pub fn component<T: Component + Clone>(&self) -> T {
self.query::<&T>(&mut self.ecs.lock()).clone()
@ -486,7 +491,7 @@ impl Client {
/// 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<Instance>> {
let world_name = self.component::<WorldName>();
let world_name = self.component::<InstanceName>();
let ecs = self.ecs.lock();
let instance_container = ecs.resource::<InstanceContainer>();
instance_container.get(&world_name).unwrap()
@ -495,7 +500,7 @@ impl Client {
/// Returns whether we have a received the login packet yet.
pub fn logged_in(&self) -> bool {
// the login packet tells us the world name
self.query::<Option<&WorldName>>(&mut self.ecs.lock())
self.query::<Option<&InstanceName>>(&mut self.ecs.lock())
.is_some()
}
@ -560,6 +565,10 @@ pub struct JoinedClientBundle {
pub current_sequence_number: CurrentSequenceNumber,
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
pub permission_level: PermissionLevel,
pub mining: mining::MineBundle,
pub _local: Local,
}
@ -660,7 +669,7 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<(
#[derive(Resource, Deref)]
pub struct TickBroadcast(broadcast::Sender<()>);
fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
let _ = tick_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
@ -706,6 +715,7 @@ impl PluginGroup for DefaultPlugins {
.add(PlayerMovePlugin)
.add(InteractPlugin)
.add(RespawnPlugin)
.add(MinePlugin)
.add(TickBroadcastPlugin);
#[cfg(feature = "log")]
{

View file

@ -15,10 +15,10 @@ impl Client {
///
/// # Examples
/// ```
/// # use azalea_world::entity::WorldName;
/// # use azalea_world::InstanceName;
/// # fn example(mut client: azalea_client::Client) {
/// let is_logged_in = client
/// .query::<Option<&WorldName>>(&mut client.ecs.lock())
/// .query::<Option<&InstanceName>>(&mut client.ecs.lock())
/// .is_some();
/// # }
/// ```
@ -39,7 +39,7 @@ impl Client {
/// ```
/// use azalea_client::{Client, GameProfileComponent};
/// use bevy_ecs::query::With;
/// use azalea_world::entity::{Position, metadata::Player};
/// use azalea_entity::{Position, metadata::Player};
///
/// # fn example(mut bot: Client, sender_name: String) {
/// let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>(

View file

@ -6,7 +6,7 @@ use std::sync::Arc;
use azalea_protocol::packets::game::{
clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, ClientboundGamePacket,
};
use azalea_world::entity::MinecraftEntityId;
use azalea_world::MinecraftEntityId;
use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::{component::Component, event::EventReader, query::Added, system::Query};
use derive_more::{Deref, DerefMut};

View file

@ -1,22 +1,22 @@
use std::ops::AddAssign;
use azalea_block::BlockState;
use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3};
use azalea_entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position};
use azalea_inventory::{ItemSlot, ItemSlotData};
use azalea_nbt::NbtList;
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_protocol::packets::game::{
serverbound_interact_packet::InteractionHand,
serverbound_swing_packet::ServerboundSwingPacket,
serverbound_use_item_on_packet::{BlockHit, ServerboundUseItemOnPacket},
};
use azalea_world::{
entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position, WorldName},
Instance, InstanceContainer,
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
prelude::Event,
event::{Event, EventReader, EventWriter},
schedule::IntoSystemConfigs,
system::{Commands, Query, Res},
};
@ -24,8 +24,9 @@ use derive_more::{Deref, DerefMut};
use log::warn;
use crate::{
client::{PermissionLevel, PlayerAbilities},
inventory::InventoryComponent,
local_player::{handle_send_packet_event, LocalGameMode},
local_player::{handle_send_packet_event, LocalGameMode, SendPacketEvent},
Client, LocalPlayer,
};
@ -33,15 +34,18 @@ use crate::{
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>().add_systems(
Update,
(
update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
)
.before(handle_send_packet_event)
.chain(),
);
app.add_event::<BlockInteractEvent>()
.add_event::<SwingArmEvent>()
.add_systems(
Update,
(
update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
handle_swing_arm_event,
)
.before(handle_send_packet_event)
.chain(),
);
}
}
@ -73,9 +77,15 @@ pub struct BlockInteractEvent {
/// A component that contains the number of changes this client has made to
/// blocks.
#[derive(Component, Copy, Clone, Debug, Default, Deref, DerefMut)]
#[derive(Component, Copy, Clone, Debug, Default, Deref)]
pub struct CurrentSequenceNumber(u32);
impl AddAssign<u32> for CurrentSequenceNumber {
fn add_assign(&mut self, rhs: u32) {
self.0 += rhs;
}
}
/// A component that contains the block that the player is currently looking at.
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct HitResultComponent(BlockHitResult);
@ -89,14 +99,15 @@ pub fn handle_block_interact_event(
)>,
) {
for event in events.iter() {
let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity)
else {
warn!("Sent BlockInteractEvent for entity that isn't LocalPlayer");
continue;
};
// TODO: check to make sure we're within the world border
**sequence_number += 1;
*sequence_number += 1;
// minecraft also does the interaction client-side (so it looks like clicking a
// button is instant) but we don't really need that
@ -143,7 +154,7 @@ fn update_hit_result_component(
&Position,
&EyeHeight,
&LookDirection,
&WorldName,
&InstanceName,
)>,
instance_container: Res<InstanceContainer>,
) {
@ -246,10 +257,11 @@ pub fn check_block_can_be_broken_by_item_in_adventure_mode(
.nbt
.as_compound()
.and_then(|nbt| nbt.get("tag").and_then(|nbt| nbt.as_compound()))
.and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list())) else {
// no CanDestroy tag
return false;
};
.and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list()))
else {
// no CanDestroy tag
return false;
};
let NbtList::String(_can_destroy) = can_destroy else {
// CanDestroy tag must be a list of strings
@ -265,3 +277,31 @@ pub fn check_block_can_be_broken_by_item_in_adventure_mode(
// true
}
pub fn can_use_game_master_blocks(
abilities: &PlayerAbilities,
permission_level: &PermissionLevel,
) -> bool {
abilities.instant_break && **permission_level >= 2
}
/// Swing your arm. This is purely a visual effect and won't interact with
/// anything in the world.
#[derive(Event)]
pub struct SwingArmEvent {
pub entity: Entity,
}
fn handle_swing_arm_event(
mut events: EventReader<SwingArmEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
) {
for event in events.iter() {
send_packet_events.send(SendPacketEvent {
entity: event.entity,
packet: ServerboundSwingPacket {
hand: InteractionHand::MainHand,
}
.get(),
});
}
}

View file

@ -101,6 +101,7 @@ pub struct InventoryComponent {
/// the scroll wheel.
pub selected_hotbar_slot: u8,
}
impl InventoryComponent {
/// Returns a reference to the currently active menu. If a container is open
/// it'll return [`Self::container_menu`], otherwise
@ -220,10 +221,10 @@ impl InventoryComponent {
loop {
let Some(&next_slot) = quick_craft_slots_iter.next() else {
carried.count = carried_count;
self.carried = ItemSlot::Present(carried);
return self.reset_quick_craft();
};
carried.count = carried_count;
self.carried = ItemSlot::Present(carried);
return self.reset_quick_craft();
};
slot = self.menu().slot(next_slot as usize).unwrap();
slot_index = next_slot;
@ -244,8 +245,8 @@ impl InventoryComponent {
// get the ItemSlotData for the slot
let ItemSlot::Present(slot) = slot else {
unreachable!("the loop above requires the slot to be present to break")
};
unreachable!("the loop above requires the slot to be present to break")
};
// if self.can_drag_to(slot) {
let mut new_carried = carried.clone();
@ -480,8 +481,8 @@ impl InventoryComponent {
// now extend the carried item
let target_slot = &mut self.carried;
let ItemSlot::Present(target_slot_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
unreachable!("target slot is not empty but is not present");
};
target_slot_item.count += taken_item.count();
}
}
@ -512,13 +513,13 @@ fn can_item_quick_replace(
ignore_item_count: bool,
) -> bool {
let ItemSlot::Present(target_slot) = target_slot else {
return false;
};
return false;
};
let ItemSlot::Present(item) = item else {
// i *think* this is what vanilla does
// not 100% sure lol probably doesn't matter though
return false;
};
return false;
};
if !item.is_same_item_and_nbt(target_slot) {
return false;

View file

@ -21,7 +21,7 @@ mod get_mc_dir;
pub mod interact;
pub mod inventory;
mod local_player;
mod mining;
pub mod mining;
mod movement;
pub mod packet_handling;
pub mod ping;

View file

@ -2,11 +2,9 @@ use std::{io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::{ChunkPos, GameMode};
use azalea_entity::{Dead, Position};
use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{
entity::{self, Dead, WorldName},
Instance, InstanceContainer, PartialInstance,
};
use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance};
use bevy_ecs::{
component::Component,
entity::Entity,
@ -32,7 +30,7 @@ use crate::{
/// You can also use the [`Local`] marker component for queries if you're only
/// checking for a local player and don't need the contents of this component.
///
/// [`Local`]: azalea_world::entity::Local
/// [`Local`]: azalea_entity::Local
/// [`Client`]: crate::Client
#[derive(Component)]
pub struct LocalPlayer {
@ -135,7 +133,7 @@ impl Drop for LocalPlayer {
/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s.
pub fn update_in_loaded_chunk(
mut commands: bevy_ecs::system::Commands,
query: Query<(Entity, &WorldName, &entity::Position)>,
query: Query<(Entity, &InstanceName, &Position)>,
instance_container: Res<InstanceContainer>,
) {
for (entity, local_player, position) in &query {

View file

@ -1,36 +1,543 @@
use azalea_core::BlockPos;
use bevy_app::{App, Plugin, Update};
use azalea_block::{Block, BlockState, FluidState};
use azalea_core::{BlockPos, Direction, GameMode};
use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
use azalea_inventory::ItemSlot;
use azalea_protocol::packets::game::serverbound_player_action_packet::{
self, ServerboundPlayerActionPacket,
};
use azalea_world::{InstanceContainer, InstanceName};
use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use crate::Client;
use crate::{
client::{PermissionLevel, PlayerAbilities},
interact::{
can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
HitResultComponent, SwingArmEvent,
},
inventory::InventoryComponent,
local_player::{LocalGameMode, SendPacketEvent},
};
/// A plugin that allows clients to break blocks in the world.
pub struct MinePlugin;
impl Plugin for MinePlugin {
fn build(&self, app: &mut App) {
app.add_event::<StartMiningBlockEvent>()
.add_systems(Update, handle_start_mining_block_event);
.add_event::<StartMiningBlockWithDirectionEvent>()
.add_event::<FinishMiningBlockEvent>()
.add_event::<StopMiningBlockEvent>()
.add_event::<MineBlockProgressEvent>()
.add_event::<AttackBlockEvent>()
.add_systems(FixedUpdate, continue_mining_block)
.add_systems(
Update,
(
handle_start_mining_block_event,
handle_start_mining_block_with_direction_event,
handle_finish_mining_block_event,
handle_stop_mining_block_event,
)
.chain(),
);
}
}
impl Client {
/// Start mining a block.
pub fn start_mining_block(&self, position: BlockPos) {
self.ecs.lock().send_event(StartMiningBlockEvent {
entity: self.entity,
position,
});
}
/// Information about the block we're currently mining. This is only present if
/// we're currently mining a block.
#[derive(Component)]
pub struct Mining {
pub pos: BlockPos,
pub dir: Direction,
}
/// Start mining the block at the given position.
///
/// If we're looking at the block then the correct direction will be used,
/// otherwise it'll be [`Direction::Down`].
#[derive(Event)]
pub struct StartMiningBlockEvent {
pub entity: Entity,
pub position: BlockPos,
}
fn handle_start_mining_block_event(mut _events: EventReader<StartMiningBlockEvent>) {
// for event in events.iter() {
// //
// }
fn handle_start_mining_block_event(
mut events: EventReader<StartMiningBlockEvent>,
mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
mut query: Query<&HitResultComponent>,
) {
for event in events.iter() {
let hit_result = query.get_mut(event.entity).unwrap();
let direction = if hit_result.block_pos == event.position {
// we're looking at the block
hit_result.direction
} else {
// we're not looking at the block, arbitrary direction
Direction::Down
};
start_mining_events.send(StartMiningBlockWithDirectionEvent {
entity: event.entity,
position: event.position,
direction,
});
}
}
#[derive(Event)]
pub struct StartMiningBlockWithDirectionEvent {
pub entity: Entity,
pub position: BlockPos,
pub direction: Direction,
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn handle_start_mining_block_with_direction_event(
mut events: EventReader<StartMiningBlockWithDirectionEvent>,
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
mut attack_block_events: EventWriter<AttackBlockEvent>,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut query: Query<(
&InstanceName,
&LocalGameMode,
&InventoryComponent,
&FluidOnEyes,
&Physics,
Option<&Mining>,
&mut CurrentSequenceNumber,
&mut MineDelay,
&mut MineProgress,
&mut MineTicks,
&mut MineItem,
&mut MineBlockPos,
)>,
instances: Res<InstanceContainer>,
mut commands: Commands,
) {
for event in events.iter() {
let (
instance_name,
game_mode,
inventory,
fluid_on_eyes,
physics,
mining,
mut sequence_number,
mut mine_delay,
mut mine_progress,
mut mine_ticks,
mut current_mining_item,
mut current_mining_pos,
) = query.get_mut(event.entity).unwrap();
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
if check_is_interaction_restricted(
&instance,
&event.position,
&game_mode.current,
inventory,
) {
continue;
}
// TODO (when world border is implemented): vanilla ignores if the block
// is outside of the worldborder
if game_mode.current == GameMode::Creative {
*sequence_number += 1;
finish_mining_events.send(FinishMiningBlockEvent {
entity: event.entity,
position: event.position,
});
**mine_delay = 5;
} else if mining.is_none()
|| !is_same_mining_target(
event.position,
inventory,
&current_mining_pos,
&current_mining_item,
)
{
if mining.is_some() {
// send a packet to stop mining since we just changed target
send_packet_events.send(SendPacketEvent {
entity: event.entity,
packet: ServerboundPlayerActionPacket {
action: serverbound_player_action_packet::Action::AbortDestroyBlock,
pos: current_mining_pos
.expect("IsMining is true so MineBlockPos must be present"),
direction: event.direction,
sequence: 0,
}
.get(),
});
}
let target_block_state = instance
.get_block_state(&event.position)
.unwrap_or_default();
*sequence_number += 1;
let block_is_solid = !target_block_state.is_air();
if block_is_solid && **mine_progress == 0. {
// interact with the block (like note block left click) here
attack_block_events.send(AttackBlockEvent {
entity: event.entity,
position: event.position,
});
}
let block = Box::<dyn Block>::from(target_block_state);
let held_item = inventory.held_item();
if block_is_solid
&& get_mine_progress(
block.as_ref(),
held_item.kind(),
&inventory.inventory_menu,
fluid_on_eyes,
physics,
) >= 1.
{
// block was broken instantly
finish_mining_events.send(FinishMiningBlockEvent {
entity: event.entity,
position: event.position,
});
} else {
commands.entity(event.entity).insert(Mining {
pos: event.position,
dir: event.direction,
});
**current_mining_pos = Some(event.position);
**current_mining_item = held_item;
**mine_progress = 0.;
**mine_ticks = 0.;
mine_block_progress_events.send(MineBlockProgressEvent {
entity: event.entity,
position: event.position,
destroy_stage: mine_progress.destroy_stage(),
})
}
send_packet_events.send(SendPacketEvent {
entity: event.entity,
packet: ServerboundPlayerActionPacket {
action: serverbound_player_action_packet::Action::StartDestroyBlock,
pos: event.position,
direction: event.direction,
sequence: **sequence_number,
}
.get(),
});
}
}
}
#[derive(Event)]
pub struct MineBlockProgressEvent {
pub entity: Entity,
pub position: BlockPos,
pub destroy_stage: Option<u32>,
}
/// A player left clicked on a block, used for stuff like interacting with note
/// blocks.
#[derive(Event)]
pub struct AttackBlockEvent {
pub entity: Entity,
pub position: BlockPos,
}
/// Returns whether the block and item are still the same as when we started
/// mining.
fn is_same_mining_target(
target_block: BlockPos,
inventory: &InventoryComponent,
current_mining_pos: &MineBlockPos,
current_mining_item: &MineItem,
) -> bool {
let held_item = inventory.held_item();
Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
}
/// A component bundle for players that can mine blocks.
#[derive(Bundle, Default)]
pub struct MineBundle {
pub delay: MineDelay,
pub progress: MineProgress,
pub ticks: MineTicks,
pub mining_pos: MineBlockPos,
pub mine_item: MineItem,
}
/// A component that counts down until we start mining the next block.
#[derive(Component, Debug, Default, Deref, DerefMut)]
pub struct MineDelay(pub u32);
/// A component that stores the progress of the current mining operation. This
/// is a value between 0 and 1.
#[derive(Component, Debug, Default, Deref, DerefMut)]
pub struct MineProgress(pub f32);
impl MineProgress {
pub fn destroy_stage(&self) -> Option<u32> {
if self.0 > 0. {
Some((self.0 * 10.) as u32)
} else {
None
}
}
}
/// A component that stores the number of ticks that we've been mining the same
/// block for. This is a float even though it should only ever be a round
/// number.
#[derive(Component, Debug, Default, Deref, DerefMut)]
pub struct MineTicks(pub f32);
/// A component that stores the position of the block we're currently mining.
#[derive(Component, Debug, Default, Deref, DerefMut)]
pub struct MineBlockPos(pub Option<BlockPos>);
/// A component that contains the item we're currently using to mine. If we're
/// not mining anything, it'll be [`ItemSlot::Empty`].
#[derive(Component, Debug, Default, Deref, DerefMut)]
pub struct MineItem(pub ItemSlot);
/// Sent when we completed mining a block.
#[derive(Event)]
pub struct FinishMiningBlockEvent {
pub entity: Entity,
pub position: BlockPos,
}
fn handle_finish_mining_block_event(
mut events: EventReader<FinishMiningBlockEvent>,
mut query: Query<(
&InstanceName,
&LocalGameMode,
&InventoryComponent,
&PlayerAbilities,
&PermissionLevel,
&mut CurrentSequenceNumber,
)>,
instances: Res<InstanceContainer>,
) {
for event in events.iter() {
let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
query.get_mut(event.entity).unwrap();
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
if check_is_interaction_restricted(
&instance,
&event.position,
&game_mode.current,
inventory,
) {
continue;
}
if game_mode.current == GameMode::Creative {
let held_item = inventory.held_item().kind();
if matches!(
held_item,
azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
) || azalea_registry::tags::items::SWORDS.contains(&held_item)
{
continue;
}
}
let Some(block_state) = instance.get_block_state(&event.position) else {
continue;
};
let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
if !can_use_game_master_blocks(abilities, permission_level)
&& matches!(
registry_block,
azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
)
{
continue;
}
if block_state == BlockState::AIR {
continue;
}
// when we break a waterlogged block we want to keep the water there
let fluid_state = FluidState::from(block_state);
let block_state_for_fluid = BlockState::from(fluid_state);
instance.set_block_state(&event.position, block_state_for_fluid);
}
}
/// Abort mining a block.
#[derive(Event)]
pub struct StopMiningBlockEvent {
pub entity: Entity,
}
fn handle_stop_mining_block_event(
mut events: EventReader<StopMiningBlockEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
mut commands: Commands,
) {
for event in events.iter() {
let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
let mine_block_pos =
mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
send_packet_events.send(SendPacketEvent {
entity: event.entity,
packet: ServerboundPlayerActionPacket {
action: serverbound_player_action_packet::Action::AbortDestroyBlock,
pos: mine_block_pos,
direction: Direction::Down,
sequence: 0,
}
.get(),
});
commands.entity(event.entity).remove::<Mining>();
**mine_progress = 0.;
mine_block_progress_events.send(MineBlockProgressEvent {
entity: event.entity,
position: mine_block_pos,
destroy_stage: None,
});
}
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn continue_mining_block(
mut query: Query<(
Entity,
&InstanceName,
&LocalGameMode,
&InventoryComponent,
&MineBlockPos,
&MineItem,
&FluidOnEyes,
&Physics,
&Mining,
&mut MineDelay,
&mut MineProgress,
&mut MineTicks,
&mut CurrentSequenceNumber,
)>,
mut send_packet_events: EventWriter<SendPacketEvent>,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
mut swing_arm_events: EventWriter<SwingArmEvent>,
instances: Res<InstanceContainer>,
mut commands: Commands,
) {
for (
entity,
instance_name,
game_mode,
inventory,
current_mining_pos,
current_mining_item,
fluid_on_eyes,
physics,
mining,
mut mine_delay,
mut mine_progress,
mut mine_ticks,
mut sequence_number,
) in query.iter_mut()
{
if **mine_delay > 0 {
**mine_delay -= 1;
continue;
}
if game_mode.current == GameMode::Creative {
// TODO: worldborder check
**mine_delay = 5;
finish_mining_events.send(FinishMiningBlockEvent {
entity,
position: mining.pos,
});
*sequence_number += 1;
send_packet_events.send(SendPacketEvent {
entity,
packet: ServerboundPlayerActionPacket {
action: serverbound_player_action_packet::Action::StartDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
}
.get(),
});
swing_arm_events.send(SwingArmEvent { entity });
} else if is_same_mining_target(
mining.pos,
inventory,
current_mining_pos,
current_mining_item,
) {
let instance_lock = instances.get(instance_name).unwrap();
let instance = instance_lock.read();
let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
if target_block_state.is_air() {
commands.entity(entity).remove::<Mining>();
continue;
}
let block = Box::<dyn Block>::from(target_block_state);
**mine_progress += get_mine_progress(
block.as_ref(),
current_mining_item.kind(),
&inventory.inventory_menu,
fluid_on_eyes,
physics,
);
if **mine_ticks % 4. == 0. {
// vanilla makes a mining sound here
}
**mine_ticks += 1.;
if **mine_progress >= 1. {
commands.entity(entity).remove::<Mining>();
*sequence_number += 1;
finish_mining_events.send(FinishMiningBlockEvent {
entity,
position: mining.pos,
});
send_packet_events.send(SendPacketEvent {
entity,
packet: ServerboundPlayerActionPacket {
action: serverbound_player_action_packet::Action::StopDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
}
.get(),
});
**mine_progress = 0.;
**mine_ticks = 0.;
**mine_delay = 0;
}
mine_block_progress_events.send(MineBlockProgressEvent {
entity,
position: mining.pos,
destroy_stage: mine_progress.destroy_stage(),
});
swing_arm_events.send(SwingArmEvent { entity });
} else {
start_mining_events.send(StartMiningBlockWithDirectionEvent {
entity,
position: mining.pos,
direction: mining.dir,
})
}
swing_arm_events.send(SwingArmEvent { entity });
}
}

View file

@ -2,6 +2,8 @@ use crate::client::Client;
use crate::local_player::{
update_in_loaded_chunk, LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState,
};
use azalea_entity::{metadata::Sprinting, Attributes, Jumping};
use azalea_entity::{LastSentPosition, LookDirection, Physics, Position};
use azalea_physics::{force_jump_listener, PhysicsSet};
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
use azalea_protocol::packets::game::{
@ -10,10 +12,7 @@ use azalea_protocol::packets::game::{
serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket,
serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket,
};
use azalea_world::{
entity::{self, metadata::Sprinting, Attributes, Jumping, MinecraftEntityId},
MoveEntityError,
};
use azalea_world::{MinecraftEntityId, MoveEntityError};
use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::{
@ -89,7 +88,7 @@ impl Client {
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
pub fn set_direction(&mut self, y_rot: f32, x_rot: f32) {
let mut ecs = self.ecs.lock();
let mut look_direction = self.query::<&mut entity::LookDirection>(&mut ecs);
let mut look_direction = self.query::<&mut LookDirection>(&mut ecs);
(look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
@ -110,12 +109,12 @@ pub(crate) fn send_position(
&MinecraftEntityId,
&mut LocalPlayer,
&mut PhysicsState,
&entity::Position,
&mut entity::LastSentPosition,
&mut entity::Physics,
&entity::LookDirection,
&Position,
&mut LastSentPosition,
&mut Physics,
&LookDirection,
&mut LastSentLookDirection,
&entity::metadata::Sprinting,
&Sprinting,
),
&LocalPlayerInLoadedChunk,
>,
@ -223,7 +222,7 @@ impl LocalPlayer {
fn send_sprinting_if_needed(
&mut self,
id: &MinecraftEntityId,
sprinting: &entity::metadata::Sprinting,
sprinting: &Sprinting,
physics_state: &mut PhysicsState,
) {
let was_sprinting = physics_state.was_sprinting;
@ -287,9 +286,9 @@ pub fn local_player_ai_step(
mut query: Query<
(
&mut PhysicsState,
&mut entity::Physics,
&mut entity::metadata::Sprinting,
&mut entity::Attributes,
&mut Physics,
&mut Sprinting,
&mut Attributes,
),
With<LocalPlayerInLoadedChunk>,
>,
@ -431,12 +430,12 @@ fn set_sprinting(
if sprinting {
attributes
.speed
.insert(entity::attributes::sprinting_modifier())
.insert(azalea_entity::attributes::sprinting_modifier())
.is_ok()
} else {
attributes
.speed
.remove(&entity::attributes::sprinting_modifier().uuid)
.remove(&azalea_entity::attributes::sprinting_modifier().uuid)
.is_none()
}
}

View file

@ -1,6 +1,11 @@
use std::{collections::HashSet, io::Cursor, sync::Arc};
use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3};
use azalea_entity::{
metadata::{apply_metadata, Health, PlayerMetadataBundle},
Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection,
Physics, PlayerBundle, Position, RelativeEntityUpdate,
};
use azalea_protocol::{
connect::{ReadConnection, WriteConnection},
packets::game::{
@ -15,15 +20,7 @@ use azalea_protocol::{
},
read::ReadPacketError,
};
use azalea_world::{
entity::{
metadata::{apply_metadata, Health, PlayerMetadataBundle},
Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LookDirection,
MinecraftEntityId, Physics, PlayerBundle, Position, WorldName,
},
entity::{LoadedBy, RelativeEntityUpdate},
InstanceContainer, PartialInstance,
};
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_app::{App, First, Plugin, PreUpdate};
use bevy_ecs::{
component::Component,
@ -195,7 +192,7 @@ fn process_packet_events(ecs: &mut World) {
Commands,
Query<(
&mut LocalPlayer,
Option<&mut WorldName>,
Option<&mut InstanceName>,
&GameProfileComponent,
&ClientInformation,
)>,
@ -225,7 +222,7 @@ fn process_packet_events(ecs: &mut World) {
} else {
commands
.entity(player_entity)
.insert(WorldName(new_world_name.clone()));
.insert(InstanceName(new_world_name.clone()));
}
// add this world to the instance_container (or don't if it's already
// there)
@ -348,10 +345,16 @@ fn process_packet_events(ecs: &mut World) {
)>,
> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let Ok((local_player, mut physics, mut direction, mut position, mut last_sent_position)) =
query.get_mut(player_entity) else {
continue;
};
let Ok((
local_player,
mut physics,
mut direction,
mut position,
mut last_sent_position,
)) = query.get_mut(player_entity)
else {
continue;
};
let delta_movement = physics.delta;
@ -555,12 +558,12 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p);
let mut system_state: SystemState<(Commands, Query<Option<&WorldName>>)> =
let mut system_state: SystemState<(Commands, Query<Option<&InstanceName>>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let world_name = query.get_mut(player_entity).unwrap();
if let Some(WorldName(world_name)) = world_name {
if let Some(InstanceName(world_name)) = world_name {
let bundle = p.as_entity_bundle(world_name.clone());
let mut entity_commands = commands.spawn((
MinecraftEntityId(p.id),
@ -622,12 +625,12 @@ fn process_packet_events(ecs: &mut World) {
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
Query<(&TabList, Option<&WorldName>)>,
Query<(&TabList, Option<&InstanceName>)>,
)> = SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let (tab_list, world_name) = query.get_mut(player_entity).unwrap();
if let Some(WorldName(world_name)) = world_name {
if let Some(InstanceName(world_name)) = world_name {
let bundle = p.as_player_bundle(world_name.clone());
let mut spawned = commands.spawn((
MinecraftEntityId(p.id),

View file

@ -1,7 +1,7 @@
use azalea_auth::game_profile::GameProfile;
use azalea_chat::FormattedText;
use azalea_core::GameMode;
use azalea_world::entity::EntityInfos;
use azalea_entity::EntityInfos;
use bevy_ecs::{
event::EventReader,
system::{Commands, Res},

View file

@ -22,8 +22,7 @@ pub use direction::*;
mod delta;
pub use delta::*;
mod particle;
pub use particle::*;
pub mod particle;
mod cursor3d;
pub use cursor3d::*;
@ -37,61 +36,5 @@ pub use aabb::*;
mod block_hit_result;
pub use block_hit_result::*;
// some random math things used in minecraft are defined down here
// TODO: make this generic
pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 {
let mut diff = max - min;
while diff > 0 {
let diff_mid = diff / 2;
let mid = min + diff_mid;
if predicate(mid) {
diff = diff_mid;
} else {
min = mid + 1;
diff -= diff_mid + 1;
}
}
min
}
pub fn lcm(a: u32, b: u32) -> u64 {
let gcd = gcd(a, b);
(a as u64) * (b / gcd) as u64
}
pub fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
pub fn lerp<T: num_traits::Float>(amount: T, a: T, b: T) -> T {
a + amount * (b - a)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gcd() {
assert_eq!(gcd(0, 0), 0);
assert_eq!(gcd(1, 1), 1);
assert_eq!(gcd(0, 1), 1);
assert_eq!(gcd(1, 0), 1);
assert_eq!(gcd(12, 8), 4);
assert_eq!(gcd(8, 12), 4);
assert_eq!(gcd(12, 9), 3);
assert_eq!(gcd(9, 12), 3);
assert_eq!(gcd(12, 7), 1);
assert_eq!(gcd(7, 12), 1);
}
}
pub mod math;
pub mod tier;

56
azalea-core/src/math.rs Normal file
View file

@ -0,0 +1,56 @@
// TODO: make this generic
pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 {
let mut diff = max - min;
while diff > 0 {
let diff_mid = diff / 2;
let mid = min + diff_mid;
if predicate(mid) {
diff = diff_mid;
} else {
min = mid + 1;
diff -= diff_mid + 1;
}
}
min
}
pub fn lcm(a: u32, b: u32) -> u64 {
let gcd = gcd(a, b);
(a as u64) * (b / gcd) as u64
}
pub fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
pub fn lerp<T: num_traits::Float>(amount: T, a: T, b: T) -> T {
a + amount * (b - a)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gcd() {
assert_eq!(gcd(0, 0), 0);
assert_eq!(gcd(1, 1), 1);
assert_eq!(gcd(0, 1), 1);
assert_eq!(gcd(1, 0), 1);
assert_eq!(gcd(12, 8), 4);
assert_eq!(gcd(8, 12), 4);
assert_eq!(gcd(12, 9), 3);
assert_eq!(gcd(9, 12), 3);
assert_eq!(gcd(12, 7), 1);
assert_eq!(gcd(7, 12), 1);
}
}

46
azalea-core/src/tier.rs Normal file
View file

@ -0,0 +1,46 @@
pub fn get_item_tier(item: azalea_registry::Item) -> Option<Tier> {
use azalea_registry::Item::*;
Some(match item {
WoodenPickaxe | WoodenShovel | WoodenAxe | WoodenHoe | WoodenSword => Tier::Wood,
StonePickaxe | StoneShovel | StoneAxe | StoneHoe | StoneSword => Tier::Stone,
IronPickaxe | IronShovel | IronAxe | IronHoe | IronSword => Tier::Iron,
DiamondPickaxe | DiamondShovel | DiamondAxe | DiamondHoe | DiamondSword => Tier::Diamond,
GoldenPickaxe | GoldenShovel | GoldenAxe | GoldenHoe | GoldenSword => Tier::Gold,
NetheritePickaxe | NetheriteShovel | NetheriteAxe | NetheriteHoe | NetheriteSword => {
Tier::Netherite
}
_ => return None,
})
}
pub enum Tier {
Wood,
Stone,
Iron,
Diamond,
Gold,
Netherite,
}
impl Tier {
pub fn level(&self) -> u8 {
match self {
Tier::Wood => 0,
Tier::Stone => 1,
Tier::Iron => 2,
Tier::Diamond => 3,
Tier::Gold => 0, // gold is the same tier as wood
Tier::Netherite => 4,
}
}
pub fn speed(&self) -> f32 {
match self {
Tier::Wood => 2.,
Tier::Stone => 4.,
Tier::Iron => 6.,
Tier::Diamond => 8.,
Tier::Gold => 12.,
Tier::Netherite => 9.,
}
}
}

25
azalea-entity/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "azalea-entity"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-block = { version = "0.7.0", path = "../azalea-block" }
azalea-buf = { version = "0.7.0", path = "../azalea-buf" }
azalea-chat = { version = "0.7.0", path = "../azalea-chat" }
azalea-core = { version = "0.7.0", path = "../azalea-core" }
azalea-inventory = { version = "0.7.0", path = "../azalea-inventory" }
azalea-nbt = { version = "0.7.0", path = "../azalea-nbt" }
azalea-registry = { version = "0.7.0", path = "../azalea-registry" }
azalea-world = { version = "0.7.0", path = "../azalea-world" }
bevy_app = "0.11.0"
bevy_ecs = "0.11.0"
derive_more = "0.99.17"
enum-as-inner = "0.6.0"
log = "0.4.19"
nohash-hasher = "0.2.0"
parking_lot = "0.12.1"
thiserror = "1.0.43"
uuid = "1.4.0"

View file

@ -4,7 +4,7 @@ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_chat::FormattedText;
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Vec3};
use azalea_core::{particle::Particle, BlockPos, Direction, GlobalPos, Vec3};
use azalea_inventory::ItemSlot;
use bevy_ecs::component::Component;
use derive_more::Deref;

View file

@ -0,0 +1,26 @@
// TODO
// pub struct ActiveEffects(HashMap<azalea_registry::MobEffect, MobEffectData>);
/// Returns the level of the given effect, or `None` if the effect is not
/// active. The lowest level is 0.
pub fn get_effect(_effect: azalea_registry::MobEffect) -> Option<u32> {
// TODO
None
}
pub fn get_dig_speed_amplifier() -> Option<u32> {
let effect_plus_one = u32::max(
get_effect(azalea_registry::MobEffect::Haste)
.map(|x| x + 1)
.unwrap_or_default(),
get_effect(azalea_registry::MobEffect::ConduitPower)
.map(|x| x + 1)
.unwrap_or_default(),
);
if effect_plus_one > 0 {
Some(effect_plus_one - 1)
} else {
None
}
}

View file

@ -0,0 +1,8 @@
pub fn get_enchant_level(
_enchantment: azalea_registry::Enchantment,
_player_inventory: &azalea_inventory::Menu,
) -> u32 {
// TODO
0
}

View file

@ -1,14 +1,8 @@
//! Implement things relating to entity datas, like an index of uuids to
//! entities.
use crate::{
deduplicate_entities, deduplicate_local_entities,
entity::{
self, add_dead, update_bounding_box, EntityUuid, MinecraftEntityId, Position, WorldName,
},
update_entity_by_id_index, update_uuid_index, InstanceContainer, PartialInstance,
};
use azalea_core::ChunkPos;
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_app::{App, Plugin, PostUpdate, PreUpdate, Update};
use bevy_ecs::{
component::Component,
@ -20,7 +14,6 @@ use bevy_ecs::{
};
use derive_more::{Deref, DerefMut};
use log::{debug, warn};
use nohash_hasher::IntMap;
use parking_lot::RwLock;
use std::{
collections::{HashMap, HashSet},
@ -29,6 +22,15 @@ use std::{
};
use uuid::Uuid;
use crate::{
add_dead,
systems::{
deduplicate_entities, deduplicate_local_entities, update_entity_by_id_index,
update_fluid_on_eyes, update_uuid_index,
},
update_bounding_box, EntityUuid, LastSentPosition, Position,
};
use super::{Local, LookDirection};
/// A Bevy [`SystemSet`] for various types of entity updates.
@ -75,6 +77,7 @@ impl Plugin for EntityPlugin {
add_dead,
update_bounding_box,
clamp_look_direction,
update_fluid_on_eyes,
),
),
)
@ -109,33 +112,6 @@ fn debug_new_entity(query: Query<(Entity, Option<&Local>), Added<MinecraftEntity
// "updates received" if not, then we simply increment our local "updates
// received" and do nothing else
/// Keep track of certain metadatas that are only relevant for this partial
/// world.
#[derive(Debug, Default)]
pub struct PartialEntityInfos {
// note: using MinecraftEntityId for entity ids is acceptable here since
// there's no chance of collisions here
/// The entity id of the player that owns this partial world. This will
/// make [`RelativeEntityUpdate`] pretend the entity doesn't exist so
/// it doesn't get modified from outside sources.
pub owner_entity: Option<Entity>,
/// A counter for each entity that tracks how many updates we've observed
/// for it.
///
/// This is used for shared worlds (i.e. swarms), to make sure we don't
/// update entities twice on accident.
pub updates_received: IntMap<MinecraftEntityId, u32>,
}
impl PartialEntityInfos {
pub fn new(owner_entity: Option<Entity>) -> Self {
Self {
owner_entity,
updates_received: IntMap::default(),
}
}
}
/// 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.
@ -211,15 +187,7 @@ impl EntityInfos {
/// Update the chunk position indexes in [`EntityInfos`].
fn update_entity_chunk_positions(
mut query: Query<
(
Entity,
&entity::Position,
&mut entity::LastSentPosition,
&entity::WorldName,
),
Changed<entity::Position>,
>,
mut query: Query<(Entity, &Position, &mut LastSentPosition, &InstanceName), Changed<Position>>,
instance_container: Res<InstanceContainer>,
) {
for (entity, pos, last_pos, world_name) in query.iter_mut() {
@ -288,7 +256,7 @@ fn remove_despawned_entities_from_indexes(
mut commands: Commands,
mut entity_infos: ResMut<EntityInfos>,
instance_container: Res<InstanceContainer>,
query: Query<(Entity, &EntityUuid, &Position, &WorldName, &LoadedBy), Changed<LoadedBy>>,
query: Query<(Entity, &EntityUuid, &Position, &InstanceName, &LoadedBy), Changed<LoadedBy>>,
) {
for (entity, uuid, position, world_name, loaded_by) in &query {
let world_lock = instance_container.get(world_name).unwrap();

View file

@ -3,15 +3,18 @@
pub mod attributes;
mod data;
mod dimensions;
mod effects;
mod enchantments;
mod info;
pub mod metadata;
use crate::ChunkStorage;
pub mod mining;
mod systems;
use self::{attributes::AttributeInstance, metadata::Health};
pub use attributes::Attributes;
use azalea_block::BlockState;
use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB};
use azalea_world::{ChunkStorage, InstanceName};
use bevy_ecs::{
bundle::Bundle,
component::Component,
@ -23,23 +26,12 @@ pub use data::*;
use derive_more::{Deref, DerefMut};
pub use dimensions::{update_bounding_box, EntityDimensions};
pub use info::{
clamp_look_direction, EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, PartialEntityInfos,
clamp_look_direction, EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy,
RelativeEntityUpdate,
};
use std::fmt::Debug;
use uuid::Uuid;
/// An entity ID used by Minecraft. These are not guaranteed to be unique in
/// shared worlds, that's what [`Entity`] is for.
#[derive(Component, Copy, Clone, Debug, PartialEq, Eq, Deref, DerefMut)]
pub struct MinecraftEntityId(pub u32);
impl std::hash::Hash for MinecraftEntityId {
fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
hasher.write_u32(self.0);
}
}
impl nohash_hasher::IsEnabled for MinecraftEntityId {}
pub fn move_relative(
physics: &mut Physics,
direction: &LookDirection,
@ -197,11 +189,6 @@ impl From<&LastSentPosition> for BlockPos {
}
}
/// The name of the world the entity is in. If two entities share the same world
/// name, we assume they're in the same world.
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct WorldName(pub ResourceLocation);
/// A component for entities that can jump.
///
/// If this is true, the entity will try to jump every tick. (It's equivalent to
@ -303,7 +290,7 @@ pub struct EntityKind(pub azalea_registry::EntityKind);
pub struct EntityBundle {
pub kind: EntityKind,
pub uuid: EntityUuid,
pub world_name: WorldName,
pub world_name: InstanceName,
pub position: Position,
pub last_sent_position: LastSentPosition,
pub physics: Physics,
@ -311,6 +298,7 @@ pub struct EntityBundle {
pub eye_height: EyeHeight,
pub attributes: Attributes,
pub jumping: Jumping,
pub fluid_on_eyes: FluidOnEyes,
}
impl EntityBundle {
@ -330,7 +318,7 @@ impl EntityBundle {
Self {
kind: EntityKind(kind),
uuid: EntityUuid(uuid),
world_name: WorldName(world_name),
world_name: InstanceName(world_name),
position: Position(pos),
last_sent_position: LastSentPosition(pos),
physics: Physics {
@ -359,6 +347,7 @@ impl EntityBundle {
},
jumping: Jumping(false),
fluid_on_eyes: FluidOnEyes(azalea_registry::Fluid::Empty),
}
}
}
@ -375,6 +364,9 @@ pub struct PlayerBundle {
#[derive(Component)]
pub struct Local;
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct FluidOnEyes(azalea_registry::Fluid);
// #[cfg(test)]
// mod tests {
// use super::*;

View file

@ -8,7 +8,7 @@ use super::{
SnifferState, VillagerData,
};
use azalea_chat::FormattedText;
use azalea_core::{BlockPos, Direction, Particle, Vec3};
use azalea_core::{particle::Particle, BlockPos, Direction, Vec3};
use azalea_inventory::ItemSlot;
use bevy_ecs::{bundle::Bundle, component::Component};
use derive_more::{Deref, DerefMut};

170
azalea-entity/src/mining.rs Normal file
View file

@ -0,0 +1,170 @@
use azalea_block::{Block, BlockBehavior};
use azalea_core::tier::get_item_tier;
use azalea_registry as registry;
use crate::{effects, enchantments, FluidOnEyes, Physics};
pub fn get_mine_progress(
block: &dyn Block,
held_item: registry::Item,
player_inventory: &azalea_inventory::Menu,
fluid_on_eyes: &FluidOnEyes,
physics: &Physics,
) -> f32 {
// public float getDestroyProgress(BlockState blockState, Player player,
// BlockGetter world, BlockPos blockPos) { float destroySpeed =
// blockState.getDestroySpeed(world, blockPos); if (destroySpeed ==
// -1.0F) { return 0.0F;
// } else {
// int divider = player.hasCorrectToolForDrops(blockState) ? 30 : 100;
// return player.getDestroySpeed(blockState) / destroySpeed /
// (float)divider; }
// }
let block_behavior: BlockBehavior = block.behavior();
let destroy_time = block_behavior.destroy_time;
if destroy_time == -1. {
return 0.;
}
let divider = if has_correct_tool_for_drops(block, held_item) {
30
} else {
100
};
(destroy_speed(
block.as_registry_block(),
held_item,
player_inventory,
fluid_on_eyes,
physics,
) / destroy_time)
/ divider as f32
}
fn has_correct_tool_for_drops(block: &dyn Block, tool: registry::Item) -> bool {
if !block.behavior().requires_correct_tool_for_drops {
return true;
}
let registry_block = block.as_registry_block();
if tool == registry::Item::Shears {
matches!(
registry_block,
registry::Block::Cobweb | registry::Block::RedstoneWire | registry::Block::Tripwire
)
} else if registry::tags::items::SWORDS.contains(&tool) {
registry_block == registry::Block::Cobweb
} else if registry::tags::items::PICKAXES.contains(&tool)
|| registry::tags::items::SHOVELS.contains(&tool)
|| registry::tags::items::HOES.contains(&tool)
|| registry::tags::items::AXES.contains(&tool)
{
let tier = get_item_tier(tool).expect("all pickaxes and shovels should be matched");
let tier_level = tier.level();
!((tier_level < 3 && registry::tags::blocks::NEEDS_DIAMOND_TOOL.contains(&registry_block))
|| (tier_level < 2
&& registry::tags::blocks::NEEDS_IRON_TOOL.contains(&registry_block))
|| (tier_level < 1
&& registry::tags::blocks::NEEDS_STONE_TOOL.contains(&registry_block)))
} else {
false
}
}
/// Returns the destroy speed of the given block with the given tool, taking
/// into account enchantments and effects. If the player is not holding anything
/// then `tool` should be `Item::Air`.
fn destroy_speed(
block: registry::Block,
tool: registry::Item,
player_inventory: &azalea_inventory::Menu,
fluid_on_eyes: &FluidOnEyes,
physics: &Physics,
) -> f32 {
let mut base_destroy_speed = base_destroy_speed(block, tool);
// add efficiency enchantment
if base_destroy_speed > 1. {
let efficiency_level =
enchantments::get_enchant_level(registry::Enchantment::Efficiency, player_inventory);
if efficiency_level > 0 && tool != registry::Item::Air {
base_destroy_speed += (efficiency_level * efficiency_level + 1) as f32;
}
}
if let Some(dig_speed_amplifier) = effects::get_dig_speed_amplifier() {
base_destroy_speed *= 1. + (dig_speed_amplifier + 1) as f32 * 0.2;
}
if let Some(dig_slowdown) = effects::get_effect(registry::MobEffect::MiningFatigue) {
let multiplier = match dig_slowdown {
0 => 0.3,
1 => 0.09,
2 => 0.0027,
_ => 8.1E-4,
};
base_destroy_speed *= multiplier;
}
if registry::tags::fluids::WATER.contains(fluid_on_eyes)
&& enchantments::get_enchant_level(registry::Enchantment::AquaAffinity, player_inventory)
== 0
{
base_destroy_speed /= 5.;
}
if !physics.on_ground {
base_destroy_speed /= 5.;
}
base_destroy_speed
}
fn base_destroy_speed(block: registry::Block, tool: registry::Item) -> f32 {
if tool == registry::Item::Shears {
if block == registry::Block::Cobweb || registry::tags::blocks::LEAVES.contains(&block) {
15.
} else if registry::tags::blocks::WOOL.contains(&block) {
5.
} else if matches!(block, registry::Block::Vine | registry::Block::GlowLichen) {
2.
} else {
1.
}
} else if registry::tags::items::SWORDS.contains(&tool) {
if block == registry::Block::Cobweb {
15.
} else if registry::tags::blocks::SWORD_EFFICIENT.contains(&block) {
1.5
} else {
1.
}
} else if registry::tags::items::PICKAXES.contains(&tool) {
if registry::tags::blocks::MINEABLE_PICKAXE.contains(&block) {
get_item_tier(tool).unwrap().speed()
} else {
1.
}
} else if registry::tags::items::SHOVELS.contains(&tool) {
if registry::tags::blocks::MINEABLE_SHOVEL.contains(&block) {
get_item_tier(tool).unwrap().speed()
} else {
1.
}
} else if registry::tags::items::HOES.contains(&tool) {
if registry::tags::blocks::MINEABLE_HOE.contains(&block) {
get_item_tier(tool).unwrap().speed()
} else {
1.
}
} else if registry::tags::items::AXES.contains(&tool) {
if registry::tags::blocks::MINEABLE_AXE.contains(&block) {
get_item_tier(tool).unwrap().speed()
} else {
1.
}
} else {
1.
}
}

View file

@ -0,0 +1,181 @@
use azalea_core::{BlockPos, Vec3};
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
use bevy_ecs::prelude::*;
use log::{debug, error, info};
use crate::{EntityInfos, EntityUuid, EyeHeight, FluidOnEyes, LoadedBy, Local, Position};
/// Remove new entities that have the same id as an existing entity, and
/// increase the reference counts.
///
/// This is the reason why spawning entities into the ECS when you get a spawn
/// entity packet is okay. This system will make sure the new entity gets
/// combined into the old one.
#[allow(clippy::type_complexity)]
pub fn deduplicate_entities(
mut commands: Commands,
mut query: Query<
(Entity, &MinecraftEntityId, &InstanceName),
(Changed<MinecraftEntityId>, Without<Local>),
>,
mut loaded_by_query: Query<&mut LoadedBy>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove it
for (new_entity, id, world_name) in query.iter_mut() {
if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
continue;
}
// this entity already exists!!! remove the one we just added but increase
// the reference count
let new_loaded_by = loaded_by_query
.get(new_entity)
.unwrap_or_else(|_| panic!(
"Entities should always have the LoadedBy component ({new_entity:?} did not)"
))
.clone();
let old_loaded_by = loaded_by_query.get_mut(*old_entity);
// merge them if possible
if let Ok(mut old_loaded_by) = old_loaded_by {
old_loaded_by.extend(new_loaded_by.iter());
}
commands.entity(new_entity).despawn();
info!(
"Entity with id {id:?} / {new_entity:?} already existed in the world, merging it with {old_entity:?}"
);
break;
}
} else {
error!("Entity was inserted into a world that doesn't exist.");
}
}
}
// when a local entity is added, if there was already an entity with the same id
// then delete the old entity
#[allow(clippy::type_complexity)]
pub fn deduplicate_local_entities(
mut commands: Commands,
mut query: Query<
(Entity, &MinecraftEntityId, &InstanceName),
(Changed<MinecraftEntityId>, With<Local>),
>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove the old one
for (new_entity, id, world_name) in query.iter_mut() {
if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
// lol
continue;
}
commands.entity(*old_entity).despawn();
debug!(
"Added local entity {id:?} / {new_entity:?} but already existed in world as {old_entity:?}, despawning {old_entity:?}"
);
break;
}
} else {
error!("Entity was inserted into a world that doesn't exist.");
}
}
}
pub fn update_uuid_index(
mut entity_infos: ResMut<EntityInfos>,
query: Query<(Entity, &EntityUuid, Option<&Local>), Changed<EntityUuid>>,
) {
for (entity, &uuid, local) in query.iter() {
// only add it if it doesn't already exist in
// entity_infos.entity_by_uuid
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);
}
}
// /// Clear all entities in a chunk. This will not clear them from the
// /// shared storage unless there are no other references to them.
// pub fn clear_entities_in_chunk(
// mut commands: Commands,
// partial_entity_infos: &mut PartialEntityInfos,
// chunk: &ChunkPos,
// instance_container: &WorldContainer,
// world_name: &InstanceName,
// mut query: Query<(&MinecraftEntityId, &mut ReferenceCount)>,
// ) { let world_lock = instance_container.get(world_name).unwrap(); let world =
// world_lock.read();
// if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() {
// for &entity in &entities {
// let (id, mut reference_count) = query.get_mut(entity).unwrap();
// if partial_entity_infos.loaded_entity_ids.remove(id) {
// // decrease the reference count
// **reference_count -= 1;
// }
// }
// }
// }
/// System to keep the entity_by_id index up-to-date.
pub fn update_entity_by_id_index(
mut query: Query<
(Entity, &MinecraftEntityId, &InstanceName, Option<&Local>),
Changed<MinecraftEntityId>,
>,
instance_container: Res<InstanceContainer>,
) {
for (entity, id, world_name, local) in query.iter_mut() {
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
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:?}.");
}
}
pub fn update_fluid_on_eyes(
mut query: Query<(&mut FluidOnEyes, &Position, &EyeHeight, &InstanceName)>,
instance_container: Res<InstanceContainer>,
) {
for (mut fluid_on_eyes, position, eye_height, instance_name) in query.iter_mut() {
let Some(instance) = instance_container.get(instance_name) else {
continue;
};
let adjusted_eye_y = position.y + (**eye_height as f64) - 0.1111111119389534;
let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z));
let fluid_at_eye = instance
.read()
.get_fluid_state(&eye_block_pos)
.unwrap_or_default();
let fluid_cutoff_y = eye_block_pos.y as f64 + (fluid_at_eye.height as f64 / 16f64);
if fluid_cutoff_y > adjusted_eye_y {
**fluid_on_eyes = fluid_at_eye.fluid;
} else {
**fluid_on_eyes = azalea_registry::Fluid::Empty;
}
}
}

View file

@ -10,8 +10,8 @@ use azalea_inventory_macros::declare_menus;
pub use slot::{ItemSlot, ItemSlotData};
// TODO: remove this here and in azalea-inventory-macros when rust makes
// Default be implemented for all array sizes (since right now it's only up to
// 32)
// Default be implemented for all array sizes
// https://github.com/rust-lang/rust/issues/61415
/// A fixed-size list of [`ItemSlot`]s.
#[derive(Debug, Clone)]

View file

@ -11,6 +11,7 @@ version = "0.7.0"
[dependencies]
azalea-block = { path = "../azalea-block", version = "^0.7.0" }
azalea-core = { path = "../azalea-core", version = "^0.7.0" }
azalea-entity = { version = "0.1.0", path = "../azalea-entity" }
azalea-inventory = { version = "0.7.0", path = "../azalea-inventory" }
azalea-registry = { path = "../azalea-registry", version = "^0.7.0" }
azalea-world = { path = "../azalea-world", version = "^0.7.0" }

View file

@ -1,5 +1,5 @@
use azalea_block::BlockState;
use azalea_core::{lerp, BlockHitResult, BlockPos, Direction, Vec3, EPSILON};
use azalea_core::{math::lerp, BlockHitResult, BlockPos, Direction, Vec3, EPSILON};
use azalea_inventory::ItemSlot;
use azalea_world::ChunkStorage;
use bevy_ecs::entity::Entity;

View file

@ -1,7 +1,10 @@
use std::{cmp::Ordering, convert::TryInto};
use super::CubePointRange;
use azalea_core::{gcd, lcm, EPSILON};
use azalea_core::{
math::{gcd, lcm},
EPSILON,
};
#[derive(Debug)]
pub enum IndexMerger {

View file

@ -5,7 +5,7 @@ mod shape;
mod world_collisions;
use azalea_core::{Axis, Vec3, AABB, EPSILON};
use azalea_world::{entity, Instance, MoveEntityError};
use azalea_world::{Instance, MoveEntityError};
pub use blocks::BlockWithShape;
pub use discrete_voxel_shape::*;
pub use shape::*;
@ -49,7 +49,7 @@ pub enum MoverType {
// return var4;
// }
fn collide(movement: &Vec3, world: &Instance, physics: &entity::Physics) -> Vec3 {
fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics) -> Vec3 {
let entity_bounding_box = physics.bounding_box;
// TODO: get_entity_collisions
// let entity_collisions = world.get_entity_collisions(self,
@ -71,8 +71,8 @@ pub fn move_colliding(
_mover_type: &MoverType,
movement: &Vec3,
world: &Instance,
position: &mut entity::Position,
physics: &mut entity::Physics,
position: &mut azalea_entity::Position,
physics: &mut azalea_entity::Physics,
) -> Result<(), MoveEntityError> {
// TODO: do all these
@ -122,7 +122,7 @@ pub fn move_colliding(
// TODO: minecraft checks for a "minor" horizontal collision here
let _block_pos_below = entity::on_pos_legacy(&world.chunks, position);
let _block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, position);
// let _block_state_below = self
// .world
// .get_block_state(&block_pos_below)

View file

@ -1,7 +1,7 @@
use super::mergers::IndexMerger;
use crate::collision::{BitSetDiscreteVoxelShape, DiscreteVoxelShape, AABB};
use azalea_core::{
binary_search, Axis, AxisCycle, BlockHitResult, BlockPos, Direction, Vec3, EPSILON,
math::binary_search, Axis, AxisCycle, BlockHitResult, BlockPos, Direction, Vec3, EPSILON,
};
use std::{cmp, num::NonZeroU32};

View file

@ -71,9 +71,7 @@ impl<'a> Iterator for BlockCollisions<'a> {
}
let chunk = self.get_chunk(item.pos.x, item.pos.z);
let Some(chunk) = chunk else {
continue
};
let Some(chunk) = chunk else { continue };
let pos = item.pos;
let block_state: BlockState = chunk

View file

@ -6,13 +6,12 @@ pub mod collision;
use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3};
use azalea_world::{
entity::{
clamp_look_direction, metadata::Sprinting, move_relative, Attributes, Jumping, Local,
LookDirection, Physics, Position, WorldName,
},
Instance, InstanceContainer,
use azalea_entity::update_bounding_box;
use azalea_entity::{
clamp_look_direction, metadata::Sprinting, move_relative, Attributes, Jumping, Local,
LookDirection, Physics, Position,
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::{
entity::Entity,
@ -35,7 +34,7 @@ impl Plugin for PhysicsPlugin {
.add_systems(
Update,
force_jump_listener
.before(azalea_world::entity::update_bounding_box)
.before(update_bounding_box)
.after(clamp_look_direction),
)
.add_systems(FixedUpdate, (ai_step, travel).chain().in_set(PhysicsSet));
@ -51,7 +50,7 @@ fn travel(
&mut LookDirection,
&mut Position,
&Attributes,
&WorldName,
&InstanceName,
),
With<Local>,
>,
@ -176,7 +175,7 @@ pub fn force_jump_listener(
&Position,
&LookDirection,
&Sprinting,
&WorldName,
&InstanceName,
)>,
instance_container: Res<InstanceContainer>,
mut events: EventReader<ForceJumpEvent>,
@ -327,10 +326,8 @@ mod tests {
use super::*;
use azalea_core::{ChunkPos, ResourceLocation};
use azalea_world::{
entity::{EntityBundle, EntityPlugin, MinecraftEntityId},
Chunk, PartialInstance,
};
use azalea_entity::{EntityBundle, EntityPlugin};
use azalea_world::{Chunk, MinecraftEntityId, PartialInstance};
use bevy_app::App;
use bevy_time::fixed_timestep::FixedTime;
use uuid::Uuid;

View file

@ -25,6 +25,7 @@ azalea-core = { path = "../azalea-core", optional = true, version = "^0.7.0", fe
"serde",
] }
azalea-crypto = { path = "../azalea-crypto", version = "^0.7.0" }
azalea-entity = { version = "0.1.0", path = "../azalea-entity" }
azalea-inventory = { version = "0.7.0", path = "../azalea-inventory" }
azalea-nbt = { path = "../azalea-nbt", version = "^0.7.0", features = [
"serde",

View file

@ -1,7 +1,7 @@
use azalea_buf::McBuf;
use azalea_core::{ResourceLocation, Vec3};
use azalea_entity::{metadata::apply_default_metadata, EntityBundle};
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::{metadata::apply_default_metadata, EntityBundle};
use uuid::Uuid;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]

View file

@ -1,8 +1,8 @@
use azalea_buf::McBuf;
use azalea_core::{ResourceLocation, Vec3};
use azalea_entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle};
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_registry::EntityKind;
use azalea_world::entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle};
use uuid::Uuid;
/// This packet is sent by the server when a player comes into visible range,

View file

@ -1,5 +1,5 @@
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable};
use azalea_core::ParticleData;
use azalea_core::particle::ParticleData;
use azalea_protocol_macros::ClientboundGamePacket;
use std::io::{Cursor, Write};

View file

@ -1,6 +1,6 @@
use azalea_buf::McBuf;
use azalea_entity::EntityMetadataItems;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::EntityMetadataItems;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundSetEntityDataPacket {

View file

@ -1,7 +1,7 @@
use azalea_buf::McBuf;
use azalea_core::ResourceLocation;
use azalea_entity::attributes::AttributeModifier;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::attributes::AttributeModifier;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundUpdateAttributesPacket {

View file

@ -221,10 +221,12 @@ impl McBufWritable for Recipe {
impl McBufReadable for Recipe {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let recipe_serializer_name = ResourceLocation::read_from(buf)?;
let Ok(recipe_serializer) =
RecipeSerializer::from_str(&recipe_serializer_name.to_string()) else {
return Err(BufReadError::UnexpectedStringEnumVariant { id: recipe_serializer_name.to_string() });
};
let Ok(recipe_serializer) = RecipeSerializer::from_str(&recipe_serializer_name.to_string())
else {
return Err(BufReadError::UnexpectedStringEnumVariant {
id: recipe_serializer_name.to_string(),
});
};
let identifier = ResourceLocation::read_from(buf)?;
// rust doesn't let us match ResourceLocation so we have to do a big

View file

@ -11,6 +11,7 @@ version = "0.7.0"
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "^0.7.0" }
azalea-registry-macros = { path = "./azalea-registry-macros", version = "^0.7.0" }
once_cell = "1.18.0"
serde = { version = "^1.0", optional = true }
[features]

View file

@ -5,6 +5,8 @@
// auto-generated (so you can add doc comments to the registry enums if you
// want)
pub mod tags;
use std::io::{Cursor, Write};
use azalea_buf::{BufReadError, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
// This file was generated by codegen/lib/code/tags.py, don't edit it manually!
use std::collections::HashSet;
use once_cell::sync::Lazy;
use crate::Fluid;
pub static LAVA: Lazy<HashSet<Fluid>> =
Lazy::new(|| HashSet::from_iter(vec![Fluid::Lava, Fluid::FlowingLava]));
pub static WATER: Lazy<HashSet<Fluid>> =
Lazy::new(|| HashSet::from_iter(vec![Fluid::Water, Fluid::FlowingWater]));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
pub mod blocks;
pub mod fluids;
pub mod items;

View file

@ -1,5 +1,6 @@
use azalea_core::ResourceLocation;
use bevy_ecs::system::Resource;
use bevy_ecs::{component::Component, system::Resource};
use derive_more::{Deref, DerefMut};
use log::error;
use nohash_hasher::IntMap;
use parking_lot::RwLock;
@ -8,7 +9,7 @@ use std::{
sync::{Arc, Weak},
};
use crate::{entity::WorldName, ChunkStorage, Instance};
use crate::{ChunkStorage, Instance};
/// 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.
@ -37,7 +38,7 @@ impl InstanceContainer {
}
/// Get a world from the container.
pub fn get(&self, name: &WorldName) -> Option<Arc<RwLock<Instance>>> {
pub fn get(&self, name: &InstanceName) -> Option<Arc<RwLock<Instance>>> {
self.worlds.get(name).and_then(|world| world.upgrade())
}
@ -76,3 +77,9 @@ impl InstanceContainer {
}
}
}
/// The name of the [`Instance`](crate::Instance) (world) the entity is
/// in. If two entities share the same world name, we assume they're in the same
/// instance.
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct InstanceName(pub ResourceLocation);

View file

@ -6,7 +6,6 @@
mod bit_storage;
mod chunk_storage;
mod container;
pub mod entity;
pub mod iterators;
pub mod palette;
mod world;

View file

@ -1,19 +1,8 @@
use crate::{
entity::{
EntityInfos, EntityUuid, LoadedBy, Local, MinecraftEntityId, PartialEntityInfos, WorldName,
},
iterators::ChunkIterator,
palette::Palette,
ChunkStorage, InstanceContainer, PartialChunkStorage,
};
use azalea_block::{BlockState, BlockStates};
use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, PartialChunkStorage};
use azalea_block::{BlockState, BlockStates, FluidState};
use azalea_core::{BlockPos, ChunkPos};
use bevy_ecs::{
entity::Entity,
query::{Changed, With, Without},
system::{Commands, Query, Res, ResMut},
};
use log::{debug, error, info};
use bevy_ecs::{component::Component, entity::Entity};
use derive_more::{Deref, DerefMut};
use nohash_hasher::IntMap;
use std::fmt::Formatter;
use std::{
@ -45,133 +34,45 @@ impl PartialInstance {
}
}
/// Remove new entities that have the same id as an existing entity, and
/// increase the reference counts.
///
/// This is the reason why spawning entities into the ECS when you get a spawn
/// entity packet is okay. This system will make sure the new entity gets
/// combined into the old one.
#[allow(clippy::type_complexity)]
pub fn deduplicate_entities(
mut commands: Commands,
mut query: Query<
(Entity, &MinecraftEntityId, &WorldName),
(Changed<MinecraftEntityId>, Without<Local>),
>,
mut loaded_by_query: Query<&mut LoadedBy>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove it
for (new_entity, id, world_name) in query.iter_mut() {
if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
continue;
}
/// An entity ID used by Minecraft. These are not guaranteed to be unique in
/// shared worlds, that's what [`Entity`] is for.
#[derive(Component, Copy, Clone, Debug, PartialEq, Eq, Deref, DerefMut)]
pub struct MinecraftEntityId(pub u32);
// this entity already exists!!! remove the one we just added but increase
// the reference count
let new_loaded_by = loaded_by_query
.get(new_entity)
.unwrap_or_else(|_| panic!(
"Entities should always have the LoadedBy component ({new_entity:?} did not)"
))
.clone();
let old_loaded_by = loaded_by_query.get_mut(*old_entity);
// merge them if possible
if let Ok(mut old_loaded_by) = old_loaded_by {
old_loaded_by.extend(new_loaded_by.iter());
}
commands.entity(new_entity).despawn();
info!(
"Entity with id {id:?} / {new_entity:?} already existed in the world, merging it with {old_entity:?}"
);
break;
}
} else {
error!("Entity was inserted into a world that doesn't exist.");
impl std::hash::Hash for MinecraftEntityId {
fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
hasher.write_u32(self.0);
}
}
impl nohash_hasher::IsEnabled for MinecraftEntityId {}
/// Keep track of certain metadatas that are only relevant for this partial
/// world.
#[derive(Debug, Default)]
pub struct PartialEntityInfos {
// note: using MinecraftEntityId for entity ids is acceptable here since
// there's no chance of collisions here
/// The entity id of the player that owns this partial world. This will
/// make `RelativeEntityUpdate` pretend the entity doesn't exist so
/// it doesn't get modified from outside sources.
pub owner_entity: Option<Entity>,
/// A counter for each entity that tracks how many updates we've observed
/// for it.
///
/// This is used for shared worlds (i.e. swarms), to make sure we don't
/// update entities twice on accident.
pub updates_received: IntMap<MinecraftEntityId, u32>,
}
impl PartialEntityInfos {
pub fn new(owner_entity: Option<Entity>) -> Self {
Self {
owner_entity,
updates_received: IntMap::default(),
}
}
}
// when a local entity is added, if there was already an entity with the same id
// then delete the old entity
#[allow(clippy::type_complexity)]
pub fn deduplicate_local_entities(
mut commands: Commands,
mut query: Query<
(Entity, &MinecraftEntityId, &WorldName),
(Changed<MinecraftEntityId>, With<Local>),
>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove the old one
for (new_entity, id, world_name) in query.iter_mut() {
if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
// lol
continue;
}
commands.entity(*old_entity).despawn();
debug!(
"Added local entity {id:?} / {new_entity:?} but already existed in world as {old_entity:?}, despawning {old_entity:?}"
);
break;
}
} else {
error!("Entity was inserted into a world that doesn't exist.");
}
}
}
pub fn update_uuid_index(
mut entity_infos: ResMut<EntityInfos>,
query: Query<(Entity, &EntityUuid, Option<&Local>), Changed<EntityUuid>>,
) {
for (entity, &uuid, local) in query.iter() {
// only add it if it doesn't already exist in
// entity_infos.entity_by_uuid
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);
}
}
// /// Clear all entities in a chunk. This will not clear them from the
// /// shared storage unless there are no other references to them.
// pub fn clear_entities_in_chunk(
// mut commands: Commands,
// partial_entity_infos: &mut PartialEntityInfos,
// chunk: &ChunkPos,
// instance_container: &WorldContainer,
// world_name: &WorldName,
// mut query: Query<(&MinecraftEntityId, &mut ReferenceCount)>,
// ) {
// let world_lock = instance_container.get(world_name).unwrap();
// let world = world_lock.read();
// if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() {
// for &entity in &entities {
// let (id, mut reference_count) = query.get_mut(entity).unwrap();
// if partial_entity_infos.loaded_entity_ids.remove(id) {
// // decrease the reference count
// **reference_count -= 1;
// }
// }
// }
// }
/// A world where the chunks are stored as weak pointers. This is used for
/// shared worlds.
#[derive(Default, Debug)]
@ -191,6 +92,18 @@ impl Instance {
self.entity_by_id.get(entity_id).copied()
}
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
self.chunks.get_block_state(pos)
}
pub fn get_fluid_state(&self, pos: &BlockPos) -> Option<FluidState> {
self.chunks.get_block_state(pos).map(FluidState::from)
}
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
self.chunks.set_block_state(pos, state)
}
/// 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
@ -290,31 +203,6 @@ impl Default for PartialInstance {
}
}
/// System to keep the entity_by_id index up-to-date.
pub fn update_entity_by_id_index(
mut query: Query<
(Entity, &MinecraftEntityId, &WorldName, Option<&Local>),
Changed<MinecraftEntityId>,
>,
instance_container: Res<InstanceContainer>,
) {
for (entity, id, world_name, local) in query.iter_mut() {
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
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 Instance {
/// Make an empty world from this `ChunkStorage`. This is meant to be a
/// convenience function for tests.

View file

@ -40,6 +40,7 @@ thiserror = "^1.0.43"
tokio = "^1.29.1"
uuid = "1.4.0"
bevy_log = "0.11.0"
azalea-entity = { version = "0.1.0", path = "../azalea-entity" }
[features]
default = ["log"]

View file

@ -89,7 +89,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.disconnect();
}
let Some(sender) = m.username() else {
return Ok(())
return Ok(());
};
// let mut ecs = bot.ecs.lock();
// let entity = bot
@ -164,6 +164,21 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat("no diamond block found");
}
}
"mineblock" => {
let target_pos = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::DiamondBlock.into());
if let Some(target_pos) = target_pos {
// +1 to stand on top of the block
bot.chat("ok mining diamond block");
bot.look_at(target_pos.center());
bot.mine(target_pos).await;
bot.chat("finished mining");
} else {
bot.chat("no diamond block found");
}
}
"lever" => {
let target_pos = bot
.world()
@ -171,7 +186,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
.find_block(bot.position(), &azalea::Block::Lever.into());
let Some(target_pos) = target_pos else {
bot.chat("no lever found");
return Ok(())
return Ok(());
};
bot.goto(BlockPosGoal::from(target_pos));
bot.look_at(target_pos.center());
@ -188,7 +203,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
.find_block(bot.position(), &azalea::Block::Chest.into());
let Some(target_pos) = target_pos else {
bot.chat("no chest found");
return Ok(())
return Ok(());
};
bot.look_at(target_pos.center());
let container = bot.open_container(target_pos).await;

View file

@ -9,9 +9,10 @@ use crate::ecs::{
system::{Commands, Query},
};
use azalea_core::Vec3;
use azalea_entity::{
clamp_look_direction, metadata::Player, EyeHeight, Jumping, Local, LookDirection, Position,
};
use azalea_physics::{force_jump_listener, PhysicsSet};
use azalea_world::entity::{clamp_look_direction, EyeHeight, LookDirection};
use azalea_world::entity::{metadata::Player, Jumping, Local, Position};
use bevy_app::{FixedUpdate, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::schedule::IntoSystemConfigs;

View file

@ -6,6 +6,7 @@
mod auto_respawn;
mod bot;
mod container;
pub mod mining;
pub mod pathfinder;
pub mod prelude;
pub mod swarm;
@ -17,9 +18,10 @@ pub use azalea_brigadier as brigadier;
pub use azalea_chat::FormattedText;
pub use azalea_client::*;
pub use azalea_core::{BlockPos, Vec3};
pub use azalea_entity as entity;
pub use azalea_protocol as protocol;
pub use azalea_registry::{Block, EntityKind, Item};
pub use azalea_world::{entity, Instance};
pub use azalea_world::Instance;
pub use bot::DefaultBotPlugins;
use ecs::component::Component;
use futures::Future;

40
azalea/src/mining.rs Normal file
View file

@ -0,0 +1,40 @@
use azalea_client::{
interact::SwingArmEvent,
mining::{Mining, StartMiningBlockEvent},
Client, TickBroadcast,
};
use azalea_core::BlockPos;
pub trait MiningExt {
/// Start mining a block.
async fn mine(&mut self, position: BlockPos);
}
impl MiningExt for Client {
/// Start mining a block. This won't turn the bot's head towards the block,
/// so you'll have to do that yourself with [`look_at`].
///
/// [`look_at`]: crate::prelude::BotClientExt::look_at
async fn mine(&mut self, position: BlockPos) {
self.ecs.lock().send_event(StartMiningBlockEvent {
entity: self.entity,
position,
});
// vanilla sends an extra swing arm packet when we start mining
self.ecs.lock().send_event(SwingArmEvent {
entity: self.entity,
});
let mut receiver = {
let ecs = self.ecs.lock();
let tick_broadcast = ecs.resource::<TickBroadcast>();
tick_broadcast.subscribe()
};
while receiver.recv().await.is_ok() {
let ecs = self.ecs.lock();
if ecs.get::<Mining>(self.entity).is_none() {
break;
}
}
}
}

View file

@ -16,13 +16,11 @@ use crate::ecs::{
use astar::Edge;
use azalea_client::{StartSprintEvent, StartWalkEvent};
use azalea_core::{BlockPos, CardinalDirection};
use azalea_entity::metadata::Player;
use azalea_entity::Local;
use azalea_entity::{Physics, Position};
use azalea_physics::PhysicsSet;
use azalea_world::entity::metadata::Player;
use azalea_world::entity::Local;
use azalea_world::{
entity::{Physics, Position, WorldName},
InstanceContainer,
};
use azalea_world::{InstanceContainer, InstanceName};
use bevy_app::{FixedUpdate, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::schedule::IntoSystemConfigs;
@ -106,7 +104,7 @@ pub struct ComputePath(Task<Option<PathFoundEvent>>);
fn goto_listener(
mut commands: Commands,
mut events: EventReader<GotoEvent>,
mut query: Query<(&Position, &WorldName)>,
mut query: Query<(&Position, &InstanceName)>,
instance_container: Res<InstanceContainer>,
) {
let thread_pool = AsyncComputeTaskPool::get();

View file

@ -2,8 +2,8 @@
//! re-exported here.
pub use crate::{
bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt,
ClientBuilder,
bot::BotClientExt, container::ContainerClientExt, mining::MiningExt,
pathfinder::PathfinderClientExt, ClientBuilder,
};
pub use azalea_client::{Account, Client, Event};
// this is necessary to make the macros that reference bevy_ecs work

View file

@ -1,5 +1,5 @@
use azalea_client::LocalPlayer;
use azalea_world::entity::MinecraftEntityId;
use azalea_world::MinecraftEntityId;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};

4
codegen/.gitignore vendored
View file

@ -1,3 +1,5 @@
downloads
__pycache__
*.tmp
downloads
__cache__

View file

@ -20,7 +20,7 @@ ordered_blocks = lib.extract.get_ordered_blocks_burger(version_id)
block_states_report = lib.extract.get_block_states_report(version_id)
lib.code.blocks.generate_blocks(
block_states_burger, block_states_report, ordered_blocks, mappings)
block_states_burger, block_states_report, pixlyzer_block_datas, ordered_blocks, mappings)
lib.code.shapes.generate_block_shapes(
pixlyzer_block_datas, shape_datas['shapes'], shape_datas['aabbs'], block_states_report, block_states_burger, mappings)

View file

@ -3,16 +3,30 @@ import lib.code.registry
import lib.code.version
import lib.code.packet
import lib.code.utils
import lib.code.tags
import lib.download
import lib.extract
import lib.utils
version_id = lib.code.version.get_version_id()
registries = lib.extract.get_registries_report(version_id)
def generate(version_id: str):
registries = lib.extract.get_registries_report(version_id)
lib.code.registry.generate_registries(registries)
lib.code.inventory.update_menus(registries['minecraft:menu']['entries'])
lib.code.registry.generate_registries(registries)
lib.code.inventory.update_menus(registries['minecraft:menu']['entries'])
lib.code.utils.fmt()
print('Done!')
block_tags = lib.extract.get_registry_tags(version_id, 'blocks')
item_tags = lib.extract.get_registry_tags(version_id, 'items')
fluid_tags = lib.extract.get_registry_tags(version_id, 'fluids')
lib.code.tags.generate_tags(block_tags, 'blocks', 'Block')
lib.code.tags.generate_tags(item_tags, 'items', 'Item')
lib.code.tags.generate_tags(fluid_tags, 'fluids', 'Fluid')
lib.code.utils.fmt()
print('Done!')
if __name__ == '__main__':
version_id = lib.code.version.get_version_id()
generate(version_id)

View file

@ -13,7 +13,7 @@ BLOCKS_RS_DIR = get_dir_location('../azalea-block/src/generated.rs')
# - Block: Has properties and states.
def generate_blocks(blocks_burger: dict, blocks_report: dict, ordered_blocks: list[str], mappings: Mappings):
def generate_blocks(blocks_burger: dict, blocks_report: dict, pixlyzer_block_datas: dict, ordered_blocks: list[str], mappings: Mappings):
with open(BLOCKS_RS_DIR, 'r') as f:
existing_code = f.read().splitlines()
@ -90,6 +90,7 @@ def generate_blocks(blocks_burger: dict, blocks_report: dict, ordered_blocks: li
for block_id in ordered_blocks:
block_data_burger = blocks_burger[block_id]
block_data_report = blocks_report['minecraft:' + block_id]
block_data_pixlyzer = pixlyzer_block_datas[f'minecraft:{block_id}']
block_properties = block_data_burger.get('states', [])
block_properties_burger = block_data_burger.get('states', [])
@ -134,9 +135,28 @@ def generate_blocks(blocks_burger: dict, blocks_report: dict, ordered_blocks: li
else:
properties_code += '\n }'
# make the block behavior
behavior_constructor = 'BlockBehavior::new()'
# requires tool
if block_data_pixlyzer.get('requires_tool'):
behavior_constructor += '.requires_correct_tool_for_drops()'
# strength
destroy_time = block_data_pixlyzer.get('hardness')
explosion_resistance = block_data_pixlyzer.get('explosion_resistance')
if destroy_time and explosion_resistance:
behavior_constructor += f'.strength({destroy_time}, {explosion_resistance})'
elif destroy_time:
behavior_constructor += f'.destroy_time({destroy_time})'
elif explosion_resistance:
behavior_constructor += f'.explosion_resistance({explosion_resistance})'
# friction
friction = block_data_pixlyzer.get('friction')
if friction != None:
behavior_constructor += f'.friction({friction})'
# TODO: use burger to generate the blockbehavior
new_make_block_states_macro_code.append(
f' {block_id} => BlockBehavior::default(), {properties_code},')
f' {block_id} => {behavior_constructor}, {properties_code},')
new_make_block_states_macro_code.append(' }')
new_make_block_states_macro_code.append('}')

View file

@ -66,7 +66,7 @@ def simplify_shapes(blocks: dict, shapes: dict, aabbs: dict):
def generate_block_shapes_code(blocks: dict, shapes: dict, block_states_report, block_datas_burger, mappings: Mappings):
# look at downloads/generator-mod-*/blockCollisionShapes.json for format of blocks and shapes
# look at __cache__/generator-mod-*/blockCollisionShapes.json for format of blocks and shapes
generated_shape_code = ''
for (shape_id, shape) in sorted(shapes.items(), key=lambda shape: int(shape[0])):

35
codegen/lib/code/tags.py Normal file
View file

@ -0,0 +1,35 @@
from lib.utils import to_snake_case, upper_first_letter, get_dir_location, to_camel_case
REGISTRIES_DIR = get_dir_location('../azalea-registry/src/tags')
def generate_tags(registries: dict, file_name: str, struct_name: str):
tags_dir = f'{REGISTRIES_DIR}/{file_name}.rs'
generated = f'''// This file was generated by codegen/lib/code/tags.py, don't edit it manually!
use std::collections::HashSet;
use once_cell::sync::Lazy;
use crate::{struct_name};
'''
for tag_name, tag in registries.items():
tag_name = tag_name.replace('/', '_')
static_set_name = to_snake_case(tag_name).upper()
generated += f'pub static {static_set_name}: Lazy<HashSet<{struct_name}>> = Lazy::new(|| HashSet::from_iter(vec!['
queue = tag['values'].copy()
while queue != []:
item = queue.pop(0)
namespace, item_name = item.split(':')
if namespace[0] == '#':
queue += registries[item_name]['values']
continue
generated += f'{struct_name}::{upper_first_letter(to_camel_case(item_name))},\n'
generated += ']));\n'
with open(tags_dir, 'w') as f:
f.write(generated)

View file

@ -5,47 +5,47 @@ import requests
import json
import os
# make sure the downloads directory exists
print('Making downloads')
if not os.path.exists(get_dir_location('downloads')):
print('Made downloads directory.', get_dir_location('downloads'))
os.mkdir(get_dir_location('downloads'))
# make sure the cache directory exists
print('Making __cache__')
if not os.path.exists(get_dir_location('__cache__')):
print('Made __cache__ directory.', get_dir_location('__cache__'))
os.mkdir(get_dir_location('__cache__'))
def get_burger():
if not os.path.exists(get_dir_location('downloads/Burger')):
if not os.path.exists(get_dir_location('__cache__/Burger')):
print('\033[92mDownloading Burger...\033[m')
os.system(
f'cd {get_dir_location("downloads")} && git clone https://github.com/pokechu22/Burger && cd Burger && git pull')
f'cd {get_dir_location("__cache__")} && git clone https://github.com/pokechu22/Burger && cd Burger && git pull')
print('\033[92mInstalling dependencies...\033[m')
os.system(f'cd {get_dir_location("downloads")}/Burger && pip install six jawa')
os.system(f'cd {get_dir_location("__cache__")}/Burger && pip install six jawa')
def get_pixlyzer():
if not os.path.exists(get_dir_location('downloads/pixlyzer')):
if not os.path.exists(get_dir_location('__cache__/pixlyzer')):
print('\033[92mDownloading bixilon/pixlyzer...\033[m')
os.system(
f'cd {get_dir_location("downloads")} && git clone https://gitlab.bixilon.de/bixilon/pixlyzer.git && cd pixlyzer && git pull')
return get_dir_location('downloads/pixlyzer')
f'cd {get_dir_location("__cache__")} && git clone https://gitlab.bixilon.de/bixilon/pixlyzer.git && cd pixlyzer && git pull')
return get_dir_location('__cache__/pixlyzer')
def get_version_manifest():
if not os.path.exists(get_dir_location(f'downloads/version_manifest.json')):
if not os.path.exists(get_dir_location(f'__cache__/version_manifest.json')):
print(
f'\033[92mDownloading version manifest...\033[m')
version_manifest_data = requests.get(
'https://piston-meta.mojang.com/mc/game/version_manifest_v2.json').json()
with open(get_dir_location(f'downloads/version_manifest.json'), 'w') as f:
with open(get_dir_location(f'__cache__/version_manifest.json'), 'w') as f:
json.dump(version_manifest_data, f)
else:
with open(get_dir_location(f'downloads/version_manifest.json'), 'r') as f:
with open(get_dir_location(f'__cache__/version_manifest.json'), 'r') as f:
version_manifest_data = json.load(f)
return version_manifest_data
def get_version_data(version_id: str):
if not os.path.exists(get_dir_location(f'downloads/{version_id}.json')):
if not os.path.exists(get_dir_location(f'__cache__/{version_id}.json')):
version_manifest_data = get_version_manifest()
print(
@ -55,60 +55,60 @@ def get_version_data(version_id: str):
filter(lambda v: v['id'] == version_id, version_manifest_data['versions']))['url']
except StopIteration:
raise ValueError(
f'No version with id {version_id} found. Maybe delete downloads/version_manifest.json and try again?')
f'No version with id {version_id} found. Maybe delete __cache__/version_manifest.json and try again?')
package_data = requests.get(package_url).json()
with open(get_dir_location(f'downloads/{version_id}.json'), 'w') as f:
with open(get_dir_location(f'__cache__/{version_id}.json'), 'w') as f:
json.dump(package_data, f)
else:
with open(get_dir_location(f'downloads/{version_id}.json'), 'r') as f:
with open(get_dir_location(f'__cache__/{version_id}.json'), 'r') as f:
package_data = json.load(f)
return package_data
def get_client_jar(version_id: str):
if not os.path.exists(get_dir_location(f'downloads/client-{version_id}.jar')):
if not os.path.exists(get_dir_location(f'__cache__/client-{version_id}.jar')):
package_data = get_version_data(version_id)
print('\033[92mDownloading client jar...\033[m')
client_jar_url = package_data['downloads']['client']['url']
with open(get_dir_location(f'downloads/client-{version_id}.jar'), 'wb') as f:
with open(get_dir_location(f'__cache__/client-{version_id}.jar'), 'wb') as f:
f.write(requests.get(client_jar_url).content)
def get_server_jar(version_id: str):
if not os.path.exists(get_dir_location(f'downloads/server-{version_id}.jar')):
if not os.path.exists(get_dir_location(f'__cache__/server-{version_id}.jar')):
package_data = get_version_data(version_id)
print('\033[92mDownloading server jar...\033[m')
server_jar_url = package_data['downloads']['server']['url']
with open(get_dir_location(f'downloads/server-{version_id}.jar'), 'wb') as f:
with open(get_dir_location(f'__cache__/server-{version_id}.jar'), 'wb') as f:
f.write(requests.get(server_jar_url).content)
def get_mappings_for_version(version_id: str):
if not os.path.exists(get_dir_location(f'downloads/mappings-{version_id}.txt')):
if not os.path.exists(get_dir_location(f'__cache__/mappings-{version_id}.txt')):
package_data = get_version_data(version_id)
client_mappings_url = package_data['downloads']['client_mappings']['url']
mappings_text = requests.get(client_mappings_url).text
with open(get_dir_location(f'downloads/mappings-{version_id}.txt'), 'w') as f:
with open(get_dir_location(f'__cache__/mappings-{version_id}.txt'), 'w') as f:
f.write(mappings_text)
else:
with open(get_dir_location(f'downloads/mappings-{version_id}.txt'), 'r') as f:
with open(get_dir_location(f'__cache__/mappings-{version_id}.txt'), 'r') as f:
mappings_text = f.read()
return Mappings.parse(mappings_text)
def get_yarn_versions():
# https://meta.fabricmc.net/v2/versions/yarn
if not os.path.exists(get_dir_location('downloads/yarn_versions.json')):
if not os.path.exists(get_dir_location('__cache__/yarn_versions.json')):
print('\033[92mDownloading yarn versions...\033[m')
yarn_versions_data = requests.get(
'https://meta.fabricmc.net/v2/versions/yarn').json()
with open(get_dir_location('downloads/yarn_versions.json'), 'w') as f:
with open(get_dir_location('__cache__/yarn_versions.json'), 'w') as f:
json.dump(yarn_versions_data, f)
else:
with open(get_dir_location('downloads/yarn_versions.json'), 'r') as f:
with open(get_dir_location('__cache__/yarn_versions.json'), 'r') as f:
yarn_versions_data = json.load(f)
return yarn_versions_data
@ -121,7 +121,7 @@ def get_yarn_data(version_id: str):
def get_fabric_api_versions():
# https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/maven-metadata.xml
if not os.path.exists(get_dir_location('downloads/fabric_api_versions.json')):
if not os.path.exists(get_dir_location('__cache__/fabric_api_versions.json')):
print('\033[92mDownloading Fabric API versions...\033[m')
fabric_api_versions_xml_text = requests.get(
'https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/maven-metadata.xml').text
@ -138,17 +138,17 @@ def get_fabric_api_versions():
for version_el in versions_el.findall('version'):
fabric_api_versions.append(version_el.text)
with open(get_dir_location('downloads/fabric_api_versions.json'), 'w') as f:
with open(get_dir_location('__cache__/fabric_api_versions.json'), 'w') as f:
f.write(json.dumps(fabric_api_versions))
else:
with open(get_dir_location('downloads/fabric_api_versions.json'), 'r') as f:
with open(get_dir_location('__cache__/fabric_api_versions.json'), 'r') as f:
fabric_api_versions = json.loads(f.read())
return fabric_api_versions
def get_fabric_loader_versions():
# https://meta.fabricmc.net/v2/versions/loader
if not os.path.exists(get_dir_location('downloads/fabric_loader_versions.json')):
if not os.path.exists(get_dir_location('__cache__/fabric_loader_versions.json')):
print('\033[92mDownloading Fabric loader versions...\033[m')
fabric_api_versions_json = requests.get(
'https://meta.fabricmc.net/v2/versions/loader').json()
@ -157,10 +157,10 @@ def get_fabric_loader_versions():
for version in fabric_api_versions_json:
fabric_api_versions.append(version['version'])
with open(get_dir_location('downloads/fabric_loader_versions.json'), 'w') as f:
with open(get_dir_location('__cache__/fabric_loader_versions.json'), 'w') as f:
f.write(json.dumps(fabric_api_versions))
else:
with open(get_dir_location('downloads/fabric_loader_versions.json'), 'r') as f:
with open(get_dir_location('__cache__/fabric_loader_versions.json'), 'r') as f:
fabric_api_versions = json.loads(f.read())
return fabric_api_versions
@ -174,14 +174,14 @@ def clear_version_cache():
'fabric_loader_versions.json'
]
for file in files:
if os.path.exists(get_dir_location(f'downloads/{file}')):
os.remove(get_dir_location(f'downloads/{file}'))
if os.path.exists(get_dir_location(f'__cache__/{file}')):
os.remove(get_dir_location(f'__cache__/{file}'))
burger_path = get_dir_location("downloads/Burger")
burger_path = get_dir_location("__cache__/Burger")
if os.path.exists(burger_path):
os.system(
f'cd {burger_path} && git pull')
pixlyzer_path = get_dir_location('downloads/pixlyzer')
pixlyzer_path = get_dir_location('__cache__/pixlyzer')
if os.path.exists(pixlyzer_path):
os.system(
f'cd {pixlyzer_path} && git pull')

View file

@ -12,27 +12,44 @@ import os
def generate_data_from_server_jar(version_id: str):
if os.path.exists(get_dir_location(f'downloads/generated-{version_id}')):
if os.path.exists(get_dir_location(f'__cache__/generated-{version_id}')):
return
get_server_jar(version_id)
os.system(
f'cd {get_dir_location(f"downloads")} && java -DbundlerMainClass=net.minecraft.data.Main -jar {get_dir_location(f"downloads/server-{version_id}.jar")} --all --output \"{get_dir_location(f"downloads/generated-{version_id}")}\"'
f'cd {get_dir_location(f"__cache__")} && java -DbundlerMainClass=net.minecraft.data.Main -jar {get_dir_location(f"__cache__/server-{version_id}.jar")} --all --output \"{get_dir_location(f"__cache__/generated-{version_id}")}\"'
)
def get_block_states_report(version_id: str):
generate_data_from_server_jar(version_id)
with open(get_dir_location(f'downloads/generated-{version_id}/reports/blocks.json'), 'r') as f:
with open(get_dir_location(f'__cache__/generated-{version_id}/reports/blocks.json'), 'r') as f:
return json.load(f)
def get_registries_report(version_id: str):
generate_data_from_server_jar(version_id)
with open(get_dir_location(f'downloads/generated-{version_id}/reports/registries.json'), 'r') as f:
with open(get_dir_location(f'__cache__/generated-{version_id}/reports/registries.json'), 'r') as f:
return json.load(f)
def get_registry_tags(version_id: str, name: str):
generate_data_from_server_jar(version_id)
tags_directory = get_dir_location(f'__cache__/generated-{version_id}/data/minecraft/tags/{name}')
if not os.path.exists(tags_directory):
return {}
tags = {}
for root, dirs, files in os.walk(tags_directory, topdown=False):
for name in files:
file = os.path.join(root, name)
relative_path = file.replace(tags_directory, '')[1:]
if not file.endswith('.json'):
continue
with open(file, 'r') as f:
tags[relative_path[:-5]] = json.load(f)
return tags
def get_block_states_burger(version_id: str):
burger_data = get_burger_data_for_version(version_id)
return burger_data[0]['blocks']['block']
@ -96,15 +113,15 @@ def run_python_command_and_download_deps(command):
def get_burger_data_for_version(version_id: str):
if not os.path.exists(get_dir_location(f'downloads/burger-{version_id}.json')):
if not os.path.exists(get_dir_location(f'__cache__/burger-{version_id}.json')):
get_burger()
get_client_jar(version_id)
print('\033[92mRunning Burger...\033[m')
run_python_command_and_download_deps(
f'cd {get_dir_location("downloads/Burger")} && {determine_python_command()} munch.py {get_dir_location("downloads")}/client-{version_id}.jar --output {get_dir_location("downloads")}/burger-{version_id}.json'
f'cd {get_dir_location("__cache__/Burger")} && {determine_python_command()} munch.py {get_dir_location("__cache__")}/client-{version_id}.jar --output {get_dir_location("__cache__")}/burger-{version_id}.json'
)
with open(get_dir_location(f'downloads/burger-{version_id}.json'), 'r') as f:
with open(get_dir_location(f'__cache__/burger-{version_id}.json'), 'r') as f:
return json.load(f)
@ -113,7 +130,7 @@ def get_pixlyzer_data(version_id: str, category: str):
Gets data from Pixlyzer. Note that this requires Yarn to release updates first.
'''
target_dir = get_dir_location(f'downloads/pixlyzer-{version_id}')
target_dir = get_dir_location(f'__cache__/pixlyzer-{version_id}')
# TODO: right now this False is hard-coded, it should retry with this
# enabled if # initially getting the data fails
@ -249,7 +266,7 @@ def get_pixlyzer_data(version_id: str, category: str):
def get_file_from_jar(version_id: str, file_dir: str):
get_client_jar(version_id)
with ZipFile(get_dir_location(f'downloads/client-{version_id}.jar')) as z:
with ZipFile(get_dir_location(f'__cache__/client-{version_id}.jar')) as z:
with z.open(file_dir) as f:
return f.read()

View file

@ -133,9 +133,8 @@ language = lib.extract.get_en_us_lang(new_version_id)
lib.code.language.write_language(language)
print('Generating registries...')
registries = lib.extract.get_registries_report(new_version_id)
lib.code.registry.generate_registries(registries)
lib.code.inventory.update_menus(registries['minecraft:menu']['entries'])
import genregistries
genregistries.generate(new_version_id)
print('Generating entity metadata...')
burger_entities_data = new_burger_data[0]['entities']