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>
This commit is contained in:
@@ -1016,6 +1016,15 @@ pub enum SessionEvent {
|
||||
equip_error: Option<u32>,
|
||||
item: Option<(u32, u32)>,
|
||||
},
|
||||
/// `SMSG_PAGE_TEXT_QUERY_RESPONSE` — one page of a book, answering `CMSG_PAGE_TEXT_QUERY`.
|
||||
/// The ask-once page cache both readables reach: a readable item template's `PageText` and a
|
||||
/// `GAMEOBJECT_TYPE_TEXT` object's `data[0]` (decision 1105). `next_page_id == 0` ends the
|
||||
/// chain; vmangos pushes the whole chain in answer to the first query.
|
||||
PageText {
|
||||
page_id: u32,
|
||||
text: String,
|
||||
next_page_id: u32,
|
||||
},
|
||||
/// `SMSG_ITEM_TEXT_QUERY_RESPONSE` — a letter's body text, answering `CMSG_ITEM_TEXT_QUERY`
|
||||
/// (a mail's nonzero `item_text_id` triggers the ask-once fetch; decision 0544).
|
||||
MailItemText { text_id: u32, text: String },
|
||||
|
||||
@@ -650,6 +650,15 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
racial_leader,
|
||||
}]
|
||||
}
|
||||
ServerPacket::PageTextQueryResponse {
|
||||
page_id,
|
||||
text,
|
||||
next_page_id,
|
||||
} => vec![SessionEvent::PageText {
|
||||
page_id,
|
||||
text,
|
||||
next_page_id,
|
||||
}],
|
||||
ServerPacket::GameObjectQueryResponse { entry, info } => {
|
||||
let event = match info {
|
||||
Some(i) => SessionEvent::GameObjectInfo {
|
||||
|
||||
@@ -37,6 +37,7 @@ mod movement;
|
||||
pub mod opcode;
|
||||
mod opcode_names;
|
||||
mod packet;
|
||||
mod page_text;
|
||||
mod parse;
|
||||
mod pet;
|
||||
mod pose;
|
||||
@@ -128,6 +129,7 @@ pub use mirror_timer::{
|
||||
pub use movement::{JumpInfo, MoveMode, MovementInfo, SpeedKind, TransportPose};
|
||||
pub use opcode_names::opcode_name;
|
||||
pub use packet::{CreatureQueryInfo, MonsterMoveFacing, ServerPacket};
|
||||
pub use page_text::page_text_query;
|
||||
pub use parse::parse_server;
|
||||
pub use pet::{
|
||||
pet_abandon, pet_action, pet_cancel_aura, pet_rename, pet_set_action, pet_spell_autocast,
|
||||
|
||||
@@ -23,6 +23,15 @@ pub const SMSG_GAMEOBJECT_QUERY_RESPONSE: u16 = 0x005F; // 95
|
||||
/// VERIFIED vmangos `Opcodes_1_12_1.h`: 93. Answers `CMSG_QUEST_QUERY`; body in
|
||||
/// [`super::quest::read_quest_query_response`] ([`super::QuestTemplate`]).
|
||||
pub const SMSG_QUEST_QUERY_RESPONSE: u16 = 0x005D; // 93
|
||||
|
||||
// The **page-text** query pair (VERIFIED vmangos `Opcodes_1_12_1.h`: 90/91) — the ask-once cache
|
||||
// behind every readable BOOK, and a different one from the mail letter's `CMSG_ITEM_TEXT_QUERY`
|
||||
// further down: a `PageText.wdb` id names one *page*, chained by `nextPageId`, and it is what a
|
||||
// readable item TEMPLATE (`PageText`) and a world book/plaque GameObject (`GAMEOBJECT_TYPE_TEXT`'s
|
||||
// `data[0]`) both read from. Bodies in [`super::page_text`]; decision 1105.
|
||||
pub const CMSG_PAGE_TEXT_QUERY: u16 = 0x005A; // 90
|
||||
pub const SMSG_PAGE_TEXT_QUERY_RESPONSE: u16 = 0x005B; // 91
|
||||
|
||||
pub const SMSG_NEW_WORLD: u16 = 0x003E;
|
||||
// The far-teleport preamble pair (VERIFIED vmangos `Opcodes_1_12_1.h`: 63/64). TRANSFER_PENDING's
|
||||
// optional transport block decides how NEW_WORLD's coordinates read — boat-local when present,
|
||||
|
||||
@@ -260,6 +260,16 @@ pub enum ServerPacket {
|
||||
entry: u32,
|
||||
info: Option<GameObjectQueryInfo>,
|
||||
},
|
||||
/// `SMSG_PAGE_TEXT_QUERY_RESPONSE` — one page of a book, answering `CMSG_PAGE_TEXT_QUERY`
|
||||
/// (the ask-once page cache every readable reaches: a readable item template's `PageText` and a
|
||||
/// `GAMEOBJECT_TYPE_TEXT` object's `data[0]`, decision 1105). Pages **chain** —
|
||||
/// `next_page_id == 0` is the last one — and vmangos answers a single query with one of these
|
||||
/// per page of the whole chain.
|
||||
PageTextQueryResponse {
|
||||
page_id: u32,
|
||||
text: String,
|
||||
next_page_id: u32,
|
||||
},
|
||||
/// `SMSG_GAMEOBJECT_CUSTOM_ANIM` — a GameObject plays a one-shot Custom animation. Payload
|
||||
/// VERIFIED vmangos `GameObject::SendGameObjectCustomAnim`: `u64 guid, u32 animId`. The
|
||||
/// client arms GO substate `8 + animId` (AnimationData 153..156, `animId >= 4` rejected) —
|
||||
@@ -1110,6 +1120,7 @@ impl ServerPacket {
|
||||
ServerPacket::CreatureQueryResponse { .. } => "SMSG_CREATURE_QUERY_RESPONSE".into(),
|
||||
ServerPacket::PetNameQueryResponse { .. } => "SMSG_PET_NAME_QUERY_RESPONSE".into(),
|
||||
ServerPacket::GameObjectQueryResponse { .. } => "SMSG_GAMEOBJECT_QUERY_RESPONSE".into(),
|
||||
ServerPacket::PageTextQueryResponse { .. } => "SMSG_PAGE_TEXT_QUERY_RESPONSE".into(),
|
||||
ServerPacket::GameObjectCustomAnim { .. } => "SMSG_GAMEOBJECT_CUSTOM_ANIM".into(),
|
||||
ServerPacket::FishNotHooked => "SMSG_FISH_NOT_HOOKED".into(),
|
||||
ServerPacket::FishEscaped => "SMSG_FISH_ESCAPED".into(),
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use crate::wire::{
|
||||
|
||||
use super::{
|
||||
action_bar, area_trigger, attack, bank, channel, chat, combat_log, death, duel, gameobject,
|
||||
gossip, group, items, loot, mail, mirror_timer, monster_move, movement, opcode, pet,
|
||||
gossip, group, items, loot, mail, mirror_timer, monster_move, movement, opcode, page_text, pet,
|
||||
progression, quest, social, spellbook, spells, taxi, trade, trainer, update_object, vendor,
|
||||
world_state, Character, CreatureQueryInfo, MoveMode, ServerPacket, SpeedKind,
|
||||
};
|
||||
@@ -808,6 +808,14 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
let (entry, info) = gameobject::read_gameobject_query_response(&mut r)?;
|
||||
ServerPacket::GameObjectQueryResponse { entry, info }
|
||||
}
|
||||
opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE => {
|
||||
let (page_id, text, next_page_id) = page_text::read_page_text_query_response(&mut r)?;
|
||||
ServerPacket::PageTextQueryResponse {
|
||||
page_id,
|
||||
text,
|
||||
next_page_id,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_GAMEOBJECT_CUSTOM_ANIM => {
|
||||
let (guid, anim_id) = gameobject::read_gameobject_custom_anim(&mut r)?;
|
||||
ServerPacket::GameObjectCustomAnim { guid, anim_id }
|
||||
|
||||
@@ -34,4 +34,17 @@ impl WorldWriter {
|
||||
&messages::gameobject_query(entry, guid),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask for one page of a book (`CMSG_PAGE_TEXT_QUERY`, layout in
|
||||
/// [`messages::page_text_query`]) — the ask-once page cache a readable reaches when the reader
|
||||
/// opens on it (decision 1105). `guid` names the asking object (the book item or the
|
||||
/// `GAMEOBJECT_TYPE_TEXT` world object); the server resolves the page from the id alone.
|
||||
/// Answered by `SMSG_PAGE_TEXT_QUERY_RESPONSE` — once per page of the whole chain, not just
|
||||
/// the one asked for.
|
||||
pub fn page_text_query(&mut self, page_id: u32, guid: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_PAGE_TEXT_QUERY,
|
||||
&messages::page_text_query(page_id, guid),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user