Files
benilla-octo/crates/benilla-protocol/src/messages/page_text.rs
T
SamandClaude Fable 5 d16d33dfca world books open, and anvils stop offering a cursor gear
- clicking a book in the world opens it and turns its pages: letters and world books share one reader
- the cursor's object rules are the client's own 31-row strategy table now, so decorative objects stop pretending to be usable
- trainer and tradeskill windows draw each row's icon by the reference's own law, and the trainer's filter dropdown toggles again
- water color follows the zone's light blend, so lakes stop snapping between area tints

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:03:35 +07:00

93 lines
4.0 KiB
Rust

//! The **page-text** query pair (decision 1105) — the ask-once cache behind every readable *book*.
//!
//! Distinct from the mail letter's `CMSG_ITEM_TEXT_QUERY` ([`super::mail`]), which fetches one
//! whole player-written body by an *instance* text id. A `PageText.wdb` id names a single **page**,
//! and pages **chain**: each answer carries the next page's id (`0` = last). Two things read from
//! this cache, and both reach it the same way — the client opens its reader on an object guid and
//! asks the object for its page id:
//!
//! - a readable **item template** (`ItemPrototype::PageText`) — a book in your bags;
//! - a **`GAMEOBJECT_TYPE_TEXT`** world object (its template `data[0]`) — a book, plaque or sign
//! lying in the world.
//!
//! The client's request carries the asking object's guid after the page id (VERIFIED: the cache's
//! request writer `0x564730` appends the 8-byte guid at `0x56485d` under the per-cache "requests
//! carry a guid" flag `[cache+0x38]` — the very address vmangos cites in
//! `QueryPageText::ReadFromWorldPacket` when it reads the tail as optional). vmangos resolves the
//! page from the id alone and skips the guid.
use std::io;
use crate::wire::{read_cstring, read_u32_le};
/// Body of `CMSG_PAGE_TEXT_QUERY` (opcode `0x005A`/90 — VERIFIED vmangos
/// `Server/Protocol/Opcodes_1_12_1.h`): `u32 pageId` then the asking object's `u64 guid` (VERIFIED
/// `QueryPageText::ReadFromWorldPacket`, which reads the guid only `if (rpos() < size())` and
/// discards it; the real client writes it — see the module doc).
pub fn page_text_query(page_id: u32, guid: u64) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&page_id.to_le_bytes());
body.extend_from_slice(&guid.to_le_bytes());
body
}
/// Read `SMSG_PAGE_TEXT_QUERY_RESPONSE` (VERIFIED vmangos `Server/Packets/Query.cpp`,
/// `PageTextQueryResponse::AppendBodyTo`): `u32 pageId, cstr text, u32 nextPageId`.
///
/// vmangos answers **one query with the whole chain** — `HandlePageTextQueryOpcode` loops on
/// `next_page`, sending one of these per page — so asking for page 1 lands every page of the book
/// unsolicited. A page the server doesn't know answers `"Item page missing."` with `nextPageId = 0`,
/// which terminates the chain rather than erroring.
pub(super) fn read_page_text_query_response(r: &mut &[u8]) -> io::Result<(u32, String, u32)> {
Ok((read_u32_le(r)?, read_cstring(r)?, read_u32_le(r)?))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::messages::{opcode, parse_server, ServerPacket};
#[test]
fn query_body_is_page_id_then_guid() {
assert_eq!(
page_text_query(333, 0xF110_0000_0000_0042),
[
0x4D, 0x01, 0x00, 0x00, // pageId 333
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xF1, // guid
]
);
}
#[test]
fn response_reads_the_chain_link() {
let mut body = 333u32.to_le_bytes().to_vec();
body.extend_from_slice(b"Page one.\0");
body.extend_from_slice(&334u32.to_le_bytes());
match parse_server(opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::PageTextQueryResponse {
page_id,
text,
next_page_id,
} => {
assert_eq!(page_id, 333);
assert_eq!(text, "Page one.");
assert_eq!(next_page_id, 334);
}
other => panic!("expected PageTextQueryResponse, got {}", other.name()),
}
}
#[test]
fn last_page_terminates_the_chain() {
let mut body = 9u32.to_le_bytes().to_vec();
body.extend_from_slice(b"The end.\0");
body.extend_from_slice(&0u32.to_le_bytes());
match parse_server(opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE, &body).unwrap() {
ServerPacket::PageTextQueryResponse { next_page_id, .. } => {
assert_eq!(next_page_id, 0)
}
other => panic!("expected PageTextQueryResponse, got {}", other.name()),
}
}
}