Turn a 26.2 client away before untranslated registry data

Only packet ids are translated for an older client so far (#10404): the
registry data, tags (registry ids) and play packets (chunks, light, login,
player_action numbering...) would reach a real 26.2 client in 26.3 ids and
layouts. With accept_older_clients on, such a client now logs in and is
told so in configuration, right after the known packs and before any of
them. NetConfig::older_clients_play (tests only, not in server.toml) keeps
the play path reachable for the id renumbering test until the later parts
translate the contents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Horizon agentandClaude Opus 5.5 committed 2026-11-06 07:51:40 +01:00
1 parent 5daf0853e3
commit bebf670623
6 files changed
+74 -16

No files matched your search

@@ -1123,6 +1123,7 @@ impl Game<'_> {
.raw
.get(horizon_client::lan::ACCEPT_OLDER_CLIENTS)
.is_some_and(|v| v.trim().eq_ignore_ascii_case("true")),
older_clients_play: false,
lod_distance: horizon_client::lod_net::distance_property(
props.raw.get(horizon_client::lod_net::PROPERTY),
),
+18
View File
@@ -979,6 +979,24 @@ fn login_and_configure(
_ => {}
}
};
// An older client's packet ids are translated, not yet what is inside them (#10404): the registry data,
// tags and play packets would reach it in 26.3's ids and layouts, so it is turned away before them.
if let Some(version) = c.older_client()
&& !shared.config.older_clients_play
{
let text = super::OLDER_CLIENTS_UNFINISHED;
let reason = horizon_world::nbt::Tag::String(horizon_world::nbt::JavaString::from(text));
let _ = send(c, cfg::Clientbound::Disconnect(cfg::Disconnect { reason }));
if let Some(log) = &shared.config.log {
let line = format!(
"Disconnecting {name}: its client is on protocol {} ({text})",
version.protocol()
);
(log.write)("Server thread", "INFO", &line);
}
lost(shared, &format!("{name} ({})", uuid_string(uuid)), text);
return Ok(None);
}
// Custom component IDs need Horizon's registry check. The server may also
// require Horizon clients even when it has no custom components.
if brand != "horizon"
+10
View File
@@ -153,6 +153,9 @@ pub const HORIZON_CLIENTS_ONLY_REASON: &str =
/// The server.properties key (server.toml's `server.accept_older_clients`) that lets 26.2 clients join
/// (off unless "true").
pub const ACCEPT_OLDER_CLIENTS: &str = "horizon-accept-older-clients";
/// What a 26.2 client is told while only its packet ids are translated (#10404).
pub const OLDER_CLIENTS_UNFINISHED: &str =
"This server lets 26.2 join, but Horizon cannot translate 26.2 yet: join with 26.3";
/// How the server side of the network behaves: an integrated server's LAN
/// uses the defaults, a dedicated server takes them from server.properties.
@@ -216,6 +219,12 @@ pub struct NetConfig {
/// `horizon-accept-older-clients` (off): a client of 26.2 joins too, its packets renumbered
/// (horizon_protocol::adapter::ClientSession). Off, it is turned away as vanilla turns it away.
pub accept_older_clients: bool,
/// Lets an older client past configuration although only its packet ids are translated yet
/// (#10404's later parts translate the registry ids and the packets whose layout changed): for tests
/// of the id renumbering, never in server.toml. False, such a client is told
/// [`OLDER_CLIENTS_UNFINISHED`] before any registry data, tag or play packet would reach it in 26.3's
/// ids and layouts.
pub older_clients_play: bool,
/// `horizon-lod-distance` (Horizon's own server.properties key, 64): how
/// far, in chunks, the server samples far terrain for Horizon clients
/// (`lod_net`); 0 serves none.
@@ -265,6 +274,7 @@ impl NetConfig {
server_links: Vec::new(),
horizon_clients_only: false,
accept_older_clients: false,
older_clients_play: false,
lod_distance: crate::lod_net::DEFAULT_DISTANCE,
levels: crate::chunk::LevelShapes::VANILLA,
owner_profile: None,
+38 -14
View File
@@ -1786,8 +1786,8 @@ impl Numbered {
}
}
/// A login as `version`: `Err` with the refusal's JSON, or the client once in play, with the ids of the item
/// tag `beacon_payment_items` it was sent.
/// A login as `version`: `Err` with the refusal (login's JSON, configuration's text), or the client once in
/// play, with the ids of the item tag `beacon_payment_items` it was sent.
fn join_as(
version: ServerVersion,
port: u16,
@@ -1856,6 +1856,12 @@ fn join_as(
}
}
}
cfg::Clientbound::Disconnect(d) => {
return Ok(Err(match d.reason {
horizon_world::nbt::Tag::String(s) => s.to_string_lossy(),
other => format!("{other:?}"),
}));
}
cfg::Clientbound::FinishConfiguration(_) => {
bot.send(cfg::Serverbound::FinishConfiguration(Empty).encode())?;
bot.state = State::Play;
@@ -1895,8 +1901,10 @@ fn status_protocol(port: u16, protocol: i32) -> Result<i64, String> {
}
/// `accept_older_clients` off (the default), a 26.2 client is turned away as vanilla turns it away and the
/// server list shows 26.3's protocol; on, the client is told its own protocol, logs in, plays in 26.2's
/// packet ids and sees the block it places, while a 26.3 client is served as before.
/// server list shows 26.3's protocol; on, the client is told its own protocol, logs in in 26.2's packet ids
/// and is told, before any registry data, that 26.2 isn't translated yet, while a 26.3 client is served as
/// before. Let past that (the tests-only `older_clients_play`), it plays in 26.2's packet ids and sees the
/// block it places.
#[test]
fn a_26_2_client_joins_only_a_server_that_accepts_older_clients() {
let Some(resources) = resources() else {
@@ -1932,7 +1940,31 @@ fn a_26_2_client_joins_only_a_server_that_accepts_older_clients() {
))
});
assert_eq!(statuses, (776, i64::from(PROTOCOL_VERSION)));
let placed = run(&mut on, move || {
let unfinished = run(&mut on, move || {
join_as(OLD, port, "Old")?
.err()
.ok_or_else(|| "a 26.2 client got past configuration".to_owned())
});
assert!(
unfinished.contains(horizon_client::lan::OLDER_CLIENTS_UNFINISHED),
"{unfinished}"
);
// A 26.3 client on the same server: its own ids, as on any server.
let payment = run(&mut on, move || {
let (_bot, payment) =
join_as(ServerVersion::Current, port, "New")?.map_err(|r| format!("refused: {r}"))?;
Ok(payment)
});
assert!(payment.contains(&DIAMOND_26_3), "{payment:?}");
let net = NetConfig {
accept_older_clients: true,
older_clients_play: true,
..NetConfig::loopback(0, "Test - World".into(), 8)
};
let mut play = server_with(&resources, net);
let port = play.hub.lan.port;
let placed = run(&mut play, move || {
let (mut bot, _) = join_as(OLD, port, "Old")?.map_err(|r| format!("refused: {r}"))?;
let (mut login, mut chunks, mut placed) = (false, 0, None);
let deadline = Instant::now() + Duration::from_secs(15);
@@ -2000,13 +2032,5 @@ fn a_26_2_client_joins_only_a_server_that_accepts_older_clients() {
Ok(placed.flatten())
});
assert_eq!(placed, Some(([9, MIN_Y + 5, 9], STONE as i32)));
assert_eq!(on.placed.len(), 1);
// A 26.3 client on the same server: its own ids, as on any server.
let payment = run(&mut on, move || {
let (_bot, payment) =
join_as(ServerVersion::Current, port, "New")?.map_err(|r| format!("refused: {r}"))?;
Ok(payment)
});
assert!(payment.contains(&DIAMOND_26_3), "{payment:?}");
assert_eq!(play.placed.len(), 1);
}
@@ -393,6 +393,10 @@ impl Connection {
self.reader.older_client = session.clone();
self.writer.older_client = session;
}
/// The older release this connection's client is on ([`Self::serve_older`]), `None` for the pinned one.
pub fn older_client(&self) -> Option<crate::adapter::ServerVersion> {
self.reader.older_client.as_ref().map(|s| s.version)
}
/// `Connection.isMemoryConnection`: through a [`Pipe`] rather than a socket.
pub fn is_memory(&self) -> bool {
matches!(self.reader.stream, Stream::Memory(_))
+3 -2
View File
@@ -214,8 +214,9 @@ any other release. With `accept_older_clients = true` a 26.2 client may join too
ways, with no proxy in between, and the server list shows the server as joinable for 26.2.
This is work in progress (#10404): the ids inside packets and the packets whose layout changed between 26.2 and
26.3 aren't translated yet, so a 26.2 player can't play on the server yet. 26.3 clients are served exactly as
before, whatever the setting.
26.3 aren't translated yet, so for now a 26.2 player gets through the login and is then told that 26.2 can't be
translated yet, before the server sends anything it would misread. 26.3 clients are served exactly as before,
whatever the setting.
### Seeing further: `max_view_distance`