the action bars are the reference's own, and a gift can be wrapped

- the action-bar cluster is the reference's own: `MainMenuBar.xml`, `ActionBarFrame.xml`, `MultiActionBars.xml` and `BonusActionBarFrame.xml` run off the player's own chain, and our `ActionBar.xml` (2047 lines), `MultiBars.xml` and `StanceBar.xml` are deleted — three, since the stock bonus-bar file declares the shapeshift bar too; one engine verb, `ChangeActionBarPage`, fires `ACTIONBAR_PAGE_CHANGED` and nothing else; one load blocker: stock `MultiActionBars.lua` writes the options window's table from its OnLoad, the reference's own load-order hack, so our options window moves to the reference's toc slot; and ten things the reference does that ours did not: an empty button is HIDDEN while showgrid is 0, the bonus overlay sits one pixel right of the main buttons, every slide takes one frame more than its time, and `ActionButton_ShowGrid` ghosts EVERY ring, occupied included (1938)
- a gift can be wrapped: right-clicking wrapping paper fell through to plain use and sent `CMSG_USE_ITEM`, so nothing happened — you could receive a gift and never give one; the wrap is a displayed-cursor MODE plus a stashed slot, never a payload, so `CursorHasItem()` stays false and every stock gate on it stays shut: right-click arms it, a left-click on a bag slot sends `CMSG_WRAP_ITEM`, and the client applies no eligibility test — the server's `ERR_CANT_WRAP_*` reasons are the error line (1934); a duration-limited item counts down: `SMSG_ITEM_TIME_UPDATE` parks a deadline and the tooltip's `ITEM_DURATION` line runs the enchant countdown's bucket ladder, so "Duration: 1 days" is what the reference renders; an update for an item we do not hold is DROPPED, no queue (1933); and a renamed crowd-control refusal names its mechanic — "Can't do that while stunned" — off `SpellMechanic.dbc` (1943)
- a creature paths on a boat: `SMSG_MONSTER_MOVE_TRANSPORT` carries the transport's guid and deck-local coordinates, and is the attach/detach edge; a deck sample lands on the rider or nowhere, never as a world write a few yards from the map origin; and "there is no terrain under a boat" turned out wrong: the ground probe admits GameObject meshes — a boat's `.wmo` is a GameObject, so the probe hits the deck (1936); `MSG_MOVE_TIME_SKIPPED` is read, not dropped — the reference adds the lag to the observed unit's last-wire stamp, so dropping it scheduled their next packet `lag` ms late, a hitch for every player who stalled zoning in near us; and our own settle hold reports once (1935); and the tradeskill tool order was backwards — focus FIRST, so every two-tool recipe had read "Blacksmith Hammer, Anvil" for the reference's "Anvil, Blacksmith Hammer"; the focus's `hasTool` is 1.0 in the reference too (1942)
- the crowd's second sitting, instruments first: the probe line carries the tail — cpu p95/p99 and the six worst frames — because a mean under budget hides the one frame in twenty over it (raid legs railed at 16.67 with p95 at 18–20 ms) and whether that frame was work or a wait is the first question about a stutter, and the visible meshes are counted by lane; then two cuts — a terrain tile furnishes as 4×4 cells of 4×4 chunks, one mesh, one entity, one draw per 133-yd cell instead of per 33-yd chunk (a city horizon is over a thousand chunks a frame, and a cell changes nothing a pixel can see; Stormwind 10.55 → 10.32 `cpu_ms`), and an authored point light is our own `WorldPointLight`, not Bevy's `PointLight`, which was only a data carrier while Bevy ran its whole light lane over every lamp, ~2 % of the alone frame; the sitting's record is 1945, and the code says so (1945)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:54:27 +07:00
co-authored by Claude Fable 5.1
parent 72e78ebc92
commit 2f7cfcabf5
107 changed files with 3407 additions and 3490 deletions
+22
View File
@@ -346,8 +346,21 @@ pub enum SessionEvent {
/// `[start, …waypoints…, endpoint]` — at constant (arc-length) speed over `duration_ms`. Every
/// waypoint is carried, so a curved patrol reads as its real path, not a straight `start → endpoint`
/// shortcut. A `Stop` / zero-duration / <2-point move clears the path (`path` empty).
/// `MSG_MOVE_TIME_SKIPPED` — an observed mover reported that its own client skipped `lag_ms`
/// of movement simulation. **Not a pose**: nothing about where the unit is has changed, only
/// how far its clock has run. The app advances that mover's relay-chain wire stamp by it, the
/// reference's `[CMovement+0xac] += lag` (`0x603b40` → `0x601560` → `0x61ab90`). Ignoring it
/// leaves our copy of the chain permanently short, so the mover's next real packet reads as a
/// step `lag` too large and is scheduled that much late. Decision 1935.
MoveTimeSkipped { guid: u64, lag_ms: u32 },
MonsterMove {
guid: u64,
/// `SMSG_MONSTER_MOVE_TRANSPORT` only: the transport whose frame `start` and every `path`
/// point are expressed in — deck-local offsets, composed through the transport's live pose
/// rather than read as world coordinates. `None` = the ordinary absolute path. A packet
/// naming a transport is also the *attach* signal: the unit rides that deck from here
/// (decision 1936).
transport: Option<u64>,
start: [f32; 3],
/// The server's per-move spline counter — echoed in `CMSG_MOVE_SPLINE_DONE` when this spline
/// drives our own player (Charge/knockback/taxi); ignored for a creature's walk.
@@ -361,6 +374,8 @@ pub enum SessionEvent {
duration_ms: u32,
/// `true` ⇒ a 3-D flight path (keep the spline's Z); `false` ⇒ a ground walk whose Z the app
/// re-derives from the terrain under the unit (see the renderer's creature ground-clamp).
/// The re-derive is world-space and so does not apply to a `transport` path: there is no
/// terrain under a deck, and the wire Z is already the deck-local height (decision 1936).
flying: bool,
/// `SPLINEFLAG_RUNMODE` — the path is travelled at run speed. Its **absence** forces
/// `MOVEFLAG_WALK_MODE` on for the unit the spline moves (decision 1758).
@@ -777,6 +792,13 @@ pub enum SessionEvent {
},
/// Put an item instance on the client's fixed 30 s use cooldown (`SMSG_ITEM_COOLDOWN`).
ItemCooldown { item_guid: u64, spell_id: u32 },
/// `SMSG_ITEM_TIME_UPDATE` — the seconds left on one duration-limited item instance (a
/// conjured stone, a holiday gift, a timed quest item). The item's own `ITEM_FIELD_DURATION`
/// carries the same number and is sent to the owner, but vmangos's writer says outright that
/// the field is not what the client displays from (`Item::SendTimeUpdate`,
/// `Objects/Item.cpp:1094`) — same shape as the enchant countdown (decision 0920).
/// `seconds == 0` = expired / no timer. Decision 1933.
ItemTime { item_guid: u64, seconds: u32 },
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` — the seconds left on one item's TEMPORARY enchant, in the
/// named enchant slot. The **only** feed for the tooltip's countdown: the item's own
/// `ITEM_FIELD_ENCHANTMENT` duration field is never read for it (wow-re
@@ -257,6 +257,9 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
item_guid,
spell_id,
}],
ServerPacket::ItemTime { item_guid, seconds } => {
vec![SessionEvent::ItemTime { item_guid, seconds }]
}
ServerPacket::ItemEnchantTime {
item_guid,
slot,
@@ -640,8 +643,12 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
ServerPacket::TriggerCinematic { cinematic_id } => {
vec![SessionEvent::CinematicTriggered { cinematic_id }]
}
ServerPacket::MoveTimeSkipped { guid, lag_ms } => {
vec![SessionEvent::MoveTimeSkipped { guid, lag_ms }]
}
ServerPacket::MonsterMove {
guid,
transport,
start,
spline_id,
path,
@@ -652,6 +659,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
run_mode,
} => vec![SessionEvent::MonsterMove {
guid,
transport,
start: v3(start),
spline_id,
path: path.into_iter().map(v3).collect(),
@@ -283,6 +283,19 @@ pub fn movement(info: &MovementInfo) -> Vec<u8> {
body
}
/// Body of `CMSG_MOVE_TIME_SKIPPED` (client): the mover's **full 8-byte** guid then the skipped
/// milliseconds (vmangos `MoveTimeSkipped::ReadFromWorldPacket`, `Server/Packets/Movement.cpp:10-14`
/// — `recv_data >> guid` is `ObjectGuid`'s plain `read<uint64>`, `ObjectGuid.cpp:180`, not the
/// packed reader `PackedGuidReader` takes). Note the asymmetry with the server's own relay of the
/// same fact, `MSG_MOVE_TIME_SKIPPED`, which writes `GetPackGUID()` (`MovementHandler.cpp:1014`):
/// inbound is full, outbound is packed.
pub fn move_time_skipped(guid: u64, lag_ms: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&guid.to_le_bytes());
body.extend_from_slice(&lag_ms.to_le_bytes());
body
}
/// Body of `CMSG_MOVE_SPLINE_DONE` (client): the `MovementInfo` at the ride's endpoint, the
/// `spline_id` being acknowledged (matched against the server's newest spline for our mover), and a
/// trailing float the server reads-and-discards (`MoveSplineDone::ReadFromWorldPacket` does an
+67 -2
View File
@@ -247,12 +247,25 @@ impl ItemInfo {
///
/// It is a separate predicate from [`Self::opens_loot`] because it sits at a different point
/// in the dispatcher's order — *before* the quest-starter and readable arms, where the loot
/// arm sits after them. A wrapper template whose instance is no longer wrapped takes the
/// begin-wrap cursor path instead (local, no packet; not built yet).
/// arm sits after them. A wrapper template whose instance is *not* wrapped takes the
/// begin-wrap cursor path instead ([`Self::begins_gift_wrap`]) — local, no packet.
pub fn unwraps_gift(&self, instance_flags: u32) -> bool {
self.flags & ITEM_FLAG_WRAPPER != 0 && instance_flags & ITEM_DYNFLAG_WRAPPED != 0
}
/// Does a right-click on this instance **arm the gift-wrap cursor**? The *other* side of the
/// same fork [`Self::unwraps_gift`] tests (`0x5d8d00` arm 2): template [`ITEM_FLAG_WRAPPER`]
/// with the instance's [`ITEM_DYNFLAG_WRAPPED`] **clear** — a piece of wrapping paper rather
/// than a wrapped present.
///
/// **Nothing is sent on this click.** `0x5d8dba` calls `0x5edea0`, whose only three acts are
/// `LockItem 0x4953e0` on the paper, `SetCursorBaseMode(2)` and `CursorSetMode(2)` — a purely
/// local arm (wow-re `object-layer/scratch/gift-wrap-law.md`, §5-carved). What completes it is
/// the next **left**-click on a container slot, which sends [`wrap_item`]. Decision 1934.
pub fn begins_gift_wrap(&self, instance_flags: u32) -> bool {
self.flags & ITEM_FLAG_WRAPPER != 0 && instance_flags & ITEM_DYNFLAG_WRAPPED == 0
}
/// Does a right-click on this template send `CMSG_OPEN_ITEM` to **loot it open**? The
/// reference's arm #8 (`0x5d8f7c: test al,4` → the `0x5edc80` emitter): a **bare** template
/// [`ITEM_FLAG_LOOTABLE`] test.
@@ -663,6 +676,21 @@ pub fn open_item(bag_index: u8, slot: u8) -> Vec<u8> {
vec![bag_index, slot]
}
/// Body of `CMSG_WRAP_ITEM` (VERIFIED vmangos `WrapItem::ReadFromWorldPacket`,
/// `Server/Packets/Item.cpp:121-127` + `.h:191-201`; opcode 467 `Opcodes_1_12_1.h:468`):
/// `giftBag`, `giftSlot`, `itemBag`, `itemSlot` — all `uint8`, the **paper's** position first and
/// the target's second, the same bag addressing as [`use_item`].
///
/// The server refuses every ineligible target with an `EQUIP_ERR_*` of its own
/// (`HandleWrapItemOpcode`: equipped, a bag, soulbound, stackable, unique, already wrapped, or
/// mid-cast) — so a refusal reaches the player as an `SMSG_INVENTORY_CHANGE_FAILURE` line, not as
/// a silently eaten click. On success the *target* item keeps its guid and takes the paper's
/// `WrappedGift` entry, gains `ITEM_FIELD_GIFTCREATOR` and `ITEM_DYNFLAG_WRAPPED`, and one paper
/// is destroyed — all as ordinary field updates, no answering packet of its own.
pub fn wrap_item(gift_bag: u8, gift_slot: u8, item_bag: u8, item_slot: u8) -> Vec<u8> {
vec![gift_bag, gift_slot, item_bag, item_slot]
}
/// Body of `CMSG_AUTOEQUIP_ITEM` (VERIFIED vmangos `AutoEquipItem::ReadFromWorldPacket`,
/// `Server/Packets/Item.cpp:17-21` + `.h:31-39`; opcode 266 `Opcodes_1_12_1.h:269`): source
/// `srcbag`/`srcslot` (both `uint8`), the same bag addressing as [`use_item`]. The real client
@@ -760,6 +788,21 @@ pub(super) fn read_inventory_change_failure(
Ok((reason, required_level, item_guid, bag_slot))
}
/// Read `SMSG_ITEM_TIME_UPDATE` (VERIFIED vmangos `Item::SendTimeUpdate`,
/// `Objects/Item.cpp:1096-1106`): the item guid, then the remaining **seconds**
/// (`ITEM_FIELD_DURATION`, which vmangos counts down in seconds — `Item::UpdateDuration`,
/// `Item.cpp:243-257`). No trailing player guid on this one, unlike its enchant sibling: the
/// packet is `(8 + 4)` bytes and vmangos sizes it exactly so.
///
/// Sent on world enter for every carried duration item (`Player::SendItemDurations`,
/// `Player.cpp:12007`) and again whenever one is created or its duration changes
/// (`Player.cpp:19782-19785`).
pub(super) fn read_item_time(r: &mut &[u8]) -> io::Result<(u64, u32)> {
let item_guid = read_u64_le(r)?;
let seconds = read_u32_le(r)?;
Ok((item_guid, seconds))
}
/// Read `SMSG_ITEM_ENCHANT_TIME_UPDATE` (VERIFIED vmangos
/// `WorldPackets::Item::ItemEnchantTimeUpdate::AppendBodyTo`, `Server/Packets/Item.cpp:161-169`):
/// item guid, enchant **slot**, remaining **seconds**, then the owning player's guid (present from
@@ -792,6 +835,12 @@ mod tests {
assert_eq!(auto_equip_item(255, 30), vec![255, 30]);
}
#[test]
fn wrap_item_body_paper_first() {
// 467: giftbag, giftslot, itembag, itemslot (Item.cpp:121-127) — the PAPER's pair leads.
assert_eq!(wrap_item(255, 23, 255, 24), vec![255, 23, 255, 24]);
}
#[test]
fn auto_store_bag_item_body() {
// 267: srcbag, srcslot, dstbag (Item.cpp:23-28).
@@ -878,6 +927,22 @@ mod tests {
assert_eq!(r.len(), 8);
}
/// `SMSG_ITEM_TIME_UPDATE`'s body: guid then seconds, and **nothing after** — the enchant
/// twin's trailing player guid is not on this packet (vmangos sizes it `8 + 4`). Pinned
/// because the two share a handler and it would be easy to copy the wrong tail.
#[test]
fn item_time_reads_guid_then_seconds() {
let mut buf = Vec::new();
buf.extend_from_slice(&0x4000_0000_0000_00f7u64.to_le_bytes()); // itemGuid
buf.extend_from_slice(&1800u32.to_le_bytes()); // seconds
let mut r = &buf[..];
assert_eq!(
read_item_time(&mut r).unwrap(),
(0x4000_0000_0000_00f7, 1800)
);
assert!(r.is_empty(), "the body is exactly 12 bytes");
}
#[test]
fn inventory_failure_nonlevel_branch_has_no_u32() {
// Any failed reason != 1 skips requiredLevel: item1Guid u64, item2Guid u64, bagSlot u8.
+5 -5
View File
@@ -100,8 +100,8 @@ pub use client::{
channel_set_owner, channel_unban, channel_unmoderator, channel_unmute, char_create,
creature_query, force_speed_ack, full_guid, join_channel, knock_back_ack, leave_channel,
messagechat, messagechat_channel, messagechat_kind, messagechat_whisper, move_flag_ack,
move_spline_done, movement, pet_name_query, ping, played_time, query_time, random_roll,
teleport_ack, text_emote,
move_spline_done, move_time_skipped, movement, pet_name_query, ping, played_time, query_time,
random_roll, teleport_ack, text_emote,
};
pub use combat_log::{
DamageShield, DispelFailed, EnchantmentLog, EnvironmentalDamageLog, ExecuteLog, PartyKillLog,
@@ -150,9 +150,9 @@ pub use instance::{
};
pub use items::{
auto_equip_item, auto_store_bag_item, destroy_item, item_query, open_item, set_ammo,
split_item, swap_inv_item, swap_item, use_item, ItemDamage, ItemInfo, ItemSpellEntry,
ItemUseSpell, UseItemTarget, BAG_PLAYER_INVENTORY, ITEM_DYNFLAG_UNLOCKED, ITEM_DYNFLAG_WRAPPED,
ITEM_FLAG_LOOTABLE, ITEM_FLAG_WRAPPER, SLOT_BAG_FIRST, SLOT_PACK_FIRST,
split_item, swap_inv_item, swap_item, use_item, wrap_item, ItemDamage, ItemInfo,
ItemSpellEntry, ItemUseSpell, UseItemTarget, BAG_PLAYER_INVENTORY, ITEM_DYNFLAG_UNLOCKED,
ITEM_DYNFLAG_WRAPPED, ITEM_FLAG_LOOTABLE, ITEM_FLAG_WRAPPER, SLOT_BAG_FIRST, SLOT_PACK_FIRST,
};
pub use loot::{
autostore_loot_item, loot, loot_error, loot_master_give, loot_money, loot_release, loot_roll,
@@ -1,10 +1,19 @@
//! `SMSG_MONSTER_MOVE` — the server-dictated creature movement path.
//! `SMSG_MONSTER_MOVE` — the server-dictated creature movement path — and its transport twin.
//!
//! One packet: a mover, its `start`, a spline id, a `moveType`-switched final facing, then (unless it is
//! a `Stop`) the spline block — a flag word, a duration, and the waypoints. Decodes into the full
//! travel-order polyline [`ServerPacket::MonsterMove::path`] the app rides at constant (arc-length) speed
//! (decision 0097). The wire packs the waypoints two ways, keyed by the `Flying`/`Mask_CatmullRom` flag —
//! see [`read_monster_move_spline`].
//!
//! **`SMSG_MONSTER_MOVE_TRANSPORT` is the same packet with one field inserted**: a second packed guid,
//! the transport's, immediately after the mover's — and every coordinate in the body is then an
//! **offset in that transport's frame**, not world space (decision 1936). vmangos writes exactly that
//! (`Movement::MoveSplineInit::Launch`, `spline/MoveSplineInit.cpp:138-170`: the same builder, the
//! opcode swapped and the transport packed guid appended, after `CalculatePassengerOffset` has moved
//! the start into deck space), and it computes such a path against the transport's **model** navmesh
//! rather than the map's (`PathFinder::calculate`, `Maps/PathFinder.cpp:76-84`), so the waypoints are
//! deck-local too. One reader serves both, keyed by [`read_monster_move`]'s `on_transport`.
use std::io;
@@ -39,8 +48,12 @@ const SPLINE_FLAG_RUNMODE: u32 = 0x100;
/// reading a tail anyway over-ran the body, the `0x00dd: failed to fill whole buffer` skip that dropped
/// every creature stop). Otherwise the spline block yields the full polyline `[start, …waypoints…,
/// endpoint]`.
pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
pub(super) fn read_monster_move(r: &mut &[u8], on_transport: bool) -> io::Result<ServerPacket> {
let guid = read_packed_guid(r)?;
// `SMSG_MONSTER_MOVE_TRANSPORT` only: the deck the whole body is expressed on, packed like the
// mover's guid and read immediately after it. Everything below is unchanged — the two opcodes
// share the builder server-side and differ by this one field.
let transport = on_transport.then(|| read_packed_guid(r)).transpose()?;
let start = Vector3d::read(r)?;
// The server's per-move spline counter. Discarded for a creature (nothing acks its walk), but
// the client echoes it back in `CMSG_MOVE_SPLINE_DONE` when a spline drives its OWN player
@@ -62,6 +75,7 @@ pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
Ok(if move_type == MONSTER_MOVE_STOP {
ServerPacket::MonsterMove {
guid,
transport,
start,
spline_id,
path: Vec::new(),
@@ -93,6 +107,7 @@ pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
};
ServerPacket::MonsterMove {
guid,
transport,
start,
spline_id,
path,
@@ -204,6 +219,52 @@ mod tests {
}
}
/// `SMSG_MONSTER_MOVE_TRANSPORT` is `SMSG_MONSTER_MOVE` with the transport's packed guid
/// inserted between the mover's guid and the start position — everything after it parses
/// identically, and every coordinate is then a deck-local offset. Reading the *plain* opcode's
/// layout for it would consume the transport guid as the start's X and Y and land the creature
/// somewhere near the map origin; reading the transport layout for a plain packet is the same
/// mistake in reverse. Both directions are pinned here (decision 1936).
#[test]
fn monster_move_transport_reads_the_deck_guid_first() {
let mut body = Vec::new();
write_packed_guid(0x55, &mut body).unwrap(); // the mover
write_packed_guid(0x2000_0000_0000_0007, &mut body).unwrap(); // the transport
for f in [1.5f32, -2.5, 0.75] {
body.extend_from_slice(&f.to_le_bytes()); // deck-local start
}
body.extend_from_slice(&7u32.to_le_bytes()); // splineId
body.push(0); // moveType: a plain move
body.extend_from_slice(&0u32.to_le_bytes()); // spline flags: ground, walk
body.extend_from_slice(&2_000u32.to_le_bytes()); // duration
append_linear_path(&mut body, &[[1.5, -2.5, 0.75], [4.0, -2.5, 0.75]]);
match parse_server(opcode::SMSG_MONSTER_MOVE_TRANSPORT, &body).expect("parses") {
ServerPacket::MonsterMove {
guid,
transport,
start,
path,
..
} => {
assert_eq!(guid, 0x55);
assert_eq!(transport, Some(0x2000_0000_0000_0007));
assert_eq!((start.x, start.y, start.z), (1.5, -2.5, 0.75));
assert_eq!(path.len(), 2, "start + endpoint, both deck-local");
assert_eq!((path[1].x, path[1].y, path[1].z), (4.0, -2.5, 0.75));
}
_ => panic!("expected MonsterMove"),
}
// The plain opcode over the SAME bytes must NOT come back with a transport — and it
// mis-reads the head, which is exactly why the two layouts cannot share one arm.
match parse_server(opcode::SMSG_MONSTER_MOVE, &body) {
Ok(ServerPacket::MonsterMove { transport, .. }) => assert_eq!(transport, None),
Ok(_) => panic!("expected MonsterMove"),
Err(_) => {} // an under-run is equally fine: the point is that it is not the same read
}
}
#[test]
fn monster_move_stop_has_no_tail() {
// A STOP (moveType 1) is head-only: no flags/duration/points. Parsing the tail anyway
@@ -18,6 +18,20 @@ pub(super) const MOVEMENT_FLAG_SWIMMING: u32 = 0x20_0000;
pub(super) const MOVEMENT_FLAG_SPLINE_ENABLED: u32 = 0x40_0000;
pub(super) const MOVEMENT_FLAG_SPLINE_ELEVATION: u32 = 0x400_0000;
/// Read `MSG_MOVE_TIME_SKIPPED` — one observed mover's **packed** guid and the milliseconds its
/// own client skipped (VERIFIED: the reference's handler `0x603b40` reads a packed guid through
/// `0x642ed0`, resolves under `TYPEMASK_UNIT`, then reads a plain `u32`; vmangos relays exactly
/// that shape, `MovementHandler.cpp:1011-1017`).
///
/// Note the **asymmetry with the client's own send**, which is the same fact in the other
/// direction and writes a *plain* 8-byte guid (see [`super::client::move_time_skipped`]).
/// Inbound packed, outbound plain — that is the reference's own encoding, not a slip.
pub(super) fn read_move_time_skipped(r: &mut &[u8]) -> io::Result<(u64, u32)> {
let guid = crate::wire::read_packed_guid(r)?;
let lag_ms = read_u32_le(r)?;
Ok((guid, lag_ms))
}
/// Read a wire `MovementInfo` — the body shared by every `MSG_MOVE_*` (and the teleport ack). Surfaces
/// `flags`/`position`/`orientation`/`timestamp`/`fall_time`, the **transport pose** ([`TransportPose`],
/// present iff `MOVEFLAG_ON_TRANSPORT` — this is how a boarded rider's `MSG_MOVE_*` heartbeat carries
@@ -60,6 +60,13 @@ pub const SMSG_TRIGGER_CINEMATIC: u16 = 0x00FA;
pub const CMSG_NEXT_CINEMATIC_CAMERA: u16 = 0x00FB;
pub const CMSG_COMPLETE_CINEMATIC: u16 = 0x00FC;
pub const SMSG_MONSTER_MOVE: u16 = 0x00DD;
/// `SMSG_MONSTER_MOVE_TRANSPORT` (VERIFIED vmangos `Opcodes_1_12_1.h`: 686) — the transport twin of
/// [`SMSG_MONSTER_MOVE`]: the identical body with the **transport's packed guid inserted after the
/// mover's**, and every coordinate in it a deck-local offset instead of a world position. Sent for a
/// unit pathing *on* a transport — a pet following its owner aboard a boat, anything a player pulls
/// onto a deck (`Movement::MoveSplineInit::Launch`, `spline/MoveSplineInit.cpp:146-154`). Body in
/// [`super::monster_move::read_monster_move`]; decision 1936.
pub const SMSG_MONSTER_MOVE_TRANSPORT: u16 = 0x02AE; // 686
pub const SMSG_INITIALIZE_FACTIONS: u16 = 0x0122;
/// A faction became visible in the reputation pane (VERIFIED vmangos `Opcodes_1_12_1.h`: 291,
/// sender `ReputationMgr::SendVisible`) — body one `u32` reputation-list slot. The server sets the
@@ -198,6 +205,30 @@ pub const SMSG_PET_CAST_FAILED: u16 = 0x0138; // 312
/// and a trailing float the server `read_skip`s (VERIFIED vmangos `Opcodes_1_12_1.h`: 713;
/// `MoveSplineDone::ReadFromWorldPacket`).
pub const CMSG_MOVE_SPLINE_DONE: u16 = 0x02C9; // 713
/// The client's report that its movement clock **skipped** — a stall, a long frame, a window that
/// went to the background — carrying the mover and how many milliseconds went missing (VERIFIED
/// vmangos `Opcodes_1_12_1.h`: 718). Body in [`super::client::move_time_skipped`].
///
/// vmangos adds the reported `lag` to the mover's `stime`/`ctime` so its own movement clock stays
/// aligned with ours — **and hangs a 1.12-specific transport fix off it**: a player whose last
/// movement packet was the *first* one carrying `MOVEFLAG_ONTRANSPORT` is flagged `JustBoarded`,
/// and the next `CMSG_MOVE_TIME_SKIPPED` makes the server re-send that transport's out-of-range
/// update followed by a fresh create (`HandleMoveTimeSkippedOpcode`,
/// `Handlers/MovementHandler.cpp:989-1019`, comment *"fix an 1.12 client problem with
/// transports"*). A client that never sends this never gets that refresh. Decision 1935.
pub const CMSG_MOVE_TIME_SKIPPED: u16 = 0x02CE; // 718
/// The **broadcast twin** of [`CMSG_MOVE_TIME_SKIPPED`] (VERIFIED vmangos `Opcodes_1_12_1.h`: 793):
/// the server relays one mover's skip to everyone watching it, so their copies of that unit's
/// movement clock move with it (`MovementHandler.cpp:1011-1017`, packed guid + the same `u32`).
///
/// **Not ignorable.** The reference registers a handler for it (`0x6035e0` → `0x603b40`) that reads
/// a packed guid, resolves under `TYPEMASK_UNIT`, and adds the lag to `[CMovement+0xac]` — the
/// observed unit's last-seen **wire** movement timestamp, the monotonic maximum the relay chain
/// hangs off (`0x601560` → `0x61ab90`). That cell is benilla-app's relay-chain `last_wire_ms`
/// (`net::motion::relay::RelayChain`, decision 0615, which models `+0xa8`/`+0xac` by name). Drop the packet and the chain's copy stays `lag` ms short of the sender's
/// forever: the mover's next real packet then reads as a step `lag` too large and is scheduled
/// that much late — a hitch on that unit, not a lost packet. Decision 1935.
pub const MSG_MOVE_TIME_SKIPPED: u16 = 0x0319; // 793
pub const SMSG_ATTACKSTART: u16 = 0x0143; // 323
pub const SMSG_ATTACKSTOP: u16 = 0x0144; // 324
pub const SMSG_ATTACKERSTATEUPDATE: u16 = 0x014A; // 330
@@ -228,6 +259,21 @@ pub const SMSG_COOLDOWN_EVENT: u16 = 0x0135; // 309
pub const SMSG_CLEAR_COOLDOWN: u16 = 0x01DE; // 478
pub const SMSG_COOLDOWN_CHEAT: u16 = 0x01E1; // 481
/// `SMSG_ITEM_TIME_UPDATE` (VERIFIED vmangos `Opcodes_1_12_1.h`: 490) — the
/// remaining lifetime, in **seconds**, of one duration-limited item instance (a conjured stone, a
/// holiday gift, a timed quest item). Shares its handler with
/// [`SMSG_ITEM_ENCHANT_TIME_UPDATE`]: both read an 8-byte item guid first and then fork on the
/// opcode (`0x5e4f69 sub eax,0x1ea ; je …` / `0x5e4f74 dec eax` — wow-re
/// `ui/scratch/weapon-enchant-info.md` §7).
///
/// The item's own `ITEM_FIELD_DURATION` (wire field 15) carries the same number and is sent to the
/// owner, but vmangos's writer says in as many words that it is not what the client displays —
/// *"Though the client has the information in the item's data field, we have to send
/// SMSG_ITEM_TIME_UPDATE to display the remaining time"* (`Item::SendTimeUpdate`,
/// `Objects/Item.cpp:1094`). Same shape as the enchant countdown (decision 0920): a client-local
/// deadline whose only feed is the packet. Body in [`super::items::read_item_time`]; decision 1933.
pub const SMSG_ITEM_TIME_UPDATE: u16 = 0x01EA; // 490
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` (VERIFIED vmangos `Opcodes_1_12_1.h`: 491) — the **only** source
/// of a temporary enchant's remaining time. The item's `ITEM_FIELD_ENCHANTMENT` duration field is
/// never read for it: the reference's tooltip reads a client-side per-slot deadline array
@@ -365,6 +411,17 @@ pub const CMSG_USE_ITEM: u16 = 0x00AB; // 171
/// guid** (`HandleOpenItemOpcode` → `SendLoot(item, LOOT_CORPSE)`), so the loot window opens over
/// a thing in your bag rather than a corpse in the world.
pub const CMSG_OPEN_ITEM: u16 = 0x00AC; // 172
/// Wrap an item in a piece of gift wrapping (VERIFIED vmangos `Opcodes_1_12_1.h`: 467). Body in
/// [`super::items::wrap_item`]: `giftBag`, `giftSlot`, `itemBag`, `itemSlot` — the **paper first**,
/// then the thing being wrapped.
///
/// The other half of the gift arc: [`CMSG_OPEN_ITEM`] above *unwraps* one, and this is how a gift
/// gets made. Right-clicking a `ITEM_FLAG_WRAPPER` template whose instance is **not** already
/// wrapped sends nothing at all — it arms a purely local wrap cursor (`0x5edea0`: the item lock
/// `0x4953e0` @`0x5edeb5` + `CursorSetMode(2)`, disarmed by `0x5eded0` from inside `ClearCursor`;
/// wow-re `ui/scratch/right-click-open.md`, use-dispatcher arm 2, VERIFIED) — and *this* opcode
/// goes out when the armed cursor is then clicked on a target. Decision 1934.
pub const CMSG_WRAP_ITEM: u16 = 0x01D3; // 467
/// VERIFIED vmangos `Opcodes_1_12_1.h`: 177. Body in [`super::gameobj_use`] — a full guid, the
/// GameObject to use (decision 0236). Not interchangeable with `CMSG_LOOT`: the server rejects a
/// GameObject guid on `CMSG_LOOT`, so a chest opens its loot through this opcode.
+26 -1
View File
@@ -135,8 +135,19 @@ pub enum ServerPacket {
TriggerCinematic {
cinematic_id: u32,
},
/// `MSG_MOVE_TIME_SKIPPED` — an observed mover's own client skipped `lag_ms` of movement
/// simulation, so every watcher's copy of its wire clock must move with it (layout in
/// [`super::movement::read_move_time_skipped`]). Decision 1935.
MoveTimeSkipped {
guid: u64,
lag_ms: u32,
},
MonsterMove {
guid: u64,
/// `SMSG_MONSTER_MOVE_TRANSPORT` only: the transport whose frame `start` and every `path`
/// point are expressed in (deck-local offsets, not world coordinates). `None` = the plain
/// `SMSG_MONSTER_MOVE`, whose coordinates are absolute. Decision 1936.
transport: Option<u64>,
start: Vector3d,
/// The server's per-move spline counter — echoed back in `CMSG_MOVE_SPLINE_DONE` when the
/// spline drives our own player (Charge/knockback/taxi); ignored for a creature's walk.
@@ -645,6 +656,12 @@ pub enum ServerPacket {
item_guid: u64,
spell_id: u32,
},
/// `SMSG_ITEM_TIME_UPDATE` — how long one duration-limited item instance has left, in
/// **seconds** (layout in [`super::items::read_item_time`]). Decision 1933.
ItemTime {
item_guid: u64,
seconds: u32,
},
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` — how long one item's TEMPORARY enchant has left (layout in
/// [`super::items::read_item_enchant_time`]). The tooltip's countdown has no other source
/// (decision 0920).
@@ -1496,7 +1513,14 @@ impl ServerPacket {
ServerPacket::CompressedMoves { .. } => "SMSG_COMPRESSED_MOVES".into(),
ServerPacket::DestroyObject { .. } => "SMSG_DESTROY_OBJECT".into(),
ServerPacket::TriggerCinematic { .. } => "SMSG_TRIGGER_CINEMATIC".into(),
ServerPacket::MonsterMove { .. } => "SMSG_MONSTER_MOVE".into(),
ServerPacket::MoveTimeSkipped { .. } => "MSG_MOVE_TIME_SKIPPED".into(),
ServerPacket::MonsterMove { transport, .. } => {
if transport.is_some() {
"SMSG_MONSTER_MOVE_TRANSPORT".into()
} else {
"SMSG_MONSTER_MOVE".into()
}
}
ServerPacket::PlayerMove { opcode, .. } => format!("MSG_MOVE relay ({opcode:#06x})"),
ServerPacket::Teleport { .. } => "MSG_MOVE_TELEPORT_ACK".into(),
ServerPacket::NewWorld { .. } => "SMSG_NEW_WORLD".into(),
@@ -1572,6 +1596,7 @@ impl ServerPacket {
ServerPacket::CancelAutoRepeat => "SMSG_CANCEL_AUTO_REPEAT".into(),
ServerPacket::SpellCooldownList { .. } => "SMSG_SPELL_COOLDOWN".into(),
ServerPacket::ItemCooldown { .. } => "SMSG_ITEM_COOLDOWN".into(),
ServerPacket::ItemTime { .. } => "SMSG_ITEM_TIME_UPDATE".into(),
ServerPacket::ItemEnchantTime { .. } => "SMSG_ITEM_ENCHANT_TIME_UPDATE".into(),
ServerPacket::CooldownEvent { .. } => "SMSG_COOLDOWN_EVENT".into(),
ServerPacket::ClearCooldown { .. } => "SMSG_CLEAR_COOLDOWN".into(),
+10 -1
View File
@@ -244,7 +244,12 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
opcode::SMSG_COMPRESSED_MOVES => ServerPacket::CompressedMoves {
packets: read_compressed_moves(&mut r)?,
},
opcode::SMSG_MONSTER_MOVE => monster_move::read_monster_move(&mut r)?,
opcode::MSG_MOVE_TIME_SKIPPED => {
let (guid, lag_ms) = movement::read_move_time_skipped(&mut r)?;
ServerPacket::MoveTimeSkipped { guid, lag_ms }
}
opcode::SMSG_MONSTER_MOVE => monster_move::read_monster_move(&mut r, false)?,
opcode::SMSG_MONSTER_MOVE_TRANSPORT => monster_move::read_monster_move(&mut r, true)?,
opcode::MSG_MOVE_TELEPORT_ACK => {
let guid = read_packed_guid(&mut r)?;
let counter = read_u32_le(&mut r)?;
@@ -575,6 +580,10 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
spell_id,
}
}
opcode::SMSG_ITEM_TIME_UPDATE => {
let (item_guid, seconds) = items::read_item_time(&mut r)?;
ServerPacket::ItemTime { item_guid, seconds }
}
opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE => {
let (item_guid, slot, seconds) = items::read_item_enchant_time(&mut r)?;
ServerPacket::ItemEnchantTime {
@@ -55,6 +55,23 @@ impl WorldWriter {
)
}
/// Wrap an item in gift paper (`CMSG_WRAP_ITEM`, layout in [`messages::wrap_item`]) — the
/// completion of the local wrap cursor a `ITEM_FLAG_WRAPPER` item's right-click arms. The
/// paper's `(bag, slot)` leads, the target's follows. Success is silent (field updates on the
/// target, one paper destroyed); every refusal comes back as `SMSG_INVENTORY_CHANGE_FAILURE`.
pub fn wrap_item(
&mut self,
gift_bag: u8,
gift_slot: u8,
item_bag: u8,
item_slot: u8,
) -> Result<()> {
self.send(
opcode::CMSG_WRAP_ITEM,
&messages::wrap_item(gift_bag, gift_slot, item_bag, item_slot),
)
}
/// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`, layout in [`messages::auto_equip_item`]) — the
/// server picks the destination slot. Success arrives as inventory-slot values deltas (and the
/// visible-item change everyone renders); refusal as `SMSG_INVENTORY_CHANGE_FAILURE`.
@@ -92,6 +92,18 @@ impl WorldWriter {
self.send(opcode::CMSG_MOVE_NOT_ACTIVE_MOVER, &body)
}
/// Report that our movement clock skipped `lag_ms` (`CMSG_MOVE_TIME_SKIPPED`, layout in
/// [`messages::move_time_skipped`]) for the named mover. The server folds the number into its
/// own copy of our movement clock — and, when we have just boarded a transport, answers by
/// re-sending that transport's create update (the 1.12 boarding fix; see
/// [`opcode::CMSG_MOVE_TIME_SKIPPED`]).
pub fn move_time_skipped(&mut self, guid: u64, lag_ms: u32) -> Result<()> {
self.send(
opcode::CMSG_MOVE_TIME_SKIPPED,
&messages::move_time_skipped(guid, lag_ms),
)
}
/// Acknowledge that a server-authored spline (Charge/knockback/taxi — an `SMSG_MONSTER_MOVE`
/// addressed to our own guid) finished: `CMSG_MOVE_SPLINE_DONE` with a `MovementInfo` at the
/// ride's endpoint and the `spline_id` we were driven by. The server sets `SplineDonePending` for