,
}
impl MockClientHelper {
@@ -160,38 +158,39 @@ impl MockClientHelper {
conn,
dec: PacketDecoder::new(),
scratch: BytesMut::new(),
- collected_frames: vec![],
}
}
/// Inject a packet to be treated as a packet inbound to the server. Panics
/// if the packet cannot be sent.
- fn send<'a>(&mut self, packet: &impl Packet<'a>) {
+ #[track_caller]
+ fn send(&mut self, packet: &P)
+ where
+ P: Packet + Encode,
+ {
packet
- .encode_packet((&mut self.scratch).writer())
+ .encode_with_id((&mut self.scratch).writer())
.expect("failed to encode packet");
self.conn.inject_recv(self.scratch.split());
}
/// Collect all packets that have been sent to the client.
- fn collect_sent(&mut self) -> Vec {
+ #[track_caller]
+ fn collect_sent(&mut self) -> PacketFrames {
self.dec.queue_bytes(self.conn.take_sent());
- self.collected_frames.clear();
+ let mut res = vec![];
while let Some(frame) = self
.dec
.try_next_packet()
.expect("failed to decode packet frame")
{
- self.collected_frames.push(frame);
+ res.push(frame);
}
- self.collected_frames
- .iter()
- .map(|frame| decode_packet(frame).expect("failed to decode packet"))
- .collect()
+ PacketFrames(res)
}
fn clear_sent(&mut self) {
@@ -199,36 +198,87 @@ impl MockClientHelper {
}
}
-macro_rules! assert_packet_order {
- ($sent_packets:ident, $($packets:pat),+) => {{
- let sent_packets: &Vec = &$sent_packets;
- let positions = [
- $((sent_packets.iter().position(|p| matches!(p, $packets))),)*
- ];
- assert!(positions.windows(2).all(|w: &[Option]| w[0] < w[1]));
- }};
-}
+struct PacketFrames(Vec);
+
+impl PacketFrames {
+ #[track_caller]
+ fn assert_count(&self, expected_count: usize) {
+ let actual_count = self.0.iter().filter(|f| f.id == P::ID).count();
-macro_rules! assert_packet_count {
- ($sent_packets:ident, $count:tt, $packet:pat) => {{
- let sent_packets: &Vec = &$sent_packets;
- let count = sent_packets.iter().filter(|p| matches!(p, $packet)).count();
assert_eq!(
- count,
- $count,
- "expected {} {} packets, got {}\nPackets actually found:\n[\n\t{}\n]\n",
- $count,
- stringify!($packet),
- count,
- sent_packets
- .iter()
- .map(|p| format!("{:?}", p))
- .collect::>()
- .join(",\n\t")
+ expected_count,
+ actual_count,
+ "unexpected packet count for {} (expected {expected_count}, got {actual_count})",
+ P::NAME
+ );
+ }
+
+ #[track_caller]
+ fn assert_order(&self) {
+ let positions: Vec<_> = self
+ .0
+ .iter()
+ .filter_map(|f| L::packets().iter().position(|(id, _)| f.id == *id))
+ .collect();
+
+ // TODO: replace with slice::is_sorted.
+ let is_sorted = positions.windows(2).all(|w| w[0] <= w[1]);
+
+ assert!(
+ is_sorted,
+ "packets out of order (expected {:?}, got {:?})",
+ L::packets(),
+ self.debug::()
);
- }};
+ }
+
+ fn debug(&self) -> impl std::fmt::Debug {
+ self.0
+ .iter()
+ .map(|f| {
+ L::packets()
+ .iter()
+ .find(|(id, _)| f.id == *id)
+ .cloned()
+ .unwrap_or((f.id, ""))
+ })
+ .collect::>()
+ }
+}
+
+trait PacketList {
+ fn packets() -> &'static [(i32, &'static str)];
}
+macro_rules! impl_packet_list {
+ ($($ty:ident),*) => {
+ impl<$($ty: Packet,)*> PacketList for ($($ty,)*) {
+ fn packets() -> &'static [(i32, &'static str)] {
+ &[
+ $(
+ (
+ $ty::ID,
+ $ty::NAME
+ ),
+ )*
+ ]
+ }
+ }
+ }
+}
+
+impl_packet_list!(A);
+impl_packet_list!(A, B);
+impl_packet_list!(A, B, C);
+impl_packet_list!(A, B, C, D);
+impl_packet_list!(A, B, C, D, E);
+impl_packet_list!(A, B, C, D, E, F);
+impl_packet_list!(A, B, C, D, E, F, G);
+impl_packet_list!(A, B, C, D, E, F, G, H);
+impl_packet_list!(A, B, C, D, E, F, G, H, I);
+impl_packet_list!(A, B, C, D, E, F, G, H, I, J);
+impl_packet_list!(A, B, C, D, E, F, G, H, I, J, K);
+
mod client;
mod example;
mod inventory;
diff --git a/crates/valence/src/tests/client.rs b/crates/valence/src/tests/client.rs
index f80151006..198faa631 100644
--- a/crates/valence/src/tests/client.rs
+++ b/crates/valence/src/tests/client.rs
@@ -4,8 +4,8 @@ use bevy_app::App;
use bevy_ecs::world::EntityMut;
use valence_client::ViewDistance;
use valence_core::chunk_pos::ChunkView;
-use valence_core::packet::s2c::play::{ChunkDataS2c, S2cPlayPacket, UnloadChunkS2c};
use valence_entity::Position;
+use valence_instance::packet::{ChunkDataS2c, UnloadChunkS2c};
use valence_instance::Chunk;
use super::*;
@@ -38,8 +38,9 @@ fn client_chunk_view_change() {
let mut loaded_chunks = BTreeSet::new();
- for pkt in client_helper.collect_sent() {
- if let S2cPlayPacket::ChunkDataS2c(ChunkDataS2c { pos, .. }) = pkt {
+ for f in client_helper.collect_sent().0 {
+ if f.id == ChunkDataS2c::ID {
+ let ChunkDataS2c { pos, .. } = f.decode::().unwrap();
assert!(loaded_chunks.insert(pos), "({pos:?})");
}
}
@@ -57,12 +58,14 @@ fn client_chunk_view_change() {
app.update();
let client = app.world.entity_mut(client_ent);
- for pkt in client_helper.collect_sent() {
- match pkt {
- S2cPlayPacket::ChunkDataS2c(ChunkDataS2c { pos, .. }) => {
+ for f in client_helper.collect_sent().0 {
+ match f.id {
+ ChunkDataS2c::ID => {
+ let ChunkDataS2c { pos, .. } = f.decode().unwrap();
assert!(loaded_chunks.insert(pos), "({pos:?})");
}
- S2cPlayPacket::UnloadChunkS2c(UnloadChunkS2c { pos }) => {
+ UnloadChunkS2c::ID => {
+ let UnloadChunkS2c { pos } = f.decode().unwrap();
assert!(loaded_chunks.remove(&pos), "({pos:?})");
}
_ => {}
diff --git a/crates/valence/src/tests/example.rs b/crates/valence/src/tests/example.rs
index d5c5c3492..35302c6d1 100644
--- a/crates/valence/src/tests/example.rs
+++ b/crates/valence/src/tests/example.rs
@@ -6,8 +6,8 @@
//! Some of the tests in this file may be inferior duplicates of real tests.
use bevy_app::App;
-use valence_core::packet::c2s::play::PositionAndOnGround;
-use valence_core::packet::s2c::play::S2cPlayPacket;
+use valence_client::movement::PositionAndOnGroundC2s;
+use valence_inventory::packet::{InventoryS2c, OpenScreenS2c};
use super::*;
use crate::prelude::*;
@@ -35,7 +35,7 @@ fn example_test_client_position() {
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
// Send a packet as the client to the server.
- let packet = PositionAndOnGround {
+ let packet = PositionAndOnGroundC2s {
position: DVec3::new(12.0, 64.0, 0.0),
on_ground: true,
};
@@ -76,13 +76,11 @@ fn example_test_open_inventory() {
app.world
.get::(client_ent)
.expect("client not found");
+
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::OpenScreenS2c(_));
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
- assert_packet_order!(
- sent_packets,
- S2cPlayPacket::OpenScreenS2c(_),
- S2cPlayPacket::InventoryS2c(_)
- );
+ sent_packets.assert_count::(1);
+ sent_packets.assert_count::(1);
+
+ sent_packets.assert_order::<(OpenScreenS2c, InventoryS2c)>();
}
diff --git a/crates/valence/src/tests/inventory.rs b/crates/valence/src/tests/inventory.rs
index 8ce7d012d..f14abeb1b 100644
--- a/crates/valence/src/tests/inventory.rs
+++ b/crates/valence/src/tests/inventory.rs
@@ -1,12 +1,13 @@
use bevy_app::App;
use valence_core::game_mode::GameMode;
use valence_core::item::{ItemKind, ItemStack};
-use valence_core::packet::c2s::play::click_slot::{ClickMode, Slot};
-use valence_core::packet::c2s::play::ClickSlotC2s;
-use valence_core::packet::s2c::play::S2cPlayPacket;
+use valence_inventory::packet::{
+ ClickMode, ClickSlotC2s, CloseScreenS2c, CreativeInventoryActionC2s, InventoryS2c,
+ OpenScreenS2c, ScreenHandlerSlotUpdateS2c, SlotChange, UpdateSelectedSlotC2s,
+};
use valence_inventory::{
- convert_to_player_slot_id, ClientInventoryState, CursorItem, DropItemStack, Inventory,
- InventoryKind, OpenInventory,
+ convert_to_player_slot_id, ClientInventoryState, CursorItem, DropItemStack, HeldItem,
+ Inventory, InventoryKind, OpenInventory,
};
use super::*;
@@ -35,13 +36,9 @@ fn test_should_open_inventory() {
// Make assertions
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::OpenScreenS2c(_));
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
- assert_packet_order!(
- sent_packets,
- S2cPlayPacket::OpenScreenS2c(_),
- S2cPlayPacket::InventoryS2c(_)
- );
+ sent_packets.assert_count::(1);
+ sent_packets.assert_count::(1);
+ sent_packets.assert_order::<(OpenScreenS2c, InventoryS2c)>();
}
#[test]
@@ -77,7 +74,7 @@ fn test_should_close_inventory() {
// Make assertions
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::CloseScreenS2c(_));
+ sent_packets.assert_count::(1);
}
#[test]
@@ -109,8 +106,9 @@ fn test_should_remove_invalid_open_inventory() {
// Make assertions
assert!(app.world.get::(client_ent).is_none());
+
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::CloseScreenS2c(_));
+ sent_packets.assert_count::(1);
}
#[test]
@@ -134,13 +132,14 @@ fn test_should_modify_player_inventory_click_slot() {
.get::(client_ent)
.unwrap()
.state_id();
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+
+ client_helper.send(&ClickSlotC2s {
window_id: 0,
button: 0,
- mode: valence_core::packet::c2s::play::click_slot::ClickMode::Click,
+ mode: ClickMode::Click,
state_id: VarInt(state_id.0),
slot_idx: 20,
- slot_changes: vec![valence_core::packet::c2s::play::click_slot::Slot {
+ slot_changes: vec![SlotChange {
idx: 20,
item: None,
}],
@@ -155,20 +154,21 @@ fn test_should_modify_player_inventory_click_slot() {
// because the inventory was changed as a result of the client's click, the
// server should not send any packets to the client because the client
// already knows about the change.
- assert_packet_count!(
- sent_packets,
- 0,
- S2cPlayPacket::InventoryS2c(_) | S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_)
- );
+ sent_packets.assert_count::(0);
+ sent_packets.assert_count::(0);
+
let inventory = app
.world
.get::(client_ent)
.expect("could not find inventory for client");
+
assert_eq!(inventory.slot(20), None);
+
let cursor_item = app
.world
.get::(client_ent)
.expect("could not find client");
+
assert_eq!(
cursor_item.0,
Some(ItemStack::new(ItemKind::Diamond, 2, None))
@@ -205,11 +205,7 @@ fn test_should_modify_player_inventory_server_side() {
let sent_packets = client_helper.collect_sent();
// because the inventory was modified server side, the client needs to be
// updated with the change.
- assert_packet_count!(
- sent_packets,
- 1,
- S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_)
- );
+ sent_packets.assert_count::(1);
}
#[test]
@@ -231,7 +227,7 @@ fn test_should_sync_entire_player_inventory() {
// Make assertions
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
+ sent_packets.assert_count::(1);
}
fn set_up_open_inventory(app: &mut App, client_ent: Entity) -> Entity {
@@ -267,13 +263,13 @@ fn test_should_modify_open_inventory_click_slot() {
let inv_state = app.world.get::(client_ent).unwrap();
let state_id = inv_state.state_id();
let window_id = inv_state.window_id();
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+ client_helper.send(&ClickSlotC2s {
window_id,
- button: 0,
- mode: valence_core::packet::c2s::play::click_slot::ClickMode::Click,
state_id: VarInt(state_id.0),
slot_idx: 20,
- slot_changes: vec![valence_core::packet::c2s::play::click_slot::Slot {
+ button: 0,
+ mode: ClickMode::Click,
+ slot_changes: vec![SlotChange {
idx: 20,
item: None,
}],
@@ -288,11 +284,9 @@ fn test_should_modify_open_inventory_click_slot() {
// because the inventory was modified as a result of the client's click, the
// server should not send any packets to the client because the client
// already knows about the change.
- assert_packet_count!(
- sent_packets,
- 0,
- S2cPlayPacket::InventoryS2c(_) | S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_)
- );
+ sent_packets.assert_count::(0);
+ sent_packets.assert_count::(0);
+
let inventory = app
.world
.get::(inventory_ent)
@@ -332,15 +326,13 @@ fn test_should_modify_open_inventory_server_side() {
// because the inventory was modified server side, the client needs to be
// updated with the change.
- assert_packet_count!(
- sent_packets,
- 1,
- S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_)
- );
+ sent_packets.assert_count::(1);
+
let inventory = app
.world
.get::(inventory_ent)
.expect("could not find inventory for client");
+
assert_eq!(
inventory.slot(5),
Some(&ItemStack::new(ItemKind::IronIngot, 1, None))
@@ -367,7 +359,7 @@ fn test_should_sync_entire_open_inventory() {
// Make assertions
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
+ sent_packets.assert_count::(1);
}
#[test]
@@ -384,12 +376,10 @@ fn test_set_creative_mode_slot_handling() {
app.update();
client_helper.clear_sent();
- client_helper.send(
- &valence_core::packet::c2s::play::CreativeInventoryActionC2s {
- slot: 36,
- clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
- },
- );
+ client_helper.send(&CreativeInventoryActionC2s {
+ slot: 36,
+ clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
+ });
app.update();
@@ -398,6 +388,7 @@ fn test_set_creative_mode_slot_handling() {
.world
.get::(client_ent)
.expect("could not find inventory for client");
+
assert_eq!(
inventory.slot(36),
Some(&ItemStack::new(ItemKind::Diamond, 2, None))
@@ -418,12 +409,10 @@ fn test_ignore_set_creative_mode_slot_if_not_creative() {
app.update();
client_helper.clear_sent();
- client_helper.send(
- &valence_core::packet::c2s::play::CreativeInventoryActionC2s {
- slot: 36,
- clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
- },
- );
+ client_helper.send(&CreativeInventoryActionC2s {
+ slot: 36,
+ clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
+ });
app.update();
@@ -480,16 +469,17 @@ fn test_should_handle_set_held_item() {
app.update();
client_helper.clear_sent();
- client_helper.send(&valence_core::packet::c2s::play::UpdateSelectedSlotC2s { slot: 4 });
+ client_helper.send(&UpdateSelectedSlotC2s { slot: 4 });
app.update();
// Make assertions
- let inv_state = app
+ let held = app
.world
- .get::(client_ent)
+ .get::(client_ent)
.expect("could not find client");
- assert_eq!(inv_state.held_item_slot(), 40);
+
+ assert_eq!(held.slot(), 40);
}
#[test]
@@ -525,10 +515,9 @@ fn should_not_increment_state_id_on_cursor_item_change() {
}
mod dropping_items {
+ use valence_client::packet::{PlayerAction, PlayerActionC2s};
use valence_core::block_pos::BlockPos;
use valence_core::direction::Direction;
- use valence_core::packet::c2s::play::click_slot::{ClickMode, Slot};
- use valence_core::packet::c2s::play::player_action::Action;
use valence_inventory::convert_to_player_slot_id;
use super::*;
@@ -548,8 +537,8 @@ mod dropping_items {
.expect("could not find inventory");
inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 3, None));
- client_helper.send(&valence_core::packet::c2s::play::PlayerActionC2s {
- action: Action::DropItem,
+ client_helper.send(&PlayerActionC2s {
+ action: PlayerAction::DropItem,
position: BlockPos::new(0, 0, 0),
direction: Direction::Down,
sequence: VarInt(0),
@@ -562,15 +551,19 @@ mod dropping_items {
.world
.get::(client_ent)
.expect("could not find client");
+
assert_eq!(
inventory.slot(36),
Some(&ItemStack::new(ItemKind::IronIngot, 2, None))
);
+
let events = app
.world
.get_resource::>()
.expect("expected drop item stack events");
+
let events = events.iter_current_update_events().collect::>();
+
assert_eq!(events.len(), 1);
assert_eq!(events[0].client, client_ent);
assert_eq!(events[0].from_slot, Some(36));
@@ -580,11 +573,8 @@ mod dropping_items {
);
let sent_packets = client_helper.collect_sent();
- assert_packet_count!(
- sent_packets,
- 0,
- S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_)
- );
+
+ sent_packets.assert_count::(0);
}
#[test]
@@ -602,8 +592,8 @@ mod dropping_items {
.expect("could not find inventory");
inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 32, None));
- client_helper.send(&valence_core::packet::c2s::play::PlayerActionC2s {
- action: Action::DropAllItems,
+ client_helper.send(&PlayerActionC2s {
+ action: PlayerAction::DropAllItems,
position: BlockPos::new(0, 0, 0),
direction: Direction::Down,
sequence: VarInt(0),
@@ -612,11 +602,11 @@ mod dropping_items {
app.update();
// Make assertions
- let inv_state = app
+ let held = app
.world
- .get::(client_ent)
+ .get::(client_ent)
.expect("could not find client");
- assert_eq!(inv_state.held_item_slot(), 36);
+ assert_eq!(held.slot(), 36);
let inventory = app
.world
.get::(client_ent)
@@ -647,12 +637,10 @@ mod dropping_items {
app.world.entity_mut(client_ent).insert(GameMode::Creative);
- client_helper.send(
- &valence_core::packet::c2s::play::CreativeInventoryActionC2s {
- slot: -1,
- clicked_item: Some(ItemStack::new(ItemKind::IronIngot, 32, None)),
- },
- );
+ client_helper.send(&CreativeInventoryActionC2s {
+ slot: -1,
+ clicked_item: Some(ItemStack::new(ItemKind::IronIngot, 32, None)),
+ });
app.update();
@@ -693,12 +681,12 @@ mod dropping_items {
.expect("could not find client");
let state_id = inv_state.state_id().0;
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+ client_helper.send(&ClickSlotC2s {
window_id: 0,
+ state_id: VarInt(state_id),
slot_idx: -999,
button: 0,
mode: ClickMode::Click,
- state_id: VarInt(state_id),
slot_changes: vec![],
carried_item: None,
});
@@ -710,12 +698,16 @@ mod dropping_items {
.world
.get::(client_ent)
.expect("could not find client");
+
assert_eq!(cursor_item.0, None);
+
let events = app
.world
.get_resource::>()
.expect("expected drop item stack events");
+
let events = events.iter_current_update_events().collect::>();
+
assert_eq!(events.len(), 1);
assert_eq!(events[0].client, client_ent);
assert_eq!(events[0].from_slot, None);
@@ -738,20 +730,23 @@ mod dropping_items {
.world
.get_mut::(client_ent)
.expect("could not find client");
+
let state_id = inv_state.state_id().0;
+
let mut inventory = app
.world
.get_mut::(client_ent)
.expect("could not find inventory");
+
inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None));
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+ client_helper.send(&ClickSlotC2s {
window_id: 0,
slot_idx: 40,
button: 0,
mode: ClickMode::DropKey,
state_id: VarInt(state_id),
- slot_changes: vec![Slot {
+ slot_changes: vec![SlotChange {
idx: 40,
item: Some(ItemStack::new(ItemKind::IronIngot, 31, None)),
}],
@@ -765,7 +760,9 @@ mod dropping_items {
.world
.get_resource::>()
.expect("expected drop item stack events");
+
let events = events.iter_current_update_events().collect::>();
+
assert_eq!(events.len(), 1);
assert_eq!(events[0].client, client_ent);
assert_eq!(events[0].from_slot, Some(40));
@@ -788,20 +785,23 @@ mod dropping_items {
.world
.get_mut::(client_ent)
.expect("could not find client");
+
let state_id = inv_state.state_id().0;
+
let mut inventory = app
.world
.get_mut::(client_ent)
.expect("could not find inventory");
+
inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None));
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+ client_helper.send(&ClickSlotC2s {
window_id: 0,
slot_idx: 40,
button: 1, // pressing control
mode: ClickMode::DropKey,
state_id: VarInt(state_id),
- slot_changes: vec![Slot {
+ slot_changes: vec![SlotChange {
idx: 40,
item: None,
}],
@@ -815,7 +815,9 @@ mod dropping_items {
.world
.get_resource::>()
.expect("expected drop item stack events");
+
let events = events.iter_current_update_events().collect::>();
+
assert_eq!(events.len(), 1);
assert_eq!(events[0].client, client_ent);
assert_eq!(events[0].from_slot, Some(40));
@@ -840,13 +842,16 @@ mod dropping_items {
.world
.get_mut::(client_ent)
.expect("could not find inventory");
+
inventory.set_slot(
convert_to_player_slot_id(InventoryKind::Generic9x3, 50),
ItemStack::new(ItemKind::IronIngot, 32, None),
);
+
let _inventory_ent = set_up_open_inventory(&mut app, client_ent);
app.update();
+
client_helper.clear_sent();
let inv_state = app
@@ -857,13 +862,13 @@ mod dropping_items {
let state_id = inv_state.state_id().0;
let window_id = inv_state.window_id();
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+ client_helper.send(&ClickSlotC2s {
window_id,
- slot_idx: 50,
- button: 0, // not pressing control
- mode: ClickMode::DropKey,
state_id: VarInt(state_id),
- slot_changes: vec![Slot {
+ slot_idx: 50, // not pressing control
+ button: 0,
+ mode: ClickMode::DropKey,
+ slot_changes: vec![SlotChange {
idx: 50,
item: Some(ItemStack::new(ItemKind::IronIngot, 31, None)),
}],
@@ -877,21 +882,26 @@ mod dropping_items {
.world
.get_resource::>()
.expect("expected drop item stack events");
+
let player_inventory = app
.world
.get::(client_ent)
.expect("could not find inventory");
+
let events = events.iter_current_update_events().collect::>();
+
assert_eq!(events.len(), 1);
assert_eq!(events[0].client, client_ent);
assert_eq!(
events[0].from_slot,
Some(convert_to_player_slot_id(InventoryKind::Generic9x3, 50))
);
+
assert_eq!(
events[0].stack,
ItemStack::new(ItemKind::IronIngot, 1, None)
);
+
// Also make sure that the player inventory was updated correctly.
let expected_player_slot_id = convert_to_player_slot_id(InventoryKind::Generic9x3, 50);
assert_eq!(
@@ -916,10 +926,12 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() {
.world
.get_mut::(client_ent)
.expect("could not find inventory");
+
inventory.set_slot(
convert_to_player_slot_id(InventoryKind::Generic9x3, 50),
ItemStack::new(ItemKind::IronIngot, 32, None),
);
+
let _inventory_ent = set_up_open_inventory(&mut app, client_ent);
app.update();
@@ -933,13 +945,13 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() {
let state_id = inv_state.state_id().0;
let window_id = inv_state.window_id();
- client_helper.send(&valence_core::packet::c2s::play::ClickSlotC2s {
+ client_helper.send(&ClickSlotC2s {
window_id,
- slot_idx: 50,
- button: 1, // pressing control, the whole stack is dropped
- mode: ClickMode::DropKey,
state_id: VarInt(state_id),
- slot_changes: vec![Slot {
+ slot_idx: 50, // pressing control, the whole stack is dropped
+ button: 1,
+ mode: ClickMode::DropKey,
+ slot_changes: vec![SlotChange {
idx: 50,
item: None,
}],
@@ -953,11 +965,14 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() {
.world
.get_resource::>()
.expect("expected drop item stack events");
+
let player_inventory = app
.world
.get::(client_ent)
.expect("could not find inventory");
+
let events = events.iter_current_update_events().collect::>();
+
assert_eq!(events.len(), 1);
assert_eq!(events[0].client, client_ent);
assert_eq!(
@@ -968,6 +983,7 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() {
events[0].stack,
ItemStack::new(ItemKind::IronIngot, 32, None)
);
+
// Also make sure that the player inventory was updated correctly.
let expected_player_slot_id = convert_to_player_slot_id(InventoryKind::Generic9x3, 50);
assert_eq!(player_inventory.slot(expected_player_slot_id), None);
@@ -996,15 +1012,15 @@ fn dragging_items() {
button: 2,
mode: ClickMode::Drag,
slot_changes: vec![
- Slot {
+ SlotChange {
idx: 9,
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
},
- Slot {
+ SlotChange {
idx: 10,
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
},
- Slot {
+ SlotChange {
idx: 11,
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
},
@@ -1015,20 +1031,23 @@ fn dragging_items() {
app.update();
let sent_packets = client_helper.collect_sent();
- assert_eq!(sent_packets.len(), 0);
+ assert_eq!(sent_packets.0.len(), 0);
let cursor_item = app
.world
.get::(client_ent)
.expect("could not find client");
+
assert_eq!(
cursor_item.0,
Some(ItemStack::new(ItemKind::Diamond, 1, None))
);
+
let inventory = app
.world
.get::(client_ent)
.expect("could not find inventory");
+
for i in 9..12 {
assert_eq!(
inventory.slot(i),
diff --git a/crates/valence/src/tests/weather.rs b/crates/valence/src/tests/weather.rs
index dc6822f6c..ee4166bd2 100644
--- a/crates/valence/src/tests/weather.rs
+++ b/crates/valence/src/tests/weather.rs
@@ -1,51 +1,10 @@
use bevy_app::App;
+use valence_client::packet::GameStateChangeS2c;
use valence_client::weather::{Rain, Thunder};
use valence_client::Client;
-use valence_core::packet::s2c::play::game_state_change::GameEventKind;
-use valence_core::packet::s2c::play::{GameStateChangeS2c, S2cPlayPacket};
use super::*;
-fn assert_weather_packets(sent_packets: Vec) {
- assert_packet_count!(sent_packets, 6, S2cPlayPacket::GameStateChangeS2c(_));
-
- assert_packet_order!(
- sent_packets,
- S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
- kind: GameEventKind::BeginRaining,
- value: _
- }),
- S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
- kind: GameEventKind::RainLevelChange,
- value: _
- }),
- S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
- kind: GameEventKind::ThunderLevelChange,
- value: _
- }),
- S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c {
- kind: GameEventKind::EndRaining,
- value: _
- })
- );
-
- if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[1] {
- assert_eq!(pkt.value, 0.5);
- }
-
- if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[2] {
- assert_eq!(pkt.value, 1.0);
- }
-
- if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[3] {
- assert_eq!(pkt.value, 0.5);
- }
-
- if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[4] {
- assert_eq!(pkt.value, 1.0);
- }
-}
-
#[test]
fn test_weather_instance() {
let mut app = App::new();
@@ -139,3 +98,8 @@ fn test_weather_client() {
assert_weather_packets(sent_packets);
}
+
+#[track_caller]
+fn assert_weather_packets(sent_packets: PacketFrames) {
+ sent_packets.assert_count::(6);
+}
diff --git a/crates/valence_advancement/Cargo.toml b/crates/valence_advancement/Cargo.toml
new file mode 100644
index 000000000..76d0aebd0
--- /dev/null
+++ b/crates/valence_advancement/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "valence_advancement"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+valence_core.workspace = true
+valence_client.workspace = true
+bevy_app.workspace = true
+bevy_ecs.workspace = true
+bevy_hierarchy.workspace = true
+rustc-hash.workspace = true
+anyhow.workspace = true
\ No newline at end of file
diff --git a/crates/valence_advancement/README.md b/crates/valence_advancement/README.md
new file mode 100644
index 000000000..4aa5d3ab2
--- /dev/null
+++ b/crates/valence_advancement/README.md
@@ -0,0 +1,7 @@
+# valence_advancement
+
+Everything related to Minecraft advancements.
+
+### Warning
+- Each advancement should be scheduled to be sent to each unique client.
+- Advancement identifier is not mutable and changing it can cause bugs.
\ No newline at end of file
diff --git a/crates/valence_advancement/src/event.rs b/crates/valence_advancement/src/event.rs
new file mode 100644
index 000000000..565e7d457
--- /dev/null
+++ b/crates/valence_advancement/src/event.rs
@@ -0,0 +1,30 @@
+use bevy_ecs::prelude::{Entity, EventReader, EventWriter};
+use valence_client::event_loop::PacketEvent;
+use valence_core::ident::Ident;
+
+use crate::packet::AdvancementTabC2s;
+
+/// This event sends when the client changes or closes advancement's tab.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct AdvancementTabChange {
+ pub client: Entity,
+ /// If None then the client has closed advancement's tabs.
+ pub opened_tab: Option>,
+}
+
+pub(crate) fn handle_advancement_tab_change(
+ mut packets: EventReader,
+ mut advancement_tab_change_events: EventWriter,
+) {
+ for packet in packets.iter() {
+ if let Some(pkt) = packet.decode::() {
+ advancement_tab_change_events.send(AdvancementTabChange {
+ client: packet.client,
+ opened_tab: match pkt {
+ AdvancementTabC2s::ClosedScreen => None,
+ AdvancementTabC2s::OpenedTab { tab_id } => Some(tab_id.into()),
+ },
+ })
+ }
+ }
+}
diff --git a/crates/valence_advancement/src/lib.rs b/crates/valence_advancement/src/lib.rs
new file mode 100644
index 000000000..391c675bc
--- /dev/null
+++ b/crates/valence_advancement/src/lib.rs
@@ -0,0 +1,481 @@
+#![doc = include_str!("../README.md")]
+#![allow(clippy::type_complexity)]
+
+pub mod event;
+pub mod packet;
+
+use std::borrow::Cow;
+use std::io::Write;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use bevy_app::{CoreSet, Plugin};
+use bevy_ecs::prelude::{Bundle, Component, Entity};
+use bevy_ecs::query::{Added, Changed, Or, With};
+use bevy_ecs::schedule::{IntoSystemConfig, IntoSystemSetConfig, SystemSet};
+use bevy_ecs::system::{Commands, Query, SystemParam};
+pub use bevy_hierarchy;
+use bevy_hierarchy::{Children, Parent};
+use event::{handle_advancement_tab_change, AdvancementTabChange};
+use packet::SelectAdvancementTabS2c;
+use rustc_hash::FxHashMap;
+use valence_client::{Client, FlushPacketsSet, SpawnClientsSet};
+use valence_core::ident::Ident;
+use valence_core::item::ItemStack;
+use valence_core::protocol::encode::WritePacket;
+use valence_core::protocol::raw::RawBytes;
+use valence_core::protocol::var_int::VarInt;
+use valence_core::protocol::{packet_id, Encode, Packet};
+use valence_core::text::Text;
+
+pub struct AdvancementPlugin;
+
+#[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)]
+pub struct WriteAdvancementPacketToClientsSet;
+
+#[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)]
+pub struct WriteAdvancementToCacheSet;
+
+impl Plugin for AdvancementPlugin {
+ fn build(&self, app: &mut bevy_app::App) {
+ app.configure_sets((
+ WriteAdvancementPacketToClientsSet
+ .in_base_set(CoreSet::PostUpdate)
+ .before(FlushPacketsSet),
+ WriteAdvancementToCacheSet
+ .in_base_set(CoreSet::PostUpdate)
+ .before(WriteAdvancementPacketToClientsSet),
+ ))
+ .add_event::()
+ .add_system(
+ add_advancement_update_component_to_new_clients
+ .after(SpawnClientsSet)
+ .in_base_set(CoreSet::PreUpdate),
+ )
+ .add_system(handle_advancement_tab_change.in_base_set(CoreSet::PreUpdate))
+ .add_system(update_advancement_cached_bytes.in_set(WriteAdvancementToCacheSet))
+ .add_system(send_advancement_update_packet.in_set(WriteAdvancementPacketToClientsSet));
+ }
+}
+
+/// Components for advancement that are required
+/// Optional components:
+/// [AdvancementDisplay]
+/// [Parent] - parent advancement
+#[derive(Bundle)]
+pub struct AdvancementBundle {
+ pub advancement: Advancement,
+ pub requirements: AdvancementRequirements,
+ pub cached_bytes: AdvancementCachedBytes,
+}
+
+fn add_advancement_update_component_to_new_clients(
+ mut commands: Commands,
+ query: Query>,
+) {
+ for client in query.iter() {
+ commands
+ .entity(client)
+ .insert(AdvancementClientUpdate::default());
+ }
+}
+
+#[derive(SystemParam, Debug)]
+struct UpdateAdvancementCachedBytesQuery<'w, 's> {
+ advancement_id_query: Query<'w, 's, &'static Advancement>,
+ criteria_query: Query<'w, 's, &'static AdvancementCriteria>,
+}
+
+impl<'w, 's> UpdateAdvancementCachedBytesQuery<'w, 's> {
+ fn write(
+ &self,
+ a_identifier: &Advancement,
+ a_requirements: &AdvancementRequirements,
+ a_display: Option<&AdvancementDisplay>,
+ a_children: Option<&Children>,
+ a_parent: Option<&Parent>,
+ w: impl Write,
+ ) -> anyhow::Result<()> {
+ let Self {
+ advancement_id_query,
+ criteria_query,
+ } = self;
+
+ let mut pkt = packet::Advancement {
+ parent_id: None,
+ display_data: None,
+ criteria: vec![],
+ requirements: vec![],
+ };
+
+ if let Some(a_parent) = a_parent {
+ let a_identifier = advancement_id_query.get(a_parent.get())?;
+ pkt.parent_id = Some(a_identifier.0.borrowed());
+ }
+
+ if let Some(a_display) = a_display {
+ pkt.display_data = Some(packet::AdvancementDisplay {
+ title: Cow::Borrowed(&a_display.title),
+ description: Cow::Borrowed(&a_display.description),
+ icon: &a_display.icon,
+ frame_type: VarInt(a_display.frame_type as _),
+ flags: a_display.flags(),
+ background_texture: a_display.background_texture.as_ref().map(|v| v.borrowed()),
+ x_coord: a_display.x_coord,
+ y_coord: a_display.y_coord,
+ });
+ }
+
+ if let Some(a_children) = a_children {
+ for a_child in a_children.iter() {
+ let Ok(c_identifier) = criteria_query.get(*a_child) else { continue; };
+ pkt.criteria.push((c_identifier.0.borrowed(), ()));
+ }
+ }
+
+ for requirements in a_requirements.0.iter() {
+ let mut requirements_p = vec![];
+ for requirement in requirements {
+ let c_identifier = criteria_query.get(*requirement)?;
+ requirements_p.push(c_identifier.0.as_str());
+ }
+ pkt.requirements.push(packet::AdvancementRequirements {
+ requirement: requirements_p,
+ });
+ }
+
+ (&a_identifier.0, pkt).encode(w)
+ }
+}
+
+fn update_advancement_cached_bytes(
+ mut query: Query<
+ (
+ &Advancement,
+ &AdvancementRequirements,
+ &mut AdvancementCachedBytes,
+ Option<&AdvancementDisplay>,
+ Option<&Children>,
+ Option<&Parent>,
+ ),
+ Or<(
+ Changed,
+ Changed,
+ Changed,
+ Changed,
+ )>,
+ >,
+ update_advancement_cached_bytes_query: UpdateAdvancementCachedBytesQuery,
+) {
+ for (a_identifier, a_requirements, mut a_bytes, a_display, a_children, a_parent) in
+ query.iter_mut()
+ {
+ a_bytes.0.clear();
+ update_advancement_cached_bytes_query
+ .write(
+ a_identifier,
+ a_requirements,
+ a_display,
+ a_children,
+ a_parent,
+ &mut a_bytes.0,
+ )
+ .expect("Failed to write an advancement");
+ }
+}
+
+#[derive(SystemParam, Debug)]
+#[allow(clippy::type_complexity)]
+pub(crate) struct SingleAdvancementUpdateQuery<'w, 's> {
+ advancement_bytes_query: Query<'w, 's, &'static AdvancementCachedBytes>,
+ advancement_id_query: Query<'w, 's, &'static Advancement>,
+ criteria_query: Query<'w, 's, &'static AdvancementCriteria>,
+ parent_query: Query<'w, 's, &'static Parent>,
+}
+
+#[derive(Debug)]
+pub(crate) struct AdvancementUpdateEncodeS2c<'w, 's, 'a> {
+ client_update: AdvancementClientUpdate,
+ queries: &'a SingleAdvancementUpdateQuery<'w, 's>,
+}
+
+impl<'w, 's, 'a> Encode for AdvancementUpdateEncodeS2c<'w, 's, 'a> {
+ fn encode(&self, w: impl Write) -> anyhow::Result<()> {
+ let SingleAdvancementUpdateQuery {
+ advancement_bytes_query,
+ advancement_id_query,
+ criteria_query,
+ parent_query,
+ } = self.queries;
+
+ let AdvancementClientUpdate {
+ new_advancements,
+ remove_advancements,
+ progress,
+ force_tab_update: _,
+ reset,
+ } = &self.client_update;
+
+ let mut pkt = packet::GenericAdvancementUpdateS2c {
+ reset: *reset,
+ advancement_mapping: vec![],
+ identifiers: vec![],
+ progress_mapping: vec![],
+ };
+
+ for new_advancement in new_advancements {
+ let a_cached_bytes = advancement_bytes_query.get(*new_advancement)?;
+ pkt.advancement_mapping
+ .push(RawBytes(a_cached_bytes.0.as_slice()));
+ }
+
+ for remove_advancement in remove_advancements {
+ let a_identifier = advancement_id_query.get(*remove_advancement)?;
+ pkt.identifiers.push(a_identifier.0.borrowed());
+ }
+
+ let mut progress_mapping: FxHashMap)>> =
+ FxHashMap::default();
+ for progress in progress {
+ let a = parent_query.get(progress.0)?;
+ progress_mapping
+ .entry(a.get())
+ .and_modify(|v| v.push(*progress))
+ .or_insert(vec![*progress]);
+ }
+
+ for (a, c_progresses) in progress_mapping {
+ let a_identifier = advancement_id_query.get(a)?;
+ let mut c_progresses_p = vec![];
+ for (c, c_progress) in c_progresses {
+ let c_identifier = criteria_query.get(c)?;
+ c_progresses_p.push(packet::AdvancementCriteria {
+ criterion_identifier: c_identifier.0.borrowed(),
+ criterion_progress: c_progress,
+ });
+ }
+ pkt.progress_mapping
+ .push((a_identifier.0.borrowed(), c_progresses_p));
+ }
+
+ pkt.encode(w)
+ }
+}
+
+impl<'w, 's, 'a> Packet for AdvancementUpdateEncodeS2c<'w, 's, 'a> {
+ const ID: i32 = packet_id::ADVANCEMENT_UPDATE_S2C;
+ const NAME: &'static str = "AdvancementUpdateEncodeS2c";
+}
+
+#[allow(clippy::type_complexity)]
+fn send_advancement_update_packet(
+ mut client: Query<(&mut AdvancementClientUpdate, &mut Client)>,
+ update_single_query: SingleAdvancementUpdateQuery,
+) {
+ for (mut advancement_client_update, mut client) in client.iter_mut() {
+ match advancement_client_update.force_tab_update {
+ ForceTabUpdate::None => {}
+ ForceTabUpdate::First => {
+ client.write_packet(&SelectAdvancementTabS2c { identifier: None })
+ }
+ ForceTabUpdate::Spec(spec) => {
+ if let Ok(a_identifier) = update_single_query.advancement_id_query.get(spec) {
+ client.write_packet(&SelectAdvancementTabS2c {
+ identifier: Some(a_identifier.0.borrowed()),
+ });
+ }
+ }
+ }
+
+ if ForceTabUpdate::None != advancement_client_update.force_tab_update {
+ advancement_client_update.force_tab_update = ForceTabUpdate::None;
+ }
+
+ if advancement_client_update.new_advancements.is_empty()
+ && advancement_client_update.progress.is_empty()
+ && advancement_client_update.remove_advancements.is_empty()
+ && !advancement_client_update.reset
+ {
+ continue;
+ }
+
+ let advancement_client_update = std::mem::replace(
+ advancement_client_update.as_mut(),
+ AdvancementClientUpdate {
+ reset: false,
+ ..Default::default()
+ },
+ );
+
+ client.write_packet(&AdvancementUpdateEncodeS2c {
+ queries: &update_single_query,
+ client_update: advancement_client_update,
+ });
+ }
+}
+
+/// Advancement's id. May not be updated.
+#[derive(Component)]
+pub struct Advancement(Ident>);
+
+impl Advancement {
+ pub fn new(ident: Ident>) -> Advancement {
+ Self(ident)
+ }
+
+ pub fn get(&self) -> &Ident> {
+ &self.0
+ }
+}
+
+#[derive(Clone, Copy)]
+pub enum AdvancementFrameType {
+ Task,
+ Challenge,
+ Goal,
+}
+
+/// Advancement display. Optional component
+#[derive(Component)]
+pub struct AdvancementDisplay {
+ pub title: Text,
+ pub description: Text,
+ pub icon: Option,
+ pub frame_type: AdvancementFrameType,
+ pub show_toast: bool,
+ pub hidden: bool,
+ pub background_texture: Option>>,
+ pub x_coord: f32,
+ pub y_coord: f32,
+}
+
+impl AdvancementDisplay {
+ pub(crate) fn flags(&self) -> i32 {
+ let mut flags = 0;
+ flags |= self.background_texture.is_some() as i32;
+ flags |= (self.show_toast as i32) << 1;
+ flags |= (self.hidden as i32) << 2;
+ flags
+ }
+}
+
+/// Criteria's identifier. May not be updated
+#[derive(Component)]
+pub struct AdvancementCriteria(Ident>);
+
+impl AdvancementCriteria {
+ pub fn new(ident: Ident>) -> Self {
+ Self(ident)
+ }
+
+ pub fn get(&self) -> &Ident> {
+ &self.0
+ }
+}
+
+/// Requirements for advancement to be completed.
+/// All columns should be completed, column is completed when any of criteria in
+/// this column is completed.
+#[derive(Component, Default)]
+pub struct AdvancementRequirements(pub Vec>);
+
+#[derive(Component, Default)]
+pub struct AdvancementCachedBytes(pub(crate) Vec);
+
+#[derive(Default, Debug, PartialEq)]
+pub enum ForceTabUpdate {
+ #[default]
+ None,
+ First,
+ /// Should contain only root advancement otherwise the first will be chosen
+ Spec(Entity),
+}
+
+#[derive(Component, Debug)]
+pub struct AdvancementClientUpdate {
+ /// Which advancement's descriptions send to client
+ pub new_advancements: Vec,
+ /// Which advancements remove from client
+ pub remove_advancements: Vec,
+ /// Criteria progress update.
+ /// If None then criteria is not done otherwise it is done
+ pub progress: Vec<(Entity, Option)>,
+ /// Forces client to open a tab
+ pub force_tab_update: ForceTabUpdate,
+ /// Defines if other advancements should be removed.
+ /// Also with this flag, client will not show a toast for advancements,
+ /// which are completed. When the packet is sent, turns to false
+ pub reset: bool,
+}
+
+impl Default for AdvancementClientUpdate {
+ fn default() -> Self {
+ Self {
+ new_advancements: vec![],
+ remove_advancements: vec![],
+ progress: vec![],
+ force_tab_update: ForceTabUpdate::default(),
+ reset: true,
+ }
+ }
+}
+
+impl AdvancementClientUpdate {
+ pub(crate) fn walk_advancements(
+ root: Entity,
+ children_query: &Query<&Children>,
+ advancement_check_query: &Query<(), With>,
+ func: &mut impl FnMut(Entity),
+ ) {
+ func(root);
+ if let Ok(children) = children_query.get(root) {
+ for child in children.iter() {
+ let child = *child;
+ if advancement_check_query.get(child).is_ok() {
+ Self::walk_advancements(child, children_query, advancement_check_query, func);
+ }
+ }
+ }
+ }
+
+ /// Sends all advancements from the root
+ pub fn send_advancements(
+ &mut self,
+ root: Entity,
+ children_query: &Query<&Children>,
+ advancement_check_query: &Query<(), With>,
+ ) {
+ Self::walk_advancements(root, children_query, advancement_check_query, &mut |e| {
+ self.new_advancements.push(e)
+ });
+ }
+
+ /// Removes all advancements from the root
+ pub fn remove_advancements(
+ &mut self,
+ root: Entity,
+ children_query: &Query<&Children>,
+ advancement_check_query: &Query<(), With>,
+ ) {
+ Self::walk_advancements(root, children_query, advancement_check_query, &mut |e| {
+ self.remove_advancements.push(e)
+ });
+ }
+
+ /// Marks criteria as done
+ pub fn criteria_done(&mut self, criteria: Entity) {
+ self.progress.push((
+ criteria,
+ Some(
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_millis() as i64,
+ ),
+ ))
+ }
+
+ /// Marks criteria as undone
+ pub fn criteria_undone(&mut self, criteria: Entity) {
+ self.progress.push((criteria, None))
+ }
+}
diff --git a/crates/valence_core/src/packet/s2c/play/advancement_update.rs b/crates/valence_advancement/src/packet.rs
similarity index 65%
rename from crates/valence_core/src/packet/s2c/play/advancement_update.rs
rename to crates/valence_advancement/src/packet.rs
index 91bcd4eee..cb58fd555 100644
--- a/crates/valence_core/src/packet/s2c/play/advancement_update.rs
+++ b/crates/valence_advancement/src/packet.rs
@@ -1,24 +1,28 @@
use std::borrow::Cow;
use std::io::Write;
-use crate::ident::Ident;
-use crate::item::ItemStack;
-use crate::packet::var_int::VarInt;
-use crate::packet::{Decode, Encode};
-use crate::text::Text;
-
-#[derive(Clone, Debug, Encode, Decode)]
-pub struct AdvancementUpdateS2c<'a> {
+use valence_core::ident::Ident;
+use valence_core::item::ItemStack;
+use valence_core::protocol::var_int::VarInt;
+use valence_core::protocol::{packet_id, Decode, Encode, Packet};
+use valence_core::text::Text;
+
+pub type AdvancementUpdateS2c<'a> =
+ GenericAdvancementUpdateS2c<'a, (Ident>, Advancement<'a, Option>)>;
+
+#[derive(Clone, Debug, Encode, Decode, Packet)]
+#[packet(id = packet_id::ADVANCEMENT_UPDATE_S2C)]
+pub struct GenericAdvancementUpdateS2c<'a, AM: 'a> {
pub reset: bool,
- pub advancement_mapping: Vec<(Ident>, Advancement<'a>)>,
+ pub advancement_mapping: Vec,
pub identifiers: Vec>>,
pub progress_mapping: Vec<(Ident>, Vec>)>,
}
#[derive(Clone, PartialEq, Debug, Encode, Decode)]
-pub struct Advancement<'a> {
+pub struct Advancement<'a, I> {
pub parent_id: Option>>,
- pub display_data: Option>,
+ pub display_data: Option>,
pub criteria: Vec<(Ident>, ())>,
pub requirements: Vec>,
}
@@ -29,10 +33,10 @@ pub struct AdvancementRequirements<'a> {
}
#[derive(Clone, PartialEq, Debug)]
-pub struct AdvancementDisplay<'a> {
+pub struct AdvancementDisplay<'a, I> {
pub title: Cow<'a, Text>,
pub description: Cow<'a, Text>,
- pub icon: Option,
+ pub icon: I,
pub frame_type: VarInt,
pub flags: i32,
pub background_texture: Option>>,
@@ -48,7 +52,7 @@ pub struct AdvancementCriteria<'a> {
pub criterion_progress: Option,
}
-impl Encode for AdvancementDisplay<'_> {
+impl Encode for AdvancementDisplay<'_, I> {
fn encode(&self, mut w: impl Write) -> anyhow::Result<()> {
self.title.encode(&mut w)?;
self.description.encode(&mut w)?;
@@ -68,11 +72,11 @@ impl Encode for AdvancementDisplay<'_> {
}
}
-impl<'a> Decode<'a> for AdvancementDisplay<'a> {
+impl<'a, I: Decode<'a>> Decode<'a> for AdvancementDisplay<'a, I> {
fn decode(r: &mut &'a [u8]) -> anyhow::Result {
let title = >::decode(r)?;
let description = >::decode(r)?;
- let icon = Option::::decode(r)?;
+ let icon = I::decode(r)?;
let frame_type = VarInt::decode(r)?;
let flags = i32::decode(r)?;
@@ -97,3 +101,16 @@ impl<'a> Decode<'a> for AdvancementDisplay<'a> {
})
}
}
+
+#[derive(Clone, Debug, Encode, Decode, Packet)]
+#[packet(id = packet_id::ADVANCEMENT_TAB_C2S)]
+pub enum AdvancementTabC2s<'a> {
+ OpenedTab { tab_id: Ident> },
+ ClosedScreen,
+}
+
+#[derive(Clone, Debug, Encode, Decode, Packet)]
+#[packet(id = packet_id::SELECT_ADVANCEMENT_TAB_S2C)]
+pub struct SelectAdvancementTabS2c<'a> {
+ pub identifier: Option>>,
+}
diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs
index 2b635eea9..5b5d31e24 100644
--- a/crates/valence_anvil/src/lib.rs
+++ b/crates/valence_anvil/src/lib.rs
@@ -56,7 +56,7 @@ pub enum ReadChunkError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
- Nbt(#[from] valence_nbt::Error),
+ Nbt(#[from] valence_nbt::binary::Error),
#[error("invalid chunk sector offset")]
BadSectorOffset,
#[error("invalid chunk size")]
@@ -180,7 +180,7 @@ impl AnvilWorld {
b => return Err(ReadChunkError::UnknownCompressionScheme(b)),
};
- let (data, _) = valence_nbt::from_binary_slice(&mut nbt_slice)?;
+ let (data, _) = Compound::from_binary(&mut nbt_slice)?;
if !nbt_slice.is_empty() {
return Err(ReadChunkError::IncompleteNbtRead);
diff --git a/crates/valence_block/Cargo.toml b/crates/valence_block/Cargo.toml
index 11cb1236e..0fcdcd2ce 100644
--- a/crates/valence_block/Cargo.toml
+++ b/crates/valence_block/Cargo.toml
@@ -7,6 +7,7 @@ edition.workspace = true
valence_core.workspace = true
thiserror.workspace = true
anyhow.workspace = true
+glam.workspace = true
[build-dependencies]
anyhow.workspace = true
diff --git a/crates/valence_block/build.rs b/crates/valence_block/build.rs
index 92b472f43..a3d7829e2 100644
--- a/crates/valence_block/build.rs
+++ b/crates/valence_block/build.rs
@@ -38,8 +38,8 @@ impl Block {
#[derive(Deserialize, Clone, Debug)]
struct BlockEntityKind {
id: u32,
- namespace: String,
- path: String,
+ ident: String,
+ name: String,
}
#[derive(Deserialize, Clone, Debug)]
@@ -86,7 +86,7 @@ fn build() -> anyhow::Result {
let kind_to_translation_key_arms = blocks
.iter()
.map(|b| {
- let kind = ident(b.name.to_pascal_case());
+ let kind = ident(b.name.replace('.', "_").to_pascal_case());
let translation_key = &b.translation_key;
quote! {
Self::#kind => #translation_key,
@@ -97,7 +97,7 @@ fn build() -> anyhow::Result {
let state_to_kind_arms = blocks
.iter()
.map(|b| {
- let name = ident(b.name.to_pascal_case());
+ let name = ident(b.name.replace('.', "_").to_pascal_case());
let mut token_stream = TokenStream::new();
let min_id = b.min_state_id();
@@ -161,14 +161,10 @@ fn build() -> anyhow::Result {
let max_y = s.max_y;
let max_z = s.max_z;
quote! {
- [
- #min_x,
- #min_y,
- #min_z,
- #max_x,
- #max_y,
- #max_z,
- ]
+ Aabb {
+ min: dvec3(#min_x, #min_y, #min_z),
+ max: dvec3(#max_x, #max_y, #max_z),
+ }
}
});
@@ -191,13 +187,13 @@ fn build() -> anyhow::Result {
.iter()
.filter(|&b| !b.properties.is_empty())
.map(|b| {
- let block_kind_name = ident(b.name.to_pascal_case());
+ let block_kind_name = ident(b.name.replace('.', "_").to_pascal_case());
let arms = b
.properties
.iter()
.map(|p| {
- let prop_name = ident(p.name.to_pascal_case());
+ let prop_name = ident(p.name.replace('.', "_").to_pascal_case());
let min_state_id = b.min_state_id();
let product: u16 = b
.properties
@@ -211,7 +207,7 @@ fn build() -> anyhow::Result {
let arms = p.values.iter().enumerate().map(|(i, v)| {
let value_idx = i as u16;
- let value_name = ident(v.to_pascal_case());
+ let value_name = ident(v.replace('.', "_").to_pascal_case());
quote! {
#value_idx => Some(PropValue::#value_name),
}
@@ -239,13 +235,13 @@ fn build() -> anyhow::Result {
.iter()
.filter(|&b| !b.properties.is_empty())
.map(|b| {
- let block_kind_name = ident(b.name.to_pascal_case());
+ let block_kind_name = ident(b.name.replace('.', "_").to_pascal_case());
let arms = b
.properties
.iter()
.map(|p| {
- let prop_name = ident(p.name.to_pascal_case());
+ let prop_name = ident(p.name.replace('.', "_").to_pascal_case());
let min_state_id = b.min_state_id();
let product: u16 = b
.properties
@@ -263,7 +259,7 @@ fn build() -> anyhow::Result {
.enumerate()
.map(|(i, v)| {
let val_idx = i as u16;
- let val_name = ident(v.to_pascal_case());
+ let val_name = ident(v.replace('.', "_").to_pascal_case());
quote! {
PropValue::#val_name =>
Self(self.0 - (self.0 - #min_state_id) / #product % #values_count * #product
@@ -293,7 +289,7 @@ fn build() -> anyhow::Result {
let default_block_states = blocks
.iter()
.map(|b| {
- let name = ident(b.name.to_shouty_snake_case());
+ let name = ident(b.name.replace('.', "_").to_shouty_snake_case());
let state = b.default_state_id;
let doc = format!("The default block state for `{}`.", b.name);
quote! {
@@ -307,10 +303,11 @@ fn build() -> anyhow::Result {
.iter()
.filter(|b| b.wall_variant_id.is_some())
.map(|b| {
- let block_name = ident(b.name.to_shouty_snake_case());
+ let block_name = ident(b.name.replace('.', "_").to_shouty_snake_case());
let wall_block_name = ident(
blocks[b.wall_variant_id.unwrap() as usize]
.name
+ .replace('.', "_")
.to_shouty_snake_case(),
);
quote! {
@@ -335,8 +332,8 @@ fn build() -> anyhow::Result {
let kind_to_state_arms = blocks
.iter()
.map(|b| {
- let kind = ident(b.name.to_pascal_case());
- let state = ident(b.name.to_shouty_snake_case());
+ let kind = ident(b.name.replace('.', "_").to_pascal_case());
+ let state = ident(b.name.replace('.', "_").to_shouty_snake_case());
quote! {
BlockKind::#kind => BlockState::#state,
}
@@ -345,14 +342,14 @@ fn build() -> anyhow::Result {
let block_kind_variants = blocks
.iter()
- .map(|b| ident(b.name.to_pascal_case()))
+ .map(|b| ident(b.name.replace('.', "_").to_pascal_case()))
.collect::>();
let block_kind_from_str_arms = blocks
.iter()
.map(|b| {
let name = &b.name;
- let name_ident = ident(name.to_pascal_case());
+ let name_ident = ident(name.replace('.', "_").to_pascal_case());
quote! {
#name => Some(BlockKind::#name_ident),
}
@@ -363,7 +360,7 @@ fn build() -> anyhow::Result {
.iter()
.map(|b| {
let name = &b.name;
- let name_ident = ident(name.to_pascal_case());
+ let name_ident = ident(name.replace('.', "_").to_pascal_case());
quote! {
BlockKind::#name_ident => #name,
}
@@ -374,8 +371,11 @@ fn build() -> anyhow::Result {
.iter()
.filter(|&b| !b.properties.is_empty())
.map(|b| {
- let name = ident(b.name.to_pascal_case());
- let prop_names = b.properties.iter().map(|p| ident(p.name.to_pascal_case()));
+ let name = ident(b.name.replace('.', "_").to_pascal_case());
+ let prop_names = b
+ .properties
+ .iter()
+ .map(|p| ident(p.name.replace('.', "_").to_pascal_case()));
quote! {
Self::#name => &[#(PropName::#prop_names,)*],
@@ -386,7 +386,7 @@ fn build() -> anyhow::Result {
let block_kind_to_item_kind_arms = blocks
.iter()
.map(|block| {
- let name = ident(block.name.to_pascal_case());
+ let name = ident(block.name.replace('.', "_").to_pascal_case());
let item_id = block.item_id;
quote! {
@@ -399,7 +399,7 @@ fn build() -> anyhow::Result {
.iter()
.filter(|block| block.item_id != 0)
.map(|block| {
- let name = ident(block.name.to_pascal_case());
+ let name = ident(block.name.replace('.', "_").to_pascal_case());
let item_id = block.item_id;
quote! {
@@ -411,7 +411,7 @@ fn build() -> anyhow::Result {
let block_kind_from_raw_arms = blocks
.iter()
.map(|block| {
- let name = ident(block.name.to_pascal_case());
+ let name = ident(block.name.replace('.', "_").to_pascal_case());
let id = block.id;
quote! {
@@ -423,10 +423,10 @@ fn build() -> anyhow::Result {
let block_entity_kind_variants = block_entity_types
.iter()
.map(|block_entity| {
- let name = ident(block_entity.path.to_pascal_case());
+ let name = ident(block_entity.name.replace('.', "_").to_pascal_case());
let doc = format!(
- "The block entity type `{}:{}` (ID {}).",
- block_entity.namespace, block_entity.path, block_entity.id
+ "The block entity type `{}` (ID {}).",
+ block_entity.name, block_entity.id
);
quote! {
#[doc = #doc]
@@ -439,7 +439,7 @@ fn build() -> anyhow::Result {
.iter()
.map(|block_entity| {
let id = block_entity.id;
- let name = ident(block_entity.path.to_pascal_case());
+ let name = ident(block_entity.name.replace('.', "_").to_pascal_case());
quote! {
#id => Some(Self::#name),
@@ -451,7 +451,7 @@ fn build() -> anyhow::Result {
.iter()
.map(|block_entity| {
let id = block_entity.id;
- let name = ident(block_entity.path.to_pascal_case());
+ let name = ident(block_entity.name.replace('.', "_").to_pascal_case());
quote! {
Self::#name => #id,
@@ -462,12 +462,11 @@ fn build() -> anyhow::Result {
let block_entity_kind_from_ident_arms = block_entity_types
.iter()
.map(|block_entity| {
- let name = ident(block_entity.path.to_pascal_case());
- let namespace = &block_entity.namespace;
- let path = &block_entity.path;
+ let name = ident(block_entity.name.replace('.', "_").to_pascal_case());
+ let ident = &block_entity.ident;
quote! {
- (#namespace, #path) => Some(Self::#name),
+ #ident => Some(Self::#name),
}
})
.collect::();
@@ -475,11 +474,8 @@ fn build() -> anyhow::Result {
let block_entity_kind_to_ident_arms = block_entity_types
.iter()
.map(|block_entity| {
- let name = ident(block_entity.path.to_pascal_case());
- let namespace = &block_entity.namespace;
- let path = &block_entity.path;
-
- let ident = format!("{namespace}:{path}");
+ let name = ident(block_entity.name.replace('.', "_").to_pascal_case());
+ let ident = &block_entity.ident;
quote! {
Self::#name => ident!(#ident),
@@ -496,13 +492,13 @@ fn build() -> anyhow::Result {
let prop_name_variants = prop_names
.iter()
- .map(|&name| ident(name.to_pascal_case()))
+ .map(|&name| ident(name.replace('.', "_").to_pascal_case()))
.collect::>();
let prop_name_from_str_arms = prop_names
.iter()
.map(|&name| {
- let ident = ident(name.to_pascal_case());
+ let ident = ident(name.replace('.', "_").to_pascal_case());
quote! {
#name => Some(PropName::#ident),
}
@@ -512,7 +508,7 @@ fn build() -> anyhow::Result {
let prop_name_to_str_arms = prop_names
.iter()
.map(|&name| {
- let ident = ident(name.to_pascal_case());
+ let ident = ident(name.replace('.', "_").to_pascal_case());
quote! {
PropName::#ident => #name,
}
@@ -529,13 +525,13 @@ fn build() -> anyhow::Result {
let prop_value_variants = prop_values
.iter()
- .map(|val| ident(val.to_pascal_case()))
+ .map(|val| ident(val.replace('.', "_").to_pascal_case()))
.collect::>();
let prop_value_from_str_arms = prop_values
.iter()
.map(|val| {
- let ident = ident(val.to_pascal_case());
+ let ident = ident(val.replace('.', "_").to_pascal_case());
quote! {
#val => Some(PropValue::#ident),
}
@@ -545,7 +541,7 @@ fn build() -> anyhow::Result {
let prop_value_to_str_arms = prop_values
.iter()
.map(|val| {
- let ident = ident(val.to_pascal_case());
+ let ident = ident(val.replace('.', "_").to_pascal_case());
quote! {
PropValue::#ident => #val,
}
@@ -577,6 +573,9 @@ fn build() -> anyhow::Result {
let prop_value_count = prop_values.len();
Ok(quote! {
+ use valence_core::aabb::Aabb;
+ use glam::dvec3;
+
/// Represents the state of a block. This does not include block entity data such as
/// the text on a sign, the design on a banner, or the content of a spawner.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
@@ -682,11 +681,11 @@ fn build() -> anyhow::Result {
}
}
- const SHAPES: [[f64; 6]; #shape_count] = [
+ const SHAPES: [Aabb; #shape_count] = [
#(#shapes,)*
];
- pub fn collision_shapes(self) -> impl ExactSizeIterator- + FusedIterator + Clone {
+ pub fn collision_shapes(self) -> impl ExactSizeIterator
- + FusedIterator + Clone {
let shape_idxs: &'static [u16] = match self.0 {
#state_to_collision_shapes_arms
_ => &[],
@@ -943,13 +942,13 @@ fn build() -> anyhow::Result {
}
pub fn from_ident(ident: Ident<&str>) -> Option {
- match (ident.namespace(), ident.path()) {
+ match ident.as_str() {
#block_entity_kind_from_ident_arms
_ => None
}
}
- pub const fn ident(self) -> Ident<&'static str> {
+ pub fn ident(self) -> Ident<&'static str> {
match self {
#block_entity_kind_to_ident_arms
}
diff --git a/crates/valence_block/src/lib.rs b/crates/valence_block/src/lib.rs
index 4a83c5668..d11227ed0 100644
--- a/crates/valence_block/src/lib.rs
+++ b/crates/valence_block/src/lib.rs
@@ -29,8 +29,8 @@ use thiserror::Error;
use valence_core::ident;
use valence_core::ident::Ident;
use valence_core::item::ItemKind;
-use valence_core::packet::var_int::VarInt;
-use valence_core::packet::{Decode, Encode};
+use valence_core::protocol::var_int::VarInt;
+use valence_core::protocol::{Decode, Encode};
include!(concat!(env!("OUT_DIR"), "/block.rs"));
diff --git a/crates/valence_client/Cargo.toml b/crates/valence_client/Cargo.toml
index 8394490c9..617a44924 100644
--- a/crates/valence_client/Cargo.toml
+++ b/crates/valence_client/Cargo.toml
@@ -7,6 +7,7 @@ edition.workspace = true
anyhow.workspace = true
bevy_app.workspace = true
bevy_ecs.workspace = true
+bitfield-struct.workspace = true
bytes.workspace = true
glam.workspace = true
rand.workspace = true
@@ -17,5 +18,5 @@ valence_core.workspace = true
valence_dimension.workspace = true
valence_entity.workspace = true
valence_instance.workspace = true
+valence_nbt.workspace = true
valence_registry.workspace = true
-
diff --git a/crates/valence_client/src/action.rs b/crates/valence_client/src/action.rs
index 086d4681f..4e548e6fb 100644
--- a/crates/valence_client/src/action.rs
+++ b/crates/valence_client/src/action.rs
@@ -1,13 +1,14 @@
use valence_core::block_pos::BlockPos;
use valence_core::direction::Direction;
-use valence_core::packet::c2s::play::player_action::Action;
-use valence_core::packet::c2s::play::PlayerActionC2s;
+use valence_core::protocol::var_int::VarInt;
+use valence_core::protocol::{packet_id, Decode, Encode, Packet};
use super::*;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
+use crate::packet::{PlayerAction, PlayerActionC2s};
pub(super) fn build(app: &mut App) {
- app.add_event::()
+ app.add_event::()
.add_system(
handle_player_action
.in_schedule(EventLoopSchedule)
@@ -17,7 +18,7 @@ pub(super) fn build(app: &mut App) {
}
#[derive(Copy, Clone, Debug)]
-pub struct Digging {
+pub struct DiggingEvent {
pub client: Entity,
pub position: BlockPos,
pub direction: Direction,
@@ -47,7 +48,7 @@ impl ActionSequence {
fn handle_player_action(
mut clients: Query<&mut ActionSequence>,
mut packets: EventReader,
- mut digging_events: EventWriter,
+ mut digging_events: EventWriter,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::() {
@@ -59,28 +60,28 @@ fn handle_player_action(
// TODO: check that blocks are being broken at the appropriate speeds.
match pkt.action {
- Action::StartDestroyBlock => digging_events.send(Digging {
+ PlayerAction::StartDestroyBlock => digging_events.send(DiggingEvent {
client: packet.client,
position: pkt.position,
direction: pkt.direction,
state: DiggingState::Start,
}),
- Action::AbortDestroyBlock => digging_events.send(Digging {
+ PlayerAction::AbortDestroyBlock => digging_events.send(DiggingEvent {
client: packet.client,
position: pkt.position,
direction: pkt.direction,
state: DiggingState::Abort,
}),
- Action::StopDestroyBlock => digging_events.send(Digging {
+ PlayerAction::StopDestroyBlock => digging_events.send(DiggingEvent {
client: packet.client,
position: pkt.position,
direction: pkt.direction,
state: DiggingState::Stop,
}),
- Action::DropAllItems => {}
- Action::DropItem => {}
- Action::ReleaseUseItem => todo!(), // TODO: release use item.
- Action::SwapItemWithOffhand => {}
+ PlayerAction::DropAllItems => {}
+ PlayerAction::DropItem => {}
+ PlayerAction::ReleaseUseItem => {}
+ PlayerAction::SwapItemWithOffhand => {}
}
}
}
@@ -99,3 +100,9 @@ fn acknowledge_player_actions(
}
}
}
+
+#[derive(Copy, Clone, Debug, Encode, Decode, Packet)]
+#[packet(id = packet_id::PLAYER_ACTION_RESPONSE_S2C)]
+pub struct PlayerActionResponseS2c {
+ pub sequence: VarInt,
+}
diff --git a/crates/valence_client/src/chat.rs b/crates/valence_client/src/chat.rs
new file mode 100644
index 000000000..4be42ae2a
--- /dev/null
+++ b/crates/valence_client/src/chat.rs
@@ -0,0 +1,51 @@
+// TODO: delete this module in favor of valence_chat.
+
+use bevy_app::prelude::*;
+use bevy_ecs::prelude::*;
+use valence_core::protocol::encode::WritePacket;
+use valence_core::protocol::packet::chat::{ChatMessageC2s, GameMessageS2c};
+use valence_core::text::Text;
+
+use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
+use crate::Client;
+
+pub(super) fn build(app: &mut App) {
+ app.add_event::().add_system(
+ handle_chat_message
+ .in_schedule(EventLoopSchedule)
+ .in_base_set(EventLoopSet::PreUpdate),
+ );
+}
+
+#[derive(Clone, Debug)]
+pub struct ChatMessageEvent {
+ pub client: Entity,
+ pub message: Box,
+ pub timestamp: u64,
+}
+
+impl Client {
+ /// Sends a system message to the player which is visible in the chat. The
+ /// message is only visible to this client.
+ pub fn send_message(&mut self, msg: impl Into) {
+ self.write_packet(&GameMessageS2c {
+ chat: msg.into().into(),
+ overlay: false,
+ });
+ }
+}
+
+pub fn handle_chat_message(
+ mut packets: EventReader,
+ mut events: EventWriter,
+) {
+ for packet in packets.iter() {
+ if let Some(pkt) = packet.decode::() {
+ events.send(ChatMessageEvent {
+ client: packet.client,
+ message: pkt.message.into(),
+ timestamp: pkt.timestamp,
+ });
+ }
+ }
+}
diff --git a/crates/valence_client/src/command.rs b/crates/valence_client/src/command.rs
index f6b9a915d..f3d5e4685 100644
--- a/crates/valence_client/src/command.rs
+++ b/crates/valence_client/src/command.rs
@@ -1,7 +1,7 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
-use valence_core::packet::c2s::play::client_command::Action;
-use valence_core::packet::c2s::play::ClientCommandC2s;
+use valence_core::protocol::var_int::VarInt;
+use valence_core::protocol::{packet_id, Decode, Encode, Packet};
use valence_entity::entity::Flags;
use valence_entity::{entity, Pose};
@@ -74,7 +74,7 @@ fn handle_client_command(
for packet in packets.iter() {
if let Some(pkt) = packet.decode::() {
match pkt.action {
- Action::StartSneaking => {
+ ClientCommand::StartSneaking => {
if let Ok((mut pose, mut flags)) = clients.get_mut(packet.client) {
pose.0 = Pose::Sneaking;
flags.set_sneaking(true);
@@ -85,7 +85,7 @@ fn handle_client_command(
state: SneakState::Start,
})
}
- Action::StopSneaking => {
+ ClientCommand::StopSneaking => {
if let Ok((mut pose, mut flags)) = clients.get_mut(packet.client) {
pose.0 = Pose::Standing;
flags.set_sneaking(false);
@@ -96,10 +96,10 @@ fn handle_client_command(
state: SneakState::Stop,
})
}
- Action::LeaveBed => leave_bed_events.send(LeaveBed {
+ ClientCommand::LeaveBed => leave_bed_events.send(LeaveBed {
client: packet.client,
}),
- Action::StartSprinting => {
+ ClientCommand::StartSprinting => {
if let Ok((_, mut flags)) = clients.get_mut(packet.client) {
flags.set_sprinting(true);
}
@@ -109,7 +109,7 @@ fn handle_client_command(
state: SprintState::Start,
});
}
- Action::StopSprinting => {
+ ClientCommand::StopSprinting => {
if let Ok((_, mut flags)) = clients.get_mut(packet.client) {
flags.set_sprinting(false);
}
@@ -119,18 +119,18 @@ fn handle_client_command(
state: SprintState::Stop,
})
}
- Action::StartJumpWithHorse => jump_with_horse_events.send(JumpWithHorse {
+ ClientCommand::StartJumpWithHorse => jump_with_horse_events.send(JumpWithHorse {
client: packet.client,
state: JumpWithHorseState::Start {
power: pkt.jump_boost.0 as u8,
},
}),
- Action::StopJumpWithHorse => jump_with_horse_events.send(JumpWithHorse {
+ ClientCommand::StopJumpWithHorse => jump_with_horse_events.send(JumpWithHorse {
client: packet.client,
state: JumpWithHorseState::Stop,
}),
- Action::OpenHorseInventory => {} // TODO
- Action::StartFlyingWithElytra => {
+ ClientCommand::OpenHorseInventory => {} // TODO
+ ClientCommand::StartFlyingWithElytra => {
if let Ok((mut pose, _)) = clients.get_mut(packet.client) {
pose.0 = Pose::FallFlying;
}
@@ -141,3 +141,24 @@ fn handle_client_command(
}
}
}
+
+#[derive(Copy, Clone, Debug, Encode, Decode, Packet)]
+#[packet(id = packet_id::CLIENT_COMMAND_C2S)]
+pub struct ClientCommandC2s {
+ pub entity_id: VarInt,
+ pub action: ClientCommand,
+ pub jump_boost: VarInt,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)]
+pub enum ClientCommand {
+ StartSneaking,
+ StopSneaking,
+ LeaveBed,
+ StartSprinting,
+ StopSprinting,
+ StartJumpWithHorse,
+ StopJumpWithHorse,
+ OpenHorseInventory,
+ StartFlyingWithElytra,
+}
diff --git a/crates/valence_client/src/custom_payload.rs b/crates/valence_client/src/custom_payload.rs
new file mode 100644
index 000000000..ed7b0b8b7
--- /dev/null
+++ b/crates/valence_client/src/custom_payload.rs
@@ -0,0 +1,58 @@
+use valence_core::protocol::raw::RawBytes;
+use valence_core::protocol::{packet_id, Decode, Encode};
+
+use super::*;
+use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
+
+pub(super) fn build(app: &mut App) {
+ app.add_event::().add_system(
+ handle_custom_payload
+ .in_schedule(EventLoopSchedule)
+ .in_base_set(EventLoopSet::PreUpdate),
+ );
+}
+
+#[derive(Clone, Debug)]
+pub struct CustomPayloadEvent {
+ pub client: Entity,
+ pub channel: Ident