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`).
|
//! Битовые буферы в стиле 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)]
|
#[derive(Debug, Default)]
|
||||||
|
|
@ -27,6 +35,7 @@ impl BitWriter {
|
||||||
&self.buf
|
&self.buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Записывает один бит. MSB-первый: бит 7 байта идёт первым.
|
||||||
pub fn write_one_bit(&mut self, bit: bool) {
|
pub fn write_one_bit(&mut self, bit: bool) {
|
||||||
let byte_index = self.bit_len / 8;
|
let byte_index = self.bit_len / 8;
|
||||||
let bit_index = self.bit_len % 8;
|
let bit_index = self.bit_len % 8;
|
||||||
|
|
@ -34,33 +43,53 @@ impl BitWriter {
|
||||||
self.buf.push(0);
|
self.buf.push(0);
|
||||||
}
|
}
|
||||||
if bit {
|
if bit {
|
||||||
self.buf[byte_index] |= 1 << bit_index;
|
self.buf[byte_index] |= 1 << (7 - bit_index);
|
||||||
}
|
}
|
||||||
self.bit_len += 1;
|
self.bit_len += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Записывает `bits` младших битов значения (LSB-первый).
|
/// Записывает `bits` младших битов значения, MSB-первый
|
||||||
pub fn write_ubit_long(&mut self, mut value: u32, bits: usize) {
|
/// (аналог `bf_write::WriteUBitLong`).
|
||||||
for _ in 0..bits {
|
pub fn write_ubit_long(&mut self, value: u32, bits: usize) {
|
||||||
self.write_one_bit(value & 1 != 0);
|
let bits = bits.min(32);
|
||||||
value >>= 1;
|
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) {
|
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) {
|
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) {
|
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) {
|
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`).
|
/// Записывает строку с нуль-терминатором (как `bf_write::WriteString`).
|
||||||
|
|
@ -111,38 +140,54 @@ impl<'a> BitReader<'a> {
|
||||||
self.bits_left() / 8
|
self.bits_left() / 8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Читает один бит. MSB-первый: бит 7 байта читается первым.
|
||||||
pub fn read_one_bit(&mut self) -> bool {
|
pub fn read_one_bit(&mut self) -> bool {
|
||||||
if self.bit_pos >= self.data.len() * 8 {
|
if self.bit_pos >= self.data.len() * 8 {
|
||||||
self.bit_pos += 1;
|
self.bit_pos += 1;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let byte = self.data[self.bit_pos / 8];
|
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;
|
self.bit_pos += 1;
|
||||||
bit
|
bit
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Читает `bits` бит (LSB-первый) в u32.
|
/// Читает `bits` бит, MSB-первый (аналог `bf_read::ReadUBitLong`).
|
||||||
pub fn read_ubit_long(&mut self, bits: usize) -> u32 {
|
pub fn read_ubit_long(&mut self, bits: usize) -> u32 {
|
||||||
let mut value = 0u32;
|
let mut value = 0u32;
|
||||||
for i in 0..bits.min(32) {
|
for _ in 0..bits.min(32) {
|
||||||
if self.read_one_bit() {
|
value = (value << 1) | self.read_one_bit() as u32;
|
||||||
value |= 1 << i;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Побайтово little-endian (как `ReadLong` в движке).
|
||||||
pub fn read_long(&mut self) -> u32 {
|
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 {
|
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 {
|
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 бит на байт, старший бит — продолжение).
|
/// Читает `VarInt32` (7 бит на байт, старший бит — продолжение).
|
||||||
|
|
@ -219,14 +264,28 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bit_order_matches_source() {
|
fn bit_order_matches_source() {
|
||||||
// bf_write пишет младшие биты первыми: 0b1 в первом бите байта.
|
// WriteUBitLong/WriteOneBit — MSB-первый: первый бит попадает в бит 7.
|
||||||
let mut w = BitWriter::new();
|
let mut w = BitWriter::new();
|
||||||
w.write_ubit_long(1, 1);
|
w.write_ubit_long(1, 1);
|
||||||
w.write_ubit_long(0, 7);
|
w.write_ubit_long(0, 7);
|
||||||
assert_eq!(w.bytes(), &[0x01]);
|
assert_eq!(w.bytes(), &[0x80]);
|
||||||
|
|
||||||
let mut w = BitWriter::new();
|
let mut w = BitWriter::new();
|
||||||
w.write_ubit_long(0b10000000, 8);
|
w.write_ubit_long(0b1000_0000, 8);
|
||||||
assert_eq!(w.bytes(), &[0x80]);
|
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_FRAGMENTS_PER_PACKET_BITS: usize = 5;
|
||||||
/// Биты максимального размера файла.
|
/// Биты максимального размера файла.
|
||||||
pub const MAX_FILE_SIZE_BITS: usize = 30;
|
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).
|
/// Биты номера подканала: 3 (8 подканалов, как в Source 2013 / HL2).
|
||||||
///
|
///
|
||||||
/// Проверено по эталонному разбору пакета: при 4 битах сервер читает
|
/// Проверено по эталонному разбору пакета: при 4 битах сервер читает
|
||||||
|
|
@ -157,7 +159,8 @@ pub fn parse_packet(data: &[u8], checksum: bool) -> Result<PacketHeader, NetErro
|
||||||
checksum_ok = expected == actual;
|
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 choked = (flags & PACKET_FLAG_CHOKED != 0).then(|| r.read_byte());
|
||||||
let challenge = (flags & PACKET_FLAG_CHALLENGE != 0).then(|| r.read_long());
|
let challenge = (flags & PACKET_FLAG_CHALLENGE != 0).then(|| r.read_long());
|
||||||
|
|
||||||
|
|
@ -594,7 +597,7 @@ pub fn build_hello_packet(
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse::<u16>().ok())
|
.and_then(|v| v.parse::<u16>().ok())
|
||||||
.unwrap_or(if messages.is_empty() { 0 } else { 1 });
|
.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);
|
w.write_u32(challenge);
|
||||||
|
|
||||||
if !messages.is_empty() && std::env::var_os("SOURCE_RS_UNRELIABLE_MSG").is_none() {
|
if !messages.is_empty() && std::env::var_os("SOURCE_RS_UNRELIABLE_MSG").is_none() {
|
||||||
|
|
@ -728,6 +731,42 @@ impl ClientMessage {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn parse_own_packet() {
|
fn parse_own_packet() {
|
||||||
let messages = vec![
|
let messages = vec![
|
||||||
|
|
|
||||||
|
|
@ -394,3 +394,49 @@ payload[191]: 10 bc 01 c5 82 ...
|
||||||
уже в `#GameUI_ServerRejectSteam` — это лимит Steam на частые подключения с
|
уже в `#GameUI_ServerRejectSteam` — это лимит Steam на частые подключения с
|
||||||
одного аккаунта (`k_EBeginAuthSessionResultDuplicateRequest`), а не ошибка
|
одного аккаунта (`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