mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11107 from Detimes/feat-10404-older-clients-2
🤖 Agent: 26.2 packet layouts are only used inside a session that negotiated V26_2, and accept_older_clients stays off by default, so a 26.3 client's path is unchanged; the lan_bot tests cover both.
This commit is contained in:
9 files changed
+1858
-26
No files matched your search
@@ -170,6 +170,8 @@ struct Server {
|
||||
world: World,
|
||||
next_entity: u32,
|
||||
placed: Vec<(u32, [i32; 3], u32, i32)>,
|
||||
/// Every packet the hub handed to the game, in order.
|
||||
packets: Vec<S>,
|
||||
}
|
||||
impl Server {
|
||||
fn tick(&mut self, entities: &[EntityState]) {
|
||||
@@ -193,6 +195,7 @@ impl Server {
|
||||
match event {
|
||||
HostEvent::Joined(conn) => self.hub.place_new_player(conn, &mut ctx),
|
||||
HostEvent::Packet(conn, packet) => {
|
||||
self.packets.push((*packet).clone());
|
||||
if let S::UseItemOn(p) = *packet {
|
||||
let at = [p.pos[0], p.pos[1] + 1, p.pos[2]];
|
||||
ctx.world
|
||||
@@ -260,6 +263,7 @@ fn server_sync(sync: Arc<SyncData>, net: NetConfig) -> Server {
|
||||
world: flat_world(),
|
||||
next_entity: 100,
|
||||
placed: Vec::new(),
|
||||
packets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1759,8 +1763,9 @@ fn a_boat_floating_too_long_kicks_its_driver() {
|
||||
}
|
||||
|
||||
/// A client that numbers its packets as `version` does, from the protocol adapter's own tables: for 26.2 its
|
||||
/// handshake states 776 and every id it sends and reads is 26.2's. The payloads stay the pinned ones: the
|
||||
/// packets whose layout changed are a later step of `older-clients`.
|
||||
/// handshake states 776 and every id it sends and reads is 26.2's. [`Self::send`]'s payloads are the pinned
|
||||
/// ones; [`Self::send_raw`] sends a packet as the version lays it out. The clientbound world packets arrive
|
||||
/// in 26.2's layout.
|
||||
struct Numbered {
|
||||
c: Connection,
|
||||
version: ServerVersion,
|
||||
@@ -1775,6 +1780,13 @@ impl Numbered {
|
||||
.ok_or("the client's version has no such packet")?;
|
||||
self.c.send(&body).map_err(|e| e.to_string())
|
||||
}
|
||||
/// A packet by the version's own `id`, its payload in the version's layout.
|
||||
fn send_raw(&mut self, id: i32, payload: &[u8]) -> Result<(), String> {
|
||||
let mut w = horizon_protocol::buf::Writer::new();
|
||||
w.varint(id);
|
||||
w.bytes(payload);
|
||||
self.c.send(&w.data).map_err(|e| e.to_string())
|
||||
}
|
||||
/// The next packet with its pinned id (the version's own id read off the wire).
|
||||
fn recv(&mut self) -> Result<(i32, Vec<u8>), String> {
|
||||
let (id, payload) = self.c.recv().map_err(|e| e.to_string())?;
|
||||
@@ -1975,6 +1987,28 @@ fn a_26_2_client_joins_only_a_server_that_accepts_older_clients() {
|
||||
if id == horizon_protocol::ids::play::clientbound::COMMANDS {
|
||||
continue;
|
||||
}
|
||||
// World packets come in 26.2's own layout, which the pinned readers don't read (the adapter's
|
||||
// unit tests read them back with translate's 26.2 readers).
|
||||
{
|
||||
use horizon_protocol::ids::play::clientbound as cb;
|
||||
match id {
|
||||
cb::LEVEL_CHUNK_WITH_LIGHT => {
|
||||
chunks += 1;
|
||||
continue;
|
||||
}
|
||||
cb::LIGHT_UPDATE
|
||||
| cb::LEVEL_PARTICLES
|
||||
| cb::EXPLODE
|
||||
| cb::MOVE_ENTITY_POS
|
||||
| cb::MOVE_ENTITY_POS_ROT
|
||||
| cb::MOVE_ENTITY_ROT
|
||||
| cb::ENTITY_POSITION_SYNC
|
||||
| cb::ANIMATE
|
||||
// Its tag displays are 26.2's bare tag ids.
|
||||
| cb::RECIPE_BOOK_ADD => continue,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match C::decode(id, &payload).map_err(|e| format!("packet {id}: {e}"))? {
|
||||
C::Login(_) => login = true,
|
||||
C::PlayerPosition(p) => bot.send(
|
||||
@@ -1988,7 +2022,6 @@ fn a_26_2_client_joins_only_a_server_that_accepts_older_clients() {
|
||||
})
|
||||
.encode(),
|
||||
)?,
|
||||
C::LevelChunkWithLight(_) => chunks += 1,
|
||||
C::ChunkBatchFinished(_) => {
|
||||
bot.send(
|
||||
S::ChunkBatchReceived(play::ChunkBatchReceived {
|
||||
@@ -2037,3 +2070,140 @@ fn a_26_2_client_joins_only_a_server_that_accepts_older_clients() {
|
||||
assert_eq!(placed, Some(([9, MIN_Y + 5, 9], STONE as i32)));
|
||||
assert_eq!(play.placed.len(), 1);
|
||||
}
|
||||
|
||||
/// The serverbound packets whose layout 26.3 changed reach the game from a 26.2 client as 26.3's: its
|
||||
/// teleport confirmations (the id alone) are taken, so its walk counts; its digging actions past 0 are one
|
||||
/// up (start 0 stays 0, stop 2 is 3); its main-hand swing is a punch and its off-hand swing nothing.
|
||||
#[test]
|
||||
fn a_26_2_clients_walk_dig_swing_and_teleport_confirmations_read_as_26_3s() {
|
||||
let Some(resources) = resources() else {
|
||||
return;
|
||||
};
|
||||
// 26.2's serverbound ids (play) of the packets whose layout changed.
|
||||
const ACCEPT_TELEPORTATION_26_2: i32 = 0;
|
||||
const PLAYER_ACTION_26_2: i32 = 41;
|
||||
const SWING_26_2: i32 = 63;
|
||||
let net = NetConfig {
|
||||
accept_older_clients: true,
|
||||
older_clients_play: true,
|
||||
..NetConfig::loopback(0, "Test - World".into(), 8)
|
||||
};
|
||||
let mut s = server_with(&resources, net);
|
||||
let port = s.hub.lan.port;
|
||||
let below = [9, MIN_Y + 4, 9];
|
||||
let walked = [9.5, f64::from(MIN_Y + 5), 9.5];
|
||||
let (_bot, confirmed) = run(&mut s, move || {
|
||||
let (mut bot, _) =
|
||||
join_as(ServerVersion::V26_2, port, "Old")?.map_err(|r| format!("refused: {r}"))?;
|
||||
let (mut confirmed, mut chunks, mut sent) = (0, 0, false);
|
||||
let deadline = Instant::now() + Duration::from_secs(15);
|
||||
while Instant::now() < deadline {
|
||||
let (id, payload) = bot.recv()?;
|
||||
// The command tree has argument nodes the client side does not decode.
|
||||
if id == horizon_protocol::ids::play::clientbound::COMMANDS {
|
||||
continue;
|
||||
}
|
||||
// The packets written in 26.2's layout, which the pinned readers don't read.
|
||||
{
|
||||
use horizon_protocol::ids::play::clientbound as cb;
|
||||
if id == cb::LEVEL_CHUNK_WITH_LIGHT {
|
||||
chunks += 1;
|
||||
}
|
||||
if matches!(
|
||||
id,
|
||||
cb::LEVEL_CHUNK_WITH_LIGHT
|
||||
| cb::LIGHT_UPDATE
|
||||
| cb::LEVEL_PARTICLES
|
||||
| cb::EXPLODE
|
||||
| cb::MOVE_ENTITY_POS
|
||||
| cb::MOVE_ENTITY_POS_ROT
|
||||
| cb::MOVE_ENTITY_ROT
|
||||
| cb::ENTITY_POSITION_SYNC
|
||||
| cb::ANIMATE
|
||||
| cb::RECIPE_BOOK_ADD
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
match C::decode(id, &payload).map_err(|e| format!("packet {id}: {e}"))? {
|
||||
C::PlayerPosition(p) => {
|
||||
let mut w = horizon_protocol::buf::Writer::new();
|
||||
w.varint(p.teleport);
|
||||
bot.send_raw(ACCEPT_TELEPORTATION_26_2, &w.data)?;
|
||||
confirmed += 1;
|
||||
}
|
||||
C::ChunkBatchFinished(_) => {
|
||||
bot.send(
|
||||
S::ChunkBatchReceived(play::ChunkBatchReceived {
|
||||
chunks_per_tick: 20.0,
|
||||
})
|
||||
.encode(),
|
||||
)?;
|
||||
}
|
||||
C::BlockChangedAck(a) if a.sequence == 77 => break,
|
||||
_ => {}
|
||||
}
|
||||
if chunks >= 9 && confirmed > 0 && !sent {
|
||||
sent = true;
|
||||
bot.send(S::PlayerLoaded(Empty).encode())?;
|
||||
// A walk from the spawn (8.5, 8.5), on the stone: one move, as vanilla kicks a second in a tick.
|
||||
bot.send(
|
||||
S::MovePlayerPos(play::MovePlayerPos {
|
||||
x: walked[0],
|
||||
y: walked[1],
|
||||
z: walked[2],
|
||||
flags: 1,
|
||||
})
|
||||
.encode(),
|
||||
)?;
|
||||
// Start and stop destroying the block below, as 26.2 numbers the actions.
|
||||
for (action, sequence) in [(0, 5), (2, 6)] {
|
||||
let mut w = horizon_protocol::buf::Writer::new();
|
||||
w.varint(action);
|
||||
w.i64(horizon_protocol::buf::pack_block_pos(below));
|
||||
w.u8(1);
|
||||
w.varint(sequence);
|
||||
bot.send_raw(PLAYER_ACTION_26_2, &w.data)?;
|
||||
}
|
||||
// A main-hand swing, then an off-hand one.
|
||||
bot.send_raw(SWING_26_2, &[0])?;
|
||||
bot.send_raw(SWING_26_2, &[1])?;
|
||||
// Its acknowledgement says the hub has handled everything before it.
|
||||
bot.send(
|
||||
S::UseItemOn(play::UseItemOn {
|
||||
hand: 0,
|
||||
pos: [12, MIN_Y + 4, 12],
|
||||
face: 1,
|
||||
cursor: [0.5, 1.0, 0.5],
|
||||
inside: false,
|
||||
world_border: false,
|
||||
sequence: 77,
|
||||
})
|
||||
.encode(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
assert!(sent, "the client never got its chunks");
|
||||
// The bot is kept, so its guest stays to be looked at.
|
||||
Ok((bot, confirmed))
|
||||
});
|
||||
assert!(confirmed > 0, "no teleport to confirm");
|
||||
let g = &s.hub.guests[0];
|
||||
assert!(g.left_reason.is_empty(), "{}", g.left_reason);
|
||||
assert_eq!(g.position, walked, "the walk was not taken");
|
||||
let actions: Vec<i32> = s
|
||||
.packets
|
||||
.iter()
|
||||
.filter_map(|p| match p {
|
||||
S::PlayerAction(a) if a.pos == below => Some(a.action),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(actions, [0, 3]);
|
||||
let punches = s
|
||||
.packets
|
||||
.iter()
|
||||
.filter(|p| matches!(p, S::Punch(_)))
|
||||
.count();
|
||||
assert_eq!(punches, 1);
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
//! ([`ClientSession`], roadmap `older-clients`), when its owner lets such clients join.
|
||||
pub mod client_session;
|
||||
pub mod ids;
|
||||
pub mod layout_26_2;
|
||||
pub mod session;
|
||||
mod translate;
|
||||
pub mod v1_21_10;
|
||||
|
||||
@@ -3,18 +3,29 @@
|
||||
//! reads it; what the server sends is renumbered to the client's, registry ids included, with a stand-in
|
||||
//! ([`ids::Substitutes`]) for a block, item, entity, particle or sound the client's version lacks.
|
||||
//!
|
||||
//! This layer maps ids only: a packet whose layout changed between the client's release and the pinned one
|
||||
//! passes in the pinned layout (the mirror of `translate`'s readers comes next).
|
||||
//! This layer maps ids; the world packets whose layout changed between 26.2 and the pinned release are
|
||||
//! written in 26.2's layout by [`world`]. Serverbound, 26.2's `accept_teleportation`, `player_action` and
|
||||
//! `swing` are read as 26.3's (the mirror of `translate`'s writers); other packets whose layout changed
|
||||
//! still pass in the pinned layout.
|
||||
//!
|
||||
//! The states change where vanilla's server-side `Connection` switches its codecs, after the handshake the
|
||||
//! server read itself: `login_acknowledged` and `login_finished` move each direction to configuration,
|
||||
//! `finish_configuration` to play, and `configuration_acknowledged` / `start_configuration` back.
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU8, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
mod world;
|
||||
|
||||
use super::{
|
||||
ServerVersion,
|
||||
ids::{self, Ids, Way, down_or_substitute},
|
||||
translate::packet_ids_entity::FALLING_BLOCK,
|
||||
layout_26_2,
|
||||
translate::{packet_ids_entity::FALLING_BLOCK, swing_id},
|
||||
};
|
||||
use crate::{
|
||||
Result, State,
|
||||
@@ -50,6 +61,58 @@ pub struct ClientSession {
|
||||
reading: AtomicU8,
|
||||
/// What the server sends.
|
||||
writing: AtomicU8,
|
||||
/// Where the client last said or was told it is, for the place 26.3's `accept_teleportation` states and
|
||||
/// 26.2's leaves out.
|
||||
place: Mutex<Place>,
|
||||
/// Teleports (`player_position`) sent and not yet accepted, oldest first.
|
||||
teleports: Mutex<VecDeque<Teleport>>,
|
||||
}
|
||||
|
||||
/// A position, yaw and pitch.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
struct Place {
|
||||
position: [f64; 3],
|
||||
yaw: f32,
|
||||
pitch: f32,
|
||||
}
|
||||
|
||||
/// A `player_position` the server sent.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Teleport {
|
||||
id: i32,
|
||||
to: Place,
|
||||
relatives: i32,
|
||||
}
|
||||
|
||||
impl Teleport {
|
||||
/// Where a client at `from` is once it took this teleport, as `LocalPlayer` applies
|
||||
/// `ClientboundPlayerPositionPacket`'s relative flags.
|
||||
fn applied(&self, from: Place) -> Place {
|
||||
use play::relative as rel;
|
||||
let pick = |bit: i32, base: f64, v: f64| {
|
||||
if self.relatives & bit != 0 {
|
||||
base + v
|
||||
} else {
|
||||
v
|
||||
}
|
||||
};
|
||||
let angle = |bit: i32, base: f32, v: f32| {
|
||||
if self.relatives & bit != 0 {
|
||||
base + v
|
||||
} else {
|
||||
v
|
||||
}
|
||||
};
|
||||
Place {
|
||||
position: [
|
||||
pick(rel::X, from.position[0], self.to.position[0]),
|
||||
pick(rel::Y, from.position[1], self.to.position[1]),
|
||||
pick(rel::Z, from.position[2], self.to.position[2]),
|
||||
],
|
||||
yaw: angle(rel::Y_ROT, from.yaw, self.to.yaw),
|
||||
pitch: angle(rel::X_ROT, from.pitch, self.to.pitch).clamp(-90.0, 90.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientSession {
|
||||
@@ -59,6 +122,8 @@ impl ClientSession {
|
||||
version,
|
||||
reading: AtomicU8::new(to_u8(State::Login)),
|
||||
writing: AtomicU8::new(to_u8(State::Login)),
|
||||
place: Mutex::new(Place::default()),
|
||||
teleports: Mutex::new(VecDeque::new()),
|
||||
}
|
||||
}
|
||||
pub fn reading(&self) -> State {
|
||||
@@ -69,9 +134,14 @@ impl ClientSession {
|
||||
}
|
||||
|
||||
/// A packet the client sent, with the id the pinned version knows it by. `None` drops it: the pinned
|
||||
/// version has no such packet (26.2's `swing`).
|
||||
/// version has no such packet.
|
||||
pub fn serverbound(&self, id: i32, payload: Vec<u8>) -> Result<Option<(i32, Vec<u8>)>> {
|
||||
let state = self.reading();
|
||||
// 26.3's punch took over the older swing of the main hand; an off-hand swing has no packet there.
|
||||
if state == State::Play && id == swing_id(self.version) {
|
||||
let hand = Reader::new(&payload).varint()?;
|
||||
return Ok((hand == 0).then_some((packet_ids::play::serverbound::PUNCH, Vec::new())));
|
||||
}
|
||||
let Some(pinned) = self.version.serverbound_id_up(state, id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -85,10 +155,27 @@ impl ClientSession {
|
||||
_ => state,
|
||||
};
|
||||
self.reading.store(to_u8(next), Ordering::Release);
|
||||
use packet_ids::play::serverbound as sb;
|
||||
let payload = match (state, pinned) {
|
||||
(State::Play, sb::ACCEPT_TELEPORTATION) => self.accept_teleport(&payload)?,
|
||||
// 26.3 inserted CHANGE_DESTROY_DIRECTION at 1: 26.2's actions past 0 are one up.
|
||||
(State::Play, sb::PLAYER_ACTION) => {
|
||||
let mut r = Reader::new(&payload);
|
||||
let action = r.varint()?;
|
||||
let mut w = Writer::new();
|
||||
w.varint(if action == 0 { 0 } else { action + 1 });
|
||||
w.bytes(r.rest());
|
||||
w.data
|
||||
}
|
||||
(State::Play, sb::MOVE_PLAYER_POS | sb::MOVE_PLAYER_POS_ROT | sb::MOVE_PLAYER_ROT) => {
|
||||
self.moved(pinned, &payload)?;
|
||||
payload
|
||||
}
|
||||
_ => payload,
|
||||
};
|
||||
let Some(ids) = self.version.ids() else {
|
||||
return Ok(Some((pinned, payload)));
|
||||
};
|
||||
use packet_ids::play::serverbound as sb;
|
||||
let payload = match (state, pinned) {
|
||||
// The stacks a client names carry its item and component ids.
|
||||
(State::Play, sb::SET_CREATIVE_MODE_SLOT | sb::CONTAINER_CLICK) => {
|
||||
@@ -120,11 +207,32 @@ impl ClientSession {
|
||||
_ => state,
|
||||
};
|
||||
self.writing.store(to_u8(next), Ordering::Release);
|
||||
// 26.2 has no swing_animation: its hands swing through animate (world.rs).
|
||||
if state == State::Play
|
||||
&& id == packet_ids::play::clientbound::SWING_ANIMATION
|
||||
&& self.version.ids().is_some()
|
||||
{
|
||||
let animate = packet_ids::play::clientbound::ANIMATE;
|
||||
let Some(mapped) = self.version.clientbound_id_down(state, animate) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut w = Writer::new();
|
||||
w.varint(mapped);
|
||||
w.bytes(&world::swing(payload)?);
|
||||
return Ok(Some(w.data));
|
||||
}
|
||||
if (state, id) == (State::Play, packet_ids::play::clientbound::PLAYER_POSITION) {
|
||||
self.teleporting(payload)?;
|
||||
}
|
||||
let Some(mapped) = self.version.clientbound_id_down(state, id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
// A packet the renumbering can't read goes as it is, rather than the connection dropping here.
|
||||
let rewritten = match self.version.ids().map(|ids| down(ids, state, id, payload)) {
|
||||
let rewritten = match self
|
||||
.version
|
||||
.ids()
|
||||
.map(|ids| layout_then_ids(self.version, ids, state, id, payload))
|
||||
{
|
||||
None | Some(Ok(Sent::AsIs)) => None,
|
||||
Some(Ok(Sent::Rewritten(payload))) => Some(payload),
|
||||
Some(Ok(Sent::Dropped)) => return Ok(None),
|
||||
@@ -143,6 +251,82 @@ impl ClientSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 26.2 client's packets whose layout changed are written in its layout ([`layout_26_2`], when enabled),
|
||||
/// the others renumbered by [`down`].
|
||||
fn layout_then_ids(
|
||||
version: ServerVersion,
|
||||
ids: &'static Ids,
|
||||
state: State,
|
||||
id: i32,
|
||||
payload: &[u8],
|
||||
) -> Result<Sent> {
|
||||
if version == ServerVersion::V26_2 && layout_26_2::enabled() {
|
||||
match layout_26_2::clientbound(ids, state, id, payload)? {
|
||||
layout_26_2::Out::Untouched => {}
|
||||
layout_26_2::Out::Rewritten(p) => return Ok(Sent::Rewritten(p)),
|
||||
layout_26_2::Out::Dropped => return Ok(Sent::Dropped),
|
||||
}
|
||||
}
|
||||
down(ids, state, id, payload)
|
||||
}
|
||||
|
||||
impl ClientSession {
|
||||
/// A `player_position` on its way: the client accepts it later by its id alone.
|
||||
fn teleporting(&self, payload: &[u8]) -> Result<()> {
|
||||
let mut r = Reader::new(payload);
|
||||
let id = r.varint()?;
|
||||
let position = [r.f64()?, r.f64()?, r.f64()?];
|
||||
let _delta = [r.f64()?, r.f64()?, r.f64()?];
|
||||
let to = Place {
|
||||
position,
|
||||
yaw: r.f32()?,
|
||||
pitch: r.f32()?,
|
||||
};
|
||||
let relatives = r.i32()?;
|
||||
self.teleports
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_back(Teleport { id, to, relatives });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A move the client reports (`move_player_pos`, `_pos_rot`, `_rot`): where it now is.
|
||||
fn moved(&self, pinned: i32, payload: &[u8]) -> Result<()> {
|
||||
use packet_ids::play::serverbound as sb;
|
||||
let mut r = Reader::new(payload);
|
||||
let mut place = self.place.lock().unwrap();
|
||||
if pinned != sb::MOVE_PLAYER_ROT {
|
||||
place.position = [r.f64()?, r.f64()?, r.f64()?];
|
||||
}
|
||||
if pinned != sb::MOVE_PLAYER_POS {
|
||||
place.yaw = r.f32()?;
|
||||
place.pitch = r.f32()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 26.2's `accept_teleportation` (the teleport's id) as 26.3's, which also states where the client
|
||||
/// now is: the teleport it accepts, and any sent before it, applied in order to where it was.
|
||||
fn accept_teleport(&self, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let id = Reader::new(payload).varint()?;
|
||||
let mut place = self.place.lock().unwrap();
|
||||
let mut teleports = self.teleports.lock().unwrap();
|
||||
if let Some(i) = teleports.iter().position(|t| t.id == id) {
|
||||
for t in teleports.drain(..=i) {
|
||||
*place = t.applied(*place);
|
||||
}
|
||||
}
|
||||
let mut w = Writer::new();
|
||||
w.varint(id);
|
||||
for v in place.position {
|
||||
w.f64(v);
|
||||
}
|
||||
w.f32(place.yaw);
|
||||
w.f32(place.pitch);
|
||||
Ok(w.data)
|
||||
}
|
||||
}
|
||||
|
||||
/// What becomes of a clientbound payload on its way to an older client.
|
||||
enum Sent {
|
||||
AsIs,
|
||||
@@ -196,6 +380,10 @@ fn down(ids: &'static Ids, state: State, id: i32, payload: &[u8]) -> Result<Sent
|
||||
if state != State::Play {
|
||||
return Ok(Sent::AsIs);
|
||||
}
|
||||
// Packets whose layout 26.2 has differently (world.rs).
|
||||
if let Some(sent) = world::down(ids, id, payload)? {
|
||||
return Ok(sent);
|
||||
}
|
||||
match id {
|
||||
cb::UPDATE_TAGS
|
||||
| cb::ADD_ENTITY
|
||||
@@ -386,12 +574,97 @@ mod tests {
|
||||
// add_transient_block (37) is new in 26.3: not sent.
|
||||
let transient = packet_ids::play::clientbound::ADD_TRANSIENT_BLOCK;
|
||||
assert_eq!(s.clientbound(&body(transient, &[0; 9])).unwrap(), None);
|
||||
// 26.2's resource_pack (49) is 26.3's 50; its swing (63) has no pinned packet.
|
||||
// 26.2's resource_pack (49) is 26.3's 50.
|
||||
assert_eq!(
|
||||
s.serverbound(49, vec![7]).unwrap(),
|
||||
Some((packet_ids::play::serverbound::RESOURCE_PACK, vec![7]))
|
||||
);
|
||||
assert_eq!(s.serverbound(63, vec![0]).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_26_2_main_hand_swing_is_a_punch_and_an_off_hand_one_nothing() {
|
||||
let s = in_play();
|
||||
// swing: 63 in 26.2, its hand a VarInt (0 main, 1 off); 26.3's punch has no payload.
|
||||
assert_eq!(
|
||||
s.serverbound(63, vec![0]).unwrap(),
|
||||
Some((packet_ids::play::serverbound::PUNCH, Vec::new()))
|
||||
);
|
||||
assert_eq!(s.serverbound(63, vec![1]).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_26_2_digging_action_past_0_is_one_up() {
|
||||
let s = in_play();
|
||||
let action = |n: i32| {
|
||||
let mut w = Writer::new();
|
||||
w.varint(n);
|
||||
w.i64(crate::buf::pack_block_pos([1, 2, 3]));
|
||||
w.u8(1);
|
||||
w.varint(9);
|
||||
// player_action: 41 in both.
|
||||
let (id, payload) = s.serverbound(41, w.data).unwrap().unwrap();
|
||||
assert_eq!(id, packet_ids::play::serverbound::PLAYER_ACTION);
|
||||
match play::Serverbound::decode(id, &payload).unwrap() {
|
||||
play::Serverbound::PlayerAction(p) => {
|
||||
assert_eq!((p.pos, p.direction, p.sequence), ([1, 2, 3], 1, 9));
|
||||
p.action
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
};
|
||||
assert_eq!(action(0), 0); // start destroying
|
||||
assert_eq!(action(1), 2); // abort: 1 in 26.2, 2 in 26.3
|
||||
assert_eq!(action(2), 3); // stop destroying
|
||||
assert_eq!(action(6), 7); // swap held items
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_26_2_teleport_confirmation_states_where_the_teleport_put_the_client() {
|
||||
use play::relative as rel;
|
||||
let s = in_play();
|
||||
let teleport = |id: i32, position: [f64; 3], yaw: f32, relatives: i32| {
|
||||
let p = play::Clientbound::PlayerPosition(play::PlayerPosition {
|
||||
teleport: id,
|
||||
position,
|
||||
delta: [0.0; 3],
|
||||
yaw,
|
||||
pitch: 30.0,
|
||||
relatives,
|
||||
});
|
||||
s.clientbound(&p.encode()).unwrap().unwrap();
|
||||
};
|
||||
let accept = |id: i32| {
|
||||
let mut w = Writer::new();
|
||||
w.varint(id);
|
||||
// accept_teleportation: 0 in both.
|
||||
let (pinned, payload) = s.serverbound(0, w.data).unwrap().unwrap();
|
||||
assert_eq!(pinned, packet_ids::play::serverbound::ACCEPT_TELEPORTATION);
|
||||
match play::Serverbound::decode(pinned, &payload).unwrap() {
|
||||
play::Serverbound::AcceptTeleportation(p) => {
|
||||
assert_eq!(p.id, id);
|
||||
([p.x, p.y, p.z], p.yaw, p.pitch)
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
};
|
||||
teleport(1, [8.5, -60.0, 8.5], 90.0, 0);
|
||||
assert_eq!(accept(1), ([8.5, -60.0, 8.5], 90.0, 30.0));
|
||||
// The client walks (move_player_pos: 30 in both), then is moved by (1, 2, 3) and turned by 10.
|
||||
let mut w = Writer::new();
|
||||
for v in [10.0, -60.0, 10.0] {
|
||||
w.f64(v);
|
||||
}
|
||||
w.u8(1);
|
||||
s.serverbound(30, w.data).unwrap().unwrap();
|
||||
teleport(
|
||||
2,
|
||||
[1.0, 2.0, 3.0],
|
||||
10.0,
|
||||
rel::X | rel::Y | rel::Z | rel::Y_ROT,
|
||||
);
|
||||
assert_eq!(accept(2), ([11.0, -58.0, 13.0], 100.0, 30.0));
|
||||
// An unknown id: where the client last was.
|
||||
assert_eq!(accept(7), ([11.0, -58.0, 13.0], 100.0, 30.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,757 @@
|
||||
//! Clientbound world packets in 26.2's layout for a 26.2 client (#10404, the mirror of `translate`'s
|
||||
//! hand-written 26.2 readers): chunk columns and light, particles, explosions, entity moves and hand
|
||||
//! swings. Everything else in a packet keeps its pinned layout; ids go down as the rest of
|
||||
//! [`super`] renumbers them, with stand-ins where 26.2 lacks an entry.
|
||||
use super::{Sent, state_down};
|
||||
use crate::{
|
||||
Result,
|
||||
adapter::ids::{self, Ids, Way, down_or_substitute},
|
||||
buf::{Codec, Reader, Writer},
|
||||
chunk::{self, Paletted, Section, Strategy},
|
||||
entity_data::{Particle, ParticleData},
|
||||
ids as packet_ids,
|
||||
packets::play,
|
||||
};
|
||||
|
||||
/// `SoundEvents.EMPTY`: what a 26.2 client is told to play for a blast 26.3 keeps silent.
|
||||
const SILENT: &str = "minecraft:intentionally_empty";
|
||||
|
||||
/// The pinned play packet `id` in 26.2's layout; `None` when 26.2 lays it out as the pinned version does.
|
||||
pub(super) fn down(ids: &'static Ids, id: i32, payload: &[u8]) -> Result<Option<Sent>> {
|
||||
use packet_ids::play::clientbound as cb;
|
||||
Ok(Some(match id {
|
||||
cb::LEVEL_CHUNK_WITH_LIGHT => Sent::Rewritten(level_chunk(ids, payload)?),
|
||||
cb::LIGHT_UPDATE => Sent::Rewritten(light_update(payload)?),
|
||||
cb::LEVEL_PARTICLES => particles(ids, payload)?,
|
||||
cb::EXPLODE => explode(ids, payload)?,
|
||||
cb::MOVE_ENTITY_POS | cb::MOVE_ENTITY_POS_ROT | cb::MOVE_ENTITY_ROT => {
|
||||
Sent::Rewritten(move_entity(id, payload)?)
|
||||
}
|
||||
cb::ENTITY_POSITION_SYNC => Sent::Rewritten(position_sync(payload)?),
|
||||
cb::ANIMATE => animate(payload)?,
|
||||
_ => return Ok(None),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The column with its block states renumbered (stand-ins for states 26.2 lacks) and packed for 26.2's
|
||||
/// narrower global palette, the block entities 26.2 has, and the light masks as 26.2 sends them. Biomes are
|
||||
/// the server's own registry and stay as they are.
|
||||
fn level_chunk(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut p = play::LevelChunkWithLight::read(&mut Reader::new(payload))?;
|
||||
let (old_bits, new_bits) = ids.state_bits();
|
||||
let mut r = Reader::new(&p.chunk.buffer);
|
||||
let mut sections = Vec::new();
|
||||
while r.remaining() > 0 {
|
||||
let mut s = Section::read(&mut r, new_bits, 0)?;
|
||||
s.states = states_down(ids, &s.states, old_bits);
|
||||
sections.push(s);
|
||||
}
|
||||
p.chunk.buffer = chunk::ChunkData::from_sections(§ions);
|
||||
// A block entity type 26.2 lacks goes; its block still draws.
|
||||
p.chunk.block_entities.retain_mut(|b| {
|
||||
ids.block_entity_type
|
||||
.down(b.kind)
|
||||
.map(|kind| b.kind = kind)
|
||||
.is_some()
|
||||
});
|
||||
let mut w = Writer::new();
|
||||
w.i32(p.x);
|
||||
w.i32(p.z);
|
||||
chunk::ChunkData::write(&p.chunk, &mut w);
|
||||
light_down(&p.light, &mut w);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// A section's states in 26.2's numbering: a palette's entries renumbered, or the global palette repacked
|
||||
/// at 26.2's width (`old_bits`).
|
||||
fn states_down(ids: &Ids, states: &Paletted, old_bits: u8) -> Paletted {
|
||||
if !states.palette.is_empty() {
|
||||
let mut out = states.clone();
|
||||
out.palette
|
||||
.iter_mut()
|
||||
.for_each(|p| *p = state_down(ids, *p));
|
||||
return out;
|
||||
}
|
||||
let values: Vec<i32> = (0..chunk::BLOCKS)
|
||||
.map(|i| state_down(ids, states.get(i)))
|
||||
.collect();
|
||||
Paletted::from_values(&values, Strategy::Blocks, old_bits)
|
||||
}
|
||||
|
||||
/// 26.2's ClientboundLightUpdatePacketData: each mask as `BitSet.toLongArray` (a VarInt count of longs),
|
||||
/// where 26.3 sends `toByteArray` (the same bits as little-endian bytes); the layers as they are.
|
||||
fn light_down(light: &chunk::LightData, w: &mut Writer) {
|
||||
for mask in [
|
||||
&light.sky_mask,
|
||||
&light.block_mask,
|
||||
&light.empty_sky_mask,
|
||||
&light.empty_block_mask,
|
||||
] {
|
||||
w.long_array(&mask_longs(mask));
|
||||
}
|
||||
chunk::Layers::write(&light.sky, w);
|
||||
chunk::Layers::write(&light.block, w);
|
||||
}
|
||||
|
||||
/// A mask's bytes as the longs `BitSet.toLongArray` gives: none past the last set bit.
|
||||
fn mask_longs(bytes: &[u8]) -> Vec<i64> {
|
||||
let mut longs: Vec<i64> = bytes
|
||||
.chunks(8)
|
||||
.map(|c| {
|
||||
let mut b = [0u8; 8];
|
||||
b[..c.len()].copy_from_slice(c);
|
||||
i64::from_le_bytes(b)
|
||||
})
|
||||
.collect();
|
||||
while longs.last() == Some(&0) {
|
||||
longs.pop();
|
||||
}
|
||||
longs
|
||||
}
|
||||
|
||||
fn light_update(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let p = play::LightUpdate::read(&mut Reader::new(payload))?;
|
||||
let mut w = Writer::new();
|
||||
w.varint(p.x);
|
||||
w.varint(p.z);
|
||||
light_down(&p.light, &mut w);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// A particle in 26.2's numbering, its block state too, or its stand-in; `None` when 26.2 has neither (a
|
||||
/// native mod's particle). The options stay: the stand-ins in the 26.2 tables (pinned 43..=45 as 41)
|
||||
/// carry none, as the particles they stand for; a stand-in with other options would need them rebuilt.
|
||||
fn particle_down(ids: &Ids, p: &Particle) -> Option<Particle> {
|
||||
let mut p = p.clone();
|
||||
p.id = down_or_substitute(ids.particle_type, ids.substitutes.particle_type, p.id)?;
|
||||
if let Some(ParticleData::State(s)) = &mut p.data {
|
||||
*s = state_down(ids, *s);
|
||||
}
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// A particle written for 26.2: an item in its options takes 26.2's item and component ids.
|
||||
fn write_particle(ids: &'static Ids, p: &Particle, w: &mut Writer) {
|
||||
let _down = ids::renumbering(ids, Way::Down);
|
||||
p.write(w);
|
||||
}
|
||||
|
||||
/// 26.2: flags, position, spread, one max speed, an Int count, then the particle. 26.3 sends the particle
|
||||
/// first, a speed per axis, a VarInt count and how the spread is drawn, which 26.2 can't be told (it always
|
||||
/// draws a gaussian spread). Of the three speeds the largest goes.
|
||||
fn particles(ids: &'static Ids, payload: &[u8]) -> Result<Sent> {
|
||||
let p = play::LevelParticles::read(&mut Reader::new(payload))?;
|
||||
let Some(particle) = particle_down(ids, &p.particle) else {
|
||||
return Ok(Sent::Dropped);
|
||||
};
|
||||
let speed = p
|
||||
.max_speed
|
||||
.into_iter()
|
||||
.fold(0.0f32, |a, v| if v.abs() > a.abs() { v } else { a });
|
||||
let mut w = Writer::new();
|
||||
w.bool(p.override_limiter);
|
||||
w.bool(p.always_show);
|
||||
p.pos.iter().for_each(|v| w.f64(*v));
|
||||
p.dist.iter().for_each(|v| w.f32(*v));
|
||||
w.f32(speed);
|
||||
w.i32(p.count);
|
||||
write_particle(ids, &particle, &mut w);
|
||||
Ok(Sent::Rewritten(w.data))
|
||||
}
|
||||
|
||||
/// 26.2's explode is 26.3's without the trailing "sound plays" flag: 26.2 always plays the sound, so a
|
||||
/// silent blast names `SoundEvents.EMPTY`. Its particles and sound take 26.2's ids; a scattered particle
|
||||
/// 26.2 has nothing for is left out.
|
||||
fn explode(ids: &'static Ids, payload: &[u8]) -> Result<Sent> {
|
||||
let mut e = play::Explode::read(&mut Reader::new(payload))?;
|
||||
let Some(particle) = particle_down(ids, &e.particle) else {
|
||||
return Ok(Sent::Dropped);
|
||||
};
|
||||
e.particle = particle;
|
||||
e.block_particles = std::mem::take(&mut e.block_particles)
|
||||
.into_iter()
|
||||
.filter_map(|mut b| {
|
||||
b.particle = particle_down(ids, &b.particle)?;
|
||||
Some(b)
|
||||
})
|
||||
.collect();
|
||||
let silent = || (-1, Some((SILENT.to_string(), None)));
|
||||
if !e.play_sound {
|
||||
e.sound = silent();
|
||||
} else if e.sound.1.is_none() {
|
||||
// An inline sound is named, not numbered.
|
||||
match down_or_substitute(ids.sound_event, ids.substitutes.sound_event, e.sound.0) {
|
||||
Some(id) => e.sound.0 = id,
|
||||
None => e.sound = silent(),
|
||||
}
|
||||
}
|
||||
let mut w = Writer::new();
|
||||
{
|
||||
let _down = ids::renumbering(ids, Way::Down);
|
||||
play::Explode::write(&e, &mut w);
|
||||
}
|
||||
w.data.pop();
|
||||
Ok(Sent::Rewritten(w.data))
|
||||
}
|
||||
|
||||
/// 26.2 sends a move as three Short deltas (1/4096 of a block) then on-ground, and a turn as the two angles
|
||||
/// then on-ground; 26.3 packs on-ground with the delta's step count and sends it before the angles. A
|
||||
/// stepped delta goes as its sum (the next position sync corrects a sum past a Short).
|
||||
fn move_entity(id: i32, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
use packet_ids::play::clientbound as cb;
|
||||
let mut r = Reader::new(payload);
|
||||
let m = match id {
|
||||
cb::MOVE_ENTITY_POS => play::MoveEntityPos::read(&mut r)?.0,
|
||||
cb::MOVE_ENTITY_POS_ROT => play::MoveEntityPosRot::read(&mut r)?.0,
|
||||
_ => play::MoveEntityRot::read(&mut r)?.0,
|
||||
};
|
||||
let mut w = Writer::new();
|
||||
w.varint(m.id);
|
||||
if id != cb::MOVE_ENTITY_ROT {
|
||||
delta_sum(m.delta.as_ref()).iter().for_each(|v| w.i16(*v));
|
||||
}
|
||||
if id != cb::MOVE_ENTITY_POS {
|
||||
let (yaw, pitch) = m.rotation.unwrap_or((0, 0));
|
||||
w.i8(yaw);
|
||||
w.i8(pitch);
|
||||
}
|
||||
w.bool(m.on_ground);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// A delta as one linear step, clamped to a Short per axis.
|
||||
fn delta_sum(delta: Option<&play::VecDelta>) -> [i16; 3] {
|
||||
match delta {
|
||||
None => [0; 3],
|
||||
Some(play::VecDelta::Linear(d)) => *d,
|
||||
Some(play::VecDelta::Stepped(steps)) => {
|
||||
let mut sum = [0i32; 3];
|
||||
for (d, _) in steps {
|
||||
for (s, v) in sum.iter_mut().zip(d) {
|
||||
*s += i32::from(*v);
|
||||
}
|
||||
}
|
||||
sum.map(|v| v.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 26.2: the entity, PositionMoveRotation (position, motion, the two angles), on-ground. 26.3 sends where it
|
||||
/// goes as a path, which 26.2 is told the end of, and no motion (zero).
|
||||
fn position_sync(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let p = play::EntityPositionSync::read(&mut Reader::new(payload))?;
|
||||
let mut w = Writer::new();
|
||||
w.varint(p.id);
|
||||
p.path.end().iter().for_each(|v| w.f64(*v));
|
||||
(0..3).for_each(|_| w.f64(0.0));
|
||||
w.f32(p.yaw);
|
||||
w.f32(p.pitch);
|
||||
w.bool(p.on_ground);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// 26.3's animate actions as 26.2 numbers them (wake up 0 -> 2, critical hits 1, 2 -> 4, 5); 26.2's own 0
|
||||
/// and 3 swing hands, which 26.3 sends as `swing_animation` ([`swing`]).
|
||||
fn animate(payload: &[u8]) -> Result<Sent> {
|
||||
let p = play::Animate::read(&mut Reader::new(payload))?;
|
||||
let action = match p.action {
|
||||
0 => 2,
|
||||
1 => 4,
|
||||
2 => 5,
|
||||
_ => return Ok(Sent::Dropped),
|
||||
};
|
||||
let mut w = Writer::new();
|
||||
w.varint(p.id);
|
||||
w.u8(action);
|
||||
Ok(Sent::Rewritten(w.data))
|
||||
}
|
||||
|
||||
/// 26.3's `swing_animation` as the payload of 26.2's `animate`: the main hand's swing (0) or the off hand's
|
||||
/// (3). 26.2 has one kind of swing at its own pace, so the kind and duration go.
|
||||
pub(super) fn swing(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let s = play::SwingAnimation::read(&mut Reader::new(payload))?;
|
||||
let mut w = Writer::new();
|
||||
w.varint(s.id);
|
||||
w.u8(if s.hand == 1 { 3 } else { 0 });
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Each packet written in 26.2's layout here is read back with `translate`'s 26.2 reader, which brings
|
||||
//! it to the pinned layout again.
|
||||
use super::super::super::{ServerVersion, translate};
|
||||
use super::*;
|
||||
use crate::State;
|
||||
use horizon_world::nbt::Tag;
|
||||
|
||||
fn ids() -> &'static Ids {
|
||||
ServerVersion::V26_2.ids().unwrap()
|
||||
}
|
||||
|
||||
fn in_play() -> super::super::ClientSession {
|
||||
use std::sync::atomic::Ordering;
|
||||
let s = super::super::ClientSession::new(ServerVersion::V26_2);
|
||||
let play = super::super::to_u8(State::Play);
|
||||
s.reading.store(play, Ordering::Release);
|
||||
s.writing.store(play, Ordering::Release);
|
||||
s
|
||||
}
|
||||
|
||||
fn rewritten(sent: Option<Sent>) -> Vec<u8> {
|
||||
match sent {
|
||||
Some(Sent::Rewritten(body)) => body,
|
||||
Some(Sent::AsIs) => panic!("sent as is"),
|
||||
Some(Sent::Dropped) => panic!("dropped"),
|
||||
None => panic!("not a world packet"),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode<T: Codec<T>>(p: &T) -> Vec<u8> {
|
||||
let mut w = Writer::new();
|
||||
T::write(p, &mut w);
|
||||
w.data
|
||||
}
|
||||
|
||||
fn light() -> chunk::LightData {
|
||||
chunk::LightData {
|
||||
// Sections 0 and 2; section 9 (past the first byte); section 64 (past the first long).
|
||||
sky_mask: vec![0b101],
|
||||
block_mask: vec![0, 0b10],
|
||||
empty_sky_mask: vec![0, 0, 0, 0, 0, 0, 0, 0, 1],
|
||||
empty_block_mask: Vec::new(),
|
||||
sky: vec![vec![0x11; 2048], vec![0xf0; 2048]],
|
||||
block: vec![vec![0x07; 2048]],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mask_becomes_the_longs_of_bitset_to_long_array() {
|
||||
assert_eq!(mask_longs(&[0b101]), vec![0b101]);
|
||||
assert_eq!(mask_longs(&[0, 0b10]), vec![1 << 9]);
|
||||
assert_eq!(mask_longs(&[0, 0, 0, 0, 0, 0, 0, 0, 1]), vec![0, 1]);
|
||||
assert!(mask_longs(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chunk_goes_down_and_back_up() {
|
||||
let ids = ids();
|
||||
// A section of one state (chest's first: 5543 pinned, 3987 in 26.2), and one on the global palette:
|
||||
// 4096 states that 26.2 has too (its 13000.., oak_slab's 13330 among them).
|
||||
let one = Paletted::from_values(&[5543; chunk::BLOCKS], Strategy::Blocks, 16);
|
||||
let many: Vec<i32> = (0..4096)
|
||||
.map(|i| ids.block_state.up(13000 + i).unwrap_or(1))
|
||||
.collect();
|
||||
let global = Paletted::from_values(&many, Strategy::Blocks, 16);
|
||||
assert_eq!(global.bits, 16);
|
||||
let plains = Paletted::from_values(&[1; chunk::BIOMES], Strategy::Biomes, 0);
|
||||
let section = |states: Paletted| Section {
|
||||
non_empty: 4096,
|
||||
fluids: 0,
|
||||
states,
|
||||
biomes: plains.clone(),
|
||||
};
|
||||
let sections = vec![section(one), section(global)];
|
||||
// A block entity of the first type 26.2 has.
|
||||
let kind = ids.block_entity_type.up(0).unwrap();
|
||||
let p = play::LevelChunkWithLight {
|
||||
x: 3,
|
||||
z: -7,
|
||||
chunk: chunk::ChunkData {
|
||||
heightmaps: vec![(1, vec![0x0101; 37])],
|
||||
buffer: chunk::ChunkData::from_sections(§ions),
|
||||
block_entities: vec![chunk::BlockEntityInfo {
|
||||
packed_xz: 0x12,
|
||||
y: 64,
|
||||
kind,
|
||||
tag: Tag::Compound(Default::default()),
|
||||
}],
|
||||
},
|
||||
light: light(),
|
||||
};
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::LEVEL_CHUNK_WITH_LIGHT,
|
||||
&encode(&p),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
// In 26.2's own numbering: the chest is 3987, the global palette 15 bits wide, 26.2's ids.
|
||||
{
|
||||
let mut r = Reader::new(&old);
|
||||
r.i32().unwrap();
|
||||
r.i32().unwrap();
|
||||
let c = chunk::ChunkData::read(&mut r).unwrap();
|
||||
let s = c.sections(2, ids.state_bits().0, 0).unwrap();
|
||||
assert_eq!(s[0].states.get(0), 3987);
|
||||
assert_eq!(s[1].states.bits, 15);
|
||||
assert_eq!(s[1].states.get(330), 13330);
|
||||
assert_eq!(c.block_entities[0].kind, 0);
|
||||
}
|
||||
let back = translate::chunk(ids, &old).unwrap();
|
||||
let q = play::LevelChunkWithLight::read(&mut Reader::new(&back)).unwrap();
|
||||
assert_eq!((q.x, q.z), (3, -7));
|
||||
assert_eq!(q.chunk.heightmaps, p.chunk.heightmaps);
|
||||
assert_eq!(q.chunk.block_entities, p.chunk.block_entities);
|
||||
assert_eq!(q.light, p.light);
|
||||
let s = q.chunk.sections(2, ids.state_bits().1, 0).unwrap();
|
||||
for (a, b) in s.iter().zip(§ions) {
|
||||
for i in 0..chunk::BLOCKS {
|
||||
assert_eq!(a.states.get(i), b.states.get(i));
|
||||
}
|
||||
assert_eq!(a.biomes, b.biomes);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_state_26_2_lacks_goes_down_as_its_stand_in() {
|
||||
let ids = ids();
|
||||
// poplar_log axis=y (167, new in 26.3) shows as birch_log axis=y (143 in 26.2).
|
||||
let p = Paletted::from_values(&[167; chunk::BLOCKS], Strategy::Blocks, 16);
|
||||
assert_eq!(states_down(ids, &p, 15).get(0), 143);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_light_update_goes_down_and_back_up() {
|
||||
let ids = ids();
|
||||
let p = play::LightUpdate {
|
||||
x: -1,
|
||||
z: 9,
|
||||
light: light(),
|
||||
};
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::LIGHT_UPDATE,
|
||||
&encode(&p),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let back = translate::light_update(&old).unwrap();
|
||||
assert_eq!(play::LightUpdate::read(&mut Reader::new(&back)).unwrap(), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particles_go_down_and_back_up() {
|
||||
let ids = ids();
|
||||
let send = |particle: Particle| {
|
||||
let p = play::LevelParticles {
|
||||
particle,
|
||||
override_limiter: true,
|
||||
always_show: false,
|
||||
pos: [1.5, 64.0, -3.25],
|
||||
dist: [0.5, 0.25, 0.5],
|
||||
max_speed: [0.1; 3],
|
||||
count: 12,
|
||||
randomization: 0,
|
||||
};
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::LEVEL_PARTICLES,
|
||||
&encode(&p),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let back = translate::particles(ids, &old).unwrap();
|
||||
let q = play::LevelParticles::read(&mut Reader::new(&back)).unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
q.override_limiter,
|
||||
q.always_show,
|
||||
q.pos,
|
||||
q.dist,
|
||||
q.max_speed,
|
||||
q.count
|
||||
),
|
||||
(
|
||||
p.override_limiter,
|
||||
p.always_show,
|
||||
p.pos,
|
||||
p.dist,
|
||||
p.max_speed,
|
||||
p.count
|
||||
)
|
||||
);
|
||||
q.particle
|
||||
};
|
||||
// entity_effect (28) carries a colour, alike in both numberings.
|
||||
let colour = Particle {
|
||||
id: 28,
|
||||
data: Some(ParticleData::Color(0x00ff00ff)),
|
||||
};
|
||||
assert_eq!(send(colour.clone()), colour);
|
||||
// block (1) carries a state: chest's 5543 goes down to 3987 and comes back.
|
||||
let block = Particle {
|
||||
id: 1,
|
||||
data: Some(ParticleData::State(5543)),
|
||||
};
|
||||
assert_eq!(send(block.clone()), block);
|
||||
// 44 is new in 26.3: it shows as its stand-in, 41.
|
||||
assert_eq!(
|
||||
send(Particle { id: 44, data: None }),
|
||||
Particle { id: 41, data: None }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn of_three_speeds_the_largest_goes() {
|
||||
let ids = ids();
|
||||
let p = play::LevelParticles {
|
||||
particle: Particle {
|
||||
id: 28,
|
||||
data: Some(ParticleData::Color(0)),
|
||||
},
|
||||
override_limiter: false,
|
||||
always_show: false,
|
||||
pos: [0.0; 3],
|
||||
dist: [0.0; 3],
|
||||
max_speed: [0.1, -0.4, 0.2],
|
||||
count: 0,
|
||||
randomization: 1,
|
||||
};
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::LEVEL_PARTICLES,
|
||||
&encode(&p),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
// Two flags, three doubles, three floats, then the speed.
|
||||
let mut r = Reader::new(&old[2 + 24 + 12..]);
|
||||
assert_eq!(r.f32().unwrap(), -0.4);
|
||||
assert_eq!(r.i32().unwrap(), 0);
|
||||
}
|
||||
|
||||
fn blast(play_sound: bool, sound: i32) -> play::Explode {
|
||||
play::Explode {
|
||||
center: [10.0, 70.0, -4.0],
|
||||
radius: 4.0,
|
||||
block_count: 17,
|
||||
knockback: Some([0.1, 0.2, 0.3]),
|
||||
particle: Particle {
|
||||
id: 28,
|
||||
data: Some(ParticleData::Color(-1)),
|
||||
},
|
||||
sound: (sound, None),
|
||||
block_particles: vec![
|
||||
play::ExplosionParticle {
|
||||
particle: Particle {
|
||||
id: 28,
|
||||
data: Some(ParticleData::Color(7)),
|
||||
},
|
||||
scaling: 1.0,
|
||||
speed: 0.5,
|
||||
weight: 3,
|
||||
},
|
||||
// A native mod's particle: 26.2 has nothing for it.
|
||||
play::ExplosionParticle {
|
||||
particle: Particle {
|
||||
id: crate::entity_data::MOD_PARTICLE_ID,
|
||||
data: Some(ParticleData::Mod("mymod:spark".into())),
|
||||
},
|
||||
scaling: 1.0,
|
||||
speed: 0.5,
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
play_sound,
|
||||
}
|
||||
}
|
||||
|
||||
/// 26.2's explode read back as `translate` reads it: the flag 26.2 lacks appended, ids renumbered up.
|
||||
fn explode_up(old: &[u8]) -> play::Explode {
|
||||
let mut body = old.to_vec();
|
||||
body.push(1);
|
||||
let _up = ids::renumbering(ids(), Way::Up);
|
||||
play::Explode::read(&mut Reader::new(&body)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_explosion_goes_down_without_the_sound_flag() {
|
||||
let ids = ids();
|
||||
// A sound event both versions number alike.
|
||||
let sound = (0..400)
|
||||
.find(|&s| ids.sound_event.down(s) == Some(s))
|
||||
.unwrap();
|
||||
let e = blast(true, sound);
|
||||
let old =
|
||||
rewritten(down(ids, packet_ids::play::clientbound::EXPLODE, &encode(&e)).unwrap());
|
||||
assert_eq!(
|
||||
old.len() + 1,
|
||||
encode(&{
|
||||
let mut kept = e.clone();
|
||||
kept.block_particles.truncate(1);
|
||||
kept
|
||||
})
|
||||
.len()
|
||||
);
|
||||
let back = explode_up(&old);
|
||||
let mut expected = e;
|
||||
expected.block_particles.truncate(1);
|
||||
assert_eq!(back, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_silent_explosion_names_the_empty_sound() {
|
||||
let ids = ids();
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::EXPLODE,
|
||||
&encode(&blast(false, 0)),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
explode_up(&old).sound,
|
||||
(-1, Some((SILENT.to_string(), None)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moves_go_down_and_back_up() {
|
||||
use packet_ids::play::clientbound as cb;
|
||||
let ids = ids();
|
||||
let m = |delta: Option<play::VecDelta>, rotation: Option<(i8, i8)>| play::MoveEntity {
|
||||
id: 7,
|
||||
delta,
|
||||
rotation,
|
||||
on_ground: true,
|
||||
};
|
||||
let pos = play::MoveEntityPos(m(Some(play::VecDelta::Linear([4096, 0, -2048])), None));
|
||||
let old = rewritten(down(ids, cb::MOVE_ENTITY_POS, &encode(&pos)).unwrap());
|
||||
assert_eq!(old.len(), 1 + 6 + 1);
|
||||
let back = translate::move_entity(&old, true, false).unwrap();
|
||||
assert_eq!(
|
||||
play::MoveEntityPos::read(&mut Reader::new(&back)).unwrap(),
|
||||
pos
|
||||
);
|
||||
|
||||
let pos_rot =
|
||||
play::MoveEntityPosRot(m(Some(play::VecDelta::Linear([1, 2, 3])), Some((10, -20))));
|
||||
let old = rewritten(down(ids, cb::MOVE_ENTITY_POS_ROT, &encode(&pos_rot)).unwrap());
|
||||
let back = translate::move_entity(&old, true, true).unwrap();
|
||||
assert_eq!(
|
||||
play::MoveEntityPosRot::read(&mut Reader::new(&back)).unwrap(),
|
||||
pos_rot
|
||||
);
|
||||
|
||||
let rot = play::MoveEntityRot(m(None, Some((-5, 90))));
|
||||
let old = rewritten(down(ids, cb::MOVE_ENTITY_ROT, &encode(&rot)).unwrap());
|
||||
assert_eq!(old, vec![7, -5i8 as u8, 90, 1]);
|
||||
let back = translate::move_entity(&old, false, true).unwrap();
|
||||
assert_eq!(
|
||||
play::MoveEntityRot::read(&mut Reader::new(&back)).unwrap(),
|
||||
rot
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stepped_move_goes_down_as_its_sum() {
|
||||
let stepped = play::VecDelta::Stepped(vec![
|
||||
([100, 0, -5], 1),
|
||||
([200, 30000, -5], 2),
|
||||
([0, 30000, 0], 3),
|
||||
]);
|
||||
assert_eq!(delta_sum(Some(&stepped)), [300, i16::MAX, -10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_position_sync_goes_down_and_back_up() {
|
||||
let ids = ids();
|
||||
let p = play::EntityPositionSync {
|
||||
id: 42,
|
||||
path: play::PositionPath::Linear([100.5, 64.0, -20.25]),
|
||||
yaw: 90.0,
|
||||
pitch: -10.0,
|
||||
on_ground: false,
|
||||
};
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::ENTITY_POSITION_SYNC,
|
||||
&encode(&p),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(old.len(), 1 + 48 + 8 + 1);
|
||||
let back = translate::position_sync(&old).unwrap();
|
||||
assert_eq!(
|
||||
play::EntityPositionSync::read(&mut Reader::new(&back)).unwrap(),
|
||||
p
|
||||
);
|
||||
// A stepped path goes as where it ends.
|
||||
let stepped = play::EntityPositionSync {
|
||||
path: play::PositionPath::Stepped(vec![([1.0, 2.0, 3.0], 1), ([4.0, 5.0, 6.0], 2)]),
|
||||
..p
|
||||
};
|
||||
let old = rewritten(
|
||||
down(
|
||||
ids,
|
||||
packet_ids::play::clientbound::ENTITY_POSITION_SYNC,
|
||||
&encode(&stepped),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let back = translate::position_sync(&old).unwrap();
|
||||
let q = play::EntityPositionSync::read(&mut Reader::new(&back)).unwrap();
|
||||
assert_eq!(q.path, play::PositionPath::Linear([4.0, 5.0, 6.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animate_actions_take_26_2_s_numbers() {
|
||||
let ids = ids();
|
||||
let send =
|
||||
|action: u8| down(ids, packet_ids::play::clientbound::ANIMATE, &[5, action]).unwrap();
|
||||
assert_eq!(rewritten(send(0)), vec![5, 2]);
|
||||
assert_eq!(rewritten(send(1)), vec![5, 4]);
|
||||
assert_eq!(rewritten(send(2)), vec![5, 5]);
|
||||
// And back as translate reads them.
|
||||
assert_eq!(
|
||||
translate::animate(&[5, 4]).unwrap(),
|
||||
(packet_ids::play::clientbound::ANIMATE, vec![5, 1])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_swing_reaches_a_26_2_client_as_animate() {
|
||||
let s = in_play();
|
||||
let swing = play::Clientbound::SwingAnimation(play::SwingAnimation {
|
||||
id: 5,
|
||||
hand: 1,
|
||||
kind: 1,
|
||||
duration: 6,
|
||||
});
|
||||
let sent = s.clientbound(&swing.encode()).unwrap().unwrap();
|
||||
let mut r = Reader::new(&sent);
|
||||
let animate = ServerVersion::V26_2
|
||||
.clientbound_id_down(State::Play, packet_ids::play::clientbound::ANIMATE)
|
||||
.unwrap();
|
||||
assert_eq!(r.varint().unwrap(), animate);
|
||||
// translate reads 26.2's off-hand swing as the swing sent.
|
||||
let (id, body) = translate::animate(r.rest()).unwrap();
|
||||
assert_eq!(id, packet_ids::play::clientbound::SWING_ANIMATION);
|
||||
assert_eq!(play::Clientbound::decode(id, &body).unwrap(), swing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_light_update_through_the_session_is_in_26_2_s_layout() {
|
||||
let s = in_play();
|
||||
let p = play::Clientbound::LightUpdate(play::LightUpdate {
|
||||
x: 0,
|
||||
z: 0,
|
||||
light: light(),
|
||||
});
|
||||
let sent = s.clientbound(&p.encode()).unwrap().unwrap();
|
||||
let mut r = Reader::new(&sent);
|
||||
r.varint().unwrap();
|
||||
let back = translate::light_update(r.rest()).unwrap();
|
||||
assert_eq!(
|
||||
play::Clientbound::decode(packet_ids::play::clientbound::LIGHT_UPDATE, &back).unwrap(),
|
||||
p
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,15 @@ pub fn quirks() -> Quirks {
|
||||
}
|
||||
}
|
||||
|
||||
/// The older version's quirks while packets are encoded for it on this thread (an older client, or what is
|
||||
/// sent to an older server); none otherwise. The writers then lay those structures out as it reads them.
|
||||
pub fn quirks_written() -> Quirks {
|
||||
match ACTIVE.get() {
|
||||
Some((ids, Way::Down)) => ids.quirks,
|
||||
_ => Quirks::NONE,
|
||||
}
|
||||
}
|
||||
|
||||
impl Ids {
|
||||
/// Bits per entry of a chunk section's global block palette: the older version's, then the pinned one's
|
||||
/// (`Mth.ceillog2` of the state count).
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
//! The pinned (26.3) clientbound packets a 26.2 client reads in another layout, written in 26.2's: the
|
||||
//! mirror of `translate`'s 26.2 readers for a [`super::ClientSession`] (roadmap `older-clients`). Registry ids
|
||||
//! inside these packets are renumbered here too, since they never reach the id layer's generic rewrite.
|
||||
//!
|
||||
//! Off by default ([`set_enabled`]): until a client has played through every packet here, the id layer
|
||||
//! alone keeps the pinned layout.
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use super::ids::{self, Ids, Way, down_or_substitute};
|
||||
use crate::{
|
||||
Result, State,
|
||||
buf::{Codec, Reader, Writer},
|
||||
ids as packet_ids,
|
||||
packets::{configuration, play},
|
||||
};
|
||||
|
||||
static ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Whether 26.2 clients get these packets in their own layout.
|
||||
pub fn set_enabled(on: bool) {
|
||||
ENABLED.store(on, Ordering::Release);
|
||||
}
|
||||
pub fn enabled() -> bool {
|
||||
ENABLED.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// What becomes of a pinned clientbound payload rewritten here.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Out {
|
||||
/// Not one of the packets this layer rewrites.
|
||||
Untouched,
|
||||
Rewritten(Vec<u8>),
|
||||
/// The client has nothing to read it as.
|
||||
Dropped,
|
||||
}
|
||||
|
||||
/// The pinned `payload` of packet `id` in `state`, as a 26.2 client (`ids`) reads it.
|
||||
pub fn clientbound(ids: &'static Ids, state: State, id: i32, payload: &[u8]) -> Result<Out> {
|
||||
use packet_ids::play::clientbound as cb;
|
||||
Ok(match (state, id) {
|
||||
(State::Configuration, packet_ids::configuration::clientbound::REGISTRY_DATA) => {
|
||||
registry_data(payload)?
|
||||
}
|
||||
(State::Play, cb::LOGIN) => Out::Rewritten(login(payload)?),
|
||||
(State::Play, cb::RESPAWN) => Out::Rewritten(respawn(payload)?),
|
||||
(State::Play, cb::UPDATE_ADVANCEMENTS) => Out::Rewritten(advancements(ids, payload)?),
|
||||
(State::Play, cb::COMMANDS) => Out::Rewritten(commands(ids, payload)?),
|
||||
(State::Play, cb::MAP_ITEM_DATA) => Out::Rewritten(map_item_data(ids, payload)?),
|
||||
(State::Play, cb::AWARD_STATS) => Out::Rewritten(award_stats(ids, payload)?),
|
||||
_ => Out::Untouched,
|
||||
})
|
||||
}
|
||||
|
||||
/// Synchronized registries 26.3 added (RegistryDataLoader.SYNCHRONIZED_REGISTRIES of both jars compared):
|
||||
/// 26.2 has no codec for their entries.
|
||||
const NEW_REGISTRIES: [&str; 3] = [
|
||||
"minecraft:decorated_pot_pattern",
|
||||
"minecraft:block_transformer",
|
||||
"minecraft:worldgen/block_state_provider",
|
||||
];
|
||||
|
||||
/// A registry 26.2 doesn't synchronize is not sent; the others pass as they are (unknown fields in an
|
||||
/// entry's NBT are left to 26.2's record codecs, which read past them).
|
||||
fn registry_data(payload: &[u8]) -> Result<Out> {
|
||||
let p = configuration::RegistryData::read(&mut Reader::new(payload))?;
|
||||
Ok(if NEW_REGISTRIES.contains(&p.registry.as_str()) {
|
||||
Out::Dropped
|
||||
} else {
|
||||
Out::Untouched
|
||||
})
|
||||
}
|
||||
|
||||
/// 26.2's CommonPlayerSpawnInfo: the game types as bytes, -1 for no previous one (26.3: VarInts, the
|
||||
/// optional one as id + 1).
|
||||
fn spawn_info(s: &play::SpawnInfo, w: &mut Writer) {
|
||||
w.varint(s.dimension_type);
|
||||
w.string(&s.dimension);
|
||||
w.i64(s.seed);
|
||||
w.i8(s.game_type as i8);
|
||||
w.i8(s.previous_game_type.map_or(-1, |g| g as i8));
|
||||
w.bool(s.debug);
|
||||
w.bool(s.flat);
|
||||
<crate::buf::Opt<play::GlobalPos>>::write(&s.last_death, w);
|
||||
w.varint(s.portal_cooldown);
|
||||
w.varint(s.sea_level);
|
||||
}
|
||||
|
||||
fn login(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let l = play::Login::read(&mut Reader::new(payload))?;
|
||||
let mut w = Writer::new();
|
||||
w.i32(l.player_id);
|
||||
w.bool(l.hardcore);
|
||||
w.varint(l.levels.len() as i32);
|
||||
l.levels.iter().for_each(|s| w.string(s));
|
||||
w.varint(l.max_players);
|
||||
w.varint(l.chunk_radius);
|
||||
w.varint(l.simulation_distance);
|
||||
w.bool(l.reduced_debug_info);
|
||||
w.bool(l.show_death_screen);
|
||||
w.bool(l.limited_crafting);
|
||||
spawn_info(&l.spawn, &mut w);
|
||||
w.bool(l.online_mode);
|
||||
w.bool(l.enforces_secure_chat);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
fn respawn(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let p = play::Respawn::read(&mut Reader::new(payload))?;
|
||||
let mut w = Writer::new();
|
||||
spawn_info(&p.spawn, &mut w);
|
||||
w.i8(p.keep);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// 26.3 sends an advancement's tab position after the whole advancement; 26.2 inside its display, after
|
||||
/// the background (an advancement without a display has none). The icon takes the client's item ids.
|
||||
fn advancements(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(payload);
|
||||
let mut w = Writer::new();
|
||||
w.bool(r.bool()?);
|
||||
let n = r.count(r.remaining())?;
|
||||
w.varint(n as i32);
|
||||
for _ in 0..n {
|
||||
w.string(&r.string(32767)?);
|
||||
let parent = r.bool()?;
|
||||
w.bool(parent);
|
||||
if parent {
|
||||
w.string(&r.string(32767)?);
|
||||
}
|
||||
// The display waits for the position, which comes last in 26.3.
|
||||
let mut display = None;
|
||||
if r.bool()? {
|
||||
let mut d = Writer::new();
|
||||
d.nbt(&r.nbt()?);
|
||||
d.nbt(&r.nbt()?);
|
||||
let icon = crate::item::Template::read(&mut r)?;
|
||||
{
|
||||
let _down = ids::renumbering(ids, Way::Down);
|
||||
crate::item::Template::write(&icon, &mut d);
|
||||
}
|
||||
d.varint(r.varint()?);
|
||||
let flags = r.i32()?;
|
||||
d.i32(flags);
|
||||
if flags & 1 != 0 {
|
||||
d.string(&r.string(32767)?);
|
||||
}
|
||||
display = Some(d.data);
|
||||
}
|
||||
let mut rest = Writer::new();
|
||||
let groups = r.count(r.remaining())?;
|
||||
rest.varint(groups as i32);
|
||||
for _ in 0..groups {
|
||||
let k = r.count(r.remaining())?;
|
||||
rest.varint(k as i32);
|
||||
for _ in 0..k {
|
||||
rest.string(&r.string(32767)?);
|
||||
}
|
||||
}
|
||||
rest.bool(r.bool()?);
|
||||
let (x, y) = (r.f32()?, r.f32()?);
|
||||
w.bool(display.is_some());
|
||||
if let Some(d) = display {
|
||||
w.bytes(&d);
|
||||
w.f32(x);
|
||||
w.f32(y);
|
||||
}
|
||||
w.bytes(&rest.data);
|
||||
}
|
||||
w.bytes(r.rest());
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// `brigadier:string`, the same id in 26.2 and 26.3.
|
||||
const STRING_PARSER: i32 = 5;
|
||||
|
||||
/// Each argument node's parser in 26.2's numbering. A parser 26.2 lacks becomes a string (a greedy one on
|
||||
/// a node nothing follows, a word otherwise) whose suggestions the client asks the server for, so the
|
||||
/// command still parses on the client and the server checks the argument.
|
||||
fn commands(ids: &Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut tree = play::Commands::read(&mut Reader::new(payload))?;
|
||||
for node in &mut tree.nodes {
|
||||
let Some((parser, _)) = &node.parser else {
|
||||
continue;
|
||||
};
|
||||
match ids.command_argument_type.down(*parser) {
|
||||
Some(old) => {
|
||||
if let Some(p) = &mut node.parser {
|
||||
p.0 = old;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let kind = if node.children.is_empty() { 2 } else { 0 };
|
||||
node.parser = Some((STRING_PARSER, vec![kind]));
|
||||
node.suggestions = Some("minecraft:ask_server".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut w = Writer::new();
|
||||
play::Commands::write(&tree, &mut w);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// Map decorations in 26.2's `map_decoration_type` numbering; one it lacks isn't drawn.
|
||||
fn map_item_data(ids: &Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut p = play::MapItemData::read(&mut Reader::new(payload))?;
|
||||
if let Some(decorations) = &mut p.decorations {
|
||||
decorations.retain_mut(|d| {
|
||||
ids.map_decoration_type
|
||||
.down(d.kind)
|
||||
.map(|k| d.kind = k)
|
||||
.is_some()
|
||||
});
|
||||
}
|
||||
let mut w = Writer::new();
|
||||
play::MapItemData::write(&p, &mut w);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// `minecraft:stat_type` in vanilla's registration order (Stats): what each type's value is an id of.
|
||||
const STAT_MINED: i32 = 0;
|
||||
const STAT_KILLED: i32 = 6;
|
||||
const STAT_KILLED_BY: i32 = 7;
|
||||
const STAT_CUSTOM: i32 = 8;
|
||||
|
||||
/// Stats with their type and value in 26.2's numbering; one naming what 26.2 lacks isn't sent.
|
||||
fn award_stats(ids: &Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut p = play::AwardStats::read(&mut Reader::new(payload))?;
|
||||
p.stats.retain_mut(|(kind, value, _)| {
|
||||
let renumbered = match *kind {
|
||||
STAT_MINED => ids.block.down(*value),
|
||||
STAT_KILLED | STAT_KILLED_BY => ids.entity_type.down(*value),
|
||||
STAT_CUSTOM => ids.custom_stat.down(*value),
|
||||
// crafted, used, broken, picked_up, dropped
|
||||
_ => down_or_substitute(ids.item, ids.substitutes.item, *value),
|
||||
};
|
||||
match (ids.stat_type.down(*kind), renumbered) {
|
||||
(Some(k), Some(v)) => {
|
||||
(*kind, *value) = (k, v);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
let mut w = Writer::new();
|
||||
play::AwardStats::write(&p, &mut w);
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::adapter::ServerVersion;
|
||||
|
||||
fn ids() -> &'static Ids {
|
||||
ServerVersion::V26_2.ids().unwrap()
|
||||
}
|
||||
|
||||
fn spawn(previous: Option<i32>) -> play::SpawnInfo {
|
||||
play::SpawnInfo {
|
||||
dimension_type: 0,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
seed: 42,
|
||||
game_type: 1,
|
||||
previous_game_type: previous,
|
||||
debug: false,
|
||||
flat: true,
|
||||
last_death: None,
|
||||
portal_cooldown: 0,
|
||||
sea_level: 63,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respawn_game_types_are_bytes_again() {
|
||||
for previous in [None, Some(3)] {
|
||||
let mut w = Writer::new();
|
||||
play::Respawn::write(
|
||||
&play::Respawn {
|
||||
spawn: spawn(previous),
|
||||
keep: 3,
|
||||
},
|
||||
&mut w,
|
||||
);
|
||||
let body = respawn(&w.data).unwrap();
|
||||
// 26.2's own reader (translate's spawn_info) brings it back.
|
||||
let back = super::super::translate::tests_support::respawn_26_2(&body).unwrap();
|
||||
let p = play::Respawn::read(&mut Reader::new(&back)).unwrap();
|
||||
assert_eq!(p.spawn, spawn(previous));
|
||||
assert_eq!(p.keep, 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_reads_back_with_26_2_s_reader() {
|
||||
let login_packet = play::Login {
|
||||
player_id: 7,
|
||||
hardcore: false,
|
||||
levels: vec!["minecraft:overworld".into()],
|
||||
max_players: 20,
|
||||
chunk_radius: 10,
|
||||
simulation_distance: 8,
|
||||
reduced_debug_info: false,
|
||||
show_death_screen: true,
|
||||
limited_crafting: false,
|
||||
spawn: spawn(None),
|
||||
online_mode: true,
|
||||
enforces_secure_chat: false,
|
||||
};
|
||||
let mut w = Writer::new();
|
||||
play::Login::write(&login_packet, &mut w);
|
||||
let body = login(&w.data).unwrap();
|
||||
let back = super::super::translate::tests_support::login_26_2(&body).unwrap();
|
||||
assert_eq!(
|
||||
play::Login::read(&mut Reader::new(&back)).unwrap(),
|
||||
login_packet
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_advancement_s_position_goes_back_into_its_display() {
|
||||
// One advancement with a display (diamond icon, no background) at (2.5, -1), one without at (0, 0).
|
||||
let mut w = Writer::new();
|
||||
w.bool(true);
|
||||
w.varint(2);
|
||||
for (id, display) in [("minecraft:a", true), ("minecraft:b", false)] {
|
||||
w.string(id);
|
||||
w.bool(false);
|
||||
w.bool(display);
|
||||
if display {
|
||||
w.nbt(&horizon_world::nbt::Tag::String("T".into()));
|
||||
w.nbt(&horizon_world::nbt::Tag::String("D".into()));
|
||||
crate::item::Template::write(&crate::item::Stack::new(1012, 1), &mut w);
|
||||
w.varint(0);
|
||||
w.i32(2);
|
||||
}
|
||||
w.varint(1);
|
||||
w.varint(1);
|
||||
w.string("done");
|
||||
w.bool(false);
|
||||
w.f32(if display { 2.5 } else { 0.0 });
|
||||
w.f32(if display { -1.0 } else { 0.0 });
|
||||
}
|
||||
w.varint(0);
|
||||
w.varint(0);
|
||||
w.bool(true);
|
||||
let body = advancements(ids(), &w.data).unwrap();
|
||||
let back = super::super::translate::tests_support::advancements_26_2(ids(), &body).unwrap();
|
||||
// Read back with 26.2's reader the icon is the pinned diamond again (926 in 26.2).
|
||||
assert_eq!(back, w.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_26_3_only_parser_becomes_a_string_and_others_shift_down() {
|
||||
let node =
|
||||
|kind: u8, children: Vec<i32>, parser: Option<(i32, Vec<u8>)>| play::CommandNode {
|
||||
kind,
|
||||
executable: true,
|
||||
restricted: false,
|
||||
children,
|
||||
redirect: None,
|
||||
name: if kind == 0 { String::new() } else { "x".into() },
|
||||
parser,
|
||||
suggestions: None,
|
||||
};
|
||||
let uuid = play::ARGUMENT_TYPES
|
||||
.iter()
|
||||
.position(|&n| n == "minecraft:uuid")
|
||||
.unwrap() as i32;
|
||||
let tree = play::Commands {
|
||||
nodes: vec![
|
||||
node(0, vec![1, 2], None),
|
||||
node(2, vec![], Some((uuid, vec![]))),
|
||||
node(2, vec![], Some((3, vec![0]))), // brigadier:integer, no bounds
|
||||
],
|
||||
root: 0,
|
||||
};
|
||||
let mut w = Writer::new();
|
||||
play::Commands::write(&tree, &mut w);
|
||||
let body = commands(ids(), &w.data).unwrap();
|
||||
let out = play::Commands::read(&mut Reader::new(&body)).unwrap();
|
||||
let uuid_down = ids().command_argument_type.down(uuid);
|
||||
match uuid_down {
|
||||
// uuid is new in 26.3: a greedy string asking the server.
|
||||
None => {
|
||||
assert_eq!(out.nodes[1].parser, Some((STRING_PARSER, vec![2])));
|
||||
assert_eq!(
|
||||
out.nodes[1].suggestions.as_deref(),
|
||||
Some("minecraft:ask_server")
|
||||
);
|
||||
}
|
||||
Some(old) => assert_eq!(out.nodes[1].parser.as_ref().unwrap().0, old),
|
||||
}
|
||||
assert_eq!(out.nodes[2].parser, Some((3, vec![0])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registries_26_2_lacks_are_not_sent() {
|
||||
let data = |registry: &str| {
|
||||
let mut w = Writer::new();
|
||||
configuration::RegistryData::write(
|
||||
&configuration::RegistryData {
|
||||
registry: registry.into(),
|
||||
entries: vec![],
|
||||
},
|
||||
&mut w,
|
||||
);
|
||||
registry_data(&w.data).unwrap()
|
||||
};
|
||||
assert_eq!(data("minecraft:block_transformer"), Out::Dropped);
|
||||
assert_eq!(data("minecraft:worldgen/biome"), Out::Untouched);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_and_map_decorations_take_26_2_s_ids() {
|
||||
let ids = ids();
|
||||
// The last pinned custom stat and map decoration are the newest; whatever 26.2 lacks goes.
|
||||
let mut w = Writer::new();
|
||||
play::AwardStats::write(
|
||||
&play::AwardStats {
|
||||
stats: vec![(STAT_CUSTOM, 0, 5), (STAT_CUSTOM, 77, 1), (1, 1012, 2)],
|
||||
},
|
||||
&mut w,
|
||||
);
|
||||
let out = award_stats(ids, &w.data).unwrap();
|
||||
let p = play::AwardStats::read(&mut Reader::new(&out)).unwrap();
|
||||
let expected: Vec<_> = [(STAT_CUSTOM, 0, 5), (STAT_CUSTOM, 77, 1), (1, 1012, 2)]
|
||||
.into_iter()
|
||||
.filter_map(|(k, v, n)| {
|
||||
let v = match k {
|
||||
STAT_CUSTOM => ids.custom_stat.down(v),
|
||||
_ => ids.item.down(v),
|
||||
}?;
|
||||
Some((ids.stat_type.down(k)?, v, n))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(p.stats, expected);
|
||||
// diamond: 1012 pinned, 926 in 26.2.
|
||||
assert!(p.stats.contains(&(1, 926, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pot_sides_and_container_slots_take_26_2_s_layout_and_ids() {
|
||||
use crate::item::{Component, Stack, Template, component};
|
||||
let v26_2 = ids();
|
||||
// A pot with a diamond (1012 pinned, 926 in 26.2) at the back, the rest undecorated, and a
|
||||
// container holding three diamonds and an empty slot: their pinned bytes.
|
||||
let mut pot = Writer::new();
|
||||
pot.bool(true);
|
||||
Template::write(&Stack::new(1012, 1), &mut pot);
|
||||
(0..3).for_each(|_| pot.bool(false));
|
||||
let mut container = Writer::new();
|
||||
container.varint(2);
|
||||
container.bool(true);
|
||||
Template::write(&Stack::new(1012, 3), &mut container);
|
||||
container.bool(false);
|
||||
let mut stack = Stack::new(1012, 1);
|
||||
for (kind, bytes) in [
|
||||
(component::POT_DECORATIONS, &pot.data),
|
||||
(component::CONTAINER, &container.data),
|
||||
] {
|
||||
let c = Component::read(kind, &mut Reader::new(bytes)).unwrap();
|
||||
stack.added.push((kind, c));
|
||||
}
|
||||
let mut w = Writer::new();
|
||||
{
|
||||
let _down = ids::renumbering(v26_2, Way::Down);
|
||||
Template::write(&stack, &mut w);
|
||||
}
|
||||
// Read with 26.2's reader (its quirks, its ids) both come back as they were.
|
||||
let back = {
|
||||
let _up = ids::renumbering(v26_2, Way::Up);
|
||||
Template::read(&mut Reader::new(&w.data)).unwrap()
|
||||
};
|
||||
assert_eq!(back.item, 1012);
|
||||
for (kind, bytes) in [
|
||||
(component::POT_DECORATIONS, &pot.data),
|
||||
(component::CONTAINER, &container.data),
|
||||
] {
|
||||
let mut b = Writer::new();
|
||||
back.component(kind).unwrap().write(kind, &mut b);
|
||||
assert_eq!(&b.data, bytes, "component {kind}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tag_slot_display_is_a_bare_tag_id_for_26_2() {
|
||||
use crate::packets::recipes::{HolderSet, SlotDisplay};
|
||||
let round = |d: &SlotDisplay| {
|
||||
let mut w = Writer::new();
|
||||
{
|
||||
let _down = ids::renumbering(ids(), Way::Down);
|
||||
d.write(&mut w);
|
||||
}
|
||||
let _up = ids::renumbering(ids(), Way::Up);
|
||||
SlotDisplay::read(&mut Reader::new(&w.data)).unwrap()
|
||||
};
|
||||
let tag = SlotDisplay::Tag(HolderSet::Tag("minecraft:planks".into()));
|
||||
assert_eq!(round(&tag), tag);
|
||||
// Items have no TagSlotDisplay form in 26.2: a composite of the items.
|
||||
let items = SlotDisplay::Tag(HolderSet::Items(vec![1012]));
|
||||
assert_eq!(
|
||||
round(&items),
|
||||
SlotDisplay::Composite(vec![SlotDisplay::Item(1012)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -274,12 +274,7 @@ pub fn serverbound(
|
||||
(State::Play, sb::PUNCH) => {
|
||||
let mut w = Writer::new();
|
||||
w.varint(0);
|
||||
let swing = if session.version.at_most(ServerVersion::V1_21_11) {
|
||||
v1_21_11::SWING
|
||||
} else {
|
||||
V26_2_SWING
|
||||
};
|
||||
Ok(Some((swing, w.data)))
|
||||
Ok(Some((swing_id(session.version), w.data)))
|
||||
}
|
||||
(State::Play, sb::SET_CREATIVE_MODE_SLOT | sb::CONTAINER_CLICK) => {
|
||||
let p = play::Serverbound::decode(id, payload)?;
|
||||
@@ -296,6 +291,15 @@ pub fn serverbound(
|
||||
/// 26.2's serverbound `swing` (play), which 26.3 replaced with `punch`.
|
||||
const V26_2_SWING: i32 = 63;
|
||||
|
||||
/// The id an older `version` numbers its serverbound `swing` (play) by.
|
||||
pub(super) fn swing_id(version: ServerVersion) -> i32 {
|
||||
if version.at_most(ServerVersion::V1_21_11) {
|
||||
v1_21_11::SWING
|
||||
} else {
|
||||
V26_2_SWING
|
||||
}
|
||||
}
|
||||
|
||||
/// A play packet decoded with the older version's ids renumbered as they are read.
|
||||
fn decode_play(ids: &'static Ids, id: i32, payload: &[u8]) -> Result<play::Clientbound> {
|
||||
let _up = ids::renumbering(ids, Way::Up);
|
||||
@@ -382,7 +386,7 @@ fn respawn(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
/// The column's sections with their block states renumbered and packed for the pinned palette width (a
|
||||
/// section on the older global palette uses fewer bits per entry), and its light masks as 26.3 sends them.
|
||||
/// Biomes are the server's own registry and stay as they are.
|
||||
fn chunk(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
pub(super) fn chunk(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(payload);
|
||||
let x = r.i32()?;
|
||||
let z = r.i32()?;
|
||||
@@ -447,7 +451,7 @@ fn light(r: &mut Reader) -> Result<crate::chunk::LightData> {
|
||||
})
|
||||
}
|
||||
|
||||
fn light_update(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
pub(super) fn light_update(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(payload);
|
||||
let x = r.varint()?;
|
||||
let z = r.varint()?;
|
||||
@@ -524,7 +528,7 @@ fn advancements(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
/// 26.2: flags, position, spread, one max speed, an Int count, then the particle. 26.3 sends the particle
|
||||
/// first, a speed per axis, a VarInt count and how the spread is drawn (0, gaussian, as 26.2 did).
|
||||
fn particles(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
pub(super) fn particles(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(payload);
|
||||
let override_limiter = r.bool()?;
|
||||
let always_show = r.bool()?;
|
||||
@@ -555,7 +559,7 @@ fn particles(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
/// 26.2 sends a move as three Short deltas (1/4096 of a block) then on-ground, and a turn as the two angles
|
||||
/// then on-ground; 26.3 packs on-ground with the delta's step count, and sends it before the angles.
|
||||
fn move_entity(payload: &[u8], pos: bool, rot: bool) -> Result<Vec<u8>> {
|
||||
pub(super) fn move_entity(payload: &[u8], pos: bool, rot: bool) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(payload);
|
||||
let id = r.varint()?;
|
||||
let delta = if pos {
|
||||
@@ -582,7 +586,7 @@ fn move_entity(payload: &[u8], pos: bool, rot: bool) -> Result<Vec<u8>> {
|
||||
|
||||
/// 26.2: the entity, PositionMoveRotation (position, motion, the two angles), on-ground. 26.3 sends where it
|
||||
/// goes as a path, then the angles; the motion is no longer sent.
|
||||
fn position_sync(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
pub(super) fn position_sync(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(payload);
|
||||
let id = r.varint()?;
|
||||
let position = [r.f64()?, r.f64()?, r.f64()?];
|
||||
@@ -606,7 +610,7 @@ fn position_sync(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
/// 26.2's animate swings hands too (0 main, 3 off), which 26.3 sends as `swing_animation`; the others were
|
||||
/// renumbered (wake up 2 -> 0, critical hits 4, 5 -> 1, 2).
|
||||
fn animate(payload: &[u8]) -> Result<(i32, Vec<u8>)> {
|
||||
pub(super) fn animate(payload: &[u8]) -> Result<(i32, Vec<u8>)> {
|
||||
let mut r = Reader::new(payload);
|
||||
let id = r.varint()?;
|
||||
let action = r.u8()?;
|
||||
@@ -1288,3 +1292,19 @@ mod tests {
|
||||
assert_eq!(action(3), Some(2)); // stop destroying: 3 in 26.3, 2 in 26.2
|
||||
}
|
||||
}
|
||||
|
||||
/// 26.2's readers, for the tests of the layout written for a 26.2 client (`layout_26_2`).
|
||||
#[cfg(test)]
|
||||
pub(super) mod tests_support {
|
||||
use super::*;
|
||||
|
||||
pub fn login_26_2(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
login(payload)
|
||||
}
|
||||
pub fn respawn_26_2(payload: &[u8]) -> Result<Vec<u8>> {
|
||||
respawn(payload)
|
||||
}
|
||||
pub fn advancements_26_2(ids: &'static Ids, payload: &[u8]) -> Result<Vec<u8>> {
|
||||
advancements(ids, payload)
|
||||
}
|
||||
}
|
||||
@@ -993,6 +993,88 @@ impl Codec<Option<Stack>> for Slot {
|
||||
}
|
||||
}
|
||||
|
||||
/// A component value as the version being written for reads it. Values kept as their pinned bytes that hold
|
||||
/// items (a pot's sides, a container's slots, a use remainder) are read again and written with that
|
||||
/// version's item ids, and a pot's sides as 26.2's item ids when it reads them so
|
||||
/// ([`ids::Quirks::pot_decorations_are_items`]). Written as they are when nothing is renumbered.
|
||||
fn write_value(kind: i32, c: &Component, w: &mut Writer) {
|
||||
use component::*;
|
||||
let quirks = ids::quirks_written();
|
||||
let renumbered = matches!(ids::active(), Some((_, ids::Way::Down)));
|
||||
let held = match kind {
|
||||
POT_DECORATIONS => true,
|
||||
// Before 26.1 these hold ItemStacks, written by another codec.
|
||||
USE_REMAINDER | SULFUR_CUBE_CONTENT | CONTAINER => !quirks.stacks_in_components,
|
||||
_ => false,
|
||||
};
|
||||
if !renumbered || !held {
|
||||
return c.write(kind, w);
|
||||
}
|
||||
let pinned = {
|
||||
let _pinned = ids::pinned();
|
||||
let mut b = Writer::new();
|
||||
c.write(kind, &mut b);
|
||||
b.data
|
||||
};
|
||||
match value_down(kind, quirks, &pinned) {
|
||||
Ok(bytes) => w.bytes(&bytes),
|
||||
Err(_) => w.bytes(&pinned),
|
||||
}
|
||||
}
|
||||
|
||||
/// A stack template as the pinned version wrote it.
|
||||
fn read_pinned(r: &mut Reader) -> Result<Stack> {
|
||||
let _pinned = ids::pinned();
|
||||
Template::read(r)
|
||||
}
|
||||
|
||||
/// The pinned bytes of an item-holding component (see [`write_value`]) in the numbering written now.
|
||||
fn value_down(kind: i32, quirks: ids::Quirks, pinned: &[u8]) -> Result<Vec<u8>> {
|
||||
use component::*;
|
||||
let mut r = Reader::new(pinned);
|
||||
let mut w = Writer::new();
|
||||
match kind {
|
||||
// Back, left, right, front: each an optional template of one item.
|
||||
POT_DECORATIONS => {
|
||||
let mut sides = Vec::with_capacity(4);
|
||||
for _ in 0..4 {
|
||||
sides.push(if r.bool()? {
|
||||
Some(read_pinned(&mut r)?)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
}
|
||||
if quirks.pot_decorations_are_items {
|
||||
// ByteBufCodecs.registry(ITEM).apply(list(4)): a brick for an undecorated side.
|
||||
w.varint(4);
|
||||
for side in &sides {
|
||||
w.varint(ids::item(side.as_ref().map_or(BRICK, |s| s.item)));
|
||||
}
|
||||
} else {
|
||||
for side in &sides {
|
||||
w.bool(side.is_some());
|
||||
if let Some(s) = side {
|
||||
Template::write(s, &mut w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CONTAINER => {
|
||||
let n = r.count(256)?;
|
||||
w.varint(n as i32);
|
||||
for _ in 0..n {
|
||||
let slot = r.bool()?;
|
||||
w.bool(slot);
|
||||
if slot {
|
||||
Template::write(&read_pinned(&mut r)?, &mut w);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Template::write(&read_pinned(&mut r)?, &mut w),
|
||||
}
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
/// A stack's components, without those an older client being written for lacks.
|
||||
fn write_patch(s: &Stack, w: &mut Writer) {
|
||||
let has = |kind: i32| ids::component(kind) >= 0;
|
||||
@@ -1000,7 +1082,7 @@ fn write_patch(s: &Stack, w: &mut Writer) {
|
||||
w.varint(s.removed.iter().filter(|k| has(**k)).count() as i32);
|
||||
for (kind, c) in s.added.iter().filter(|(k, _)| has(*k)) {
|
||||
w.varint(ids::component(*kind));
|
||||
c.write(*kind, w);
|
||||
write_value(*kind, c, w);
|
||||
}
|
||||
for kind in s.removed.iter().filter(|k| has(**k)) {
|
||||
w.varint(ids::component(*kind));
|
||||
@@ -1089,7 +1171,7 @@ impl Codec<Option<Stack>> for DelimitedSlot {
|
||||
for (kind, c) in &s.added {
|
||||
w.varint(ids::component(*kind));
|
||||
let mut value = Writer::new();
|
||||
c.write(*kind, &mut value);
|
||||
write_value(*kind, c, &mut value);
|
||||
w.byte_array(&value.data);
|
||||
}
|
||||
for kind in &s.removed {
|
||||
|
||||
@@ -166,6 +166,20 @@ impl SlotDisplay {
|
||||
w.varint(5);
|
||||
Template::write(stack, w);
|
||||
}
|
||||
Self::Tag(set) if crate::adapter::ids::quirks_written().tag_slot_display_is_id => {
|
||||
match set {
|
||||
// TagSlotDisplay of 26.2: the tag's bare id.
|
||||
HolderSet::Tag(name) => {
|
||||
w.varint(6);
|
||||
w.string(name);
|
||||
}
|
||||
// A list of items has no such form: a composite of item displays shows the same.
|
||||
HolderSet::Items(items) => {
|
||||
Self::Composite(items.iter().map(|&item| Self::Item(item)).collect())
|
||||
.write(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Tag(set) => {
|
||||
w.varint(6);
|
||||
set.write(w);
|
||||
|
||||
Reference in new issue
Block a user