mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Protocol adapter: 26.2's packet ids against 26.3's (part of #6702)
The first layer of joining older servers: a ServerVersion type and, per connection state, how a 26.2 server numbers each packet, generated from both releases' packets reports. Nothing calls it yet. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
09a669a6c3
commit
07ad7d2abc
4 files changed
+371
No files matched your search
@@ -0,0 +1,184 @@
|
||||
//! Speaking to servers of an older release: the first layer of the native protocol adapter (roadmap
|
||||
//! `older-servers`). Horizon's packets are 26.3's; a server on another protocol numbers them differently.
|
||||
//! This maps packet ids per connection state, both ways. It does not translate packet contents.
|
||||
pub mod v26_2;
|
||||
|
||||
use crate::{
|
||||
Direction, Result, State,
|
||||
buf::{Reader, write_varint},
|
||||
};
|
||||
|
||||
/// The protocol a server speaks.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum ServerVersion {
|
||||
/// The pinned version ([`crate::PROTOCOL_VERSION`]): nothing to translate.
|
||||
#[default]
|
||||
Current,
|
||||
V26_2,
|
||||
}
|
||||
|
||||
impl ServerVersion {
|
||||
pub const ALL: [Self; 2] = [Self::Current, Self::V26_2];
|
||||
|
||||
/// The protocol number its handshake states.
|
||||
pub fn protocol(self) -> i32 {
|
||||
match self {
|
||||
Self::Current => crate::PROTOCOL_VERSION,
|
||||
Self::V26_2 => v26_2::PROTOCOL,
|
||||
}
|
||||
}
|
||||
|
||||
/// The release name, as a server list shows it.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Current => crate::GAME_VERSION,
|
||||
Self::V26_2 => v26_2::NAME,
|
||||
}
|
||||
}
|
||||
|
||||
/// The version a server's status reports by its protocol number.
|
||||
pub fn from_protocol(protocol: i32) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|v| v.protocol() == protocol)
|
||||
}
|
||||
|
||||
/// The pinned version's id for a packet this version's server sent as `id`. `None` when the pinned
|
||||
/// version has no such packet.
|
||||
pub fn clientbound_id(self, state: State, id: i32) -> Option<i32> {
|
||||
self.map(state, Direction::Clientbound, id)
|
||||
}
|
||||
|
||||
/// The id this version's server knows a packet Horizon sends as `id` by. `None` when that server has
|
||||
/// no such packet, so it must not be sent.
|
||||
pub fn serverbound_id(self, state: State, id: i32) -> Option<i32> {
|
||||
self.map(state, Direction::Serverbound, id)
|
||||
}
|
||||
|
||||
fn map(self, state: State, direction: Direction, id: i32) -> Option<i32> {
|
||||
let table = match (self, direction) {
|
||||
(Self::Current, _) => return Some(id),
|
||||
(Self::V26_2, Direction::Clientbound) => v26_2::clientbound(state),
|
||||
(Self::V26_2, Direction::Serverbound) => v26_2::serverbound(state),
|
||||
};
|
||||
let mapped = *table.get(usize::try_from(id).ok()?)?;
|
||||
mapped.map(i32::from)
|
||||
}
|
||||
|
||||
/// A packet body (VarInt id + payload) Horizon sends, renumbered for this version's server. `None`
|
||||
/// when that server has no such packet.
|
||||
pub fn serverbound_body(self, state: State, body: &[u8]) -> Result<Option<Vec<u8>>> {
|
||||
let mut r = Reader::new(body);
|
||||
let id = r.varint()?;
|
||||
let payload = r.rest();
|
||||
let Some(mapped) = self.serverbound_id(state, id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if mapped == id {
|
||||
return Ok(Some(body.to_vec()));
|
||||
}
|
||||
let mut out = Vec::with_capacity(body.len() + 1);
|
||||
write_varint(&mut out, mapped);
|
||||
out.extend_from_slice(payload);
|
||||
Ok(Some(out))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ids;
|
||||
|
||||
// Expected ids below are read off the 26.2 and 26.3 packets reports by hand
|
||||
// (`version_port.py diff 26.2 26.3`), not from the generated tables.
|
||||
|
||||
#[test]
|
||||
fn a_26_2_server_is_known_by_its_protocol() {
|
||||
assert_eq!(
|
||||
ServerVersion::from_protocol(776),
|
||||
Some(ServerVersion::V26_2)
|
||||
);
|
||||
assert_eq!(
|
||||
ServerVersion::from_protocol(crate::PROTOCOL_VERSION),
|
||||
Some(ServerVersion::Current)
|
||||
);
|
||||
assert_eq!(ServerVersion::from_protocol(775), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renumbered_clientbound_play_packets_reach_their_26_3_ids() {
|
||||
let v = ServerVersion::V26_2;
|
||||
// keep_alive 44 -> 45, login 49 -> 50, respawn 82 -> 84, clear_dialog 139 -> 142.
|
||||
assert_eq!(
|
||||
v.clientbound_id(State::Play, 44),
|
||||
Some(ids::play::clientbound::KEEP_ALIVE)
|
||||
);
|
||||
assert_eq!(
|
||||
v.clientbound_id(State::Play, 49),
|
||||
Some(ids::play::clientbound::LOGIN)
|
||||
);
|
||||
assert_eq!(
|
||||
v.clientbound_id(State::Play, 82),
|
||||
Some(ids::play::clientbound::RESPAWN)
|
||||
);
|
||||
assert_eq!(
|
||||
v.clientbound_id(State::Play, 139),
|
||||
Some(ids::play::clientbound::CLEAR_DIALOG)
|
||||
);
|
||||
// Unchanged before the first insertion (add_transient_block at 37).
|
||||
assert_eq!(v.clientbound_id(State::Play, 0), Some(0));
|
||||
// Past 26.2's 141 packets.
|
||||
assert_eq!(v.clientbound_id(State::Play, 141), None);
|
||||
// Configuration: store_cookie 10 -> 11 (post_effects took 10).
|
||||
assert_eq!(
|
||||
v.clientbound_id(State::Configuration, 10),
|
||||
Some(ids::configuration::clientbound::STORE_COOKIE)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serverbound_packets_26_2_lacks_are_not_sent() {
|
||||
let v = ServerVersion::V26_2;
|
||||
// punch is new in 26.3; swing, which 26.2 has at 63, is gone.
|
||||
assert_eq!(
|
||||
v.serverbound_id(State::Play, ids::play::serverbound::PUNCH),
|
||||
None
|
||||
);
|
||||
// resource_pack 50 -> 49, spectator_action 63 -> 62, keep_alive unchanged.
|
||||
assert_eq!(
|
||||
v.serverbound_id(State::Play, ids::play::serverbound::RESOURCE_PACK),
|
||||
Some(49)
|
||||
);
|
||||
assert_eq!(
|
||||
v.serverbound_id(State::Play, ids::play::serverbound::SPECTATOR_ACTION),
|
||||
Some(62)
|
||||
);
|
||||
assert_eq!(
|
||||
v.serverbound_id(State::Play, ids::play::serverbound::KEEP_ALIVE),
|
||||
Some(ids::play::serverbound::KEEP_ALIVE)
|
||||
);
|
||||
assert_eq!(
|
||||
v.serverbound_id(State::Handshake, ids::handshake::serverbound::INTENTION),
|
||||
Some(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serverbound_bodies_keep_their_payload_under_the_new_id() {
|
||||
let v = ServerVersion::V26_2;
|
||||
let mut body = Vec::new();
|
||||
write_varint(&mut body, ids::play::serverbound::RESOURCE_PACK);
|
||||
body.extend_from_slice(&[1, 2, 3]);
|
||||
assert_eq!(
|
||||
v.serverbound_body(State::Play, &body).unwrap(),
|
||||
Some(vec![49, 1, 2, 3])
|
||||
);
|
||||
let mut punch = Vec::new();
|
||||
write_varint(&mut punch, ids::play::serverbound::PUNCH);
|
||||
assert_eq!(v.serverbound_body(State::Play, &punch).unwrap(), None);
|
||||
assert_eq!(
|
||||
ServerVersion::Current
|
||||
.serverbound_body(State::Play, &body)
|
||||
.unwrap(),
|
||||
Some(body)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Packet ids of protocol 776 (26.2) against the pinned 777 (26.3).
|
||||
//! Generated by tools/generate_packet_id_adapter.py from both versions' packets reports;
|
||||
//! do not edit by hand.
|
||||
use crate::State;
|
||||
|
||||
pub const PROTOCOL: i32 = 776;
|
||||
pub const NAME: &str = "26.2";
|
||||
|
||||
#[rustfmt::skip]
|
||||
const HANDSHAKE_CLIENTBOUND: &[Option<u8>] = &[
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const HANDSHAKE_SERVERBOUND: &[Option<u8>] = &[
|
||||
Some(0),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const STATUS_CLIENTBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const STATUS_SERVERBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const LOGIN_CLIENTBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1), Some(2), Some(3), Some(4), Some(5),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const LOGIN_SERVERBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1), Some(2), Some(3), Some(4),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const CONFIGURATION_CLIENTBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1), Some(2), Some(3), Some(4), Some(5), Some(6), Some(7), Some(8), Some(9), Some(11), Some(12),
|
||||
Some(13), Some(14), Some(15), Some(16), Some(17), Some(18), Some(19), Some(20),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const CONFIGURATION_SERVERBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1), Some(2), Some(3), Some(4), Some(5), Some(6), Some(7), Some(8), Some(9),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const PLAY_CLIENTBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1), Some(2), Some(3), Some(4), Some(5), Some(6), Some(7), Some(8), Some(9), Some(10), Some(11),
|
||||
Some(12), Some(13), Some(14), Some(15), Some(16), Some(17), Some(18), Some(19), Some(20), Some(21), Some(22), Some(23),
|
||||
Some(24), Some(25), Some(26), Some(27), Some(28), Some(29), Some(30), Some(31), Some(32), Some(33), Some(34), Some(35),
|
||||
Some(36), Some(38), Some(39), Some(40), Some(41), Some(42), Some(43), Some(44), Some(45), Some(46), Some(47), Some(48),
|
||||
Some(49), Some(50), Some(51), Some(52), Some(53), Some(54), Some(55), Some(56), Some(57), Some(58), Some(59), Some(60),
|
||||
Some(61), Some(62), Some(63), Some(64), Some(65), Some(66), Some(67), Some(68), Some(69), Some(70), Some(71), Some(72),
|
||||
Some(73), Some(74), Some(75), Some(76), Some(77), Some(78), Some(79), Some(80), Some(81), Some(82), Some(84), Some(85),
|
||||
Some(86), Some(87), Some(88), Some(89), Some(90), Some(91), Some(92), Some(93), Some(94), Some(95), Some(96), Some(97),
|
||||
Some(98), Some(99), Some(100), Some(101), Some(102), Some(103), Some(104), Some(105), Some(106), Some(107), Some(108), Some(109),
|
||||
Some(110), Some(111), Some(112), Some(113), Some(114), Some(115), Some(116), Some(117), Some(118), Some(119), Some(120), Some(121),
|
||||
Some(122), Some(124), Some(125), Some(126), Some(127), Some(128), Some(129), Some(130), Some(131), Some(132), Some(133), Some(134),
|
||||
Some(135), Some(136), Some(137), Some(138), Some(139), Some(140), Some(141), Some(142), Some(143),
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
const PLAY_SERVERBOUND: &[Option<u8>] = &[
|
||||
Some(0), Some(1), Some(2), Some(3), Some(4), Some(5), Some(6), Some(7), Some(8), Some(9), Some(10), Some(11),
|
||||
Some(12), Some(13), Some(14), Some(15), Some(16), Some(17), Some(18), Some(19), Some(20), Some(21), Some(22), Some(23),
|
||||
Some(24), Some(25), Some(26), Some(27), Some(28), Some(29), Some(30), Some(31), Some(32), Some(33), Some(34), Some(35),
|
||||
Some(36), Some(37), Some(38), Some(39), Some(40), Some(41), Some(42), Some(43), Some(44), Some(45), None, Some(46),
|
||||
Some(47), Some(48), Some(49), Some(50), Some(51), Some(52), Some(53), Some(54), Some(55), Some(56), Some(57), Some(58),
|
||||
Some(59), Some(60), Some(61), Some(62), Some(64), Some(65), Some(66), Some(67), Some(68),
|
||||
];
|
||||
/// For each id a 26.2 server sends in `state`, the pinned version's id of that packet.
|
||||
pub fn clientbound(state: State) -> &'static [Option<u8>] {
|
||||
match state {
|
||||
State::Handshake => HANDSHAKE_CLIENTBOUND,
|
||||
State::Status => STATUS_CLIENTBOUND,
|
||||
State::Login => LOGIN_CLIENTBOUND,
|
||||
State::Configuration => CONFIGURATION_CLIENTBOUND,
|
||||
State::Play => PLAY_CLIENTBOUND,
|
||||
}
|
||||
}
|
||||
/// For each pinned-version id Horizon sends in `state`, the id a 26.2 server knows it by.
|
||||
pub fn serverbound(state: State) -> &'static [Option<u8>] {
|
||||
match state {
|
||||
State::Handshake => HANDSHAKE_SERVERBOUND,
|
||||
State::Status => STATUS_SERVERBOUND,
|
||||
State::Login => LOGIN_SERVERBOUND,
|
||||
State::Configuration => CONFIGURATION_SERVERBOUND,
|
||||
State::Play => PLAY_SERVERBOUND,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! every connection state, shared by the Horizon client and (later) server.
|
||||
// `deny`, not `forbid`: crypto/blocks.rs allows the ARMv8 AES intrinsics, nothing else.
|
||||
#![deny(unsafe_code)]
|
||||
pub mod adapter;
|
||||
pub mod buf;
|
||||
pub mod chunk;
|
||||
pub mod connection;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate crates/horizon-protocol/src/adapter/v<OLD>.rs: how an older release numbers the packets of each
|
||||
connection state against the pinned one, from both versions' data generator packets reports.
|
||||
|
||||
generate_packet_id_adapter.py --old ~/code/horizon-parity/versions/26.2
|
||||
|
||||
`--old` (and `--new`, the pinned version in .local by default) is a directory `tools/version_port.py prepare
|
||||
VERSION --download` made: `jars/client.jar` and `datagen/reports/packets.json`.
|
||||
Packets are matched by name, as both reports list them; a packet only one version has maps to `None`.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from . import lab
|
||||
except ImportError:
|
||||
import lab
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
STATES = ["handshake", "status", "login", "configuration", "play"]
|
||||
PER_LINE = 12
|
||||
|
||||
|
||||
def ids(report, state, direction):
|
||||
"""{name: id} of one state and direction, checked to number its packets 0..n."""
|
||||
packets = {name: entry["protocol_id"] for name, entry in report.get(state, {}).get(direction, {}).items()}
|
||||
assert sorted(packets.values()) == list(range(len(packets))), (state, direction)
|
||||
return packets
|
||||
|
||||
|
||||
def table(source, target):
|
||||
"""For each id of `source` (in order), the id `target` gives the same packet, or None."""
|
||||
by_id = sorted(source.items(), key=lambda kv: kv[1])
|
||||
return [target.get(name) for name, _ in by_id]
|
||||
|
||||
|
||||
def rust_table(name, values):
|
||||
cells = ["None" if v is None else f"Some({v})" for v in values]
|
||||
lines = [f"const {name}: &[Option<u8>] = &["]
|
||||
for i in range(0, len(cells), PER_LINE):
|
||||
lines.append(" " + ", ".join(cells[i:i + PER_LINE]) + ",")
|
||||
lines.append("];")
|
||||
return lines
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--old", type=Path, required=True, help="the older version's prepared directory")
|
||||
parser.add_argument("--new", type=Path, help="the pinned version's prepared directory (default: .local)")
|
||||
parser.add_argument("--out", type=Path)
|
||||
args = parser.parse_args()
|
||||
old = json.loads((args.old / "datagen/reports/packets.json").read_text())
|
||||
old_info = lab.version_info(args.old / "jars/client.jar")
|
||||
if args.new:
|
||||
new = json.loads((args.new / "datagen/reports/packets.json").read_text())
|
||||
info = lab.version_info(args.new / "jars/client.jar")
|
||||
else:
|
||||
new = json.loads((ROOT / ".local/datagen/out/reports/packets.json").read_text())
|
||||
info = lab.version_info()
|
||||
old_version, old_protocol = old_info["id"], old_info["protocol_version"]
|
||||
module = "v" + old_version.replace(".", "_")
|
||||
out = args.out or ROOT / f"crates/horizon-protocol/src/adapter/{module}.rs"
|
||||
lines = [
|
||||
f"//! Packet ids of protocol {old_protocol} ({old_version}) against the pinned"
|
||||
f" {info['protocol_version']} ({info['name']}).",
|
||||
"//! Generated by tools/generate_packet_id_adapter.py from both versions' packets reports;",
|
||||
"//! do not edit by hand.",
|
||||
"use crate::State;",
|
||||
"",
|
||||
f"pub const PROTOCOL: i32 = {old_protocol};",
|
||||
f'pub const NAME: &str = "{old_version}";',
|
||||
"",
|
||||
]
|
||||
for state in STATES:
|
||||
for direction in ["clientbound", "serverbound"]:
|
||||
o, n = ids(old, state, direction), ids(new, state, direction)
|
||||
# Clientbound: indexed by the old server's id. Serverbound: indexed by Horizon's id.
|
||||
values = table(o, n) if direction == "clientbound" else table(n, o)
|
||||
assert all(v is None or v < 256 for v in values)
|
||||
lines.append("#[rustfmt::skip]")
|
||||
lines.extend(rust_table(f"{state.upper()}_{direction.upper()}", values))
|
||||
for direction, doc in [
|
||||
("clientbound", f"For each id a {old_version} server sends in `state`, the pinned version's id"
|
||||
" of that packet."),
|
||||
("serverbound", f"For each pinned-version id Horizon sends in `state`, the id a {old_version}"
|
||||
" server knows it by."),
|
||||
]:
|
||||
lines.append(f"/// {doc}")
|
||||
lines.append(f"pub fn {direction}(state: State) -> &'static [Option<u8>] {{")
|
||||
lines.append(" match state {")
|
||||
for state in STATES:
|
||||
lines.append(f" State::{state.capitalize()} => {state.upper()}_{direction.upper()},")
|
||||
lines.append(" }")
|
||||
lines.append("}")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user