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:
Ludovic Marie authored and GitHub committed 2026-11-07 10:08:01 +01:00
commit 7ebad255b0
9 files changed
+1858 -26

No files matched your search

+173 -3
View File
@@ -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);
}
+1
View File
@@ -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(&sections);
// 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(&sections),
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(&sections) {
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)
}
}
+84 -2
View File
@@ -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);