Files
benilla-octo/crates/benilla-protocol/src/world/mod.rs
T
SamandClaude Fable 5 d3e05f32e5 the world only pays for what's on screen, and the leak is gone
- terrain draws per chunk now, and one authority culls everything outside: buildings, billboard cards, particle emitters
- the fps and memory creep on long flights is fixed twice over: material animations only tick what something draws, and far-away art is released by distance (with a 30 s dwell floor so fast cameras don't thrash it)
- the second crash ever reported (a cast racing a despawning unit at flight speed) is fixed, with a regression test that reproduces the exact panic
- every build now prints which commit it is, so a bug report can say exactly what it ran

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 19:39:56 +07:00

95 lines
4.2 KiB
Rust

//! Phase 4: world server (`mangosd`) connection.
//!
//! After the realmd logon ([`crate::logon`]) hands us the SRP6 session key, we connect to the world
//! server, prove we know that key, then stream object updates. The handshake:
//!
//! 1. Read `SMSG_AUTH_CHALLENGE` (unencrypted) → server seed.
//! 2. Send `CMSG_AUTH_SESSION` (unencrypted): build, uppercased account name, a client seed, and a
//! SHA1 digest binding the session key to both seeds (computed by `benilla-srp`).
//! 3. **1.12 header obfuscation turns on right after** `CMSG_AUTH_SESSION` — every subsequent packet
//! header (4-byte server / 6-byte client) is encrypted; bodies stay plaintext. The first encrypted
//! packet we read is `SMSG_AUTH_RESPONSE`.
//! 4. `CMSG_CHAR_ENUM` → `SMSG_CHAR_ENUM`, then `CMSG_PLAYER_LOGIN(guid)`, then the world streams
//! `SMSG_UPDATE_OBJECT`.
//!
//! Headers are (de)crypted by [`benilla_srp::vanilla_header`]; bodies by [`crate::messages`].
use std::io::{Read, Write};
use std::net::TcpStream;
use anyhow::{anyhow, Result};
use benilla_srp::vanilla_header::{DecrypterHalf, EncrypterHalf};
use crate::messages::{self, ServerPacket};
mod movement;
mod reader;
mod session;
mod writer;
pub use reader::WorldReader;
pub use session::{WardenRequired, WorldSession};
pub use writer::WorldWriter;
/// Default `mangosd` world-server port — the stock one, which our vmangos deploy maps straight
/// through. Normally the realm list reply carries the port (the deploy's
/// `VMANGOS_REALMLIST_PORT=8085` matches); this constant is the fallback for probes/examples that
/// dial the world server directly.
pub const WORLD_PORT: u16 = 8085;
/// Read one server packet from `stream`: decrypt the 4-byte header, read the body, parse by opcode.
/// `decrypter` is `None` for the (single) unencrypted `SMSG_AUTH_CHALLENGE`.
pub(super) fn recv_packet(
stream: &mut TcpStream,
decrypter: Option<&mut DecrypterHalf>,
) -> Result<ServerPacket> {
let mut header = [0u8; 4];
stream
.read_exact(&mut header)
.map_err(|e| anyhow!("reading world header: {e}"))?;
if let Some(d) = decrypter {
d.decrypt(&mut header);
}
// size is big-endian and counts the opcode (2) but not the size field; opcode is little-endian.
let size = u16::from_be_bytes([header[0], header[1]]);
let opcode = u16::from_le_bytes([header[2], header[3]]);
let body_len = size.saturating_sub(2) as usize;
let mut body = vec![0u8; body_len];
stream
.read_exact(&mut body)
.map_err(|e| anyhow!("reading world body (opcode {opcode:#x}, {body_len} bytes): {e}"))?;
messages::parse_server(opcode, &body).map_err(|e| anyhow!("parsing opcode {opcode:#x}: {e}"))
}
/// Write one client packet: an (optionally encrypted) 6-byte header + plaintext body. The header size
/// field counts the 4-byte opcode plus the body, but not the size field itself.
pub(super) fn send_packet(
stream: &mut TcpStream,
encrypter: Option<&mut EncrypterHalf>,
opcode: u16,
body: &[u8],
) -> Result<()> {
let size = (body.len() + 4) as u16;
let mut header = {
let s = size.to_be_bytes();
// The client header's opcode field is 4 bytes wide; widen the 16-bit opcode to fill it.
let o = u32::from(opcode).to_le_bytes();
[s[0], s[1], o[0], o[1], o[2], o[3]]
};
if let Some(e) = encrypter {
e.encrypt(&mut header);
}
// **One write, not two** (decision 0617). Header-then-body was a self-inflicted Nagle stall: the
// 6-byte header goes out as its own segment, and the body — small, and now behind unacknowledged
// data — is held by the kernel until the server ACKs it. With Nagle now off (see
// [`WorldSession::connect`]) that stall is gone either way, but two segments per packet is still
// two syscalls and two headers for a ≤40-byte message, and the server cannot parse the packet
// until both land. Joined here so one movement report is one segment.
let mut packet = Vec::with_capacity(header.len() + body.len());
packet.extend_from_slice(&header);
packet.extend_from_slice(body);
stream
.write_all(&packet)
.map_err(|e| anyhow!("sending opcode {opcode:#x}: {e}"))
}