gmod-rs/crates/net/tests/client_hello.rs
Niko Marmeladkov 77d70f6b9f net: добавлен недостающий userinfo-конвар cl_playermodel
Наш первый netchan-пакет был на 22 байта короче эталонного — ровно на
cl_playermodel\0male07\0. Без него сервер не знает, какую модель ставить
нашему энтити-игроку.

Набор конваров вынесен в client::userinfo_pairs() и теперь сверяется с
эталоном настоящего GMod (новая фикстура fixtures/classicbox_client_hello.bin,
511 байт из пакета #24) попарно, включая порядок. При том же имени наш пакет
теперь совпадает с эталонным побайтово: тело идентично в 513 из 514 байт,
различаются только checksum/challenge из-за другой сессии и один остаточный
бит добивки.
2026-10-12 14:51:57 +03:00

145 lines
5.8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Набор userinfo-конваров должен совпадать с настоящим клиентом GMod.
//!
//! Фикстур `fixtures/classicbox_client_hello.bin` — 511 байт, которые реальный
//! клиент GMod положил в первый netchan-пакет при подключении к ClassicBox
//! (пакет #24 в `real_client.pcap`). Это эталон: сервер формирует userinfo
//! игрока именно из этих пар, и по полноте набора видно, чего клиенту не
//! хватает.
//!
//! Тест ловит именно ту ошибку, которая была: мы не отправляли `cl_playermodel`,
//! и наш пакет был на 22 байта короче эталонного.
use net::bitbuf::{BitReader, BitWriter};
use net::client::userinfo_pairs;
use net::netchan::{ClientMessage, Message, parse_messages};
/// Полезная нагрузка первого netchan-пакета настоящего GMod.
const REFERENCE: &[u8] = include_bytes!("fixtures/classicbox_client_hello.bin");
/// Раскладывает `NET_SetConVar` из блока сообщений в список пар.
fn convars_of(block: &[u8]) -> Vec<(String, String)> {
let msgs = parse_messages(block, 0);
for m in &msgs {
if let Message::SetConVar(pairs) = m {
return pairs.clone();
}
}
panic!("в блоке нет NET_SetConVar");
}
/// Собирает наш собственный hello так же, как это делает клиент при подключении.
fn our_hello_convars() -> Vec<(String, String)> {
let mut w = BitWriter::new();
ClientMessage::SetConVar(userinfo_pairs("rpopre_apovle")).write_for_test(&mut w);
let bytes = w.bytes().to_vec();
convars_of(&bytes)
}
#[test]
fn reference_hello_parses() {
// Фикстура должна читаться: ровно SetConVar и SignonState.
let msgs = parse_messages(REFERENCE, 0);
let kinds: Vec<&str> = msgs
.iter()
.filter(|m| !matches!(m, Message::Nop))
.map(|m| match m {
Message::SetConVar(_) => "SetConVar",
Message::SignOnState { .. } => "SignonState",
other => panic!("неожиданное сообщение {other:?}"),
})
.collect();
assert_eq!(
kinds,
vec!["SetConVar", "SignonState"],
"эталонный hello должен состоять из SetConVar и SignonState"
);
let pairs = convars_of(REFERENCE);
assert_eq!(pairs.len(), 28, "эталон содержит 28 пар");
assert_eq!(pairs[0].0, "tv_nochat");
assert_eq!(
pairs.last().unwrap().0,
"cl_autowepswitch",
"последняя пара эталонного набора"
);
}
#[test]
fn reference_hello_signon_state_is_connected() {
let msgs = parse_messages(REFERENCE, 0);
let state = msgs
.iter()
.find_map(|m| match m {
Message::SignOnState { state, spawn_count } => Some((*state, *spawn_count)),
_ => None,
})
.expect("SignonState");
assert_eq!(state.0, 2, "CONNECTED");
assert_eq!(state.1, u32::MAX, "spawn_count = -1");
}
#[test]
fn our_convars_match_reference_exactly() {
let reference = convars_of(REFERENCE);
let ours = our_hello_convars();
assert_eq!(
ours.len(),
reference.len(),
"число userinfo-конваров: у нас {}, в эталоне {}\nнабор: {:?}",
ours.len(),
reference.len(),
ours
);
for (i, (ours, refpair)) in ours.iter().zip(reference.iter()).enumerate() {
// `name` зависит от того, под кем мы заходим, — сверяем отдельно.
if ours.0 == "name" {
assert_eq!(refpair.0, "name", "позиция {i}: в эталоне должен быть name");
assert_eq!(ours.1, "rpopre_apovle");
continue;
}
assert_eq!(
(ours.0.as_str(), ours.1.as_str()),
(refpair.0.as_str(), refpair.1.as_str()),
"конвар №{i} разошёлся с эталоном"
);
}
}
#[test]
fn no_duplicate_convars() {
// Сервер принимает дубликаты молча, но лишние userinfo-пары ломают
// отображение в браузере сервера — проверяем уникальность имён.
let ours = our_hello_convars();
let mut names: Vec<&str> = ours.iter().map(|(k, _)| k.as_str()).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "в наборе есть повторяющиеся имена");
}
#[test]
fn convar_values_fit_protocol_limits() {
// Имя и значение ограничены 260 байтами (см. NET_SetConVar в Source.NET),
// а их не больше 255 в одном сообщении.
let ours = our_hello_convars();
assert!(ours.len() <= 255, "конваров больше 255");
for (k, v) in &ours {
assert!(k.len() < 260, "имя конвара длиннее 260 байт: {k}");
assert!(v.len() < 260, "значение длиннее 260 байт: {k}");
assert!(
k.bytes().all(|b| b != 0),
"в имени конвара нуль-байт: {k:?}"
);
}
}
#[test]
fn reader_stays_inside_block() {
// Sanity: разбор эталонного блока не выходит за границы буфера.
let mut r = BitReader::new(REFERENCE);
assert!(r.bits_left() == REFERENCE.len() * 8);
let _ = r.read_ubit_long(6);
assert!(r.bits_left() < REFERENCE.len() * 8);
}