net: битовый буфер переведён на MSB-first, как в движке
Настоящая причина отсутствия server info: bf_write писал биты LSB-первым, а Source читает/пишет MSB-первым (подтверждено дизассемблированием bf_read::ReadUBitLongNoInline в engine_srv.so). Из-за этого WriteOneBit(true) попадал в поле соседнего потока фрагментов, а ID сообщений записывались перевёрнутыми — сервер не распознавал net_SignonState и не выставлял m_bSendServerInfo. - write_one_bit/write_ubit_long/read_one_bit/read_ubit_long: MSB-first; - write_u32/write_short/read_long/read_short: побайтово little-endian (как ReadLong в движке); - reliable_state кодируется через WriteUBitLong(16); - тесты bit_order_matches_source и reliable_block_layout_is_canonical.
This commit is contained in:
parent
50bb36304e
commit
12f94ae55a
3 changed files with 169 additions and 25 deletions
|
|
@ -1,6 +1,14 @@
|
|||
//! Битовые буферы в стиле Source (`bf_write`/`bf_read`).
|
||||
//!
|
||||
//! Порядок битов — LSB-первый внутри каждого байта (как в движке).
|
||||
//! Порядок битов — **MSB-первый внутри каждого байта**, как в движке.
|
||||
//! Проверено дизассемблированием `bf_read::ReadUBitLongNoInline` в
|
||||
//! `engine_srv.so`: значение собирается сдвигами `shr`/`shl` от старшего
|
||||
//! бита, то есть первым читается бит 7 байта.
|
||||
//!
|
||||
//! Важное различие (тоже из движка):
|
||||
//! - `ReadUBitLong`/`WriteUBitLong`/`ReadOneBit`/`WriteOneBit` — **MSB-first**;
|
||||
//! - `ReadByte`/`ReadShort`/`ReadLong` — **побайтово, little-endian**
|
||||
//! (для 8 бит обе схемы совпадают, для 16/32 — нет).
|
||||
|
||||
/// Запись битов.
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -27,6 +35,7 @@ impl BitWriter {
|
|||
&self.buf
|
||||
}
|
||||
|
||||
/// Записывает один бит. MSB-первый: бит 7 байта идёт первым.
|
||||
pub fn write_one_bit(&mut self, bit: bool) {
|
||||
let byte_index = self.bit_len / 8;
|
||||
let bit_index = self.bit_len % 8;
|
||||
|
|
@ -34,33 +43,53 @@ impl BitWriter {
|
|||
self.buf.push(0);
|
||||
}
|
||||
if bit {
|
||||
self.buf[byte_index] |= 1 << bit_index;
|
||||
self.buf[byte_index] |= 1 << (7 - bit_index);
|
||||
}
|
||||
self.bit_len += 1;
|
||||
}
|
||||
|
||||
/// Записывает `bits` младших битов значения (LSB-первый).
|
||||
pub fn write_ubit_long(&mut self, mut value: u32, bits: usize) {
|
||||
for _ in 0..bits {
|
||||
self.write_one_bit(value & 1 != 0);
|
||||
value >>= 1;
|
||||
/// Записывает `bits` младших битов значения, MSB-первый
|
||||
/// (аналог `bf_write::WriteUBitLong`).
|
||||
pub fn write_ubit_long(&mut self, value: u32, bits: usize) {
|
||||
let bits = bits.min(32);
|
||||
for i in (0..bits).rev() {
|
||||
self.write_one_bit(value >> i & 1 != 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// `bf_write::WriteLong` — побайтово little-endian.
|
||||
pub fn write_long(&mut self, value: i32) {
|
||||
self.write_ubit_long(value as u32, 32);
|
||||
self.write_u32(value as u32);
|
||||
}
|
||||
|
||||
/// Побайтово little-endian (как `ReadLong` в движке).
|
||||
pub fn write_u32(&mut self, value: u32) {
|
||||
self.write_ubit_long(value, 32);
|
||||
for i in 0..4 {
|
||||
self.write_byte((value >> (8 * i)) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Побайтово little-endian (как `ReadShort` в движке).
|
||||
pub fn write_short(&mut self, value: u16) {
|
||||
self.write_ubit_long(value as u32, 16);
|
||||
self.write_byte(value as u8);
|
||||
self.write_byte((value >> 8) as u8);
|
||||
}
|
||||
|
||||
/// Побайтово — 8 бит, для обоих порядков результат одинаков.
|
||||
pub fn write_byte(&mut self, value: u8) {
|
||||
self.write_ubit_long(value as u32, 8);
|
||||
if self.bit_len % 8 == 0 {
|
||||
if self.buf.len() == self.bit_len / 8 {
|
||||
self.buf.push(value);
|
||||
} else {
|
||||
let idx = self.bit_len / 8;
|
||||
self.buf[idx] = value;
|
||||
}
|
||||
self.bit_len += 8;
|
||||
} else {
|
||||
for i in (0..8).rev() {
|
||||
self.write_one_bit(value >> i & 1 != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Записывает строку с нуль-терминатором (как `bf_write::WriteString`).
|
||||
|
|
@ -111,38 +140,54 @@ impl<'a> BitReader<'a> {
|
|||
self.bits_left() / 8
|
||||
}
|
||||
|
||||
/// Читает один бит. MSB-первый: бит 7 байта читается первым.
|
||||
pub fn read_one_bit(&mut self) -> bool {
|
||||
if self.bit_pos >= self.data.len() * 8 {
|
||||
self.bit_pos += 1;
|
||||
return false;
|
||||
}
|
||||
let byte = self.data[self.bit_pos / 8];
|
||||
let bit = (byte >> (self.bit_pos % 8)) & 1 != 0;
|
||||
let bit = (byte >> (7 - (self.bit_pos % 8))) & 1 != 0;
|
||||
self.bit_pos += 1;
|
||||
bit
|
||||
}
|
||||
|
||||
/// Читает `bits` бит (LSB-первый) в u32.
|
||||
/// Читает `bits` бит, MSB-первый (аналог `bf_read::ReadUBitLong`).
|
||||
pub fn read_ubit_long(&mut self, bits: usize) -> u32 {
|
||||
let mut value = 0u32;
|
||||
for i in 0..bits.min(32) {
|
||||
if self.read_one_bit() {
|
||||
value |= 1 << i;
|
||||
}
|
||||
for _ in 0..bits.min(32) {
|
||||
value = (value << 1) | self.read_one_bit() as u32;
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// Побайтово little-endian (как `ReadLong` в движке).
|
||||
pub fn read_long(&mut self) -> u32 {
|
||||
self.read_ubit_long(32)
|
||||
let lo = self.read_byte() as u32;
|
||||
let mid = self.read_byte() as u32;
|
||||
let hi = self.read_byte() as u32;
|
||||
let top = self.read_byte() as u32;
|
||||
lo | (mid << 8) | (hi << 16) | (top << 24)
|
||||
}
|
||||
|
||||
/// Побайтово little-endian (как `ReadShort` в движке).
|
||||
pub fn read_short(&mut self) -> u16 {
|
||||
self.read_ubit_long(16) as u16
|
||||
let lo = self.read_byte() as u16;
|
||||
let hi = self.read_byte() as u16;
|
||||
lo | (hi << 8)
|
||||
}
|
||||
|
||||
pub fn read_byte(&mut self) -> u8 {
|
||||
self.read_ubit_long(8) as u8
|
||||
if self.bit_pos % 8 == 0 && self.bit_pos / 8 < self.data.len() {
|
||||
let byte = self.data[self.bit_pos / 8];
|
||||
self.bit_pos += 8;
|
||||
return byte;
|
||||
}
|
||||
let mut value = 0u8;
|
||||
for _ in 0..8 {
|
||||
value = (value << 1) | self.read_one_bit() as u8;
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
/// Читает `VarInt32` (7 бит на байт, старший бит — продолжение).
|
||||
|
|
@ -219,14 +264,28 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn bit_order_matches_source() {
|
||||
// bf_write пишет младшие биты первыми: 0b1 в первом бите байта.
|
||||
// WriteUBitLong/WriteOneBit — MSB-первый: первый бит попадает в бит 7.
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ubit_long(1, 1);
|
||||
w.write_ubit_long(0, 7);
|
||||
assert_eq!(w.bytes(), &[0x01]);
|
||||
assert_eq!(w.bytes(), &[0x80]);
|
||||
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ubit_long(0b10000000, 8);
|
||||
w.write_ubit_long(0b1000_0000, 8);
|
||||
assert_eq!(w.bytes(), &[0x80]);
|
||||
|
||||
// WriteLong/WriteShort — побайтово little-endian (как ReadLong).
|
||||
let mut w = BitWriter::new();
|
||||
w.write_u32(1);
|
||||
assert_eq!(w.bytes(), &[0x01, 0x00, 0x00, 0x00]);
|
||||
let mut w = BitWriter::new();
|
||||
w.write_short(0x0140);
|
||||
assert_eq!(w.bytes(), &[0x40, 0x01]);
|
||||
|
||||
// WriteUBitLong(16) — MSB-первый, то есть «старший» байт идёт первым.
|
||||
// Именно так сервер кодирует reliable_state (InReliableState == 1 -> 00 01).
|
||||
let mut w = BitWriter::new();
|
||||
w.write_ubit_long(1, 16);
|
||||
assert_eq!(w.bytes(), &[0x00, 0x01]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ pub const FRAGMENT_OFFSET_BITS: usize = 18;
|
|||
pub const MAX_FRAGMENTS_PER_PACKET_BITS: usize = 5;
|
||||
/// Биты максимального размера файла.
|
||||
pub const MAX_FILE_SIZE_BITS: usize = 30;
|
||||
/// Ширина поля reliable_state в заголовке (`IN_RELIABLE_STATE_BITS`).
|
||||
pub const IN_RELIABLE_STATE_BITS: usize = 16;
|
||||
/// Биты номера подканала: 3 (8 подканалов, как в Source 2013 / HL2).
|
||||
///
|
||||
/// Проверено по эталонному разбору пакета: при 4 битах сервер читает
|
||||
|
|
@ -157,7 +159,8 @@ pub fn parse_packet(data: &[u8], checksum: bool) -> Result<PacketHeader, NetErro
|
|||
checksum_ok = expected == actual;
|
||||
}
|
||||
|
||||
let reliable_state = r.read_short();
|
||||
// reliable_state кодируется движком через WriteUBitLong(16) — MSB-первый.
|
||||
let reliable_state = r.read_ubit_long(IN_RELIABLE_STATE_BITS) as u16;
|
||||
let choked = (flags & PACKET_FLAG_CHOKED != 0).then(|| r.read_byte());
|
||||
let challenge = (flags & PACKET_FLAG_CHALLENGE != 0).then(|| r.read_long());
|
||||
|
||||
|
|
@ -594,7 +597,7 @@ pub fn build_hello_packet(
|
|||
.ok()
|
||||
.and_then(|v| v.parse::<u16>().ok())
|
||||
.unwrap_or(if messages.is_empty() { 0 } else { 1 });
|
||||
w.write_short(rel_state); // in_reliable_state (состояние принятых подканалов)
|
||||
w.write_ubit_long(rel_state as u32, IN_RELIABLE_STATE_BITS); // in_reliable_state
|
||||
w.write_u32(challenge);
|
||||
|
||||
if !messages.is_empty() && std::env::var_os("SOURCE_RS_UNRELIABLE_MSG").is_none() {
|
||||
|
|
@ -728,6 +731,42 @@ impl ClientMessage {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Эталонный layout начала reliable-блока: после 17-байтного заголовка
|
||||
/// идут `subchannel` (3 бита), затем по одному биту на каждый поток
|
||||
/// фрагментов, затем бит single-block, бит сжатия и varint длины.
|
||||
///
|
||||
/// Именно эти биты читает сервер (`CNetChan::ProcessPacket`), поэтому
|
||||
/// layout зафиксирован тестом: при 4 битах подканала сервер читал
|
||||
/// subchannel=1 и «0 фрагментов», теряя наши надёжные сообщения.
|
||||
#[test]
|
||||
fn reliable_block_layout_is_canonical() {
|
||||
let messages = vec![ClientMessage::SignOnState {
|
||||
state: signon::CONNECTED,
|
||||
spawn_count: u32::MAX,
|
||||
}];
|
||||
let packet = build_hello_packet(1, 0, 0x1234_5678, &messages);
|
||||
assert!(packet.len() > 17, "есть reliable-блок");
|
||||
|
||||
// Заголовок: seq u32 | ack u32 | flags u8 | checksum u16 |
|
||||
// reliable_state u16 | challenge u32 = 17 байт.
|
||||
assert_eq!(&packet[..4], &[1, 0, 0, 0], "seq=1 (little-endian)");
|
||||
assert_eq!(packet[8] & PACKET_FLAG_RELIABLE, PACKET_FLAG_RELIABLE);
|
||||
assert_eq!(packet[8] & PACKET_FLAG_CHALLENGE, PACKET_FLAG_CHALLENGE);
|
||||
|
||||
// Бит 7 первого байта reliable-блока — младший бит индекса
|
||||
// подканала; бит 4 — флаг «поток 0 несёт данные».
|
||||
let first = packet[17];
|
||||
assert_eq!(
|
||||
first >> 4,
|
||||
0b0001,
|
||||
"ожидаем subchannel=0 (3 бита) и поток 0=1; получено {:04b} ({:#04x})",
|
||||
first >> 4,
|
||||
first
|
||||
);
|
||||
assert_eq!(first >> 5, 0, "индекс подканала должен быть 0");
|
||||
assert_eq!((first >> 4) & 1, 1, "поток 0 (Normal) обязан нести данные");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_own_packet() {
|
||||
let messages = vec![
|
||||
|
|
|
|||
|
|
@ -394,3 +394,49 @@ payload[191]: 10 bc 01 c5 82 ...
|
|||
уже в `#GameUI_ServerRejectSteam` — это лимит Steam на частые подключения с
|
||||
одного аккаунта (`k_EBeginAuthSessionResultDuplicateRequest`), а не ошибка
|
||||
клиента.
|
||||
|
||||
### 13.4. НАСТОЯЩАЯ ПРИЧИНА: порядок битов в `bf_write` был LSB-first
|
||||
|
||||
Разбор §13.3 вскрыл более глубокий баг: **наш битовый буфер писал биты
|
||||
LSB-первым, а движок — MSB-первым**.
|
||||
|
||||
Подтверждено дизассемблированием GMod-сервера
|
||||
(`bf_read::ReadUBitLongNoInline`, `engine_srv.so`):
|
||||
|
||||
```
|
||||
206f8d: and $0x1f,%esi ; смещение бита внутри 32-битного слова
|
||||
206f9d: shl %cl,%eax ; последнее слово сдвигаем влево
|
||||
206fa1: shr %cl,%edx ; первое слово сдвигаем вправо
|
||||
206fa7: or %edx,%eax ; значение собирается ОТ СТАРШЕГО бита
|
||||
206fb4: and %edx,%eax ; маска (1<<numbits)-1
|
||||
```
|
||||
|
||||
Первым читается бит 7 байта. Значит и `WriteOneBit`, и `WriteUBitLong`
|
||||
должны писать **MSB-first**.
|
||||
|
||||
**Почему это ломало всё.** При LSB-first:
|
||||
- `WriteOneBit(true)` ставил бит в позицию 0 байта, а сервер читал позицию 7;
|
||||
из-за этого бит «поток 0 несёт данные» попадал в поле «поток 1» →
|
||||
сервер считал, что данные лежат в потоке File, а Normal пуст;
|
||||
- ID сообщений (`net_SignonState`, `Net_SetConVar`) записывались
|
||||
перевёрнутыми и сервер их не распознавал.
|
||||
|
||||
Итог: reliable-блок уходил, но не читался — ровно наблюдаемое поведение.
|
||||
|
||||
**Важное различие, подтверждённое движком:**
|
||||
|
||||
| Операция | Порядок |
|
||||
|---|---|
|
||||
| `ReadUBitLong` / `WriteUBitLong` / `ReadOneBit` / `WriteOneBit` | **MSB-first** |
|
||||
| `ReadByte` / `ReadShort` / `ReadLong` | **побайтово little-endian** |
|
||||
|
||||
(для 8 бит разницы нет, для 16/32 — есть). Именно поэтому `reliable_state`
|
||||
кодируется через `WriteUBitLong(16)`: сервер шлёт `InReliableState == 1`
|
||||
байтами `00 01`, а не `01 00` — это и было видно в pcap.
|
||||
|
||||
Фикс: `crates/net/src/bitbuf.rs` переведён на MSB-first, `reliable_state`
|
||||
переведён на `write_ubit_long(..., 16)`. Закреплено тестами
|
||||
`bit_order_matches_source` и `reliable_block_layout_is_canonical`.
|
||||
|
||||
Побочный эффект: исправлены **все** многобитные поля на проводе (ID
|
||||
сообщений, varint-длины, биты добивки), а не только подканал.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue