LAN discovery as vanilla's list keeps it; test_lan_parity against LanServerPinger and LanServerDetection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
//! Horizon's LAN world discovery for tools/test_lan_parity.py: the cases of
|
||||
//! VanillaLanOracle (announcements sent, datagrams heard, list resets) run
|
||||
//! through `lan::discovery`, printing the same lines.
|
||||
use horizon_client::lan::discovery::{
|
||||
LanGame, add_server, parse_address, parse_motd, ping_string_text,
|
||||
};
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
fn unhex(s: &str) -> Vec<u8> {
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let cases = std::fs::read_to_string(&args[1]).unwrap();
|
||||
let mut out = String::new();
|
||||
let mut games: Vec<LanGame> = Vec::new();
|
||||
for line in cases.lines() {
|
||||
let f: Vec<&str> = line.split('\t').collect();
|
||||
match f[0] {
|
||||
"ping" => out += &format!("ping {}\n", hex(ping_string_text(f[1], f[2]).as_bytes())),
|
||||
"heard" => {
|
||||
let received = String::from_utf8_lossy(&unhex(f[2])).into_owned();
|
||||
out += &format!(
|
||||
"heard motd={} address={}\n",
|
||||
parse_motd(&received),
|
||||
parse_address(&received).unwrap_or_else(|| "null".into())
|
||||
);
|
||||
if add_server(&mut games, &received, f[1]) {
|
||||
out += "list";
|
||||
for g in &games {
|
||||
out += &format!(" [{} @ {}]", g.motd, g.address);
|
||||
}
|
||||
out += "\n";
|
||||
}
|
||||
}
|
||||
"reset" => games.clear(),
|
||||
other => panic!("{other}"),
|
||||
}
|
||||
}
|
||||
std::fs::write(&args[2], out).unwrap();
|
||||
}
|
||||
@@ -21,9 +21,8 @@ mod control;
|
||||
mod copper_golems;
|
||||
mod creaking;
|
||||
mod creatures;
|
||||
mod dedicated;
|
||||
mod net_harness;
|
||||
mod crossbow;
|
||||
mod dedicated;
|
||||
mod dispense_fixture;
|
||||
mod dispenser;
|
||||
mod dragon;
|
||||
@@ -60,6 +59,7 @@ mod music;
|
||||
mod names;
|
||||
mod natural_spawning;
|
||||
mod nautili;
|
||||
mod net_harness;
|
||||
mod options;
|
||||
mod packs;
|
||||
mod paintings;
|
||||
|
||||
@@ -37,7 +37,11 @@ pub(super) fn java_double(v: f64) -> String {
|
||||
} else {
|
||||
let s = format!("{v:E}");
|
||||
let (m, e) = s.split_once('E').unwrap_or((&s, "0"));
|
||||
let m = if m.contains('.') { m.to_owned() } else { format!("{m}.0") };
|
||||
let m = if m.contains('.') {
|
||||
m.to_owned()
|
||||
} else {
|
||||
format!("{m}.0")
|
||||
};
|
||||
format!("{m}E{e}")
|
||||
}
|
||||
}
|
||||
@@ -75,8 +79,14 @@ pub(super) fn preflight(options: &mut Options) -> Result<bool> {
|
||||
while let Some(arg) = it.next() {
|
||||
let name = arg.trim_start_matches('-');
|
||||
match name {
|
||||
"nogui" | "demo" | "bonusChest" | "forceUpgrade" | "eraseCache"
|
||||
| "recreateRegionFiles" | "safeMode" | "jfrProfile" => {}
|
||||
"nogui"
|
||||
| "demo"
|
||||
| "bonusChest"
|
||||
| "forceUpgrade"
|
||||
| "eraseCache"
|
||||
| "recreateRegionFiles"
|
||||
| "safeMode"
|
||||
| "jfrProfile" => {}
|
||||
"initSettings" => init_settings = true,
|
||||
"help" => {
|
||||
eprintln!(
|
||||
@@ -296,7 +306,10 @@ impl Game<'_> {
|
||||
let stdin = std::io::stdin();
|
||||
let mut line = String::new();
|
||||
while stdin.read_line(&mut line).is_ok_and(|n| n > 0) {
|
||||
if tx.send(line.trim_end_matches(['\r', '\n']).to_owned()).is_err() {
|
||||
if tx
|
||||
.send(line.trim_end_matches(['\r', '\n']).to_owned())
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
line.clear();
|
||||
@@ -632,13 +645,24 @@ impl Game<'_> {
|
||||
let guests: Vec<(String, u128)> = self
|
||||
.lan
|
||||
.as_ref()
|
||||
.map(|h| h.hub.guests.iter().map(|g| (g.name.clone(), g.uuid)).collect())
|
||||
.map(|h| {
|
||||
h.hub
|
||||
.guests
|
||||
.iter()
|
||||
.map(|g| (g.name.clone(), g.uuid))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let max = self.lan.as_ref().map_or(0, |h| h.hub.lan.shared.max_players());
|
||||
let max = self
|
||||
.lan
|
||||
.as_ref()
|
||||
.map_or(0, |h| h.hub.lan.shared.max_players());
|
||||
let names: Vec<String> = if rest.first() == Some(&"uuids") {
|
||||
guests
|
||||
.iter()
|
||||
.map(|(n, u)| format!("{n} ({})", horizon_protocol::crypto::uuid_string(*u)))
|
||||
.map(|(n, u)| {
|
||||
format!("{n} ({})", horizon_protocol::crypto::uuid_string(*u))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
guests.iter().map(|(n, _)| n.clone()).collect()
|
||||
@@ -697,7 +721,12 @@ impl Game<'_> {
|
||||
&[
|
||||
lit(&user.name),
|
||||
reason.as_deref().map_or_else(
|
||||
|| Text::translate("multiplayer.disconnect.banned.reason.default", &[]),
|
||||
|| {
|
||||
Text::translate(
|
||||
"multiplayer.disconnect.banned.reason.default",
|
||||
&[],
|
||||
)
|
||||
},
|
||||
lit,
|
||||
),
|
||||
],
|
||||
@@ -820,7 +849,12 @@ impl Game<'_> {
|
||||
lit(&name),
|
||||
lit(&source),
|
||||
reason.as_deref().map_or_else(
|
||||
|| Text::translate("multiplayer.disconnect.banned.reason.default", &[]),
|
||||
|| {
|
||||
Text::translate(
|
||||
"multiplayer.disconnect.banned.reason.default",
|
||||
&[],
|
||||
)
|
||||
},
|
||||
lit,
|
||||
),
|
||||
],
|
||||
@@ -932,7 +966,11 @@ impl Game<'_> {
|
||||
(false, "commands.whitelist.remove.failed")
|
||||
};
|
||||
drop(l);
|
||||
let args = if ok { vec![lit(&user.name)] } else { Vec::new() };
|
||||
let args = if ok {
|
||||
vec![lit(&user.name)]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
self.console_reply(Text::translate(key, &args));
|
||||
}
|
||||
_ => self.console_reply(Text::translate("command.unknown.argument", &[])),
|
||||
@@ -968,7 +1006,11 @@ impl Game<'_> {
|
||||
/// MinecraftServer.saveAllChunks, not silent: one line per level, in
|
||||
/// the server's level order.
|
||||
fn log_saved_levels(&self) {
|
||||
for dimension in ["minecraft:overworld", "minecraft:the_nether", "minecraft:the_end"] {
|
||||
for dimension in [
|
||||
"minecraft:overworld",
|
||||
"minecraft:the_nether",
|
||||
"minecraft:the_end",
|
||||
] {
|
||||
log(
|
||||
"INFO",
|
||||
&format!(
|
||||
@@ -988,8 +1030,10 @@ impl Game<'_> {
|
||||
}
|
||||
let conns: Vec<u32> = h.hub.guests.iter().map(|g| g.conn).collect();
|
||||
for conn in conns {
|
||||
h.hub
|
||||
.kick(conn, translatable("multiplayer.disconnect.server_shutdown", &[]));
|
||||
h.hub.kick(
|
||||
conn,
|
||||
translatable("multiplayer.disconnect.server_shutdown", &[]),
|
||||
);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
@@ -517,7 +517,13 @@ impl Game<'_> {
|
||||
let me = (self.options.dedicated.is_none() && self.player.is_some())
|
||||
.then_some((host.host_uuid, super::chat::PLAYER_NAME));
|
||||
let content = host.hub.list_reply(me, words.len() == 2);
|
||||
host.hub.send_to(conn, &C::SystemChat(play::SystemChat { content, overlay: false }));
|
||||
host.hub.send_to(
|
||||
conn,
|
||||
&C::SystemChat(play::SystemChat {
|
||||
content,
|
||||
overlay: false,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if !host.settings.guest_commands || !self.cheats {
|
||||
|
||||
@@ -45,7 +45,10 @@ impl Game<'_> {
|
||||
quiet_since = now;
|
||||
}
|
||||
match step {
|
||||
0 if spawned_at.is_some() && count > 0 && now - quiet_since > Duration::from_secs(3) => {
|
||||
0 if spawned_at.is_some()
|
||||
&& count > 0
|
||||
&& now - quiet_since > Duration::from_secs(3) =>
|
||||
{
|
||||
chunks = count;
|
||||
self.send_chat("hello from the horizon client");
|
||||
step = 1;
|
||||
|
||||
@@ -203,9 +203,9 @@ impl ChunkEncoder {
|
||||
for (i, s) in chunk.sections.iter().enumerate() {
|
||||
let mut unpacked = [0u16; BLOCKS];
|
||||
s.unpack(&mut unpacked);
|
||||
let any = unpacked.iter().any(|&v| {
|
||||
self.flags.get(usize::from(v)).copied().unwrap_or(0) & NON_AIR != 0
|
||||
});
|
||||
let any = unpacked
|
||||
.iter()
|
||||
.any(|&v| self.flags.get(usize::from(v)).copied().unwrap_or(0) & NON_AIR != 0);
|
||||
if any {
|
||||
bits |= 1 << i;
|
||||
}
|
||||
@@ -296,7 +296,11 @@ impl ChunkEncoder {
|
||||
/// (`items` in section order) went through.
|
||||
fn java_hash_order<T>(items: &mut Vec<T>, pos: impl Fn(&T) -> (i32, i32, i32)) {
|
||||
// Vec3i.hashCode.
|
||||
let hash = |&(x, y, z): &(i32, i32, i32)| y.wrapping_add(z.wrapping_mul(31)).wrapping_mul(31).wrapping_add(x);
|
||||
let hash = |&(x, y, z): &(i32, i32, i32)| {
|
||||
y.wrapping_add(z.wrapping_mul(31))
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(x)
|
||||
};
|
||||
let keys: Vec<i32> = items.iter().map(|i| hash(&pos(i))).collect();
|
||||
// java.util.HashMap: buckets in index order, insertion order within one.
|
||||
let mut capacity = 16usize;
|
||||
@@ -304,7 +308,9 @@ fn java_hash_order<T>(items: &mut Vec<T>, pos: impl Fn(&T) -> (i32, i32, i32)) {
|
||||
capacity *= 2;
|
||||
}
|
||||
let mut order: Vec<usize> = (0..keys.len()).collect();
|
||||
order.sort_by_key(|&i| ((keys[i] ^ ((keys[i] as u32) >> 16) as i32) as u32 as usize) & (capacity - 1));
|
||||
order.sort_by_key(|&i| {
|
||||
((keys[i] ^ ((keys[i] as u32) >> 16) as i32) as u32 as usize) & (capacity - 1)
|
||||
});
|
||||
// Object2ObjectOpenHashMap.
|
||||
let mix = |h: i32| {
|
||||
let h = h.wrapping_mul(0x9E37_79B9_u32 as i32) as u32;
|
||||
@@ -356,7 +362,15 @@ fn saved_update_tag(chunk: &Chunk, pos: (i32, i32, i32)) -> Option<Tag> {
|
||||
let Tag::Compound(mut c) = crate::block_entity_nbt::canonical(tag)? else {
|
||||
return None;
|
||||
};
|
||||
for key in ["id", "x", "y", "z", "keepPacked", "SpawnPotentials", "components"] {
|
||||
for key in [
|
||||
"id",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"keepPacked",
|
||||
"SpawnPotentials",
|
||||
"components",
|
||||
] {
|
||||
c.remove(&key.into());
|
||||
}
|
||||
Some(Tag::Compound(c))
|
||||
@@ -466,7 +480,9 @@ mod tests {
|
||||
#[test]
|
||||
fn light_masks_cover_the_world_plus_borders() {
|
||||
let chunk = Chunk::empty(0, 0);
|
||||
let data = light_data(&chunk);
|
||||
// A column with nothing around it has no light layers at all.
|
||||
assert!(light_data(&chunk, 0).sky.is_empty());
|
||||
let data = light_data(&chunk, u64::MAX);
|
||||
// Default sections are full sky light and no block light.
|
||||
assert_eq!(data.sky.len(), SECTIONS + 1);
|
||||
assert_eq!(data.block.len(), 0);
|
||||
|
||||
@@ -52,10 +52,37 @@ fn component_value(tag: &horizon_world::nbt::Tag) -> crate::server::json::Json {
|
||||
use crate::server::json::Json;
|
||||
use horizon_world::nbt::Tag;
|
||||
const ORDER: &[&str] = &[
|
||||
"type", "text", "translate", "fallback", "with", "score", "selector", "separator",
|
||||
"keybind", "nbt", "interpret", "block", "entity", "storage", "source", "object",
|
||||
"sprite", "atlas", "player", "hat", "extra", "color", "shadow_color", "bold", "italic",
|
||||
"underlined", "strikethrough", "obfuscated", "click_event", "hover_event", "insertion",
|
||||
"type",
|
||||
"text",
|
||||
"translate",
|
||||
"fallback",
|
||||
"with",
|
||||
"score",
|
||||
"selector",
|
||||
"separator",
|
||||
"keybind",
|
||||
"nbt",
|
||||
"interpret",
|
||||
"block",
|
||||
"entity",
|
||||
"storage",
|
||||
"source",
|
||||
"object",
|
||||
"sprite",
|
||||
"atlas",
|
||||
"player",
|
||||
"hat",
|
||||
"extra",
|
||||
"color",
|
||||
"shadow_color",
|
||||
"bold",
|
||||
"italic",
|
||||
"underlined",
|
||||
"strikethrough",
|
||||
"obfuscated",
|
||||
"click_event",
|
||||
"hover_event",
|
||||
"insertion",
|
||||
"font",
|
||||
];
|
||||
match tag {
|
||||
@@ -125,7 +152,10 @@ fn serve_legacy(mut stream: TcpStream, shared: &Shared) -> Result<(), String> {
|
||||
let bytes = d.get(*at + 2..*at + 2 + len * 2)?;
|
||||
*at += 2 + len * 2;
|
||||
Some(String::from_utf16_lossy(
|
||||
&bytes.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect::<Vec<_>>(),
|
||||
&bytes
|
||||
.chunks(2)
|
||||
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
};
|
||||
let motd = shared.motd();
|
||||
@@ -181,7 +211,10 @@ pub fn serve(
|
||||
let e = |e: horizon_protocol::Error| e.to_string();
|
||||
// LegacyQueryHandler, ahead of the frame decoder while status is on.
|
||||
let mut first = [0u8; 1];
|
||||
if shared.config.enable_status && stream.peek(&mut first).is_ok_and(|n| n == 1) && first[0] == 0xFE {
|
||||
if shared.config.enable_status
|
||||
&& stream.peek(&mut first).is_ok_and(|n| n == 1)
|
||||
&& first[0] == 0xFE
|
||||
{
|
||||
return serve_legacy(stream, &shared);
|
||||
}
|
||||
let mut c = Connection::from_stream(stream).map_err(e)?;
|
||||
|
||||
@@ -10,34 +10,61 @@ use std::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread::JoinHandle,
|
||||
time::{Duration, Instant},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub const GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 2, 60);
|
||||
pub const PORT: u16 = 4445;
|
||||
/// LanServerPinger sleeps 1500 ms between announcements.
|
||||
const INTERVAL: Duration = Duration::from_millis(1500);
|
||||
/// LanServerDetection.LanServerList drops entries not refreshed for 5 s.
|
||||
const EXPIRY: Duration = Duration::from_secs(5);
|
||||
|
||||
/// LanServerPinger.createPingString.
|
||||
pub fn ping_string(motd: &str, port: u16) -> String {
|
||||
format!("[MOTD]{motd}[/MOTD][AD]{port}[/AD]")
|
||||
ping_string_text(motd, &port.to_string())
|
||||
}
|
||||
fn between<'a>(s: &'a str, open: &str, close: &str) -> Option<&'a str> {
|
||||
let start = s.find(open)? + open.len();
|
||||
let end = s[start..].find(close)? + start;
|
||||
Some(&s[start..end])
|
||||
/// LanServerPinger.createPingString with the address as text.
|
||||
pub fn ping_string_text(motd: &str, address: &str) -> String {
|
||||
format!("[MOTD]{motd}[/MOTD][AD]{address}[/AD]")
|
||||
}
|
||||
/// LanServerPinger.parseMotd; vanilla shows "missing no" when absent.
|
||||
/// String.indexOf(needle, from) on a Rust string (byte offsets).
|
||||
fn index_of(s: &str, needle: &str, from: usize) -> Option<usize> {
|
||||
s.get(from..)?.find(needle).map(|i| i + from)
|
||||
}
|
||||
/// LanServerPinger.parseMotd; "missing no" when there is none.
|
||||
pub fn parse_motd(s: &str) -> String {
|
||||
between(s, "[MOTD]", "[/MOTD]")
|
||||
.unwrap_or("missing no")
|
||||
.to_owned()
|
||||
let Some(start) = s.find("[MOTD]") else {
|
||||
return "missing no".to_owned();
|
||||
};
|
||||
match index_of(s, "[/MOTD]", start + "[MOTD]".len()) {
|
||||
Some(end) => s[start + "[MOTD]".len()..end].to_owned(),
|
||||
None => "missing no".to_owned(),
|
||||
}
|
||||
}
|
||||
/// LanServerPinger.parseAddress: the announced port, if present and valid.
|
||||
pub fn parse_port(s: &str) -> Option<u16> {
|
||||
between(s, "[AD]", "[/AD]")?.parse().ok().filter(|p| *p > 0)
|
||||
/// LanServerPinger.parseAddress: the text between [AD] and [/AD] after the
|
||||
/// only [/MOTD], as announced (vanilla does not check it is a port).
|
||||
pub fn parse_address(s: &str) -> Option<String> {
|
||||
let end_motd = s.find("[/MOTD]")?;
|
||||
if index_of(s, "[/MOTD]", end_motd + "[/MOTD]".len()).is_some() {
|
||||
return None;
|
||||
}
|
||||
let start = index_of(s, "[AD]", end_motd + "[/MOTD]".len())?;
|
||||
let end = index_of(s, "[/AD]", start + "[AD]".len())?;
|
||||
Some(s[start + "[AD]".len()..end].to_owned())
|
||||
}
|
||||
/// LanServerDetection.LanServerList.addServer: an announcement from `ip`.
|
||||
/// A new address joins the list; a known one keeps its first MOTD. True
|
||||
/// when the list changed.
|
||||
pub fn add_server(games: &mut Vec<LanGame>, ping: &str, ip: &str) -> bool {
|
||||
let motd = parse_motd(ping);
|
||||
let Some(address) = parse_address(ping) else {
|
||||
return false;
|
||||
};
|
||||
let address = format!("{ip}:{address}");
|
||||
if games.iter().any(|g| g.address == address) {
|
||||
return false;
|
||||
}
|
||||
games.push(LanGame { motd, address });
|
||||
true
|
||||
}
|
||||
|
||||
/// Announces one hosted world until dropped.
|
||||
@@ -48,7 +75,6 @@ pub struct Pinger {
|
||||
impl Pinger {
|
||||
pub fn start(motd: String, port: u16) -> io::Result<Self> {
|
||||
let socket = UdpSocket::bind(("0.0.0.0", 0))?;
|
||||
let _ = socket.set_multicast_ttl_v4(4);
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let flag = stop.clone();
|
||||
let message = ping_string(&motd, port);
|
||||
@@ -86,7 +112,7 @@ pub struct LanGame {
|
||||
pub motd: String,
|
||||
pub address: String,
|
||||
}
|
||||
type Games = Arc<Mutex<Vec<(LanGame, Instant)>>>;
|
||||
type Games = Arc<Mutex<Vec<LanGame>>>;
|
||||
|
||||
/// Listens for announcements until the last clone is dropped.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -118,11 +144,10 @@ impl Scanner {
|
||||
inner: Arc::new(ScanInner { games, stop }),
|
||||
}
|
||||
}
|
||||
/// The live announcements, oldest first.
|
||||
/// The worlds announced since the scan started, in order (vanilla's
|
||||
/// list keeps them while the multiplayer screen is open).
|
||||
pub fn games(&self) -> Vec<LanGame> {
|
||||
let mut games = self.inner.games.lock().unwrap();
|
||||
games.retain(|(_, seen)| seen.elapsed() < EXPIRY);
|
||||
games.iter().map(|(g, _)| g.clone()).collect()
|
||||
self.inner.games.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
fn bind_listener() -> io::Result<UdpSocket> {
|
||||
@@ -138,18 +163,7 @@ fn listen(socket: UdpSocket, games: Games, stop: Arc<AtomicBool>) {
|
||||
continue;
|
||||
};
|
||||
let text = String::from_utf8_lossy(&buf[..n]);
|
||||
let Some(port) = parse_port(&text) else {
|
||||
continue;
|
||||
};
|
||||
let game = LanGame {
|
||||
motd: parse_motd(&text),
|
||||
address: format!("{}:{port}", from.ip()),
|
||||
};
|
||||
let mut games = games.lock().unwrap();
|
||||
match games.iter_mut().find(|(g, _)| g.address == game.address) {
|
||||
Some(entry) => *entry = (game, Instant::now()),
|
||||
None => games.push((game, Instant::now())),
|
||||
}
|
||||
add_server(&mut games.lock().unwrap(), &text, &from.ip().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +176,17 @@ mod tests {
|
||||
let s = ping_string("Steve - World", 25565);
|
||||
assert_eq!(s, "[MOTD]Steve - World[/MOTD][AD]25565[/AD]");
|
||||
assert_eq!(parse_motd(&s), "Steve - World");
|
||||
assert_eq!(parse_port(&s), Some(25565));
|
||||
assert_eq!(parse_port("[AD]0[/AD]"), None);
|
||||
assert_eq!(parse_address(&s).as_deref(), Some("25565"));
|
||||
assert_eq!(parse_address("[AD]1[/AD]"), None);
|
||||
assert_eq!(parse_address("[/MOTD][AD]x[/AD][/MOTD]"), None);
|
||||
assert_eq!(parse_motd("junk"), "missing no");
|
||||
let mut games = Vec::new();
|
||||
assert!(add_server(&mut games, &s, "10.0.0.2"));
|
||||
assert!(!add_server(
|
||||
&mut games,
|
||||
&ping_string("Other", 25565),
|
||||
"10.0.0.2"
|
||||
));
|
||||
assert_eq!(games[0].motd, "Steve - World");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,12 @@ pub fn yellow_translation(key: &str, args: &[&str]) -> Tag {
|
||||
/// Player.getDisplayName for a player in no team: the name, suggesting
|
||||
/// `/tell NAME `, showing the player on hover and inserting the name.
|
||||
pub fn display_name(uuid: u128, name: &str) -> Tag {
|
||||
let words = [(uuid >> 96) as i32, (uuid >> 64) as i32, (uuid >> 32) as i32, uuid as i32];
|
||||
let words = [
|
||||
(uuid >> 96) as i32,
|
||||
(uuid >> 64) as i32,
|
||||
(uuid >> 32) as i32,
|
||||
uuid as i32,
|
||||
];
|
||||
Tag::compound([
|
||||
("text", string(name)),
|
||||
(
|
||||
@@ -275,7 +280,12 @@ impl Hub {
|
||||
pub fn list_reply(&self, host: Option<(u128, &str)>, uuids: bool) -> Tag {
|
||||
let players: Vec<(u128, &str)> = host
|
||||
.into_iter()
|
||||
.chain(self.guests.iter().filter(|g| g.placed).map(|g| (g.uuid, g.name.as_str())))
|
||||
.chain(
|
||||
self.guests
|
||||
.iter()
|
||||
.filter(|g| g.placed)
|
||||
.map(|g| (g.uuid, g.name.as_str())),
|
||||
)
|
||||
.collect();
|
||||
let format = |(uuid, name): (u128, &str)| {
|
||||
if uuids {
|
||||
@@ -300,7 +310,10 @@ impl Hub {
|
||||
let mut extra = Vec::new();
|
||||
for (k, p) in players.iter().enumerate() {
|
||||
if k > 0 {
|
||||
extra.push(Tag::compound([("text", string(", ")), ("color", string("gray"))]));
|
||||
extra.push(Tag::compound([
|
||||
("text", string(", ")),
|
||||
("color", string("gray")),
|
||||
]));
|
||||
}
|
||||
extra.push(format(*p));
|
||||
}
|
||||
@@ -536,7 +549,10 @@ impl Hub {
|
||||
// ServerLevel.addNewPlayer: the chunk tracking starts at the player.
|
||||
let g = &mut self.guests[i];
|
||||
let (cx, cz) = g.chunk();
|
||||
g.send(&C::SetChunkCacheCenter(play::SetChunkCacheCenter { x: cx, z: cz }));
|
||||
g.send(&C::SetChunkCacheCenter(play::SetChunkCacheCenter {
|
||||
x: cx,
|
||||
z: cz,
|
||||
}));
|
||||
g.center_sent = Some((cx, cz));
|
||||
g.placed_at = self.ticks;
|
||||
// ServerPlayer.sendPostEffects and initInventoryMenu.
|
||||
|
||||
@@ -73,7 +73,8 @@ pub enum Out {
|
||||
|
||||
/// PlayerList.canPlayerLogin for a dedicated server: the disconnect reason
|
||||
/// (bans, whitelist) for a profile connecting from an address, if refused.
|
||||
pub type LoginGate = Arc<dyn Fn(SocketAddr, u128, &str) -> Option<horizon_world::nbt::Tag> + Send + Sync>;
|
||||
pub type LoginGate =
|
||||
Arc<dyn Fn(SocketAddr, u128, &str) -> Option<horizon_world::nbt::Tag> + Send + Sync>;
|
||||
|
||||
/// How the server side of the network behaves: an integrated server's LAN
|
||||
/// uses the defaults, a dedicated server takes them from server.properties.
|
||||
@@ -199,7 +200,9 @@ impl Shared {
|
||||
}
|
||||
fn next_random(&self, bound: usize) -> usize {
|
||||
let mut r = self.random.lock().unwrap();
|
||||
*r = r.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
|
||||
*r = r
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1_442_695_040_888_963_407);
|
||||
((*r >> 33) as usize) % bound.max(1)
|
||||
}
|
||||
/// The status response (MinecraftServer.buildServerStatus encoded with
|
||||
@@ -211,7 +214,10 @@ impl Shared {
|
||||
let online: Vec<(u128, String)> = self.online.lock().unwrap().clone();
|
||||
let count = self.online();
|
||||
let mut players = vec![
|
||||
("max".to_owned(), Json::Number(self.max_players().to_string())),
|
||||
(
|
||||
"max".to_owned(),
|
||||
Json::Number(self.max_players().to_string()),
|
||||
),
|
||||
("online".to_owned(), Json::Number(count.to_string())),
|
||||
];
|
||||
if !self.config.hide_online_players && !online.is_empty() {
|
||||
@@ -244,7 +250,10 @@ impl Shared {
|
||||
"version".to_owned(),
|
||||
Json::Object(vec![
|
||||
("name".to_owned(), Json::String(GAME_VERSION.to_owned())),
|
||||
("protocol".to_owned(), Json::Number(PROTOCOL_VERSION.to_string())),
|
||||
(
|
||||
"protocol".to_owned(),
|
||||
Json::Number(PROTOCOL_VERSION.to_string()),
|
||||
),
|
||||
]),
|
||||
),
|
||||
];
|
||||
@@ -266,12 +275,24 @@ pub fn base64(data: &[u8]) -> String {
|
||||
const A: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
|
||||
for chunk in data.chunks(3) {
|
||||
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
|
||||
let b = [
|
||||
chunk[0],
|
||||
*chunk.get(1).unwrap_or(&0),
|
||||
*chunk.get(2).unwrap_or(&0),
|
||||
];
|
||||
let n = u32::from(b[0]) << 16 | u32::from(b[1]) << 8 | u32::from(b[2]);
|
||||
out.push(char::from(A[(n >> 18) as usize & 63]));
|
||||
out.push(char::from(A[(n >> 12) as usize & 63]));
|
||||
out.push(if chunk.len() > 1 { char::from(A[(n >> 6) as usize & 63]) } else { '=' });
|
||||
out.push(if chunk.len() > 2 { char::from(A[n as usize & 63]) } else { '=' });
|
||||
out.push(if chunk.len() > 1 {
|
||||
char::from(A[(n >> 6) as usize & 63])
|
||||
} else {
|
||||
'='
|
||||
});
|
||||
out.push(if chunk.len() > 2 {
|
||||
char::from(A[n as usize & 63])
|
||||
} else {
|
||||
'='
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -651,7 +651,11 @@ pub fn client_heightmaps(chunk: &Chunk, tables: &SaveTables) -> Vec<(i32, Vec<i6
|
||||
];
|
||||
let mut maps: Vec<(i32, Heightmap)> = kinds
|
||||
.iter()
|
||||
.filter_map(|(id, k)| Heightmap::new(*k, MIN_Y, SECTIONS as i32 * 16).ok().map(|m| (*id, m)))
|
||||
.filter_map(|(id, k)| {
|
||||
Heightmap::new(*k, MIN_Y, SECTIONS as i32 * 16)
|
||||
.ok()
|
||||
.map(|m| (*id, m))
|
||||
})
|
||||
.collect();
|
||||
let view = HeightmapView { chunk, tables };
|
||||
let mut refs: Vec<&mut Heightmap> = maps.iter_mut().map(|(_, m)| m).collect();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import net.minecraft.client.server.LanServer;
|
||||
import net.minecraft.client.server.LanServerDetection;
|
||||
import net.minecraft.client.server.LanServerPinger;
|
||||
|
||||
/**
|
||||
* Vanilla's LAN world discovery for tools/test_lan_parity.py. Each input
|
||||
* line is a case: `ping MOTD PORT` prints the announcement
|
||||
* LanServerPinger sends (its UTF-8 bytes); `heard IP HEX` decodes a received
|
||||
* datagram as LanServerDetection does and adds it to the list
|
||||
* (LanServerList.addServer), printing what it parsed and the list when it
|
||||
* changed; `reset` starts a new list (a new multiplayer screen).
|
||||
*/
|
||||
public final class VanillaLanOracle {
|
||||
public static void main(String[] args) throws Exception {
|
||||
List<String> cases = Files.readAllLines(Path.of(args[0]), StandardCharsets.UTF_8);
|
||||
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8))) {
|
||||
LanServerDetection.LanServerList list = new LanServerDetection.LanServerList();
|
||||
for (String line : cases) {
|
||||
String[] f = line.split("\t", -1);
|
||||
switch (f[0]) {
|
||||
case "ping" -> {
|
||||
String ping = LanServerPinger.createPingString(f[1], f[2]);
|
||||
out.println("ping " + HexFormat.of().formatHex(ping.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
case "heard" -> {
|
||||
byte[] bytes = HexFormat.of().parseHex(f[2]);
|
||||
String received = new String(bytes, 0, bytes.length, StandardCharsets.UTF_8);
|
||||
out.println("heard motd=" + LanServerPinger.parseMotd(received) + " address=" + LanServerPinger.parseAddress(received));
|
||||
list.addServer(received, InetAddress.getByName(f[1]));
|
||||
List<LanServer> dirty = list.takeDirtyServers();
|
||||
if (dirty != null) {
|
||||
StringBuilder b = new StringBuilder("list");
|
||||
for (LanServer s : dirty) b.append(" [").append(s.getMotd()).append(" @ ").append(s.getAddress()).append(']');
|
||||
out.println(b);
|
||||
}
|
||||
}
|
||||
case "reset" -> list = new LanServerDetection.LanServerList();
|
||||
default -> throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LAN world discovery against the pinned client's LanServerPinger and
|
||||
LanServerDetection: the announcement an opened world multicasts
|
||||
(`[MOTD]...[/MOTD][AD]port[/AD]` in UTF-8), and how the multiplayer screen
|
||||
parses what it hears (parseMotd, parseAddress, UTF-8 decoding of malformed
|
||||
bytes) and keeps its list (LanServerList.addServer: one entry per
|
||||
address, the first MOTD kept, never expired while the screen is open).
|
||||
VanillaLanOracle and the `lan_probe` example must print the same lines."""
|
||||
import random
|
||||
|
||||
try:
|
||||
from . import oracle_harness as oh
|
||||
except ImportError:
|
||||
import oracle_harness as oh
|
||||
|
||||
NAME = 'lan-parity'
|
||||
|
||||
|
||||
def make_input(build):
|
||||
rng = random.Random(4445)
|
||||
lines = []
|
||||
motds = ['Player - New World', 'Steve - §aGreen §lWorld', 'été 日本語 ✓', '', '[MOTD]', 'a[/MOTD]b', 'x' * 300,
|
||||
'tab\\there', '[AD]25565[/AD]']
|
||||
for motd in motds:
|
||||
for port in ['25565', '1024', '65535', '0', 'abc', '']:
|
||||
lines.append(f'ping\t{motd}\t{port}')
|
||||
fragments = [b'[MOTD]', b'[/MOTD]', b'[AD]', b'[/AD]', b'25565', b'1234', b'Steve - World', b'\xc3\xa9', b'\xe6\x97',
|
||||
b'\xff', b'\xc0\x80', b'\xed\xa0\x80', b'\xf0\x9f\x98\x80', b'\xf0\x9f', b'abc', b' ', b'\x00', b'-1']
|
||||
ips = ['192.168.1.10', '10.0.0.2', '127.0.0.1']
|
||||
good = lambda motd, port: f'[MOTD]{motd}[/MOTD][AD]{port}[/AD]'.encode()
|
||||
for i in range(400):
|
||||
if i % 50 == 0:
|
||||
lines.append('reset')
|
||||
if rng.random() < 0.4:
|
||||
data = good(rng.choice(['A - W', 'B - W', 'é', '']), rng.choice(['25565', '25566', 'x', '']))
|
||||
else:
|
||||
data = b''.join(rng.choice(fragments) for _ in range(rng.randint(0, 9)))
|
||||
lines.append(f'heard\t{rng.choice(ips)}\t{data.hex()}')
|
||||
path = build / 'cases.txt'
|
||||
path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||||
return path, len(lines)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
oh.differential(NAME, 'VanillaLanOracle', 'lan_probe', make_input,
|
||||
'LanServerPinger.createPingString/parseMotd/parseAddress, LanServerDetection decoding and '
|
||||
'LanServerList.addServer/takeDirtyServers',
|
||||
sources=[oh.lab.ROOT / 'crates/horizon-client/src/lan/discovery.rs'])
|
||||
Reference in New Issue
Block a user