Files
benilla-octo/crates/benilla-protocol/src/messages/gameobject.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

162 lines
7.1 KiB
Rust

//! GameObject-interaction messages (decision 0236). The player-facing verb for every usable world
//! GameObject — a chest, a door, a quest object, a lever — is the single `CMSG_GAMEOBJ_USE`; the
//! server fans it out by GO type and answers on the systems we already have (the loot window for a
//! chest, the gossip/quest windows for a questgiver GO, a `GAMEOBJECT_STATE` flip for a door). This
//! module also carries the ask-once `CMSG_GAMEOBJECT_QUERY` template lookup (`entry + guid` →
//! `SMSG_GAMEOBJECT_QUERY_RESPONSE`'s type/display/name/`data[24]` head) — the GO twin of
//! `CMSG_CREATURE_QUERY`; it grows further as the arc lands its later phases.
use std::io;
use crate::wire::{read_cstring, read_i32_le, read_u32_le};
/// Body of `CMSG_GAMEOBJ_USE` (opcode `0xB1`/177 — VERIFIED vmangos
/// `Server/Protocol/Opcodes_1_12_1.h`): one full `u64` guid, the GameObject to use. Read server-side
/// by `WorldSession::HandleGameObjectUseOpcode` (`Handlers/SpellHandler.cpp`), which gates on
/// spawned · not GENERIC(5) · not `GO_FLAG_NO_INTERACT` · interact distance · `PlayerCanUse`, then
/// runs `GameObject::Use`. The wire shape is identical to `CMSG_LOOT` — a bare little-endian guid —
/// but the two are **not** interchangeable: `HandleLootOpcode` rejects a GameObject guid outright
/// (anticheat), so a chest opens its loot through *this* opcode (→ server `SendLoot` →
/// `SMSG_LOOT_RESPONSE`), never through `CMSG_LOOT`.
pub fn gameobj_use(guid: u64) -> Vec<u8> {
guid.to_le_bytes().to_vec()
}
/// Body of a `CMSG_GAMEOBJECT_QUERY` (opcode `0x005E`/94 — VERIFIED vmangos
/// `Server/Protocol/Opcodes_1_12_1.h`): the template `entry` + the asking guid (VERIFIED vmangos
/// `QueryGameObject::ReadFromWorldPacket` — `u32 entryID` then a full `u64` guid), identical shape to
/// [`super::creature_query`].
pub fn gameobject_query(entry: u32, guid: u64) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&entry.to_le_bytes());
body.extend_from_slice(&guid.to_le_bytes());
body
}
/// The head of a GameObject-template answer (`SMSG_GAMEOBJECT_QUERY_RESPONSE`, opcode `0x005F`/95,
/// answering `CMSG_GAMEOBJECT_QUERY`) — decision 0236's ask-once template lookup. `data[24]` is the
/// template's raw `GameObjectData` union tail (VERIFIED vmangos `QueryHandler.cpp`
/// `HandleGameObjectQueryOpcode`: `data << info->raw.data[i]` for `i in 0..24`); its slot layout is
/// **type-specific** (e.g. a chest's lockId lives at a different index than a door's) — this layer
/// parses the array verbatim and leaves resolving a slot to the consumer that knows `type_id`.
#[derive(Debug, Clone, PartialEq)]
pub struct GameObjectQueryInfo {
pub type_id: u32,
pub display_id: u32,
pub name: String,
pub data: [i32; 24],
}
/// Read `SMSG_GAMEOBJECT_QUERY_RESPONSE` → `(entry, Some(info))`, or `(entry, None)` on a miss.
/// VERIFIED vmangos `QueryHandler.cpp` `HandleGameObjectQueryOpcode`, the
/// `SUPPORTED_CLIENT_BUILD >= CLIENT_BUILD_1_12_1` branch — wire order: `entry, type, displayId,
/// name`, then 3 empty name slots + an `icon` C-string (the 5th name slot), all read past and
/// dropped except `name`, then `data[24]` as 24 little-endian `i32`s. There is **no** trailing float
/// `size` field in 5875 (the `data << float(info->size)` line is commented out server-side). A miss
/// is the lone `u32` of `entry | 0x8000_0000` — the same shape as the creature/item miss.
pub(super) fn read_gameobject_query_response(
r: &mut &[u8],
) -> io::Result<(u32, Option<GameObjectQueryInfo>)> {
let entry = read_u32_le(r)?;
if entry & 0x8000_0000 != 0 {
return Ok((entry & 0x7FFF_FFFF, None));
}
let type_id = read_u32_le(r)?;
let display_id = read_u32_le(r)?;
let name = read_cstring(r)?;
for _ in 0..3 {
let _ = read_cstring(r)?; // name2..name4, always empty in 5875
}
let _icon = read_cstring(r)?; // name5 — the GO's icon key; unused until a consumer needs it
let mut data = [0i32; 24];
for slot in &mut data {
*slot = read_i32_le(r)?;
}
Ok((
entry,
Some(GameObjectQueryInfo {
type_id,
display_id,
name,
data,
}),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::messages::{opcode, parse_server, ServerPacket};
fn hx(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn cmsg_gameobj_use_body_golden() {
// A full little-endian guid, no more (SpellHandler.cpp reads a single ObjectGuid).
assert_eq!(
gameobj_use(0x1234_5678_9abc_def0),
hx("f0debc9a78563412"),
"CMSG_GAMEOBJ_USE body"
);
}
#[test]
fn cmsg_gameobject_query_body_golden() {
// entry u32 + full 8-byte guid — identical shape to CMSG_CREATURE_QUERY (VERIFIED vmangos
// `QueryGameObject::ReadFromWorldPacket`).
assert_eq!(
gameobject_query(1731, 0x1234_5678_9abc_def0),
hx("c3060000f0debc9a78563412"),
"CMSG_GAMEOBJECT_QUERY body"
);
}
#[test]
fn gameobject_query_response_hit_decodes() {
let mut body = 1731u32.to_le_bytes().to_vec(); // entry
body.extend_from_slice(&3u32.to_le_bytes()); // type = GAMEOBJECT_TYPE_CHEST
body.extend_from_slice(&123u32.to_le_bytes()); // displayId
body.extend_from_slice(b"Chest\0"); // name
body.extend_from_slice(&[0, 0, 0]); // name2..name4, empty C-strings
body.extend_from_slice(b"Icon\0"); // icon (name5) — read past and dropped
let mut data = [0i32; 24];
data[0] = 57; // stand-in lockId — the type-specific slot a later consumer resolves
for v in data {
body.extend_from_slice(&v.to_le_bytes());
}
match parse_server(opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::GameObjectQueryResponse { entry, info } => {
assert_eq!(entry, 1731);
let info = info.expect("hit");
assert_eq!(info.type_id, 3);
assert_eq!(info.display_id, 123);
assert_eq!(info.name, "Chest");
assert_eq!(info.data[0], 57);
assert_eq!(info.data[1], 0);
}
other => panic!("expected GameObjectQueryResponse, got {}", other.name()),
}
}
#[test]
fn gameobject_query_response_miss_decodes() {
// A miss is the lone entry echoed with its top bit set (VERIFIED vmangos
// HandleGameObjectQueryOpcode's "GO's info was not found" branch — same shape as the
// creature/item miss).
let body = (1731u32 | 0x8000_0000).to_le_bytes();
match parse_server(opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::GameObjectQueryResponse { entry, info } => {
assert_eq!(entry, 1731);
assert!(info.is_none());
}
other => panic!("expected GameObjectQueryResponse, got {}", other.name()),
}
}
}