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

packet handling

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

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

making azalea-client use the ecs is almost done! all the hard parts are done now i hope, i just have to finish writing all the code so it actually works
This commit is contained in:
mat 2023-01-02 18:59:29 -06:00
parent 9193c1b408
commit 50f6578794
20 changed files with 1300 additions and 1234 deletions

117
Cargo.lock generated
View file

@ -278,7 +278,10 @@ dependencies = [
"azalea-protocol",
"azalea-registry",
"azalea-world",
"bevy_app",
"bevy_ecs",
"futures",
"iyes_loopless",
"log",
"nohash-hasher",
"once_cell",
@ -463,6 +466,7 @@ checksum = "536e4d0018347478545ed8b6cb6e57b9279ee984868e81b7c0e78e0fb3222e42"
dependencies = [
"bevy_derive",
"bevy_ecs",
"bevy_reflect",
"bevy_utils",
"downcast-rs",
"wasm-bindgen",
@ -489,6 +493,7 @@ dependencies = [
"async-channel",
"bevy_ecs_macros",
"bevy_ptr",
"bevy_reflect",
"bevy_tasks",
"bevy_utils",
"downcast-rs",
@ -522,12 +527,56 @@ dependencies = [
"toml",
]
[[package]]
name = "bevy_math"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434c77ab766c806ed9062ef8a7285b3b02b47df51f188d4496199c3ac062eaf"
dependencies = [
"glam",
"serde",
]
[[package]]
name = "bevy_ptr"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec44f7655039546bc5d34d98de877083473f3e9b2b81d560c528d6d74d3eff4"
[[package]]
name = "bevy_reflect"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6deae303a7f69dc243b2fa35b5e193cc920229f448942080c8eb2dbd9de6d37a"
dependencies = [
"bevy_math",
"bevy_ptr",
"bevy_reflect_derive",
"bevy_utils",
"downcast-rs",
"erased-serde",
"glam",
"once_cell",
"parking_lot",
"serde",
"smallvec",
"thiserror",
]
[[package]]
name = "bevy_reflect_derive"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bf4cb9cd5acb4193f890f36cb63679f1502e2de025e66a63b194b8b133d018"
dependencies = [
"bevy_macro_utils",
"bit-set",
"proc-macro2",
"quote",
"syn",
"uuid",
]
[[package]]
name = "bevy_tasks"
version = "0.9.1"
@ -543,6 +592,19 @@ dependencies = [
"wasm-bindgen-futures",
]
[[package]]
name = "bevy_time"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c38a6d3ea929c7f81e6adf5a6c62cf7e8c40f5106c2174d6057e9d8ea624d"
dependencies = [
"bevy_app",
"bevy_ecs",
"bevy_reflect",
"bevy_utils",
"crossbeam-channel",
]
[[package]]
name = "bevy_utils"
version = "0.9.1"
@ -557,6 +619,21 @@ dependencies = [
"uuid",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -604,6 +681,12 @@ version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "bytemuck"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -924,6 +1007,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ca605381c017ec7a5fef5e548f1cfaa419ed0f6df6367339300db74c92aa7d"
dependencies = [
"serde",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@ -1127,6 +1219,16 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "glam"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774"
dependencies = [
"bytemuck",
"serde",
]
[[package]]
name = "h2"
version = "0.3.15"
@ -1333,6 +1435,18 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "iyes_loopless"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c47fd2cbdb1d7f295c25e6bfccfd78a84b6eef3055bc9f01b34ae861721b01ee"
dependencies = [
"bevy_app",
"bevy_ecs",
"bevy_time",
"bevy_utils",
]
[[package]]
name = "js-sys"
version = "0.3.60"
@ -2076,6 +2190,9 @@ name = "smallvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
dependencies = [
"serde",
]
[[package]]
name = "socket2"

View file

@ -24,19 +24,9 @@ debug = true
# decoding packets takes forever if we don't do this
[profile.dev.package.azalea-crypto]
opt-level = 3
[profile.dev.package.cipher]
opt-level = 3
[profile.dev.package.cfb8]
opt-level = 3
[profile.dev.package.aes]
opt-level = 3
[profile.dev.package.crypto-common]
opt-level = 3
[profile.dev.package.generic-array]
opt-level = 3
[profile.dev.package.typenum]
opt-level = 3
[profile.dev.package.inout]
opt-level = 3
[profile.dev.package.flate2]
opt-level = 3

View file

@ -20,7 +20,10 @@ azalea-physics = {path = "../azalea-physics", version = "0.5.0"}
azalea-protocol = {path = "../azalea-protocol", version = "0.5.0"}
azalea-registry = {path = "../azalea-registry", version = "0.5.0"}
azalea-world = {path = "../azalea-world", version = "0.5.0"}
bevy_app = {version = "0.9.1", default-features = false}
bevy_ecs = {version = "0.9.1", default-features = false}
futures = "0.3.25"
iyes_loopless = "0.9.1"
log = "0.4.17"
nohash-hasher = "0.2.0"
once_cell = "1.16.0"

View file

@ -1,7 +1,12 @@
pub use crate::chat::ChatPacket;
use crate::{
local_player::LocalPlayer, movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo,
local_player::LocalPlayer,
movement::{send_position, WalkDirection},
packet_handling,
plugins::PluginStates,
Account, PlayerInfo,
};
use azalea_auth::{game_profile::GameProfile, sessionserver::SessionServerError};
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
use azalea_protocol::{
@ -34,16 +39,19 @@ use azalea_world::{
entity::{
self,
metadata::{self, PlayerMetadataBundle},
EntityId,
Entity,
},
PartialChunkStorage, PartialWorld, WeakWorldContainer, World,
EntityInfos, PartialChunkStorage, PartialWorld, World, WorldContainer,
};
use bevy_app::App;
use bevy_ecs::{
prelude::Component,
query::{QueryState, WorldQuery},
schedule::{Schedule, Stage, StageLabel, SystemStage},
schedule::{IntoSystemDescriptor, Schedule, Stage, StageLabel, SystemStage},
system::{Query, SystemState},
};
use futures::{stream::FuturesUnordered, StreamExt};
use iyes_loopless::prelude::*;
use log::{debug, error, info, trace, warn};
use parking_lot::{Mutex, RwLock};
use std::{
@ -52,7 +60,9 @@ use std::{
collections::HashMap,
fmt::Debug,
io::{self, Cursor},
ops::DerefMut,
sync::Arc,
time::Duration,
};
use thiserror::Error;
use tokio::{
@ -100,11 +110,12 @@ pub enum Event {
/// Things that a player in the world will want to know are in [`LocalPlayer`].
pub struct Client {
pub profile: GameProfile,
pub entity_id: Arc<RwLock<EntityId>>,
/// The entity for this client in the ECS.
pub entity: Entity,
/// A container of world names to worlds. If we're not using a shared world
/// (i.e. not a swarm), then this will only contain data about the world
/// we're currently in.
world_container: Arc<RwLock<WeakWorldContainer>>,
world_container: Arc<RwLock<WorldContainer>>,
/// The world that this client is in.
pub world: Arc<RwLock<PartialWorld>>,
@ -113,6 +124,11 @@ pub struct Client {
/// the `azalea` crate. you can ignore this field.
pub plugins: Arc<PluginStates>,
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
/// The entity component system. You probably don't need to access this
/// directly. Note that if you're using a shared world (i.e. a swarm), this
/// will contain all entities in all worlds.
pub ecs: Arc<Mutex<bevy_ecs::world::World>>,
}
/// Whether we should ignore errors when decoding packets.
@ -143,19 +159,23 @@ impl Client {
/// defaults, otherwise use [`Client::join`].
pub fn new(
profile: GameProfile,
world_container: Option<Arc<RwLock<WeakWorldContainer>>>,
world_container: Option<Arc<RwLock<WorldContainer>>>,
entity: Entity,
ecs: Arc<Mutex<bevy_ecs::world::World>>,
) -> Self {
Self {
profile,
// default our id to 0, it'll be set later
entity_id: Arc::new(RwLock::new(EntityId(0))),
entity,
world: Arc::new(RwLock::new(PartialWorld::default())),
world_container: world_container
.unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))),
.unwrap_or_else(|| Arc::new(RwLock::new(WorldContainer::new()))),
// The plugins can be modified by the user by replacing the plugins
// field right after this. No Mutex so the user doesn't need to .lock().
plugins: Arc::new(PluginStates::default()),
tasks: Arc::new(Mutex::new(Vec::new())),
ecs,
}
}
@ -188,6 +208,7 @@ impl Client {
let conn = Connection::new(&resolved_address).await?;
let (conn, game_profile) = Self::handshake(conn, account, &address).await?;
let (read_conn, write_conn) = conn.into_split();
// The buffer has to be 1 to avoid a bug where if it lags events are
// received a bit later instead of the instant they were fired.
@ -195,21 +216,43 @@ impl Client {
let (tx, rx) = mpsc::channel(1);
tx.send(Event::Init).await.expect("Failed to send event");
// we got the GameConnection, so the server is now connected :)
let client = Client::new(game_profile.clone(), None);
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let (read_conn, write_conn) = conn.into_split();
let ecs_lock = start_ecs(run_schedule_receiver).await;
let mut ecs = ecs_lock.lock();
ecs.init_resource::<EntityInfos>();
let entity_mut = ecs.spawn_empty();
let entity = entity_mut.id();
// we got the GameConnection, so the server is now connected :)
let client = Client::new(game_profile.clone(), None, entity, ecs_lock);
let world = client.world();
let local_player = crate::local_player::LocalPlayer::new(
entity,
game_profile,
write_conn,
client.world.clone(),
world.clone(),
ecs.resource_mut::<EntityInfos>().deref_mut(),
tx,
);
// just start up the game loop and we're ready!
// start receiving packets
let packet_receiver = packet_handling::PacketReceiver {
packets: Arc::new(Mutex::new(Vec::new())),
run_schedule_sender,
};
client.start_tasks(read_conn);
tokio::spawn(packet_receiver.read_task(read_conn));
ecs.entity_mut(entity)
.insert((local_player, packet_receiver));
// just start up the game loop and we're ready!
Ok((client, rx))
}
@ -359,688 +402,33 @@ impl Client {
self.query::<&mut LocalPlayer>().into_inner()
}
/// Start the protocol and game tick loop.
#[doc(hidden)]
pub fn start_tasks(&self, read_conn: ReadConnection<ClientboundGamePacket>) {
// if you get an error right here that means you're doing something with locks
// wrong read the error to see where the issue is
// you might be able to just drop the lock or put it in its own scope to fix
let mut tasks = self.tasks.lock();
tasks.push(tokio::spawn(self.protocol_loop(tx.clone(), read_conn)));
let ecs = self.world_container.clone().read().ecs;
tasks.push(tokio::spawn(Client::game_tick_loop(ecs.clone())));
}
async fn protocol_loop(
&self,
tx: Sender<Event>,
read_conn: ReadConnection<ClientboundGamePacket>,
) {
loop {
let r = read_conn.lock().await.read().await;
match r {
Ok(packet) => LocalPlayer::send_event(Event::Packet(packet.clone()), &tx),
Err(e) => {
let e = *e;
if let ReadPacketError::ConnectionClosed = e {
info!("Connection closed");
if let Err(e) = client.disconnect().await {
error!("Error shutting down connection: {:?}", e);
}
break;
}
let default_backtrace = Backtrace::capture();
if IGNORE_ERRORS {
let backtrace =
any::request_ref::<Backtrace>(&e).unwrap_or(&default_backtrace);
warn!("{e}\n{backtrace}");
match e {
ReadPacketError::FrameSplitter { .. } => panic!("Error: {e:?}"),
_ => continue,
}
} else {
let backtrace =
any::request_ref::<Backtrace>(&e).unwrap_or(&default_backtrace);
panic!("{e}\n{backtrace}")
}
}
}
}
}
// handling packets is outside of the ecs schedule since it's not bound to
// ticks.
pub fn handle_packet(
client: &Client,
packet: ClientboundGamePacket,
ecs: &mut bevy_ecs::world::World,
tx: &mpsc::Sender<Event>,
) -> Result<(), HandlePacketError> {
let (mut local_player,) = query.get_mut((*player_entity_id).into()).unwrap();
match &packet {
ClientboundGamePacket::Login(p) => {
debug!("Got login packet");
{
// // write p into login.txt
// std::io::Write::write_all(
// &mut std::fs::File::create("login.txt").unwrap(),
// format!("{:#?}", p).as_bytes(),
// )
// .unwrap();
// TODO: have registry_holder be a struct because this sucks rn
// best way would be to add serde support to azalea-nbt
let registry_holder = p
.registry_holder
.as_compound()
.expect("Registry holder is not a compound")
.get("")
.expect("No \"\" tag")
.as_compound()
.expect("\"\" tag is not a compound");
let dimension_types = registry_holder
.get("minecraft:dimension_type")
.expect("No dimension_type tag")
.as_compound()
.expect("dimension_type is not a compound")
.get("value")
.expect("No dimension_type value")
.as_list()
.expect("dimension_type value is not a list");
let dimension_type = dimension_types
.iter()
.find(|t| {
t.as_compound()
.expect("dimension_type value is not a compound")
.get("name")
.expect("No name tag")
.as_string()
.expect("name is not a string")
== p.dimension_type.to_string()
})
.unwrap_or_else(|| {
panic!("No dimension_type with name {}", p.dimension_type)
})
.as_compound()
.unwrap()
.get("element")
.expect("No element tag")
.as_compound()
.expect("element is not a compound");
let height = (*dimension_type
.get("height")
.expect("No height tag")
.as_int()
.expect("height tag is not an int"))
.try_into()
.expect("height is not a u32");
let min_y = *dimension_type
.get("min_y")
.expect("No min_y tag")
.as_int()
.expect("min_y tag is not an int");
let world_name = p.dimension.clone();
local_player.world_name = Some(world_name.clone());
// add this world to the world_container (or don't if it's already there)
let weak_world = world_container.insert(world_name, height, min_y);
// set the loaded_world to an empty world
// (when we add chunks or entities those will be in the world_container)
let mut world_lock = local_player.world.write();
*world_lock = PartialWorld::new(
local_player.client_information.view_distance.into(),
weak_world,
Some(EntityId(p.player_id)),
);
let player_bundle = entity::PlayerBundle {
entity: entity::EntityBundle::new(
local_player.profile.uuid,
Vec3::default(),
azalea_registry::EntityKind::Player,
),
metadata: PlayerMetadataBundle::default(),
};
// let entity = EntityData::new(
// client.profile.uuid,
// Vec3::default(),
// EntityMetadata::Player(metadata::Player::default()),
// );
// the first argument makes it so other entities don't update this entity in
// a shared world
world_lock.add_entity(EntityId(p.player_id), player_bundle);
*client.entity_id.write() = EntityId(p.player_id);
}
// send the client information that we have set
let client_information_packet: ClientInformation =
client.local_player().client_information.clone();
log::debug!(
"Sending client information because login: {:?}",
client_information_packet
);
client.write_packet(client_information_packet.get()).await?;
// brand
client
.write_packet(
ServerboundCustomPayloadPacket {
identifier: ResourceLocation::new("brand").unwrap(),
// they don't have to know :)
data: "vanilla".into(),
}
.get(),
)
.await?;
tx.send(Event::Login).await?;
}
ClientboundGamePacket::SetChunkCacheRadius(p) => {
debug!("Got set chunk cache radius packet {:?}", p);
}
ClientboundGamePacket::CustomPayload(p) => {
debug!("Got custom payload packet {:?}", p);
}
ClientboundGamePacket::ChangeDifficulty(p) => {
debug!("Got difficulty packet {:?}", p);
}
ClientboundGamePacket::Commands(_p) => {
debug!("Got declare commands packet");
}
ClientboundGamePacket::PlayerAbilities(p) => {
debug!("Got player abilities packet {:?}", p);
}
ClientboundGamePacket::SetCarriedItem(p) => {
debug!("Got set carried item packet {:?}", p);
}
ClientboundGamePacket::UpdateTags(_p) => {
debug!("Got update tags packet");
}
ClientboundGamePacket::Disconnect(p) => {
debug!("Got disconnect packet {:?}", p);
client.disconnect().await?;
}
ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet");
}
ClientboundGamePacket::EntityEvent(_p) => {
// debug!("Got entity event packet {:?}", p);
}
ClientboundGamePacket::Recipe(_p) => {
debug!("Got recipe packet");
}
ClientboundGamePacket::PlayerPosition(p) => {
// TODO: reply with teleport confirm
debug!("Got player position packet {:?}", p);
let (new_pos, y_rot, x_rot) = {
let player_entity_id = *client.entity();
let world = client.world();
// let mut player_entity = world.entity_mut(player_entity_id).unwrap();
let (mut physics, position) =
client.query::<(&mut entity::Physics, &mut entity::Position)>();
let delta_movement = physics.delta;
let is_x_relative = p.relative_arguments.x;
let is_y_relative = p.relative_arguments.y;
let is_z_relative = p.relative_arguments.z;
let (delta_x, new_pos_x) = if is_x_relative {
physics.last_pos.x += p.x;
(delta_movement.x, position.x + p.x)
} else {
physics.last_pos.x = p.x;
(0.0, p.x)
};
let (delta_y, new_pos_y) = if is_y_relative {
physics.last_pos.y += p.y;
(delta_movement.y, position.y + p.y)
} else {
physics.last_pos.y = p.y;
(0.0, p.y)
};
let (delta_z, new_pos_z) = if is_z_relative {
physics.last_pos.z += p.z;
(delta_movement.z, position.z + p.z)
} else {
physics.last_pos.z = p.z;
(0.0, p.z)
};
let mut y_rot = p.y_rot;
let mut x_rot = p.x_rot;
if p.relative_arguments.x_rot {
x_rot += physics.x_rot;
}
if p.relative_arguments.y_rot {
y_rot += physics.y_rot;
}
physics.delta = Vec3 {
x: delta_x,
y: delta_y,
z: delta_z,
};
entity::set_rotation(physics.into_inner(), y_rot, x_rot);
// TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means
// so investigate that ig
let new_pos = Vec3 {
x: new_pos_x,
y: new_pos_y,
z: new_pos_z,
};
world
.set_entity_pos(
player_entity_id,
new_pos,
position.into_inner(),
physics.into_inner(),
)
.expect("The player entity should always exist");
(new_pos, y_rot, x_rot)
};
client
.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get())
.await?;
client
.write_packet(
ServerboundMovePlayerPosRotPacket {
x: new_pos.x,
y: new_pos.y,
z: new_pos.z,
y_rot,
x_rot,
// this is always false
on_ground: false,
}
.get(),
)
.await?;
}
ClientboundGamePacket::PlayerInfoUpdate(p) => {
debug!("Got player info packet {:?}", p);
let mut events = Vec::new();
{
let mut players_lock = client.players.write();
for updated_info in &p.entries {
// add the new player maybe
if p.actions.add_player {
let player_info = PlayerInfo {
profile: updated_info.profile.clone(),
uuid: updated_info.profile.uuid,
gamemode: updated_info.game_mode,
latency: updated_info.latency,
display_name: updated_info.display_name.clone(),
};
players_lock.insert(updated_info.profile.uuid, player_info.clone());
events.push(Event::AddPlayer(player_info));
} else if let Some(info) = players_lock.get_mut(&updated_info.profile.uuid)
{
// `else if` because the block for add_player above
// already sets all the fields
if p.actions.update_game_mode {
info.gamemode = updated_info.game_mode;
}
if p.actions.update_latency {
info.latency = updated_info.latency;
}
if p.actions.update_display_name {
info.display_name = updated_info.display_name.clone();
}
events.push(Event::UpdatePlayer(info.clone()));
} else {
warn!(
"Ignoring PlayerInfoUpdate for unknown player {}",
updated_info.profile.uuid
);
}
}
}
for event in events {
tx.send(event).await?;
}
}
ClientboundGamePacket::PlayerInfoRemove(p) => {
let mut events = Vec::new();
{
let mut players_lock = client.players.write();
for uuid in &p.profile_ids {
if let Some(info) = players_lock.remove(uuid) {
events.push(Event::RemovePlayer(info));
}
}
}
for event in events {
tx.send(event).await?;
}
}
ClientboundGamePacket::SetChunkCacheCenter(p) => {
debug!("Got chunk cache center packet {:?}", p);
client
.world
.write()
.update_view_center(&ChunkPos::new(p.x, p.z));
}
ClientboundGamePacket::LevelChunkWithLight(p) => {
// debug!("Got chunk with light packet {} {}", p.x, p.z);
let pos = ChunkPos::new(p.x, p.z);
// OPTIMIZATION: if we already know about the chunk from the
// shared world (and not ourselves), then we don't need to
// parse it again. This is only used when we have a shared
// world, since we check that the chunk isn't currently owned
// by this client.
let shared_has_chunk = client.world.read().get_chunk(&pos).is_some();
let this_client_has_chunk = client.world.read().chunks.limited_get(&pos).is_some();
if shared_has_chunk && !this_client_has_chunk {
trace!(
"Skipping parsing chunk {:?} because we already know about it",
pos
);
return Ok(());
}
// let chunk = Chunk::read_with_world_height(&mut p.chunk_data);
// debug("chunk {:?}")
if let Err(e) = client
.world
.write()
.replace_with_packet_data(&pos, &mut Cursor::new(&p.chunk_data.data))
{
error!("Couldn't set chunk data: {}", e);
}
}
ClientboundGamePacket::LightUpdate(_p) => {
// debug!("Got light update packet {:?}", p);
}
ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p);
let bundle = p.as_entity_bundle();
let mut world = client.world.write();
world.add_entity(EntityId(p.id), bundle);
// the bundle doesn't include the default entity metadata so we add that
// separately
let mut entities = world.entity_infos.shared.write();
let mut entity = entities.ecs_entity_mut(EntityId(p.id)).unwrap();
p.apply_metadata(&mut entity);
}
ClientboundGamePacket::SetEntityData(p) => {
debug!("Got set entity data packet {:?}", p);
let world = client.world.write();
let mut entities = world.entity_infos.shared.write();
let entity = entities.ecs_entity_mut(EntityId(p.id));
if let Some(mut entity) = entity {
entity::metadata::apply_metadata(&mut entity, p.packed_items.0.clone());
} else {
// warn!("Server sent an entity data packet for an
// entity id ({}) that we don't
// know about", p.id);
}
}
ClientboundGamePacket::UpdateAttributes(_p) => {
// debug!("Got update attributes packet {:?}", p);
}
ClientboundGamePacket::SetEntityMotion(_p) => {
// debug!("Got entity velocity packet {:?}", p);
}
ClientboundGamePacket::SetEntityLink(p) => {
debug!("Got set entity link packet {:?}", p);
}
ClientboundGamePacket::AddPlayer(p) => {
debug!("Got add player packet {:?}", p);
let bundle = p.as_player_bundle();
let mut world = client.world.write();
world.add_entity(EntityId(p.id), bundle);
// the default metadata was already included in the bundle
// for us
}
ClientboundGamePacket::InitializeBorder(p) => {
debug!("Got initialize border packet {:?}", p);
}
ClientboundGamePacket::SetTime(p) => {
debug!("Got set time packet {:?}", p);
}
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
debug!("Got set default spawn position packet {:?}", p);
}
ClientboundGamePacket::ContainerSetContent(p) => {
debug!("Got container set content packet {:?}", p);
}
ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p);
if p.health == 0.0 {
// we can't define a variable here with client.dead.lock()
// because of https://github.com/rust-lang/rust/issues/57478
if !*client.dead.lock() {
*client.dead.lock() = true;
tx.send(Event::Death(None)).await?;
}
}
}
ClientboundGamePacket::SetExperience(p) => {
debug!("Got set experience packet {:?}", p);
}
ClientboundGamePacket::TeleportEntity(p) => {
let mut world = client.world.write();
let (pos, physics) = self.query::<(&entity::Position, &entity::Physics)>();
let _ = world.set_entity_pos(
EntityId(p.id),
Vec3 {
x: p.x,
y: p.y,
z: p.z,
},
pos,
physics,
);
}
ClientboundGamePacket::UpdateAdvancements(p) => {
debug!("Got update advancements packet {:?}", p);
}
ClientboundGamePacket::RotateHead(_p) => {
// debug!("Got rotate head packet {:?}", p);
}
ClientboundGamePacket::MoveEntityPos(p) => {
let mut world = client.world.write();
let _ = world.move_entity_with_delta(EntityId(p.entity_id), &p.delta);
}
ClientboundGamePacket::MoveEntityPosRot(p) => {
let mut world = client.world.write();
let _ = world.move_entity_with_delta(EntityId(p.entity_id), &p.delta);
}
ClientboundGamePacket::MoveEntityRot(_p) => {
// debug!("Got move entity rot packet {:?}", p);
}
ClientboundGamePacket::KeepAlive(p) => {
debug!("Got keep alive packet {:?}", p);
client
.write_packet(ServerboundKeepAlivePacket { id: p.id }.get())
.await?;
}
ClientboundGamePacket::RemoveEntities(p) => {
debug!("Got remove entities packet {:?}", p);
}
ClientboundGamePacket::PlayerChat(p) => {
debug!("Got player chat packet {:?}", p);
tx.send(Event::Chat(ChatPacket::Player(Arc::new(p.clone()))))
.await?;
}
ClientboundGamePacket::SystemChat(p) => {
debug!("Got system chat packet {:?}", p);
tx.send(Event::Chat(ChatPacket::System(Arc::new(p.clone()))))
.await?;
}
ClientboundGamePacket::Sound(_p) => {
// debug!("Got sound packet {:?}", p);
}
ClientboundGamePacket::LevelEvent(p) => {
debug!("Got level event packet {:?}", p);
}
ClientboundGamePacket::BlockUpdate(p) => {
debug!("Got block update packet {:?}", p);
let mut world = client.world.write();
world.set_block_state(&p.pos, p.block_state);
}
ClientboundGamePacket::Animate(p) => {
debug!("Got animate packet {:?}", p);
}
ClientboundGamePacket::SectionBlocksUpdate(p) => {
debug!("Got section blocks update packet {:?}", p);
let mut world = client.world.write();
for state in &p.states {
world.set_block_state(&(p.section_pos + state.pos.clone()), state.state);
}
}
ClientboundGamePacket::GameEvent(p) => {
debug!("Got game event packet {:?}", p);
}
ClientboundGamePacket::LevelParticles(p) => {
debug!("Got level particles packet {:?}", p);
}
ClientboundGamePacket::ServerData(p) => {
debug!("Got server data packet {:?}", p);
}
ClientboundGamePacket::SetEquipment(p) => {
debug!("Got set equipment packet {:?}", p);
}
ClientboundGamePacket::UpdateMobEffect(p) => {
debug!("Got update mob effect packet {:?}", p);
}
ClientboundGamePacket::AddExperienceOrb(_) => {}
ClientboundGamePacket::AwardStats(_) => {}
ClientboundGamePacket::BlockChangedAck(_) => {}
ClientboundGamePacket::BlockDestruction(_) => {}
ClientboundGamePacket::BlockEntityData(_) => {}
ClientboundGamePacket::BlockEvent(_) => {}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
ClientboundGamePacket::ContainerSetData(_) => {}
ClientboundGamePacket::ContainerSetSlot(_) => {}
ClientboundGamePacket::Cooldown(_) => {}
ClientboundGamePacket::CustomChatCompletions(_) => {}
ClientboundGamePacket::DeleteChat(_) => {}
ClientboundGamePacket::Explode(_) => {}
ClientboundGamePacket::ForgetLevelChunk(_) => {}
ClientboundGamePacket::HorseScreenOpen(_) => {}
ClientboundGamePacket::MapItemData(_) => {}
ClientboundGamePacket::MerchantOffers(_) => {}
ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {}
ClientboundGamePacket::OpenScreen(_) => {}
ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(_) => {}
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
ClientboundGamePacket::PlayerCombatEnd(_) => {}
ClientboundGamePacket::PlayerCombatEnter(_) => {}
ClientboundGamePacket::PlayerCombatKill(p) => {
debug!("Got player kill packet {:?}", p);
if client.entity() == EntityId(p.player_id) {
// we can't define a variable here with client.dead.lock()
// because of https://github.com/rust-lang/rust/issues/57478
if !*client.dead.lock() {
*client.dead.lock() = true;
tx.send(Event::Death(Some(Arc::new(p.clone())))).await?;
}
}
}
ClientboundGamePacket::PlayerLookAt(_) => {}
ClientboundGamePacket::RemoveMobEffect(_) => {}
ClientboundGamePacket::ResourcePack(_) => {}
ClientboundGamePacket::Respawn(p) => {
debug!("Got respawn packet {:?}", p);
// Sets clients dead state to false.
let mut dead_lock = client.dead.lock();
*dead_lock = false;
}
ClientboundGamePacket::SelectAdvancementsTab(_) => {}
ClientboundGamePacket::SetActionBarText(_) => {}
ClientboundGamePacket::SetBorderCenter(_) => {}
ClientboundGamePacket::SetBorderLerpSize(_) => {}
ClientboundGamePacket::SetBorderSize(_) => {}
ClientboundGamePacket::SetBorderWarningDelay(_) => {}
ClientboundGamePacket::SetBorderWarningDistance(_) => {}
ClientboundGamePacket::SetCamera(_) => {}
ClientboundGamePacket::SetDisplayObjective(_) => {}
ClientboundGamePacket::SetObjective(_) => {}
ClientboundGamePacket::SetPassengers(_) => {}
ClientboundGamePacket::SetPlayerTeam(_) => {}
ClientboundGamePacket::SetScore(_) => {}
ClientboundGamePacket::SetSimulationDistance(_) => {}
ClientboundGamePacket::SetSubtitleText(_) => {}
ClientboundGamePacket::SetTitleText(_) => {}
ClientboundGamePacket::SetTitlesAnimation(_) => {}
ClientboundGamePacket::SoundEntity(_) => {}
ClientboundGamePacket::StopSound(_) => {}
ClientboundGamePacket::TabList(_) => {}
ClientboundGamePacket::TagQuery(_) => {}
ClientboundGamePacket::TakeItemEntity(_) => {}
ClientboundGamePacket::DisguisedChat(_) => {}
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
ClientboundGamePacket::ContainerClose(_) => {}
}
Ok(())
}
/// Start the game tick loop for every client in the shared world. This
/// should only be run once per shared world!
async fn game_tick_loop(ecs: Arc<Mutex<bevy_ecs::world::World>>) {
let mut game_tick_interval = time::interval(time::Duration::from_millis(50));
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
let mut schedule = Schedule::default();
#[derive(StageLabel)]
pub struct Tick;
schedule.add_stage(
Tick,
SystemStage::single_threaded()
.with_system(LocalPlayer::update_in_loaded_chunk)
.with_system(LocalPlayer::send_position)
.with_system(LocalPlayer::ai_step),
);
loop {
game_tick_interval.tick().await;
schedule.run(&mut ecs.lock());
}
}
/// Get a reference to our (potentially shared) world.
///
/// This gets the [`WeakWorld`] from our world container. If it's a normal
/// client, then it'll be the same as the world the client has loaded.
/// If the client using a shared world, then the shared world will be a
/// superset of the client's world.
pub fn world(&self) -> Arc<World> {
self.world.read().shared.clone()
}
pub fn entity(&self) -> EntityId {
*self.entity_id.read()
pub fn world(&self) -> Arc<RwLock<World>> {
let world_name = self
.local_player()
.world_name
.expect("World name must be known if we're doing Client::world");
let world_container = self.world_container.read();
world_container.get(&world_name).unwrap()
}
/// Query data of our player's entity.
pub fn query<'w, 's, Q: WorldQuery>(&'w self) -> <Q as WorldQuery>::Item<'_> {
let mut ecs = &mut self.world_container.write().ecs.lock();
let mut ecs = &mut self.ecs.lock();
QueryState::<Q>::new(ecs)
.get_mut(ecs, self.entity().into())
.get_mut(ecs, self.entity)
.expect("Player entity should always exist when being queried")
}
/// 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.world_name.read().is_some()
self.local_player().world_name.is_some()
}
/// Tell the server we changed our game options (i.e. render distance, main
@ -1078,3 +466,55 @@ impl Client {
Ok(())
}
}
/// Start the protocol and game tick loop.
#[doc(hidden)]
pub async fn start_ecs(
run_schedule_receiver: mpsc::UnboundedReceiver<()>,
) -> Arc<Mutex<bevy_ecs::world::World>> {
// if you get an error right here that means you're doing something with locks
// wrong read the error to see where the issue is
// you might be able to just drop the lock or put it in its own scope to fix
let mut app = App::new();
app.add_fixed_timestep(Duration::from_millis(50), "tick");
app.add_fixed_timestep_system("tick", 0, send_position);
// .add_system(LocalPlayer::update_in_loaded_chunk)
// .add_system(send_position)
// .add_system(LocalPlayer::ai_step);
app.add_system(packet_handling::handle_packets.label("handle_packets"));
// should happen last
app.add_system(packet_handling::clear_packets.after("handle_packets"));
// all resources should have been added by now so we can take the ecs from the
// app
let ecs = Arc::new(Mutex::new(app.world));
let mut game_tick_interval = time::interval(time::Duration::from_millis(50));
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
tokio::spawn(run_schedule_loop(
ecs.clone(),
app.schedule,
run_schedule_receiver,
));
ecs
}
async fn run_schedule_loop(
ecs: Arc<Mutex<bevy_ecs::world::World>>,
mut schedule: Schedule,
mut run_schedule_receiver: mpsc::UnboundedReceiver<()>,
) {
loop {
// whenever we get an event from run_schedule_receiver, run the schedule
run_schedule_receiver.recv().await;
schedule.run(&mut ecs.lock());
}
}

View file

@ -16,6 +16,7 @@ mod client;
mod get_mc_dir;
mod local_player;
mod movement;
pub mod packet_handling;
pub mod ping;
mod player;
mod plugins;

View file

@ -10,8 +10,8 @@ use azalea_protocol::{
},
};
use azalea_world::{
entity::{self, metadata::PlayerMetadataBundle, EntityId},
PartialWorld, WeakWorldContainer,
entity::{self, metadata::PlayerMetadataBundle, Entity, EntityId},
EntityInfos, PartialWorld, World, WorldContainer,
};
use bevy_ecs::{
component::Component,
@ -32,6 +32,7 @@ pub struct LocalPlayer {
pub profile: GameProfile,
pub write_conn: WriteConnection<ServerboundGamePacket>,
// pub world: Arc<RwLock<PartialWorld>>,
pub physics_state: PhysicsState,
pub client_information: ClientInformation,
@ -39,7 +40,8 @@ pub struct LocalPlayer {
/// A map of player uuids to their information in the tab list
pub players: HashMap<Uuid, PlayerInfo>,
pub world: Arc<RwLock<PartialWorld>>,
pub partial_world: Arc<RwLock<PartialWorld>>,
pub world: Arc<RwLock<World>>,
pub world_name: Option<ResourceLocation>,
pub tx: mpsc::Sender<Event>,
@ -66,28 +68,39 @@ pub struct PhysicsState {
pub struct LocalPlayerInLoadedChunk;
impl LocalPlayer {
/// Create a new client from the given GameProfile, Connection, and World.
/// Create a new `LocalPlayer`.
///
/// You should only use this if you want to change these fields from the
/// defaults, otherwise use [`Client::join`].
pub fn new(
entity: Entity,
profile: GameProfile,
write_conn: WriteConnection<ServerboundGamePacket>,
world: Arc<RwLock<PartialWorld>>,
world: Arc<RwLock<World>>,
entity_infos: &mut EntityInfos,
tx: mpsc::Sender<Event>,
) -> Self {
let client_information = ClientInformation::default();
LocalPlayer {
profile,
write_conn,
physics_state: PhysicsState::default(),
client_information: ClientInformation::default(),
dead: false,
players: HashMap::new(),
world,
world_name: Arc::new(RwLock::new(None)),
partial_world: Arc::new(RwLock::new(PartialWorld::new(
client_information.view_distance.into(),
Some(entity),
entity_infos,
))),
world_name: None,
tx,
world_name: None,
}
}

View file

@ -1,5 +1,5 @@
use crate::client::{Client, LocalPlayerInLoadedChunk};
use crate::{LocalPlayer, PhysicsState};
use crate::client::Client;
use crate::local_player::{LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState};
use azalea_core::Vec3;
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
use azalea_protocol::packets::game::{
@ -8,7 +8,7 @@ use azalea_protocol::packets::game::{
serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket,
serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket,
};
use azalea_world::entity::EcsEntityId;
use azalea_world::entity::{Entity, MinecraftEntityId};
use azalea_world::{entity, MoveEntityError};
use bevy_ecs::system::Query;
use std::backtrace::Backtrace;
@ -60,117 +60,118 @@ impl Client {
}
}
impl LocalPlayer {
/// This gets called automatically every tick.
pub(crate) fn send_position(
mut query: Query<
(
EcsEntityId,
&LocalPlayer,
&entity::Position,
&mut entity::Physics,
&entity::metadata::Sprinting,
),
&LocalPlayerInLoadedChunk,
>,
) {
for (entity, local_player, position, physics, sprinting) in &query {
local_player.send_sprinting_if_needed(
entity.into(),
sprinting,
&mut local_player.physics_state,
);
pub(crate) fn send_position(
mut query: Query<
(
Entity,
&MinecraftEntityId,
&LocalPlayer,
&entity::Position,
&entity::LastSentPosition,
&mut entity::Physics,
&entity::metadata::Sprinting,
),
&LocalPlayerInLoadedChunk,
>,
) {
for (entity, id, local_player, position, last_sent_position, physics, sprinting) in &query {
local_player.send_sprinting_if_needed(
entity.into(),
id,
sprinting,
&mut local_player.physics_state,
);
let packet = {
// TODO: the camera being able to be controlled by other entities isn't
// implemented yet if !self.is_controlled_camera() { return };
let packet = {
// TODO: the camera being able to be controlled by other entities isn't
// implemented yet if !self.is_controlled_camera() { return };
let old_position = physics.last_pos;
let x_delta = position.x - last_sent_position.x;
let y_delta = position.y - last_sent_position.y;
let z_delta = position.z - last_sent_position.z;
let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
let x_delta = position.x - old_position.x;
let y_delta = position.y - old_position.y;
let z_delta = position.z - old_position.z;
let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
local_player.physics_state.position_remainder += 1;
local_player.physics_state.position_remainder += 1;
// boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
// Mth.square(2.0E-4D) || this.positionReminder >= 20;
let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
> 2.0e-4f64.powi(2))
|| local_player.physics_state.position_remainder >= 20;
let sending_rotation = y_rot_delta != 0.0 || x_rot_delta != 0.0;
// boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
// Mth.square(2.0E-4D) || this.positionReminder >= 20;
let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
> 2.0e-4f64.powi(2))
|| local_player.physics_state.position_remainder >= 20;
let sending_rotation = y_rot_delta != 0.0 || x_rot_delta != 0.0;
// if self.is_passenger() {
// TODO: posrot packet for being a passenger
// }
let packet = if sending_position && sending_rotation {
Some(
ServerboundMovePlayerPosRotPacket {
x: position.x,
y: position.y,
z: position.z,
x_rot: physics.x_rot,
y_rot: physics.y_rot,
on_ground: physics.on_ground,
}
.get(),
)
} else if sending_position {
Some(
ServerboundMovePlayerPosPacket {
x: position.x,
y: position.y,
z: position.z,
on_ground: physics.on_ground,
}
.get(),
)
} else if sending_rotation {
Some(
ServerboundMovePlayerRotPacket {
x_rot: physics.x_rot,
y_rot: physics.y_rot,
on_ground: physics.on_ground,
}
.get(),
)
} else if physics.last_on_ground != physics.on_ground {
Some(
ServerboundMovePlayerStatusOnlyPacket {
on_ground: physics.on_ground,
}
.get(),
)
} else {
None
};
if sending_position {
physics.last_pos = **position;
local_player.physics_state.position_remainder = 0;
}
if sending_rotation {
physics.y_rot_last = physics.y_rot;
physics.x_rot_last = physics.x_rot;
}
physics.last_on_ground = physics.on_ground;
// minecraft checks for autojump here, but also autojump is bad so
packet
// if self.is_passenger() {
// TODO: posrot packet for being a passenger
// }
let packet = if sending_position && sending_rotation {
Some(
ServerboundMovePlayerPosRotPacket {
x: position.x,
y: position.y,
z: position.z,
x_rot: physics.x_rot,
y_rot: physics.y_rot,
on_ground: physics.on_ground,
}
.get(),
)
} else if sending_position {
Some(
ServerboundMovePlayerPosPacket {
x: position.x,
y: position.y,
z: position.z,
on_ground: physics.on_ground,
}
.get(),
)
} else if sending_rotation {
Some(
ServerboundMovePlayerRotPacket {
x_rot: physics.x_rot,
y_rot: physics.y_rot,
on_ground: physics.on_ground,
}
.get(),
)
} else if physics.last_on_ground != physics.on_ground {
Some(
ServerboundMovePlayerStatusOnlyPacket {
on_ground: physics.on_ground,
}
.get(),
)
} else {
None
};
if let Some(packet) = packet {
tokio::spawn(local_player.write_packet(packet));
if sending_position {
**last_sent_position = **position;
local_player.physics_state.position_remainder = 0;
}
if sending_rotation {
physics.y_rot_last = physics.y_rot;
physics.x_rot_last = physics.x_rot;
}
physics.last_on_ground = physics.on_ground;
// minecraft checks for autojump here, but also autojump is bad so
packet
};
if let Some(packet) = packet {
local_player.write_packet(packet);
}
}
}
impl LocalPlayer {
fn send_sprinting_if_needed(
&mut self,
entity: entity::EntityId,
entity: Entity,
id: &MinecraftEntityId,
sprinting: &entity::metadata::Sprinting,
physics_state: &mut PhysicsState,
) {
@ -181,15 +182,13 @@ impl LocalPlayer {
} else {
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting
};
tokio::spawn(
self.write_packet(
ServerboundPlayerCommandPacket {
id: *entity,
action: sprinting_action,
data: 0,
}
.get(),
),
self.write_packet(
ServerboundPlayerCommandPacket {
id: **id,
action: sprinting_action,
data: 0,
}
.get(),
);
physics_state.was_sprinting = **sprinting;
}
@ -200,7 +199,7 @@ impl LocalPlayer {
pub fn ai_step(
query: Query<
(
EcsEntityId,
Entity,
&mut LocalPlayer,
&mut entity::Physics,
&mut entity::Position,
@ -251,8 +250,7 @@ impl LocalPlayer {
}
azalea_physics::ai_step(
ecs_entity_id.into(),
&local_player.world.read().shared,
&local_player.world.read(),
&mut physics,
&mut position,
&sprinting,

View file

@ -0,0 +1,616 @@
use std::sync::Arc;
use azalea_protocol::{connect::ReadConnection, packets::game::ClientboundGamePacket};
use bevy_ecs::{component::Component, prelude::Entity, query::Changed, system::Query};
use parking_lot::Mutex;
use tokio::sync::mpsc;
/// Something that receives packets from the server.
#[derive(Component, Clone)]
pub struct PacketReceiver {
pub packets: Arc<Mutex<Vec<ClientboundGamePacket>>>,
pub run_schedule_sender: mpsc::UnboundedSender<()>,
}
pub fn handle_packets(
query: Query<(Entity, &PacketReceiver), Changed<PacketReceiver>>,
// ecs: &mut bevy_ecs::world::World,
) {
for (entity, packet_events) in &query {
for packet in packet_events.packets.lock().iter() {
handle_packet(entity, packet);
}
}
}
pub fn handle_packet(entity: Entity, packet: &ClientboundGamePacket) {
// match packet {
// ClientboundGamePacket::Login(p) => {
// debug!("Got login packet");
// {
// // // write p into login.txt
// // std::io::Write::write_all(
// // &mut std::fs::File::create("login.txt").unwrap(),
// // format!("{:#?}", p).as_bytes(),
// // )
// // .unwrap();
// // TODO: have registry_holder be a struct because this sucks
// rn // best way would be to add serde support to
// azalea-nbt
// let registry_holder = p
// .registry_holder
// .as_compound()
// .expect("Registry holder is not a compound")
// .get("")
// .expect("No \"\" tag")
// .as_compound()
// .expect("\"\" tag is not a compound");
// let dimension_types = registry_holder
// .get("minecraft:dimension_type")
// .expect("No dimension_type tag")
// .as_compound()
// .expect("dimension_type is not a compound")
// .get("value")
// .expect("No dimension_type value")
// .as_list()
// .expect("dimension_type value is not a list");
// let dimension_type = dimension_types
// .iter()
// .find(|t| {
// t.as_compound()
// .expect("dimension_type value is not a compound")
// .get("name")
// .expect("No name tag")
// .as_string()
// .expect("name is not a string")
// == p.dimension_type.to_string()
// })
// .unwrap_or_else(|| panic!("No dimension_type with name
// {}", p.dimension_type)) .as_compound()
// .unwrap()
// .get("element")
// .expect("No element tag")
// .as_compound()
// .expect("element is not a compound");
// let height = (*dimension_type
// .get("height")
// .expect("No height tag")
// .as_int()
// .expect("height tag is not an int"))
// .try_into()
// .expect("height is not a u32");
// let min_y = *dimension_type
// .get("min_y")
// .expect("No min_y tag")
// .as_int()
// .expect("min_y tag is not an int");
// let world_name = p.dimension.clone();
// local_player.world_name = Some(world_name.clone());
// // add this world to the world_container (or don't if it's
// already there) let weak_world =
// world_container.insert(world_name, height, min_y); // set
// the loaded_world to an empty world // (when we add chunks
// or entities those will be in the world_container) let mut
// world_lock = local_player.world.write(); *world_lock =
// PartialWorld::new(
// local_player.client_information.view_distance.into(),
// weak_world, Some(EntityId(p.player_id)),
// );
// let player_bundle = entity::PlayerBundle {
// entity: entity::EntityBundle::new(
// local_player.profile.uuid,
// Vec3::default(),
// azalea_registry::EntityKind::Player,
// ),
// metadata: PlayerMetadataBundle::default(),
// };
// // let entity = EntityData::new(
// // client.profile.uuid,
// // Vec3::default(),
// // EntityMetadata::Player(metadata::Player::default()),
// // );
// // the first argument makes it so other entities don't update
// this entity in // a shared world
// world_lock.add_entity(EntityId(p.player_id), player_bundle);
// *client.entity_id.write() = EntityId(p.player_id);
// }
// // send the client information that we have set
// let client_information_packet: ClientInformation =
// client.local_player().client_information.clone();
// log::debug!(
// "Sending client information because login: {:?}",
// client_information_packet
// );
// client.write_packet(client_information_packet.get()).await?;
// // brand
// client
// .write_packet(
// ServerboundCustomPayloadPacket {
// identifier: ResourceLocation::new("brand").unwrap(),
// // they don't have to know :)
// data: "vanilla".into(),
// }
// .get(),
// )
// .await?;
// tx.send(Event::Login).await?;
// }
// ClientboundGamePacket::SetChunkCacheRadius(p) => {
// debug!("Got set chunk cache radius packet {:?}", p);
// }
// ClientboundGamePacket::CustomPayload(p) => {
// debug!("Got custom payload packet {:?}", p);
// }
// ClientboundGamePacket::ChangeDifficulty(p) => {
// debug!("Got difficulty packet {:?}", p);
// }
// ClientboundGamePacket::Commands(_p) => {
// debug!("Got declare commands packet");
// }
// ClientboundGamePacket::PlayerAbilities(p) => {
// debug!("Got player abilities packet {:?}", p);
// }
// ClientboundGamePacket::SetCarriedItem(p) => {
// debug!("Got set carried item packet {:?}", p);
// }
// ClientboundGamePacket::UpdateTags(_p) => {
// debug!("Got update tags packet");
// }
// ClientboundGamePacket::Disconnect(p) => {
// debug!("Got disconnect packet {:?}", p);
// client.disconnect().await?;
// }
// ClientboundGamePacket::UpdateRecipes(_p) => {
// debug!("Got update recipes packet");
// }
// ClientboundGamePacket::EntityEvent(_p) => {
// // debug!("Got entity event packet {:?}", p);
// }
// ClientboundGamePacket::Recipe(_p) => {
// debug!("Got recipe packet");
// }
// ClientboundGamePacket::PlayerPosition(p) => {
// // TODO: reply with teleport confirm
// debug!("Got player position packet {:?}", p);
// let (new_pos, y_rot, x_rot) = {
// let player_entity_id = *client.entity();
// let world = client.world();
// // let mut player_entity =
// world.entity_mut(player_entity_id).unwrap(); let (mut
// physics, position) = client.query::<(&mut
// entity::Physics, &mut entity::Position)>();
// let delta_movement = physics.delta;
// let is_x_relative = p.relative_arguments.x;
// let is_y_relative = p.relative_arguments.y;
// let is_z_relative = p.relative_arguments.z;
// let (delta_x, new_pos_x) = if is_x_relative {
// physics.last_pos.x += p.x;
// (delta_movement.x, position.x + p.x)
// } else {
// physics.last_pos.x = p.x;
// (0.0, p.x)
// };
// let (delta_y, new_pos_y) = if is_y_relative {
// physics.last_pos.y += p.y;
// (delta_movement.y, position.y + p.y)
// } else {
// physics.last_pos.y = p.y;
// (0.0, p.y)
// };
// let (delta_z, new_pos_z) = if is_z_relative {
// physics.last_pos.z += p.z;
// (delta_movement.z, position.z + p.z)
// } else {
// physics.last_pos.z = p.z;
// (0.0, p.z)
// };
// let mut y_rot = p.y_rot;
// let mut x_rot = p.x_rot;
// if p.relative_arguments.x_rot {
// x_rot += physics.x_rot;
// }
// if p.relative_arguments.y_rot {
// y_rot += physics.y_rot;
// }
// physics.delta = Vec3 {
// x: delta_x,
// y: delta_y,
// z: delta_z,
// };
// entity::set_rotation(physics.into_inner(), y_rot, x_rot);
// // TODO: minecraft sets "xo", "yo", and "zo" here but idk
// what that means // so investigate that ig
// let new_pos = Vec3 {
// x: new_pos_x,
// y: new_pos_y,
// z: new_pos_z,
// };
// world
// .set_entity_pos(
// player_entity_id,
// new_pos,
// position.into_inner(),
// physics.into_inner(),
// )
// .expect("The player entity should always exist");
// (new_pos, y_rot, x_rot)
// };
// client
// .write_packet(ServerboundAcceptTeleportationPacket { id: p.id
// }.get()) .await?;
// client
// .write_packet(
// ServerboundMovePlayerPosRotPacket {
// x: new_pos.x,
// y: new_pos.y,
// z: new_pos.z,
// y_rot,
// x_rot,
// // this is always false
// on_ground: false,
// }
// .get(),
// )
// .await?;
// }
// ClientboundGamePacket::PlayerInfoUpdate(p) => {
// debug!("Got player info packet {:?}", p);
// let mut events = Vec::new();
// {
// let mut players_lock = client.players.write();
// for updated_info in &p.entries {
// // add the new player maybe
// if p.actions.add_player {
// let player_info = PlayerInfo {
// profile: updated_info.profile.clone(),
// uuid: updated_info.profile.uuid,
// gamemode: updated_info.game_mode,
// latency: updated_info.latency,
// display_name: updated_info.display_name.clone(),
// };
// players_lock.insert(updated_info.profile.uuid,
// player_info.clone());
// events.push(Event::AddPlayer(player_info)); } else if
// let Some(info) = players_lock.get_mut(&updated_info.profile.uuid) {
// // `else if` because the block for add_player above
// // already sets all the fields
// if p.actions.update_game_mode {
// info.gamemode = updated_info.game_mode;
// }
// if p.actions.update_latency {
// info.latency = updated_info.latency;
// }
// if p.actions.update_display_name {
// info.display_name =
// updated_info.display_name.clone(); }
// events.push(Event::UpdatePlayer(info.clone()));
// } else {
// warn!(
// "Ignoring PlayerInfoUpdate for unknown player
// {}", updated_info.profile.uuid
// );
// }
// }
// }
// for event in events {
// tx.send(event).await?;
// }
// }
// ClientboundGamePacket::PlayerInfoRemove(p) => {
// let mut events = Vec::new();
// {
// let mut players_lock = client.players.write();
// for uuid in &p.profile_ids {
// if let Some(info) = players_lock.remove(uuid) {
// events.push(Event::RemovePlayer(info));
// }
// }
// }
// for event in events {
// tx.send(event).await?;
// }
// }
// ClientboundGamePacket::SetChunkCacheCenter(p) => {
// debug!("Got chunk cache center packet {:?}", p);
// client
// .world
// .write()
// .update_view_center(&ChunkPos::new(p.x, p.z));
// }
// ClientboundGamePacket::LevelChunkWithLight(p) => {
// // debug!("Got chunk with light packet {} {}", p.x, p.z);
// let pos = ChunkPos::new(p.x, p.z);
// // OPTIMIZATION: if we already know about the chunk from the
// // shared world (and not ourselves), then we don't need to
// // parse it again. This is only used when we have a shared
// // world, since we check that the chunk isn't currently owned
// // by this client.
// let shared_has_chunk =
// client.world.read().get_chunk(&pos).is_some(); let
// this_client_has_chunk =
// client.world.read().chunks.limited_get(&pos).is_some(); if
// shared_has_chunk && !this_client_has_chunk { trace!(
// "Skipping parsing chunk {:?} because we already know
// about it", pos
// );
// return Ok(());
// }
// // let chunk = Chunk::read_with_world_height(&mut p.chunk_data);
// // debug("chunk {:?}")
// if let Err(e) = client
// .world
// .write()
// .replace_with_packet_data(&pos, &mut
// Cursor::new(&p.chunk_data.data)) {
// error!("Couldn't set chunk data: {}", e);
// }
// }
// ClientboundGamePacket::LightUpdate(_p) => {
// // debug!("Got light update packet {:?}", p);
// }
// ClientboundGamePacket::AddEntity(p) => {
// debug!("Got add entity packet {:?}", p);
// let bundle = p.as_entity_bundle();
// let mut world = client.world.write();
// world.add_entity(EntityId(p.id), bundle);
// // the bundle doesn't include the default entity metadata so we
// add that // separately
// let mut entities = world.entity_infos.shared.write();
// let mut entity =
// entities.ecs_entity_mut(EntityId(p.id)).unwrap();
// p.apply_metadata(&mut entity);
// }
// ClientboundGamePacket::SetEntityData(p) => {
// debug!("Got set entity data packet {:?}", p);
// let world = client.world.write();
// let mut entities = world.entity_infos.shared.write();
// let entity = entities.ecs_entity_mut(EntityId(p.id));
// if let Some(mut entity) = entity {
// entity::metadata::apply_metadata(&mut entity,
// p.packed_items.0.clone()); } else {
// // warn!("Server sent an entity data packet for an
// // entity id ({}) that we don't
// // know about", p.id);
// }
// }
// ClientboundGamePacket::UpdateAttributes(_p) => {
// // debug!("Got update attributes packet {:?}", p);
// }
// ClientboundGamePacket::SetEntityMotion(_p) => {
// // debug!("Got entity velocity packet {:?}", p);
// }
// ClientboundGamePacket::SetEntityLink(p) => {
// debug!("Got set entity link packet {:?}", p);
// }
// ClientboundGamePacket::AddPlayer(p) => {
// debug!("Got add player packet {:?}", p);
// let bundle = p.as_player_bundle();
// let mut world = client.world.write();
// world.add_entity(EntityId(p.id), bundle);
// // the default metadata was already included in the bundle
// // for us
// }
// ClientboundGamePacket::InitializeBorder(p) => {
// debug!("Got initialize border packet {:?}", p);
// }
// ClientboundGamePacket::SetTime(p) => {
// debug!("Got set time packet {:?}", p);
// }
// ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
// debug!("Got set default spawn position packet {:?}", p);
// }
// ClientboundGamePacket::ContainerSetContent(p) => {
// debug!("Got container set content packet {:?}", p);
// }
// ClientboundGamePacket::SetHealth(p) => {
// debug!("Got set health packet {:?}", p);
// if p.health == 0.0 {
// // we can't define a variable here with client.dead.lock()
// // because of https://github.com/rust-lang/rust/issues/57478
// if !*client.dead.lock() {
// *client.dead.lock() = true;
// tx.send(Event::Death(None)).await?;
// }
// }
// }
// ClientboundGamePacket::SetExperience(p) => {
// debug!("Got set experience packet {:?}", p);
// }
// ClientboundGamePacket::TeleportEntity(p) => {
// let mut world = client.world.write();
// let (pos, physics) = self.query::<(&entity::Position,
// &entity::Physics)>(); let _ = world.set_entity_pos(
// EntityId(p.id),
// Vec3 {
// x: p.x,
// y: p.y,
// z: p.z,
// },
// pos,
// physics,
// );
// }
// ClientboundGamePacket::UpdateAdvancements(p) => {
// debug!("Got update advancements packet {:?}", p);
// }
// ClientboundGamePacket::RotateHead(_p) => {
// // debug!("Got rotate head packet {:?}", p);
// }
// ClientboundGamePacket::MoveEntityPos(p) => {
// let mut world = client.world.write();
// let _ = world.move_entity_with_delta(EntityId(p.entity_id),
// &p.delta); }
// ClientboundGamePacket::MoveEntityPosRot(p) => {
// let mut world = client.world.write();
// let _ = world.move_entity_with_delta(EntityId(p.entity_id),
// &p.delta); }
// ClientboundGamePacket::MoveEntityRot(_p) => {
// // debug!("Got move entity rot packet {:?}", p);
// }
// ClientboundGamePacket::KeepAlive(p) => {
// debug!("Got keep alive packet {:?}", p);
// client
// .write_packet(ServerboundKeepAlivePacket { id: p.id }.get())
// .await?;
// }
// ClientboundGamePacket::RemoveEntities(p) => {
// debug!("Got remove entities packet {:?}", p);
// }
// ClientboundGamePacket::PlayerChat(p) => {
// debug!("Got player chat packet {:?}", p);
// tx.send(Event::Chat(ChatPacket::Player(Arc::new(p.clone()))))
// .await?;
// }
// ClientboundGamePacket::SystemChat(p) => {
// debug!("Got system chat packet {:?}", p);
// tx.send(Event::Chat(ChatPacket::System(Arc::new(p.clone()))))
// .await?;
// }
// ClientboundGamePacket::Sound(_p) => {
// // debug!("Got sound packet {:?}", p);
// }
// ClientboundGamePacket::LevelEvent(p) => {
// debug!("Got level event packet {:?}", p);
// }
// ClientboundGamePacket::BlockUpdate(p) => {
// debug!("Got block update packet {:?}", p);
// let mut world = client.world.write();
// world.set_block_state(&p.pos, p.block_state);
// }
// ClientboundGamePacket::Animate(p) => {
// debug!("Got animate packet {:?}", p);
// }
// ClientboundGamePacket::SectionBlocksUpdate(p) => {
// debug!("Got section blocks update packet {:?}", p);
// let mut world = client.world.write();
// for state in &p.states {
// world.set_block_state(&(p.section_pos + state.pos.clone()),
// state.state); }
// }
// ClientboundGamePacket::GameEvent(p) => {
// debug!("Got game event packet {:?}", p);
// }
// ClientboundGamePacket::LevelParticles(p) => {
// debug!("Got level particles packet {:?}", p);
// }
// ClientboundGamePacket::ServerData(p) => {
// debug!("Got server data packet {:?}", p);
// }
// ClientboundGamePacket::SetEquipment(p) => {
// debug!("Got set equipment packet {:?}", p);
// }
// ClientboundGamePacket::UpdateMobEffect(p) => {
// debug!("Got update mob effect packet {:?}", p);
// }
// ClientboundGamePacket::AddExperienceOrb(_) => {}
// ClientboundGamePacket::AwardStats(_) => {}
// ClientboundGamePacket::BlockChangedAck(_) => {}
// ClientboundGamePacket::BlockDestruction(_) => {}
// ClientboundGamePacket::BlockEntityData(_) => {}
// ClientboundGamePacket::BlockEvent(_) => {}
// ClientboundGamePacket::BossEvent(_) => {}
// ClientboundGamePacket::CommandSuggestions(_) => {}
// ClientboundGamePacket::ContainerSetData(_) => {}
// ClientboundGamePacket::ContainerSetSlot(_) => {}
// ClientboundGamePacket::Cooldown(_) => {}
// ClientboundGamePacket::CustomChatCompletions(_) => {}
// ClientboundGamePacket::DeleteChat(_) => {}
// ClientboundGamePacket::Explode(_) => {}
// ClientboundGamePacket::ForgetLevelChunk(_) => {}
// ClientboundGamePacket::HorseScreenOpen(_) => {}
// ClientboundGamePacket::MapItemData(_) => {}
// ClientboundGamePacket::MerchantOffers(_) => {}
// ClientboundGamePacket::MoveVehicle(_) => {}
// ClientboundGamePacket::OpenBook(_) => {}
// ClientboundGamePacket::OpenScreen(_) => {}
// ClientboundGamePacket::OpenSignEditor(_) => {}
// ClientboundGamePacket::Ping(_) => {}
// ClientboundGamePacket::PlaceGhostRecipe(_) => {}
// ClientboundGamePacket::PlayerCombatEnd(_) => {}
// ClientboundGamePacket::PlayerCombatEnter(_) => {}
// ClientboundGamePacket::PlayerCombatKill(p) => {
// debug!("Got player kill packet {:?}", p);
// if client.entity() == EntityId(p.player_id) {
// // we can't define a variable here with client.dead.lock()
// // because of https://github.com/rust-lang/rust/issues/57478
// if !*client.dead.lock() {
// *client.dead.lock() = true;
// tx.send(Event::Death(Some(Arc::new(p.clone())))).await?;
// }
// }
// }
// ClientboundGamePacket::PlayerLookAt(_) => {}
// ClientboundGamePacket::RemoveMobEffect(_) => {}
// ClientboundGamePacket::ResourcePack(_) => {}
// ClientboundGamePacket::Respawn(p) => {
// debug!("Got respawn packet {:?}", p);
// // Sets clients dead state to false.
// let mut dead_lock = client.dead.lock();
// *dead_lock = false;
// }
// ClientboundGamePacket::SelectAdvancementsTab(_) => {}
// ClientboundGamePacket::SetActionBarText(_) => {}
// ClientboundGamePacket::SetBorderCenter(_) => {}
// ClientboundGamePacket::SetBorderLerpSize(_) => {}
// ClientboundGamePacket::SetBorderSize(_) => {}
// ClientboundGamePacket::SetBorderWarningDelay(_) => {}
// ClientboundGamePacket::SetBorderWarningDistance(_) => {}
// ClientboundGamePacket::SetCamera(_) => {}
// ClientboundGamePacket::SetDisplayObjective(_) => {}
// ClientboundGamePacket::SetObjective(_) => {}
// ClientboundGamePacket::SetPassengers(_) => {}
// ClientboundGamePacket::SetPlayerTeam(_) => {}
// ClientboundGamePacket::SetScore(_) => {}
// ClientboundGamePacket::SetSimulationDistance(_) => {}
// ClientboundGamePacket::SetSubtitleText(_) => {}
// ClientboundGamePacket::SetTitleText(_) => {}
// ClientboundGamePacket::SetTitlesAnimation(_) => {}
// ClientboundGamePacket::SoundEntity(_) => {}
// ClientboundGamePacket::StopSound(_) => {}
// ClientboundGamePacket::TabList(_) => {}
// ClientboundGamePacket::TagQuery(_) => {}
// ClientboundGamePacket::TakeItemEntity(_) => {}
// ClientboundGamePacket::DisguisedChat(_) => {}
// ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
// ClientboundGamePacket::ContainerClose(_) => {}
// }
}
/// A system that clears all packets in the clientbound packet events.
pub fn clear_packets(mut query: Query<&mut PacketReceiver>) {
for mut packets in query.iter_mut() {
packets.packets.lock().clear();
}
}
impl PacketReceiver {
/// Loop that reads from the connection and adds the packets to the queue +
/// runs the schedule.
pub async fn read_task(&self, mut read_conn: ReadConnection<ClientboundGamePacket>) {
while let Ok(packet) = read_conn.read().await {
self.packets.lock().push(packet);
// tell the client to run all the systems
self.run_schedule_sender.send(());
}
}
}

View file

@ -6,13 +6,12 @@ mod world_collisions;
use azalea_core::{Axis, Vec3, AABB, EPSILON};
use azalea_world::{
entity::{self, EntityId},
entity::{self},
MoveEntityError, World,
};
pub use blocks::BlockWithShape;
pub use discrete_voxel_shape::*;
pub use shape::*;
use std::ops::Deref;
use self::world_collisions::get_block_collisions;
@ -74,7 +73,6 @@ fn collide(movement: &Vec3, world: &World, physics: &entity::Physics) -> Vec3 {
pub fn move_colliding(
_mover_type: &MoverType,
movement: &Vec3,
entity_id: EntityId,
world: &World,
position: &mut entity::Position,
physics: &mut entity::Physics,
@ -100,7 +98,7 @@ pub fn move_colliding(
// movement = this.maybeBackOffFromEdge(movement, moverType);
let collide_result = { collide(movement, world, physics) };
let collide_result = collide(movement, world, physics);
let move_distance = collide_result.length_sqr();
@ -115,7 +113,7 @@ pub fn move_colliding(
}
};
world.set_entity_pos(entity_id, new_pos, position, physics);
**position = new_pos;
}
let x_collision = movement.x != collide_result.x;
@ -127,7 +125,7 @@ pub fn move_colliding(
// TODO: minecraft checks for a "minor" horizontal collision here
let _block_pos_below = entity::on_pos_legacy(world, position, physics);
let _block_pos_below = entity::on_pos_legacy(&world.chunks, position, physics);
// let _block_state_below = self
// .world
// .get_block_state(&block_pos_below)

View file

@ -2,7 +2,7 @@ use super::Shapes;
use crate::collision::{BlockWithShape, VoxelShape, AABB};
use azalea_block::BlockState;
use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON};
use azalea_world::{entity::EntityId, Chunk, World};
use azalea_world::{Chunk, World};
use parking_lot::RwLock;
use std::sync::Arc;
@ -57,7 +57,7 @@ impl<'a> BlockCollisions<'a> {
// return var7;
// }
self.world.get_chunk(&chunk_pos)
self.world.chunks.get(&chunk_pos)
}
}
@ -79,7 +79,7 @@ impl<'a> Iterator for BlockCollisions<'a> {
let pos = item.pos;
let block_state: BlockState = chunk
.read()
.get(&(&pos).into(), self.world.min_y())
.get(&(&pos).into(), self.world.chunks.min_y)
.unwrap_or(BlockState::Air);
// TODO: continue if self.only_suffocating_blocks and the block is not

View file

@ -5,16 +5,14 @@ pub mod collision;
use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3};
use azalea_world::{
entity::{self, EntityId},
entity::{self},
World,
};
use collision::{move_colliding, MoverType};
use std::ops::Deref;
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
fn travel(
entity_id: EntityId,
world: &World,
physics: &mut entity::Physics,
position: &mut entity::Position,
@ -38,6 +36,7 @@ fn travel(
let block_pos_below = get_block_pos_below_that_affects_movement(position);
let block_state_below = world
.chunks
.get_block_state(&block_pos_below)
.unwrap_or(BlockState::Air);
let block_below: Box<dyn Block> = block_state_below.into();
@ -53,7 +52,6 @@ fn travel(
let mut movement = handle_relative_friction_and_calculate_movement(
acceleration,
block_friction,
entity_id,
world,
physics,
position,
@ -83,7 +81,6 @@ fn travel(
/// applies air resistance, calls self.travel(), and some other random
/// stuff.
pub fn ai_step(
entity_id: EntityId,
world: &World,
physics: &mut entity::Physics,
position: &mut entity::Position,
@ -115,7 +112,6 @@ pub fn ai_step(
physics.zza *= 0.98;
travel(
entity_id,
world,
physics,
position,
@ -168,7 +164,6 @@ fn get_block_pos_below_that_affects_movement(position: &entity::Position) -> Blo
fn handle_relative_friction_and_calculate_movement(
acceleration: &Vec3,
block_friction: f32,
entity_id: EntityId,
world: &World,
physics: &mut entity::Physics,
position: &mut entity::Position,
@ -183,7 +178,6 @@ fn handle_relative_friction_and_calculate_movement(
move_colliding(
&MoverType::Own,
&physics.delta.clone(),
entity_id,
world,
position,
physics,
@ -221,8 +215,10 @@ fn get_friction_influenced_speed(
/// Returns the what the entity's jump should be multiplied by based on the
/// block they're standing on.
fn block_jump_factor(world: &World, position: &entity::Position) -> f32 {
let block_at_pos = world.get_block_state(&position.into());
let block_below = world.get_block_state(&get_block_pos_below_that_affects_movement(position));
let block_at_pos = world.chunks.get_block_state(&position.into());
let block_below = world
.chunks
.get_block_state(&get_block_pos_below_that_affects_movement(position));
let block_at_pos_jump_factor = if let Some(block) = block_at_pos {
Box::<dyn Block>::from(block).behavior().jump_factor

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf;
use azalea_core::Vec3;
use azalea_core::{ResourceLocation, Vec3};
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::{
metadata::{apply_default_metadata, PlayerMetadataBundle, UpdateMetadataError},
@ -41,8 +41,11 @@ pub struct ClientboundAddEntityPacket {
// }
impl ClientboundAddEntityPacket {
pub fn as_entity_bundle(&self) -> EntityBundle {
EntityBundle::new(self.uuid, self.position, self.entity_type)
/// Make the entity into a bundle that can be inserted into the ECS. You
/// must apply the metadata after inserting the bundle with
/// [`Self::apply_metadata`].
pub fn as_entity_bundle(&self, world_name: ResourceLocation) -> EntityBundle {
EntityBundle::new(self.uuid, self.position, self.entity_type, world_name)
}
pub fn apply_metadata(&self, entity: &mut bevy_ecs::world::EntityMut) {

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf;
use azalea_core::Vec3;
use azalea_core::{ResourceLocation, Vec3};
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_registry::EntityKind;
use azalea_world::entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle};
@ -18,9 +18,9 @@ pub struct ClientboundAddPlayerPacket {
}
impl ClientboundAddPlayerPacket {
pub fn as_player_bundle(&self) -> PlayerBundle {
pub fn as_player_bundle(&self, world_name: ResourceLocation) -> PlayerBundle {
PlayerBundle {
entity: EntityBundle::new(self.uuid, self.position, EntityKind::Player),
entity: EntityBundle::new(self.uuid, self.position, EntityKind::Player, world_name),
metadata: PlayerMetadataBundle::default(),
}
}

View file

@ -15,13 +15,8 @@ use std::{
const SECTION_HEIGHT: u32 = 16;
/// An efficient storage of chunks for a client that has a limited render
/// distance. This has support for using a shared [`WeakChunkStorage`]. If you
/// have an infinite render distance (like a server), you should use
/// [`ChunkStorage`] instead.
/// distance. This has support for using a shared [`ChunkStorage`].
pub struct PartialChunkStorage {
/// Chunk storage that can be shared by clients.
pub shared: Arc<RwLock<WeakChunkStorage>>,
pub view_center: ChunkPos,
chunk_radius: u32,
view_range: u32,
@ -33,23 +28,16 @@ pub struct PartialChunkStorage {
/// actively being used somewhere else they'll be forgotten. This is used for
/// shared worlds.
#[derive(Debug)]
pub struct WeakChunkStorage {
pub struct ChunkStorage {
pub height: u32,
pub min_y: i32,
pub chunks: HashMap<ChunkPos, Weak<RwLock<Chunk>>>,
}
/// A storage of potentially infinite chunks in a world. Chunks are stored as
/// an `Arc<Mutex>` so they can be shared across threads.
pub struct ChunkStorage {
pub height: u32,
pub min_y: i32,
pub chunks: HashMap<ChunkPos, Arc<RwLock<Chunk>>>,
}
/// A single chunk in a world (16*?*16 blocks). This only contains the blocks
/// and biomes. You can derive the height of the chunk from the number of
/// sections, but you need a [`ChunkStorage`] to get the minimum Y coordinate.
/// sections, but you need a [`ChunkStorage`] to get the minimum Y
/// coordinate.
#[derive(Debug)]
pub struct Chunk {
pub sections: Vec<Section>,
@ -82,10 +70,9 @@ impl Default for Chunk {
}
impl PartialChunkStorage {
pub fn new(chunk_radius: u32, shared: Arc<RwLock<WeakChunkStorage>>) -> Self {
pub fn new(chunk_radius: u32) -> Self {
let view_range = chunk_radius * 2 + 1;
PartialChunkStorage {
shared,
view_center: ChunkPos::new(0, 0),
chunk_radius,
view_range,
@ -93,13 +80,6 @@ impl PartialChunkStorage {
}
}
pub fn min_y(&self) -> i32 {
self.shared.read().min_y
}
pub fn height(&self) -> u32 {
self.shared.read().height
}
fn get_index(&self, chunk_pos: &ChunkPos) -> usize {
(i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32)
+ i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize
@ -110,20 +90,28 @@ impl PartialChunkStorage {
&& (chunk_pos.z - self.view_center.z).unsigned_abs() <= self.chunk_radius
}
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) {
pub fn set_block_state(
&self,
pos: &BlockPos,
state: BlockState,
chunk_storage: &mut ChunkStorage,
) -> Option<BlockState> {
if pos.y < chunk_storage.min_y
|| pos.y >= (chunk_storage.min_y + chunk_storage.height as i32)
{
return None;
}
let chunk_pos = ChunkPos::from(pos);
let chunk = self.get(&chunk_pos)?;
let mut chunk = chunk.write();
Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y()))
let chunk_lock = chunk_storage.get(&chunk_pos)?;
let mut chunk = chunk_lock.write();
Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, chunk_storage.min_y))
}
pub fn replace_with_packet_data(
&mut self,
pos: &ChunkPos,
data: &mut Cursor<&[u8]>,
chunk_storage: &mut ChunkStorage,
) -> Result<(), BufReadError> {
debug!("Replacing chunk at {:?}", pos);
if !self.in_range(pos) {
@ -137,11 +125,11 @@ impl PartialChunkStorage {
let chunk = Arc::new(RwLock::new(Chunk::read_with_dimension_height(
data,
self.height(),
chunk_storage.height,
)?));
trace!("Loaded chunk {:?}", pos);
self.set(pos, Some(chunk));
self.set(pos, Some(chunk), chunk_storage);
Ok(())
}
@ -172,26 +160,19 @@ impl PartialChunkStorage {
Some(&mut self.chunks[index])
}
/// Get a chunk,
pub fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
self.shared
.read()
.chunks
.get(pos)
.and_then(|chunk| chunk.upgrade())
}
/// Set a chunk in the shared storage and reference it from the limited
/// storage.
///
/// # Panics
/// If the chunk is not in the render distance.
pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) {
pub fn set(
&mut self,
pos: &ChunkPos,
chunk: Option<Arc<RwLock<Chunk>>>,
chunk_storage: &mut ChunkStorage,
) {
if let Some(chunk) = &chunk {
self.shared
.write()
.chunks
.insert(*pos, Arc::downgrade(chunk));
chunk_storage.chunks.insert(*pos, Arc::downgrade(chunk));
} else {
// don't remove it from the shared storage, since it'll be removed
// automatically if this was the last reference
@ -201,9 +182,9 @@ impl PartialChunkStorage {
}
}
}
impl WeakChunkStorage {
impl ChunkStorage {
pub fn new(height: u32, min_y: i32) -> Self {
WeakChunkStorage {
ChunkStorage {
height,
min_y,
chunks: HashMap::new(),
@ -295,12 +276,10 @@ impl McBufWritable for Chunk {
impl Debug for PartialChunkStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChunkStorage")
f.debug_struct("PartialChunkStorage")
.field("view_center", &self.view_center)
.field("chunk_radius", &self.chunk_radius)
.field("view_range", &self.view_range)
.field("height", &self.height())
.field("min_y", &self.min_y())
// .field("chunks", &self.chunks)
.field("chunks", &format_args!("{} items", self.chunks.len()))
.finish()
@ -373,10 +352,10 @@ impl Section {
impl Default for PartialChunkStorage {
fn default() -> Self {
Self::new(8, Arc::new(RwLock::new(WeakChunkStorage::default())))
Self::new(8)
}
}
impl Default for WeakChunkStorage {
impl Default for ChunkStorage {
fn default() -> Self {
Self::new(384, -64)
}
@ -408,34 +387,26 @@ mod tests {
#[test]
fn test_out_of_bounds_y() {
let mut chunk_storage = PartialChunkStorage::default();
chunk_storage.set(
let mut chunk_storage = ChunkStorage::default();
let mut partial_chunk_storage = PartialChunkStorage::default();
partial_chunk_storage.set(
&ChunkPos { x: 0, z: 0 },
Some(Arc::new(RwLock::new(Chunk::default()))),
&mut chunk_storage,
);
assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: 319, z: 0 })
.is_some());
assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: 320, z: 0 })
.is_none());
assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: 338, z: 0 })
.is_none());
assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: -64, z: 0 })
.is_some());
assert!(chunk_storage
.shared
.read()
.get_block_state(&BlockPos { x: 0, y: -65, z: 0 })
.is_none());
}

View file

@ -1,18 +1,18 @@
use azalea_core::ResourceLocation;
use bevy_ecs::system::Resource;
use log::error;
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use crate::World;
use crate::{ChunkStorage, World};
/// A container of [`WeakWorld`]s. Worlds are stored as a Weak pointer here, so
/// A container of [`World`]s. Worlds are stored as a Weak pointer here, so
/// if no clients are using a world it will be forgotten.
#[derive(Default, Resource)]
pub struct WeakWorldContainer {
pub struct WorldContainer {
// We just refer to the chunks here and don't include entities because there's not that many
// cases where we'd want to get every entity in the world (just getting the entities in chunks
// should work fine).
@ -23,49 +23,52 @@ pub struct WeakWorldContainer {
// If it looks like we're relying on the server giving us unique world names, that's because we
// are. An evil server could give us two worlds with the same name and then we'd have no way of
// telling them apart. We hope most servers are nice and don't do that though. It's only an
// issue when there's multiple clients with the same WorldContainer in different worlds
// issue when there's multiple clients with the same WeakWorldContainer in different worlds
// anyways.
pub worlds: HashMap<ResourceLocation, WeakChunkStorage>,
/// The ECS world that contains all of the entities in all of the worlds.
pub ecs: Arc<Mutex<bevy_ecs::world::World>>,
pub worlds: HashMap<ResourceLocation, Weak<RwLock<World>>>,
}
impl WeakWorldContainer {
impl WorldContainer {
pub fn new() -> Self {
WeakWorldContainer {
WorldContainer {
worlds: HashMap::new(),
ecs: Arc::new(Mutex::new(bevy_ecs::world::World::new())),
}
}
/// Get a world from the container.
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<World>> {
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<RwLock<World>>> {
self.worlds.get(name).and_then(|world| world.upgrade())
}
/// Add an empty world to the container (or not if it already exists) and
/// returns a strong reference to the world.
#[must_use = "the world will be immediately forgotten if unused"]
pub fn insert(&mut self, name: ResourceLocation, height: u32, min_y: i32) -> Arc<World> {
if let Some(existing) = self.worlds.get(&name).and_then(|world| world.upgrade()) {
if existing.height() != height {
pub fn insert(
&mut self,
name: ResourceLocation,
height: u32,
min_y: i32,
) -> Arc<RwLock<World>> {
if let Some(existing_lock) = self.worlds.get(&name).and_then(|world| world.upgrade()) {
let existing = existing_lock.read();
if existing.chunks.height != height {
error!(
"Shared dimension height mismatch: {} != {}",
existing.height(),
height,
existing.chunks.height, height,
);
}
if existing.min_y() != min_y {
if existing.chunks.min_y != min_y {
error!(
"Shared world min_y mismatch: {} != {}",
existing.min_y(),
min_y,
existing.chunks.min_y, min_y,
);
}
existing
existing_lock.clone()
} else {
let world = Arc::new(World::new(height, min_y, self.ecs.clone()));
let world = Arc::new(RwLock::new(World {
chunks: ChunkStorage::new(height, min_y),
entities_by_chunk: HashMap::new(),
}));
self.worlds.insert(name, Arc::downgrade(&world));
world
}

View file

@ -3,17 +3,17 @@ mod data;
mod dimensions;
pub mod metadata;
use crate::ChunkStorage;
use self::{attributes::AttributeInstance, metadata::UpdateMetadataError};
pub use attributes::Attributes;
use azalea_block::BlockState;
use azalea_core::{BlockPos, ChunkPos, Vec3, AABB};
use bevy_ecs::{
bundle::Bundle, component::Component, query::Changed, system::Query, world::EntityMut,
};
use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB};
use bevy_ecs::{bundle::Bundle, component::Component, query::Changed, system::Query};
pub use data::*;
use derive_more::{Deref, DerefMut};
pub use dimensions::EntityDimensions;
use std::fmt::{Debug, Display, Formatter};
use std::fmt::Debug;
use uuid::Uuid;
/// A lightweight identifier of an entity.
@ -41,8 +41,8 @@ impl nohash_hasher::IsEnabled for MinecraftEntityId {}
///
/// # Safety
/// Cached position in the world must be updated.
pub fn update_bounding_box(query: Query<(&mut Position, &Physics), Changed<Position>>) {
for (mut position, physics) in query.iter_mut() {
pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed<Position>>) {
for (position, mut physics) in query.iter_mut() {
let bounding_box = physics.dimensions.make_bounding_box(&position);
physics.bounding_box = bounding_box;
}
@ -95,8 +95,12 @@ pub fn make_bounding_box(pos: &Position, physics: &Physics) -> AABB {
}
/// Get the position of the block below the entity, but a little lower.
pub fn on_pos_legacy(world: &World, position: &Position, physics: &Physics) -> BlockPos {
on_pos(world, position, physics, 0.2)
pub fn on_pos_legacy(
chunk_storage: &ChunkStorage,
position: &Position,
physics: &Physics,
) -> BlockPos {
on_pos(0.2, chunk_storage, position, physics)
}
// int x = Mth.floor(this.position.x);
@ -111,7 +115,12 @@ pub fn on_pos_legacy(world: &World, position: &Position, physics: &Physics) -> B
// }
// }
// return var5;
pub fn on_pos(world: &World, pos: &Position, physics: &Physics, offset: f32) -> BlockPos {
pub fn on_pos(
offset: f32,
chunk_storage: &ChunkStorage,
pos: &Position,
physics: &Physics,
) -> BlockPos {
let x = pos.x.floor() as i32;
let y = (pos.y - offset as f64).floor() as i32;
let z = pos.z.floor() as i32;
@ -119,10 +128,10 @@ pub fn on_pos(world: &World, pos: &Position, physics: &Physics, offset: f32) ->
// TODO: check if block below is a fence, wall, or fence gate
let block_pos = pos.down(1);
let block_state = world.get_block_state(&block_pos);
let block_state = chunk_storage.get_block_state(&block_pos);
if block_state == Some(BlockState::Air) {
let block_pos_below = block_pos.down(1);
let block_state_below = world.get_block_state(&block_pos_below);
let block_state_below = chunk_storage.get_block_state(&block_pos_below);
if let Some(_block_state_below) = block_state_below {
// if block_state_below.is_fence()
// || block_state_below.is_wall()
@ -154,30 +163,45 @@ impl From<Position> for BlockPos {
BlockPos::from(&value.0)
}
}
impl From<&Position> for ChunkPos {
fn from(value: &Position) -> Self {
ChunkPos::from(value.0)
}
}
impl From<&Position> for BlockPos {
fn from(value: &Position) -> Self {
BlockPos::from(value.0)
}
}
/// The position of the entity last tick.
/// The last position of the entity that was sent to the network.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct LastPosition(Vec3);
impl From<LastPosition> for ChunkPos {
fn from(value: LastPosition) -> Self {
pub struct LastSentPosition(Vec3);
impl From<LastSentPosition> for ChunkPos {
fn from(value: LastSentPosition) -> Self {
ChunkPos::from(&value.0)
}
}
impl From<LastPosition> for BlockPos {
fn from(value: LastPosition) -> Self {
impl From<LastSentPosition> for BlockPos {
fn from(value: LastSentPosition) -> Self {
BlockPos::from(&value.0)
}
}
/// Set the [`LastPosition`] component to the current [`Position`] component.
/// This should happen at the end of every tick.
pub fn update_last_position(
mut query: Query<(&mut Position, &mut LastPosition), Changed<Position>>,
) {
for (mut position, mut last_position) in query.iter_mut() {
*last_position = LastPosition(**position);
impl From<&LastSentPosition> for ChunkPos {
fn from(value: &LastSentPosition) -> Self {
ChunkPos::from(value.0)
}
}
impl From<&LastSentPosition> for BlockPos {
fn from(value: &LastSentPosition) -> Self {
BlockPos::from(value.0)
}
}
/// 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(ResourceLocation);
/// The physics data relating to the entity, such as position, velocity, and
/// bounding box.
@ -227,14 +251,20 @@ pub struct EntityKind(azalea_registry::EntityKind);
pub struct EntityBundle {
pub kind: EntityKind,
pub uuid: EntityUuid,
pub world_name: WorldName,
pub position: Position,
pub last_position: LastPosition,
pub last_sent_position: LastSentPosition,
pub physics: Physics,
pub attributes: Attributes,
}
impl EntityBundle {
pub fn new(uuid: Uuid, pos: Vec3, kind: azalea_registry::EntityKind) -> Self {
pub fn new(
uuid: Uuid,
pos: Vec3,
kind: azalea_registry::EntityKind,
world_name: ResourceLocation,
) -> Self {
let dimensions = EntityDimensions {
width: 0.6,
height: 1.8,
@ -243,8 +273,9 @@ impl EntityBundle {
Self {
kind: EntityKind(kind),
uuid: EntityUuid(uuid),
world_name: WorldName(world_name),
position: Position(pos),
last_position: Position(pos),
last_sent_position: LastSentPosition(pos),
physics: Physics {
delta: Vec3::default(),

View file

@ -1,20 +1,21 @@
use crate::entity::{self, Entity, EntityUuid, MinecraftEntityId, Position};
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
use crate::{
entity::{self, update_bounding_box, Entity, MinecraftEntityId},
MaybeRemovedEntity, World, WorldContainer,
};
use azalea_core::ChunkPos;
use bevy_app::{App, CoreStage, Plugin};
use bevy_ecs::{
query::{Changed, QueryEntityError, QueryState, ReadOnlyWorldQuery, WorldQuery},
query::Changed,
schedule::SystemSet,
system::{Query, ResMut, Resource},
world::{EntityMut, EntityRef},
system::{Query, Res, ResMut, Resource},
};
use log::warn;
use nohash_hasher::{IntMap, IntSet};
use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock};
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
sync::{Arc, Weak},
ops::DerefMut,
};
use uuid::Uuid;
@ -23,8 +24,11 @@ pub struct EntityPlugin;
impl Plugin for EntityPlugin {
fn build(&self, app: &mut App) {
app.add_system_set_to_stage(
CoreStage::Last,
SystemSet::new().with_system(update_entity_chunk_positions),
CoreStage::PostUpdate,
SystemSet::new()
.with_system(update_entity_chunk_positions)
.with_system(remove_despawned_entities_from_indexes)
.with_system(update_bounding_box),
);
}
}
@ -115,21 +119,25 @@ impl PartialEntityInfos {
/// we WILL update it if it's true. Don't call this unless you actually
/// got an entity update that all other clients within render distance
/// will get too.
pub fn maybe_update(&mut self, id: MinecraftEntityId, entity_infos: &mut EntityInfos) -> bool {
let this_client_updates_received = self.updates_received.get(&id).copied();
pub fn maybe_update(
&mut self,
entity: Entity,
id: &MinecraftEntityId,
entity_infos: &mut EntityInfos,
) -> bool {
if Some(entity) == self.owner_entity {
// the owner of the entity is always allowed to update it
return true;
};
let entity = entity_infos
.minecraft_entity_ids_to_azalea_entities
.get(&id)
.expect("entity should be in minecraft_entity_ids_to_azalea_entities")
.clone();
let this_client_updates_received = self.updates_received.get(&id).copied();
let shared_updates_received = entity_infos.updates_received.get(&entity).copied();
let can_update = this_client_updates_received == shared_updates_received;
if can_update {
let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
self.updates_received.insert(id, new_updates_received);
self.updates_received.insert(*id, new_updates_received);
entity_infos
.updates_received
.insert(entity, new_updates_received);
@ -140,10 +148,6 @@ impl PartialEntityInfos {
}
}
/// This is useful so functions that return an IntSet of entity ids can return a
/// reference to nothing.
static EMPTY_ENTITY_ID_INTSET: Lazy<HashSet<Entity>> = Lazy::new(|| HashSet::default());
// TODO: optimization: switch out the `HashMap<Entity, _>`s for `IntMap`s
/// Things that are shared between all the partial worlds.
@ -154,9 +158,6 @@ pub struct EntityInfos {
/// The number of `PartialWorld`s that have this entity loaded.
/// (this is reference counting)
pub(crate) entity_reference_count: HashMap<Entity, usize>,
/// An index of all the entities we know are in a chunk
pub(crate) entities_by_world_name_and_chunk:
HashMap<ResourceLocation, HashMap<ChunkPos, HashSet<Entity>>>,
/// An index of entities by their UUIDs
pub(crate) entity_by_uuid: HashMap<Uuid, Entity>,
@ -171,7 +172,6 @@ impl EntityInfos {
pub fn new() -> Self {
Self {
entity_reference_count: HashMap::default(),
entities_by_world_name_and_chunk: HashMap::default(),
entity_by_uuid: HashMap::default(),
updates_received: HashMap::default(),
@ -185,7 +185,13 @@ impl EntityInfos {
/// storage if there's no more references to it.
///
/// Returns whether the entity was removed.
pub fn remove_entity_if_unused(&mut self, entity: Entity, uuid: Uuid, chunk: ChunkPos) -> bool {
pub fn remove_entity_if_unused(
&mut self,
entity: Entity,
uuid: Uuid,
chunk: ChunkPos,
world: &mut World,
) -> bool {
if let Some(count) = self.entity_reference_count.get_mut(&entity) {
*count -= 1;
if *count == 0 {
@ -196,7 +202,7 @@ impl EntityInfos {
warn!("Tried to remove entity but it was not found.");
return false;
}
if self.entities_by_chunk.remove(&chunk).is_none() {
if world.entities_by_chunk.remove(&chunk).is_none() {
warn!("Tried to remove entity from chunk {chunk:?} but it was not found.");
}
if self.entity_by_uuid.remove(&uuid).is_none() {
@ -210,16 +216,6 @@ impl EntityInfos {
true
}
/// Remove a chunk from the storage if the entities in it have no strong
/// references left.
pub fn remove_chunk_if_unused(&mut self, chunk: &ChunkPos) {
if let Some(entities) = self.entities_by_chunk.get(chunk) {
if entities.is_empty() {
self.entities_by_chunk.remove(chunk);
}
}
}
/// Whether the entity is in the shared storage. To check if a Minecraft
/// entity ID is in the storage, you'll have to use
/// [`PartialEntityInfo::limited_contains_id`].
@ -228,13 +224,6 @@ impl EntityInfos {
self.entity_reference_count.contains_key(&id)
}
/// Returns a set of entities in the given chunk.
pub fn entities_in_chunk<F>(&self, chunk: &ChunkPos) -> &HashSet<Entity> {
self.entities_by_chunk
.get(chunk)
.unwrap_or(&EMPTY_ENTITY_ID_INTSET)
}
pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option<&Entity> {
self.entity_by_uuid.get(uuid)
}
@ -242,27 +231,30 @@ impl EntityInfos {
/// Update the chunk position indexes in [`EntityInfos`].
fn update_entity_chunk_positions(
query: Query<
mut query: Query<
(
Entity,
&entity::Position,
&mut entity::LastPosition,
&mut entity::Physics,
&mut entity::LastSentPosition,
&entity::WorldName,
),
Changed<entity::Position>,
>,
entity_infos: ResMut<EntityInfos>,
world_container: Res<WorldContainer>,
) {
for (entity, pos, last_pos, mut physics) in query.iter_mut() {
for (entity, pos, last_pos, world_name) in query.iter_mut() {
let world_lock = world_container.get(&**world_name).unwrap();
let mut world = world_lock.write();
let old_chunk = ChunkPos::from(*last_pos);
let new_chunk = ChunkPos::from(*pos);
if old_chunk != new_chunk {
// move the entity from the old chunk to the new one
if let Some(entities) = entity_infos.entities_by_chunk.get_mut(&old_chunk) {
if let Some(entities) = world.entities_by_chunk.get_mut(&old_chunk) {
entities.remove(&entity);
}
entity_infos
world
.entities_by_chunk
.entry(new_chunk)
.or_default()
@ -271,6 +263,40 @@ fn update_entity_chunk_positions(
}
}
pub fn remove_despawned_entities_from_indexes(
mut entity_infos: ResMut<EntityInfos>,
world_container: Res<WorldContainer>,
query: Query<
(
Entity,
&entity::EntityUuid,
&entity::Position,
&entity::WorldName,
),
&MaybeRemovedEntity,
>,
) {
for (entity, uuid, position, world_name) in &query {
let world = world_container.get(world_name).unwrap();
entity_infos.remove_entity_if_unused(
entity,
**uuid,
(*position).into(),
world.write().deref_mut(),
);
}
}
/// Remove a chunk from the storage if the entities in it have no strong
/// references left.
pub fn remove_chunk_if_unused(world: &mut World, chunk: &ChunkPos) {
if let Some(entities) = world.entities_by_chunk.get(chunk) {
if entities.is_empty() {
world.entities_by_chunk.remove(chunk);
}
}
}
impl Debug for EntityInfos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntityInfos").finish()

View file

@ -13,7 +13,7 @@ mod world;
use std::backtrace::Backtrace;
pub use bit_storage::BitStorage;
pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage, WeakChunkStorage};
pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage};
pub use container::*;
pub use entity_info::{EntityInfos, PartialEntityInfos};
use thiserror::Error;

View file

@ -1,15 +1,18 @@
use crate::{
entity::{self, Entity, MinecraftEntityId},
Chunk, EntityInfos, MoveEntityError, PartialChunkStorage, PartialEntityInfos, WeakChunkStorage,
entity::{self, Entity, MinecraftEntityId, WorldName},
Chunk, ChunkStorage, EntityInfos, MoveEntityError, PartialChunkStorage, PartialEntityInfos,
WorldContainer,
};
use azalea_core::{ChunkPos, PositionDelta8};
use bevy_ecs::{
component::Component,
system::{Commands, Query},
};
use std::fmt::Formatter;
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
};
use azalea_block::BlockState;
use azalea_buf::BufReadError;
use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3};
use log::warn;
use parking_lot::{Mutex, RwLock};
use std::{backtrace::Backtrace, fmt::Debug};
use std::{fmt::Formatter, io::Cursor, sync::Arc};
use uuid::Uuid;
/// PartialWorlds are usually owned by clients, and hold strong references to
/// chunks and entities in [`WeakWorld`]s.
@ -20,265 +23,127 @@ use uuid::Uuid;
///
/// This is primarily useful for having multiple clients in the same world.
pub struct PartialWorld {
// we just need to keep a strong reference to `shared` so it doesn't get
// dropped, we don't need to do anything with it
pub shared: Arc<World>,
pub chunks: PartialChunkStorage,
/// Some metadata about entities, like what entities are in certain chunks.
/// This does not contain the entity data itself, that's in the ECS.
pub entity_infos: PartialEntityInfos,
}
impl PartialWorld {
pub fn new(
chunk_radius: u32,
shared: Arc<World>,
owner_entity: Option<Entity>,
entity_infos: &mut EntityInfos,
) -> Self {
PartialWorld {
shared: shared.clone(),
chunks: PartialChunkStorage::new(chunk_radius, shared.chunks.clone()),
chunks: PartialChunkStorage::new(chunk_radius),
entity_infos: PartialEntityInfos::new(owner_entity, entity_infos),
}
}
pub fn replace_with_packet_data(
&mut self,
pos: &ChunkPos,
data: &mut Cursor<&[u8]>,
) -> Result<(), BufReadError> {
self.chunks.replace_with_packet_data(pos, data)
}
pub fn get_chunk(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
self.chunks.get(pos)
}
pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option<Chunk>) -> Result<(), BufReadError> {
self.chunks
.set(pos, chunk.map(|c| Arc::new(RwLock::new(c))));
Ok(())
}
pub fn update_view_center(&mut self, pos: &ChunkPos) {
self.chunks.view_center = *pos;
}
pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
self.chunks.set_block_state(pos, state)
}
/// Whether we're allowed to update the entity with the given ID.
///
/// Only call this if you're actually updating the entity, otherwise it'll
/// cause the update tracker to get out of sync.
fn maybe_update_entity(&mut self, entity: Entity, entity_infos: &mut EntityInfos) -> bool {
// no entity for you (we're processing this entity somewhere else)
if Some(entity) != self.entity_infos.owner_entity
&& !self.entity_infos.maybe_update(id, entity_infos)
{
false
} else {
true
}
}
/// Makes sure we can modify this EntityId, and returns it if we can.
///
/// Only call this if you're actually updating the entity, otherwise it'll
/// cause the update tracker to get out of sync.
pub fn entity_mut(&mut self, id: EntityId) -> Option<EntityId> {
if self.maybe_update_entity(id) {
Some(id)
} else {
None
}
}
pub fn set_entity_pos(
&mut self,
entity_id: EntityId,
new_pos: Vec3,
pos: &mut entity::Position,
physics: &mut entity::Physics,
) -> Result<(), MoveEntityError> {
if self.maybe_update_entity(entity_id) {
self.shared
.entity_infos
.write()
.set_entity_pos(entity_id, new_pos, pos, physics);
Ok(())
} else {
Err(MoveEntityError::EntityDoesNotExist(Backtrace::capture()))
}
}
pub fn move_entity_with_delta(
&mut self,
entity_id: EntityId,
delta: &PositionDelta8,
) -> Result<(), MoveEntityError> {
let global_ecs_lock = self.shared.global_ecs.clone();
let mut ecs = global_ecs_lock.lock();
let mut query = ecs.query::<(&mut entity::Position, &mut entity::Physics)>();
let (mut pos, mut physics) = query
.get_mut(&mut ecs, entity_id.into())
.map_err(|_| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?;
let new_pos = pos.with_delta(delta);
self.set_entity_pos(entity_id, new_pos, &mut pos, &mut physics)
}
/// Add an entity to the storage.
#[inline]
pub fn add_entity(&mut self, id: EntityId, bundle: impl bevy_ecs::bundle::Bundle) {
// if the entity is already in the shared world, we don't need to do
// anything
if self.shared.contains_entity(id) {
return;
pub fn add_entity(
&mut self,
commands: &mut Commands,
bundle: impl bevy_ecs::bundle::Bundle,
entity_infos: &mut EntityInfos,
world: &mut World,
query: Query<(&entity::Position, &MinecraftEntityId, &entity::EntityUuid)>,
id_query: Query<&MinecraftEntityId>,
) {
let mut entity_commands = commands.spawn(bundle);
let entity = entity_commands.id();
let (position, &id, uuid) = query.get(entity).unwrap();
let chunk_pos = ChunkPos::from(*position);
// check every entity in this entitys chunk to make sure it doesn't already
// exist there
if let Some(entities_in_chunk) = world.entities_by_chunk.get(&chunk_pos) {
for entity in entities_in_chunk {
if id_query.get(*entity).unwrap() == &id {
// the entity is already in the world, so remove that extra entity we just made
entity_commands.despawn();
return;
}
}
}
let mut ecs = self.shared.global_ecs.lock();
entity::new_entity(&mut ecs, id, bundle);
let mut query = ecs.query::<(&entity::EntityUuid, &entity::Position)>();
let (&uuid, &pos) = query.get(&mut ecs, id.into()).unwrap();
let partial_entity_infos = &mut self.entity_infos;
let mut shared_entity_infos = self.shared.entity_infos.write();
partial_entity_infos.loaded_entity_ids.insert(id);
// add the entity to the indexes
shared_entity_infos
.ids_by_chunk
.entry(ChunkPos::from(&pos))
world
.entities_by_chunk
.entry(chunk_pos)
.or_default()
.insert(id);
shared_entity_infos.id_by_uuid.insert(*uuid, id);
partial_entity_infos.loaded_entity_ids.insert(id);
.insert(entity);
entity_infos.entity_by_uuid.insert(**uuid, entity);
// set our updates_received to the shared updates_received, unless it's
// not there in which case set both to 1
if let Some(&shared_updates_received) = shared_entity_infos.updates_received.get(&id) {
if let Some(&shared_updates_received) = entity_infos.updates_received.get(&entity) {
// 0 means we're never tracking updates for this entity
if shared_updates_received != 0 || Some(id) == partial_entity_infos.owner_entity_id {
partial_entity_infos.updates_received.insert(id, 1);
if shared_updates_received != 0 || Some(entity) == partial_entity_infos.owner_entity {
partial_entity_infos
.updates_received
.insert(id, shared_updates_received);
}
} else {
shared_entity_infos.updates_received.insert(id, 1);
entity_infos.updates_received.insert(entity, 1);
partial_entity_infos.updates_received.insert(id, 1);
}
}
}
/// Remove an entity from this storage by its id. It will only be removed
/// from the shared storage if there are no other references to it.
#[inline]
pub fn remove_entity_by_id(&mut self, id: EntityId) {
let partial_entity_infos = &mut self.entity_infos;
let mut shared_entity_infos = self.shared.entity_infos.write();
pub unsafe fn move_entity_with_delta(delta: &PositionDelta8, position: &mut entity::Position) {
let new_pos = position.with_delta(delta);
**position = new_pos;
}
if partial_entity_infos.loaded_entity_ids.remove(&id) {
let mut ecs = self.shared.global_ecs.lock();
/// A component marker signifying that the entity may have been removed from the
/// world, but we're not entirely sure.
#[derive(Component)]
pub struct MaybeRemovedEntity;
let mut query = ecs.query::<(&entity::EntityUuid, &entity::Position)>();
let (uuid, pos) = query.get(&mut ecs, id.into()).expect(
"If the entity was being loaded by this storage, it must be in the shared
storage.",
);
let chunk = ChunkPos::from(pos);
let uuid = **uuid;
/// 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(
commands: &mut Commands,
partial_entity_infos: &mut PartialEntityInfos,
chunk: &ChunkPos,
world_container: &WorldContainer,
world_name: &WorldName,
query: Query<&MinecraftEntityId>,
) {
let world_lock = world_container.get(world_name).unwrap();
let world = world_lock.read();
// TODO: is this line actually necessary? test if it doesn't immediately
// panic/deadlock without this line
drop(query);
partial_entity_infos.updates_received.remove(&id);
shared_entity_infos.remove_entity_if_unused(id, uuid, chunk);
} else {
warn!("Tried to remove entity with id {id} but it was not found.")
}
}
/// 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 self, chunk: &ChunkPos) {
let partial_entity_infos = &mut self.entity_infos;
let mut shared_entity_infos = self.shared.entity_infos.write();
let mut ecs = self.shared.global_ecs.lock();
let mut query = ecs.query::<&entity::EntityUuid>();
if let Some(entities) = shared_entity_infos.ids_by_chunk.get(chunk).cloned() {
for &id in &entities {
if partial_entity_infos.loaded_entity_ids.remove(&id) {
let uuid = **query.get(&ecs, id.into()).unwrap();
// maybe remove it from the storage
shared_entity_infos.remove_entity_if_unused(id, uuid, *chunk);
}
if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() {
for &entity in &entities {
let id = query.get(entity).unwrap();
if partial_entity_infos.loaded_entity_ids.remove(&id) {
// maybe remove it from the storage
commands.entity(entity).insert(MaybeRemovedEntity);
}
}
}
}
// /// A world where the chunks are stored as weak pointers. This is used for
// /// shared worlds.
// #[derive(Default, Debug)]
// pub struct World {
// pub chunks: Arc<RwLock<WeakChunkStorage>>,
// }
/// A world where the chunks are stored as weak pointers. This is used for
/// shared worlds.
#[derive(Default, Debug)]
pub struct World {
pub chunks: ChunkStorage,
// impl World {
// pub fn new(height: u32, min_y: i32) -> Self {
// World {
// chunks: Arc::new(RwLock::new(WeakChunkStorage::new(height,
// min_y))), }
// }
// /// Read the total height of the world. You can add this to
// [`Self::min_y`] /// to get the highest possible y coordinate a block can
// be placed at. pub fn height(&self) -> u32 {
// self.chunks.read().height
// }
// /// Get the lowest possible y coordinate a block can be placed at.
// pub fn min_y(&self) -> i32 {
// self.chunks.read().min_y
// }
// pub fn contains_entity(&self, id: EntityId) -> bool {
// self.entity_infos.read().contains_entity(id)
// }
// pub fn id_by_uuid(&self, uuid: &Uuid) -> Option<EntityId> {
// self.entity_infos.read().id_by_uuid(uuid).copied()
// }
// pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
// self.chunks.read().get_block_state(pos)
// }
// pub fn get_chunk(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
// self.chunks.read().get(pos)
// }
// pub fn set_entity_pos(
// &self,
// entity_id: EntityId,
// new_pos: Vec3,
// pos: &mut entity::Position,
// physics: &mut entity::Physics,
// ) {
// self.entity_infos
// .write()
// .set_entity_pos(entity_id, new_pos, pos, physics);
// }
// }
/// An index of all the entities we know are in the chunks of the world
pub entities_by_chunk: HashMap<ChunkPos, HashSet<Entity>>,
}
impl Debug for PartialWorld {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("World")
.field("chunk_storage", &self.chunks)
.field("entity_storage", &self.entity_infos)
.field("shared", &self.shared)
.finish()
}
}
@ -290,11 +155,6 @@ impl Default for PartialWorld {
let chunk_storage = PartialChunkStorage::default();
let entity_storage = PartialEntityInfos::default();
Self {
shared: Arc::new(World {
chunks: chunk_storage.shared.clone(),
entity_infos: entity_storage.shared.clone(),
global_ecs: Arc::new(Mutex::new(bevy_ecs::world::World::default())),
}),
chunks: chunk_storage,
entity_infos: entity_storage,
}

View file

@ -10,7 +10,7 @@ use azalea_protocol::{
resolver::{self, ResolverError},
ServerAddress,
};
use azalea_world::WeakWorldContainer;
use azalea_world::WorldContainer;
use futures::future::join_all;
use log::error;
use parking_lot::{Mutex, RwLock};
@ -52,7 +52,7 @@ pub struct Swarm<S> {
resolved_address: SocketAddr,
address: ServerAddress,
pub worlds: Arc<RwLock<WeakWorldContainer>>,
pub worlds: Arc<RwLock<WorldContainer>>,
/// Plugins that are set for new bots
plugins: Plugins,
@ -228,7 +228,7 @@ pub async fn start_swarm<
// resolve the address
let resolved_address = resolver::resolve_address(&address).await?;
let world_container = Arc::new(RwLock::new(WeakWorldContainer::default()));
let world_container = Arc::new(RwLock::new(WorldContainer::default()));
let mut plugins = options.plugins;
let swarm_plugins = options.swarm_plugins;