the minimap and world map go stock, and the chat loses its white plate
- the minimap cluster and the battleground list are the reference's own `Minimap.xml` and `BattlefieldFrame.xml`, ours deleted, and the sixteen engine names the static probe found unbound are built, not stubbed: the battleground list and queue family (the as-group join refused client-side with no packet when the map's `MaxPlayers` is below the group), the bodyless `CMSG_BATTLEFIELD_STATUS` on world enter — benilla never sent it, so a queue joined before a reconnect was invisible — the 5.5556 yd `BATTLEFIELDS_CLOSED` leash, and the meeting-stone pair over the `0x295` five-way table with both asymmetries; the ping's lifetime is the stock Lua's now (held 5 s, faded 0.5 s), `GetPingPosition` answers two numbers always — our nil was a Lua error on every frame of the stock `Minimap_OnUpdate` — and the map-change drop the reference never had is gone (1974)
- the world map is the reference's own `WorldMapFrame.xml`, our transcription deleted; built for it: the arrow-frame family (an anonymous `Model` child at its 33.6 px footprint, drawn app-side as the minimap's own arrow sprite through one shared constant), the battleground position family over `MSG_BATTLEGROUND_PLAYER_POSITIONS` both ways (the reference's 5000 ms request throttle), and `GetPlayerMapPosition` resolving `raid1..raid40` where it knew only the party (B320); `SetupFullscreenScale` — the engine binding `0x48c270` the map's and the cinematic frame's OnShow call, which our map had stood in for with Lua — is bound to the carved law `min(0.75 · aspect, 1)` off the configured aspect, written through the frame's own `SetScale` (1.0 at 4:3 and wider, 0.9375 at 5:4); a hovered POI writes the map's own area label, not the tooltip; named gap: `WorldMapPing` draws nothing yet (1980, 1981)
- the tutorial frame is the reference's own `TutorialFrame.xml`, over a tutorial system benilla never had: two bit banks filled from `SMSG_TUTORIAL_FLAGS` — the fire-once bank `TriggerTutorial` reads and sets (never a send) and the acknowledged bank `FlagTutorial` writes and sends; there is no tutorial CVar in the binary (210 `CVar::Register` sites censused), so "disable tutorials" is every bit acknowledged; the sites this app can produce are wired (the world-enter Welcome cascade, the level-up arm, item pushes, the loot sparkle, the meeting-stone and battleground arms), the rest named; the loader learned `<TitleRegion>` — the stock frame's drag handle, a region that emits no quad; and the `TabardModel` widget kind arrives ahead of its window, five ints bounded by the `.rdata` table `{170, 17, 6, 17, 51}` and the constant 100 000 tabard cost (1976, 1977)
- the chat window drew as an opaque white plate: the chat-cache loader, gated on `InWorld` alone, restored into the boot VM 130 ms before the deferred entry load filled it, and its per-VM memo then blocked the restore that would have reached the real frames — the plate the stock Lua tints black stayed the XML's white; between the entry edge and the load there is now no VM in the world (the edge parks the boot VM, the load takes it back), so no in-world feed pushes into a VM with no interface; the perf pill reads `59 fps 7.0 ms 17.3 cpu` — the MAIN thread's milliseconds in full text and the process-wide sum dim, because the sum of every thread's CPU time had been read as a frame time (a raid read 17 on it at a solid 60 with the main thread at 7); and `WOW_PROBE_UI_ONE_TEX=1` is a pricing lever, never a look, for the draw-count ceiling a UI atlas would reach before one is built (1978, 1954)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -203,6 +203,9 @@ pub enum SessionEvent {
|
||||
/// Rides the connect event because that is the only moment it ever arrives: the reference
|
||||
/// keeps it in a process-lifetime global written once by the auth parser.
|
||||
billing_time_rested: u32,
|
||||
/// The tutorial bank, when `SMSG_TUTORIAL_FLAGS` landed during the login handshake
|
||||
/// (decision 1976); `None` when it will arrive in the world stream instead.
|
||||
tutorial_flags: Option<Vec<u8>>,
|
||||
},
|
||||
/// The server confirmed our logout (`SMSG_LOGOUT_COMPLETE`) — we are back at character select.
|
||||
/// The IO thread cycles the connection immediately; a fresh [`Self::CharacterList`] follows.
|
||||
@@ -483,9 +486,27 @@ pub enum SessionEvent {
|
||||
/// The battleground scoreboard (`MSG_PVP_LOG_DATA`), rows in wire order — the app resolves
|
||||
/// the names, derives each row's team and pushes the sorted board (decision 1972).
|
||||
PvpLogData(crate::messages::PvpLogData),
|
||||
/// The battleground instance list (`SMSG_BATTLEFIELD_LIST`): the battlemaster, the map, the
|
||||
/// bracket and the instance ids — fires `BATTLEFIELDS_SHOW` (decision 1974).
|
||||
BattlefieldList(crate::messages::BattlefieldList),
|
||||
/// The battleground teammates' positions and the flag carrier
|
||||
/// (`MSG_BATTLEGROUND_PLAYER_POSITIONS`), raw world floats (decision 1980).
|
||||
BattlefieldPositions(crate::messages::BattlefieldPositions),
|
||||
/// A group join's verdict (`SMSG_GROUP_JOINED_BATTLEGROUND`): `0xFFFFFFFE` deserters, a map id
|
||||
/// joined, anything else the generic failure — three message lines, no state (decision 1974).
|
||||
GroupJoinedBattleground { result: u32 },
|
||||
/// A player joined or left the battleground (`SMSG_BATTLEGROUND_PLAYER_JOINED` / `_LEFT`):
|
||||
/// printed once the name cache resolves the guid (decision 1974).
|
||||
BattlegroundPlayer { guid: u64, joined: bool },
|
||||
/// The meeting-stone queue state (`SMSG 0x295`): the area queued for and a status byte
|
||||
/// (decision 1963).
|
||||
MeetingStoneSetQueue { area: u32, status: u8 },
|
||||
/// One of the meeting stone's four display-only replies (`0x297/0x298/0x299/0x2BB`): a chat
|
||||
/// line each, no state, no event (decision 1974).
|
||||
MeetingStoneNotice(crate::messages::MeetingStoneNotice),
|
||||
/// The account's tutorial bank (`SMSG_TUTORIAL_FLAGS`): the raw bytes, both of the client's
|
||||
/// banks copied from them (decision 1976).
|
||||
TutorialFlags(Vec<u8>),
|
||||
/// The bind took (`SMSG_PLAYERBOUND`): `area` is the AreaTable id we are now bound in, the
|
||||
/// same one [`Self::BindPoint`] carries in the packet beside it.
|
||||
PlayerBound { binder: u64, area: u32 },
|
||||
|
||||
@@ -785,9 +785,19 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
}
|
||||
ServerPacket::BattlefieldStatus(status) => vec![SessionEvent::BattlefieldStatus(status)],
|
||||
ServerPacket::PvpLogData(data) => vec![SessionEvent::PvpLogData(data)],
|
||||
ServerPacket::BattlefieldList(list) => vec![SessionEvent::BattlefieldList(list)],
|
||||
ServerPacket::BattlefieldPositions(p) => vec![SessionEvent::BattlefieldPositions(p)],
|
||||
ServerPacket::GroupJoinedBattleground { result } => {
|
||||
vec![SessionEvent::GroupJoinedBattleground { result }]
|
||||
}
|
||||
ServerPacket::BattlegroundPlayer { guid, joined } => {
|
||||
vec![SessionEvent::BattlegroundPlayer { guid, joined }]
|
||||
}
|
||||
ServerPacket::MeetingStoneSetQueue { area, status } => {
|
||||
vec![SessionEvent::MeetingStoneSetQueue { area, status }]
|
||||
}
|
||||
ServerPacket::MeetingStoneNotice(notice) => vec![SessionEvent::MeetingStoneNotice(notice)],
|
||||
ServerPacket::TutorialFlags(flags) => vec![SessionEvent::TutorialFlags(flags.bytes)],
|
||||
ServerPacket::PlayerBound { binder, area } => {
|
||||
vec![SessionEvent::PlayerBound { binder, area }]
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ pub struct BattlefieldStatus {
|
||||
/// The battleground's Map.dbc row id; zero clears the slot.
|
||||
pub map_id: u32,
|
||||
pub bracket: u8,
|
||||
/// Read into `+0x10`; no Lua reader was found for it (parked by the carve).
|
||||
pub unknown: u32,
|
||||
/// `+0x10` — the instance id, `GetBattlefieldStatus`'s third value (1963 parked this dword
|
||||
/// as reader-less; `battlefield-verb-family.md` §2.2 found the reader, 1974).
|
||||
pub instance_id: u32,
|
||||
pub status: u32,
|
||||
/// Status 2: the port deadline's delta, in ms (`+0x14 = now + Δ`, `GetBattlefieldPortExpiration`).
|
||||
pub time_ms: Option<u32>,
|
||||
@@ -30,8 +31,8 @@ pub struct BattlefieldStatus {
|
||||
}
|
||||
|
||||
/// Parse `SMSG_BATTLEFIELD_STATUS`: `u32 slot`, `u32 mapId`, and — only for a non-zero map —
|
||||
/// `u8 bracket`, `u32`, `u32 status`, then the status-conditional tail: two `u32` for status 1,
|
||||
/// one for status 2, two for status 3.
|
||||
/// `u8 bracket`, `u32 instanceId`, `u32 status`, then the status-conditional tail: two `u32` for
|
||||
/// status 1, one for status 2, two for status 3.
|
||||
pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<BattlefieldStatus> {
|
||||
let slot = read_u32_le(r)?;
|
||||
let map_id = read_u32_le(r)?;
|
||||
@@ -40,7 +41,7 @@ pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<Battlefie
|
||||
slot,
|
||||
map_id,
|
||||
bracket: 0,
|
||||
unknown: 0,
|
||||
instance_id: 0,
|
||||
status: 0,
|
||||
time_ms: None,
|
||||
in_progress: None,
|
||||
@@ -48,7 +49,7 @@ pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<Battlefie
|
||||
});
|
||||
}
|
||||
let bracket = read_u8(r)?;
|
||||
let unknown = read_u32_le(r)?;
|
||||
let instance_id = read_u32_le(r)?;
|
||||
let status = read_u32_le(r)?;
|
||||
let time_ms = if status == 2 {
|
||||
Some(read_u32_le(r)?)
|
||||
@@ -69,7 +70,7 @@ pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<Battlefie
|
||||
slot,
|
||||
map_id,
|
||||
bracket,
|
||||
unknown,
|
||||
instance_id,
|
||||
status,
|
||||
time_ms,
|
||||
in_progress,
|
||||
@@ -149,6 +150,118 @@ pub fn leave_battlefield(map_id: u32) -> Vec<u8> {
|
||||
map_id.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// `SMSG_BATTLEFIELD_LIST` (VERIFIED at the bytes, handler `0x4aa6c0`; wow-re
|
||||
/// `battlefield-verb-family.md` §4.1, 1974): the instance list a battlemaster (or
|
||||
/// `ShowBattlefieldList`) opens.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct BattlefieldList {
|
||||
/// The battlemaster's guid — `0` when the list was opened without an NPC. Cached by the
|
||||
/// client and read by `JoinBattlefield` to choose between the two join opcodes.
|
||||
pub battlemaster: u64,
|
||||
/// The battleground's Map.dbc row id.
|
||||
pub map_id: u32,
|
||||
/// The level-bracket index; the client derives the bracket's min/max from it and the map row.
|
||||
pub bracket: u8,
|
||||
/// The instance ids, wire order — nothing sorts them; index 0 on the wire is instance 1 in Lua.
|
||||
pub instances: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Parse `SMSG_BATTLEFIELD_LIST` (§4.1): `u64 battlemaster`, `u32 mapId`, `u8 bracket`,
|
||||
/// `u32 count`, `count × u32 instanceId`.
|
||||
pub(super) fn read_battlefield_list(r: &mut impl Read) -> io::Result<BattlefieldList> {
|
||||
let battlemaster = crate::wire::read_u64_le(r)?;
|
||||
let map_id = read_u32_le(r)?;
|
||||
let bracket = read_u8(r)?;
|
||||
let count = read_u32_le(r)?;
|
||||
let mut instances = Vec::with_capacity(count.min(64) as usize);
|
||||
for _ in 0..count {
|
||||
instances.push(read_u32_le(r)?);
|
||||
}
|
||||
Ok(BattlefieldList {
|
||||
battlemaster,
|
||||
map_id,
|
||||
bracket,
|
||||
instances,
|
||||
})
|
||||
}
|
||||
|
||||
/// One teammate's map position off `MSG_BATTLEGROUND_PLAYER_POSITIONS` (1980): raw world
|
||||
/// floats — the client prefers the live object's position when it has one, and normalizes
|
||||
/// either through the world-map projection under the active battleground's map.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct BattlefieldPosition {
|
||||
pub guid: u64,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
/// `MSG_BATTLEGROUND_PLAYER_POSITIONS` inbound (VERIFIED at the bytes, handler `0x4aad40`; wow-re
|
||||
/// `worldmap-arrow-and-positions.md` §3.1): the teammates not in the requester's group, then the
|
||||
/// friendly flag carrier when there is one.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct BattlefieldPositions {
|
||||
pub players: Vec<BattlefieldPosition>,
|
||||
pub carrier: Option<BattlefieldPosition>,
|
||||
}
|
||||
|
||||
/// The client's store holds 40 entries and its handler writes past them for a larger count
|
||||
/// (§3.2, an overrun the reference has); ours keeps the first 40.
|
||||
pub const BATTLEFIELD_POSITIONS_MAX: usize = 40;
|
||||
|
||||
/// Parse it (§3.1): `u32 count`, `count × (u64, f32, f32)`, `u8 hasCarrier`, and the carrier
|
||||
/// triple only when that byte is non-zero.
|
||||
pub(super) fn read_battlefield_positions(r: &mut impl Read) -> io::Result<BattlefieldPositions> {
|
||||
let count = read_u32_le(r)?;
|
||||
let mut players = Vec::with_capacity((count as usize).min(BATTLEFIELD_POSITIONS_MAX));
|
||||
for i in 0..count {
|
||||
let guid = crate::wire::read_u64_le(r)?;
|
||||
let x = crate::wire::read_f32_le(r)?;
|
||||
let y = crate::wire::read_f32_le(r)?;
|
||||
if (i as usize) < BATTLEFIELD_POSITIONS_MAX {
|
||||
players.push(BattlefieldPosition { guid, x, y });
|
||||
}
|
||||
}
|
||||
let carrier = if read_u8(r)? != 0 {
|
||||
Some(BattlefieldPosition {
|
||||
guid: crate::wire::read_u64_le(r)?,
|
||||
x: crate::wire::read_f32_le(r)?,
|
||||
y: crate::wire::read_f32_le(r)?,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(BattlefieldPositions { players, carrier })
|
||||
}
|
||||
|
||||
/// Body of `CMSG_BATTLEFIELD_LIST` (VERIFIED, `0x4ab8c0`): `u32 mapId` of the queued slot.
|
||||
pub fn battlefield_list(map_id: u32) -> Vec<u8> {
|
||||
map_id.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Body of `CMSG_BATTLEMASTER_JOIN` (VERIFIED, `0x4a9f60`'s GUID arm): `u64 battlemaster`,
|
||||
/// `u32 mapId`, `u32 instanceId` (`0` = first available), `u8 asGroup`.
|
||||
pub fn battlemaster_join(
|
||||
battlemaster: u64,
|
||||
map_id: u32,
|
||||
instance_id: u32,
|
||||
as_group: bool,
|
||||
) -> Vec<u8> {
|
||||
let mut body = battlemaster.to_le_bytes().to_vec();
|
||||
body.extend_from_slice(&map_id.to_le_bytes());
|
||||
body.extend_from_slice(&instance_id.to_le_bytes());
|
||||
body.push(u8::from(as_group));
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_BATTLEFIELD_JOIN` (VERIFIED, `0x4a9f60`'s no-GUID arm): `u32 mapId`,
|
||||
/// `u32 instanceId` (`0` = first available), `u8 asGroup`.
|
||||
pub fn battlefield_join(map_id: u32, instance_id: u32, as_group: bool) -> Vec<u8> {
|
||||
let mut body = map_id.to_le_bytes().to_vec();
|
||||
body.extend_from_slice(&instance_id.to_le_bytes());
|
||||
body.push(u8::from(as_group));
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_BATTLEFIELD_PORT` (VERIFIED, `0x4ab3b0`): `u32 mapId` then a genuinely
|
||||
/// one-byte `accept`, normalised to 0/1 before it reaches the wire.
|
||||
pub fn battlefield_port(map_id: u32, accept: bool) -> Vec<u8> {
|
||||
@@ -168,7 +281,7 @@ mod tests {
|
||||
];
|
||||
let s = read_battlefield_status(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(
|
||||
(s.slot, s.map_id, s.bracket, s.unknown, s.status),
|
||||
(s.slot, s.map_id, s.bracket, s.instance_id, s.status),
|
||||
(1, 30, 5, 7, 2)
|
||||
);
|
||||
assert_eq!(s.time_ms, Some(100));
|
||||
@@ -227,6 +340,94 @@ mod pvp_log_tests {
|
||||
assert_eq!(leave_battlefield(489), vec![0xE9, 1, 0, 0]);
|
||||
}
|
||||
|
||||
/// The instance list: the guid, the map, the bracket byte and the ids in wire order.
|
||||
#[test]
|
||||
fn the_list_reads_its_guid_bracket_and_instances() {
|
||||
let mut body = 0x1234_5678_9abc_def0u64.to_le_bytes().to_vec();
|
||||
body.extend_from_slice(&489u32.to_le_bytes());
|
||||
body.push(2);
|
||||
body.extend_from_slice(&3u32.to_le_bytes());
|
||||
for id in [7u32, 3, 11] {
|
||||
body.extend_from_slice(&id.to_le_bytes());
|
||||
}
|
||||
let l = read_battlefield_list(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(l.battlemaster, 0x1234_5678_9abc_def0);
|
||||
assert_eq!((l.map_id, l.bracket), (489, 2));
|
||||
assert_eq!(l.instances, vec![7, 3, 11], "wire order, nothing sorts it");
|
||||
let body = [0u8; 8]
|
||||
.iter()
|
||||
.chain(&30u32.to_le_bytes())
|
||||
.chain(&[0u8])
|
||||
.chain(&0u32.to_le_bytes())
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let l = read_battlefield_list(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(l.battlemaster, 0, "a list opened without an NPC");
|
||||
assert!(l.instances.is_empty());
|
||||
}
|
||||
|
||||
/// The positions reply: the count-led list, the carrier byte, and the 40-entry cap.
|
||||
#[test]
|
||||
fn the_positions_reply_reads_the_list_and_the_carrier() {
|
||||
let mut body = 2u32.to_le_bytes().to_vec();
|
||||
for (g, x, y) in [(0x10u64, 1.5f32, -2.0f32), (0x11, 3.0, 4.0)] {
|
||||
body.extend_from_slice(&g.to_le_bytes());
|
||||
body.extend_from_slice(&x.to_le_bytes());
|
||||
body.extend_from_slice(&y.to_le_bytes());
|
||||
}
|
||||
body.push(1);
|
||||
body.extend_from_slice(&0x20u64.to_le_bytes());
|
||||
body.extend_from_slice(&9.0f32.to_le_bytes());
|
||||
body.extend_from_slice(&8.0f32.to_le_bytes());
|
||||
let p = read_battlefield_positions(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(p.players.len(), 2);
|
||||
assert_eq!(
|
||||
p.players[1],
|
||||
BattlefieldPosition {
|
||||
guid: 0x11,
|
||||
x: 3.0,
|
||||
y: 4.0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
p.carrier,
|
||||
Some(BattlefieldPosition {
|
||||
guid: 0x20,
|
||||
x: 9.0,
|
||||
y: 8.0
|
||||
})
|
||||
);
|
||||
let body = [0u8, 0, 0, 0, 0];
|
||||
let p = read_battlefield_positions(&mut body.as_slice()).unwrap();
|
||||
assert!(p.players.is_empty() && p.carrier.is_none());
|
||||
let mut body = 41u32.to_le_bytes().to_vec();
|
||||
for i in 0..41u64 {
|
||||
body.extend_from_slice(&i.to_le_bytes());
|
||||
body.extend_from_slice(&[0u8; 8]);
|
||||
}
|
||||
body.push(0);
|
||||
let p = read_battlefield_positions(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(
|
||||
p.players.len(),
|
||||
40,
|
||||
"the client's store, not the wire's count"
|
||||
);
|
||||
}
|
||||
|
||||
/// The two join bodies differ only by the leading guid; the list request is the map alone.
|
||||
#[test]
|
||||
fn the_join_bodies_and_the_list_request() {
|
||||
assert_eq!(
|
||||
battlefield_join(489, 0, true),
|
||||
vec![0xE9, 1, 0, 0, 0, 0, 0, 0, 1],
|
||||
"first available, as a group"
|
||||
);
|
||||
let mut want = 0x42u64.to_le_bytes().to_vec();
|
||||
want.extend_from_slice(&[0xE9, 1, 0, 0, 5, 0, 0, 0, 0]);
|
||||
assert_eq!(battlemaster_join(0x42, 489, 5, false), want);
|
||||
assert_eq!(battlefield_list(529), vec![0x11, 2, 0, 0]);
|
||||
}
|
||||
|
||||
/// Status 1 carries the two wait dwords 1963's reader dropped.
|
||||
#[test]
|
||||
fn a_queued_status_carries_its_wait_pair() {
|
||||
|
||||
@@ -25,3 +25,29 @@ pub(super) fn read_meeting_stone_set_queue(r: &mut impl Read) -> io::Result<Meet
|
||||
pub fn meeting_stone_leave() -> Vec<u8> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// The four display-only replies one handler serves (`0x4ca3c0`, dispatched on the opcode; wow-re
|
||||
/// `meeting-stone-status.md` §9, 1974). None touches the queue state, none fires an event.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MeetingStoneNotice {
|
||||
/// `0x297`, empty.
|
||||
Success,
|
||||
/// `0x298`, empty.
|
||||
InProgress,
|
||||
/// `0x299`, `u64 guid` — the line waits for the name cache.
|
||||
MemberAdded { guid: u64 },
|
||||
/// `0x2BB`, `u8 code`.
|
||||
JoinFailed { code: u8 },
|
||||
}
|
||||
|
||||
/// Parse `0x299`'s guid.
|
||||
pub(super) fn read_meeting_stone_member_added(r: &mut impl Read) -> io::Result<MeetingStoneNotice> {
|
||||
Ok(MeetingStoneNotice::MemberAdded {
|
||||
guid: crate::wire::read_u64_le(r)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse `0x2BB`'s code byte.
|
||||
pub(super) fn read_meeting_stone_join_failed(r: &mut impl Read) -> io::Result<MeetingStoneNotice> {
|
||||
Ok(MeetingStoneNotice::JoinFailed { code: read_u8(r)? })
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ mod summon;
|
||||
mod taxi;
|
||||
mod trade;
|
||||
mod trainer;
|
||||
mod tutorial;
|
||||
mod update_object;
|
||||
mod vendor;
|
||||
mod world_state;
|
||||
@@ -85,7 +86,9 @@ pub use bank::{
|
||||
autobank_item, autostore_bank_item, bank_slot_result, banker_activate, buy_bank_slot,
|
||||
};
|
||||
pub use battlefield::{
|
||||
battlefield_port, leave_battlefield, BattlefieldStatus, PvpLogData, PvpLogRow,
|
||||
battlefield_join, battlefield_list, battlefield_port, battlemaster_join, leave_battlefield,
|
||||
BattlefieldList, BattlefieldPosition, BattlefieldPositions, BattlefieldStatus, PvpLogData,
|
||||
PvpLogRow, BATTLEFIELD_POSITIONS_MAX,
|
||||
};
|
||||
pub use binder::{binder_activate, PlayerBound};
|
||||
pub use channel::{channel_notice, ChannelNoticeTail, ChannelNotify};
|
||||
@@ -169,7 +172,7 @@ pub use mail::{
|
||||
mail_mark_as_read, mail_message_type, mail_return_to_sender, mail_take_item, mail_take_money,
|
||||
send_mail, MailAttachment, MailListEntry,
|
||||
};
|
||||
pub use meeting_stone::{meeting_stone_leave, MeetingStoneSetQueue};
|
||||
pub use meeting_stone::{meeting_stone_leave, MeetingStoneNotice, MeetingStoneSetQueue};
|
||||
pub use mirror_timer::{
|
||||
read_pause_mirror_timer, read_start_mirror_timer, read_stop_mirror_timer, MirrorTimerKind,
|
||||
MirrorTimerStart,
|
||||
@@ -245,6 +248,7 @@ pub use trade::{
|
||||
TRADE_SLOT_TRADED_COUNT,
|
||||
};
|
||||
pub use trainer::{train_fail, trainer_buy_spell, trainer_list, trainer_spell_state, TrainerSpell};
|
||||
pub use tutorial::{tutorial_flag, TutorialFlags};
|
||||
pub use update_object::{
|
||||
quest_slot_state, CorpseLook, CreateSpline, MovementBlock, MoverState, Object, ObjectFields,
|
||||
ObjectType, OwnerFallback, PlayerSkillSlot, QuestLogSlot, UnitAuraSlot, AURA_FLAG_CANCELABLE,
|
||||
|
||||
@@ -95,8 +95,18 @@ pub const SMSG_COMPRESSED_UPDATE_OBJECT: u16 = 0x01F6;
|
||||
/// [`super::ServerPacket::CompressedMoves`].
|
||||
pub const SMSG_COMPRESSED_MOVES: u16 = 0x02FB;
|
||||
pub const SMSG_LOGIN_VERIFY_WORLD: u16 = 0x0236;
|
||||
// The server-pushed sound trio (1.12.1 values VERIFIED vmangos `Opcodes_1_12_1.h`:
|
||||
// 631/632/722).
|
||||
/// The account's tutorial bank (wow-re `tutorial-flags.md` §7, decision 1976): every remaining
|
||||
/// byte of the body is the bank, both of the client's banks copied from it — vmangos sends 32
|
||||
/// bytes (256 bits) after the login. Until it lands no tutorial can fire.
|
||||
pub const SMSG_TUTORIAL_FLAGS: u16 = 0x00FD; // 253
|
||||
/// `FlagTutorial(n)` and the six auto-acknowledge sites (§5/§6, 1976): one `u32`, the 0-based id.
|
||||
pub const CMSG_TUTORIAL_FLAG: u16 = 0x00FE; // 254
|
||||
/// `ClearTutorials()` (§6, 1976): EMPTY — every bit set locally and on the server.
|
||||
pub const CMSG_TUTORIAL_CLEAR: u16 = 0x00FF; // 255
|
||||
/// `ResetTutorials()` (§6, 1976): EMPTY — every bit cleared locally and on the server.
|
||||
pub const CMSG_TUTORIAL_RESET: u16 = 0x0100; // 256
|
||||
// The server-pushed sound trio (1.12.1 values VERIFIED vmangos `Opcodes_1_12_1.h`:
|
||||
// 631/632/722).
|
||||
pub const SMSG_PLAY_MUSIC: u16 = 0x0277;
|
||||
pub const SMSG_PLAY_OBJECT_SOUND: u16 = 0x0278;
|
||||
pub const SMSG_PLAY_SOUND: u16 = 0x02D2;
|
||||
@@ -1006,6 +1016,37 @@ pub const MSG_PVP_LOG_DATA: u16 = 0x02E0; // 736
|
||||
/// `LeaveBattlefield()`'s packet (§5.3, 1972): `u32 mapId` of the battleground the client is in,
|
||||
/// sent only once the scoreboard's "ended" byte has arrived.
|
||||
pub const CMSG_LEAVE_BATTLEFIELD: u16 = 0x02E1; // 737
|
||||
/// The battleground teammate positions, both ways (wow-re `worldmap-arrow-and-positions.md` §3.1,
|
||||
/// 1980): `RequestBattlefieldPositions()` sends it EMPTY (5000 ms throttle, only with an active
|
||||
/// slot); the server's answer is `u32 count`, `count × (u64 guid, f32 x, f32 y)`, then
|
||||
/// `u8 hasCarrier` and, when set, one `(u64 guid, f32 x, f32 y)` flag carrier. No event fires on
|
||||
/// arrival — the UI polls.
|
||||
pub const MSG_BATTLEGROUND_PLAYER_POSITIONS: u16 = 0x02E9; // 745
|
||||
/// `ShowBattlefieldList(index)`'s packet (wow-re `battlefield-verb-family.md` §5, 1974): `u32 mapId`
|
||||
/// of a QUEUED slot — the battlemaster-less way to reopen the instance list.
|
||||
pub const CMSG_BATTLEFIELD_LIST: u16 = 0x023C; // 572
|
||||
/// The battleground instance list (§4.1, 1974): `u64 battlemaster` (zero when the list was not
|
||||
/// opened at an NPC), `u32 mapId`, `u8 bracket`, `u32 count`, `count × u32 instanceId`. Fires
|
||||
/// `BATTLEFIELDS_SHOW`; the cached battlemaster guid decides which join opcode a later
|
||||
/// `JoinBattlefield` sends.
|
||||
pub const SMSG_BATTLEFIELD_LIST: u16 = 0x023D; // 573
|
||||
/// `JoinBattlefield`'s packet when the list arrived WITHOUT a battlemaster (§5.2, 1974): `u32 mapId`,
|
||||
/// `u32 instanceId` (0 = first available), `u8 asGroup`.
|
||||
pub const CMSG_BATTLEFIELD_JOIN: u16 = 0x023E; // 574
|
||||
/// `JoinBattlefield`'s packet when the list arrived from a battlemaster (§5.2, 1974): `u64 guid`,
|
||||
/// `u32 mapId`, `u32 instanceId` (0 = first available), `u8 asGroup`.
|
||||
pub const CMSG_BATTLEMASTER_JOIN: u16 = 0x02EE; // 750
|
||||
/// The world-enter status request (§5/§8, 1974): EMPTY; the server answers with one
|
||||
/// `SMSG_BATTLEFIELD_STATUS` per live queue slot.
|
||||
pub const CMSG_BATTLEFIELD_STATUS: u16 = 0x02D3; // 723
|
||||
/// A group join's verdict (§4.4, 1974): `u32 result` — `0xFFFFFFFE` deserters (message 439), a
|
||||
/// Map.dbc id joined (440, with the map's name), anything else the generic failure (441).
|
||||
pub const SMSG_GROUP_JOINED_BATTLEGROUND: u16 = 0x02E8; // 744
|
||||
/// A player joined the battleground (§4.5, 1974): `u64 guid`, printed as message 444 once the name
|
||||
/// cache answers.
|
||||
pub const SMSG_BATTLEGROUND_PLAYER_JOINED: u16 = 0x02EC; // 748
|
||||
/// A player left the battleground (§4.5, 1974): `u64 guid`, message 445.
|
||||
pub const SMSG_BATTLEGROUND_PLAYER_LEFT: u16 = 0x02ED; // 749
|
||||
/// The area spirit healer query the client sends when it adopts a new healer (§6): `u64 guid`;
|
||||
/// answered by [`SMSG_AREA_SPIRIT_HEALER_TIME`] (decision 1963).
|
||||
pub const CMSG_AREA_SPIRIT_HEALER_QUERY: u16 = 0x02E2; // 738
|
||||
@@ -1023,6 +1064,21 @@ pub const CMSG_MEETINGSTONE_LEAVE: u16 = 0x0293; // 659
|
||||
/// The meeting-stone queue state (§8): `u32 areaId`, `u8 status`; stored unconditionally, a
|
||||
/// status message per value, then `MEETINGSTONE_CHANGED` (decision 1963).
|
||||
pub const SMSG_MEETINGSTONE_SETQUEUE: u16 = 0x0295; // 661
|
||||
/// The enter-world "what is my meeting-stone status" query (wow-re `meeting-stone-status.md`
|
||||
/// §6, 1974): EMPTY, sent once per world session right after the status text resets to the
|
||||
/// localized `UNKNOWN`; the server answers with `0x295`. The emulators' names for this block do
|
||||
/// not line up with the client's; the numbers are what is verified.
|
||||
pub const CMSG_MEETINGSTONE_STATUS_QUERY: u16 = 0x0296; // 662
|
||||
/// Display-only (§9): empty; `ERR_MEETING_STONE_SUCCESS` (1974).
|
||||
pub const SMSG_MEETINGSTONE_SUCCESS: u16 = 0x0297; // 663
|
||||
/// Display-only (§9): empty; `ERR_MEETING_STONE_IN_PROGRESS` (1974).
|
||||
pub const SMSG_MEETINGSTONE_IN_PROGRESS: u16 = 0x0298; // 664
|
||||
/// Display-only (§9): `u64 guid`; `ERR_MEETING_STONE_MEMBER_ADDED_S` with the name once the name
|
||||
/// cache answers, nothing on a miss that never resolves (1974).
|
||||
pub const SMSG_MEETINGSTONE_MEMBER_ADDED: u16 = 0x0299; // 665
|
||||
/// Display-only (§9): `u8 code` — `1` must-be-leader, `2` group full, `3` no raid group, anything
|
||||
/// else nothing (1974).
|
||||
pub const SMSG_MEETINGSTONE_JOIN_FAILED: u16 = 0x02BB; // 699
|
||||
/// `ConfirmPetUnlearn()`'s packet (§9): `u64 trainerGuid`, the latch — never an argument
|
||||
/// (decision 1963).
|
||||
pub const CMSG_PET_UNLEARN: u16 = 0x02F0; // 752
|
||||
|
||||
@@ -316,11 +316,28 @@ pub enum ServerPacket {
|
||||
BattlefieldStatus(crate::messages::BattlefieldStatus),
|
||||
/// `MSG_PVP_LOG_DATA` inbound — the battleground scoreboard (decision 1972).
|
||||
PvpLogData(crate::messages::PvpLogData),
|
||||
/// `SMSG_BATTLEFIELD_LIST` — the battleground instance list (decision 1974).
|
||||
BattlefieldList(crate::messages::BattlefieldList),
|
||||
/// `MSG_BATTLEGROUND_PLAYER_POSITIONS` inbound — the teammates and the flag carrier (1980).
|
||||
BattlefieldPositions(crate::messages::BattlefieldPositions),
|
||||
/// `SMSG_GROUP_JOINED_BATTLEGROUND` — a group join's verdict (decision 1974).
|
||||
GroupJoinedBattleground {
|
||||
result: u32,
|
||||
},
|
||||
/// `SMSG_BATTLEGROUND_PLAYER_JOINED` / `_LEFT` — one guid each (decision 1974).
|
||||
BattlegroundPlayer {
|
||||
guid: u64,
|
||||
joined: bool,
|
||||
},
|
||||
/// `SMSG 0x295` — the meeting-stone queue state (decision 1963).
|
||||
MeetingStoneSetQueue {
|
||||
area: u32,
|
||||
status: u8,
|
||||
},
|
||||
/// `SMSG 0x297/0x298/0x299/0x2BB` — the meeting stone's display-only replies (decision 1974).
|
||||
MeetingStoneNotice(crate::messages::MeetingStoneNotice),
|
||||
/// `SMSG_TUTORIAL_FLAGS` — the account's tutorial bank (decision 1976).
|
||||
TutorialFlags(crate::messages::TutorialFlags),
|
||||
/// `SMSG_SUMMON_REQUEST` — someone is *asking* to pull us to them (decision 1747): a
|
||||
/// warlock's ritual, a meeting stone, a GM. The twin of [`Self::BinderConfirm`] in shape —
|
||||
/// nothing moves until `CMSG_SUMMON_RESPONSE` goes back — and its opposite in teardown:
|
||||
@@ -1572,7 +1589,29 @@ impl ServerPacket {
|
||||
ServerPacket::AreaSpiritHealerTime { .. } => "SMSG_AREA_SPIRIT_HEALER_TIME".into(),
|
||||
ServerPacket::BattlefieldStatus(_) => "SMSG_BATTLEFIELD_STATUS".into(),
|
||||
ServerPacket::PvpLogData(_) => "MSG_PVP_LOG_DATA".into(),
|
||||
ServerPacket::BattlefieldList(_) => "SMSG_BATTLEFIELD_LIST".into(),
|
||||
ServerPacket::BattlefieldPositions(_) => "MSG_BATTLEGROUND_PLAYER_POSITIONS".into(),
|
||||
ServerPacket::GroupJoinedBattleground { .. } => "SMSG_GROUP_JOINED_BATTLEGROUND".into(),
|
||||
ServerPacket::BattlegroundPlayer { joined: true, .. } => {
|
||||
"SMSG_BATTLEGROUND_PLAYER_JOINED".into()
|
||||
}
|
||||
ServerPacket::BattlegroundPlayer { joined: false, .. } => {
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT".into()
|
||||
}
|
||||
ServerPacket::MeetingStoneSetQueue { .. } => "SMSG_MEETINGSTONE_SETQUEUE".into(),
|
||||
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::Success) => {
|
||||
"SMSG_MEETINGSTONE_SUCCESS".into()
|
||||
}
|
||||
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::InProgress) => {
|
||||
"SMSG_MEETINGSTONE_IN_PROGRESS".into()
|
||||
}
|
||||
ServerPacket::MeetingStoneNotice(
|
||||
crate::messages::MeetingStoneNotice::MemberAdded { .. },
|
||||
) => "SMSG_MEETINGSTONE_MEMBER_ADDED".into(),
|
||||
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::JoinFailed {
|
||||
..
|
||||
}) => "SMSG_MEETINGSTONE_JOIN_FAILED".into(),
|
||||
ServerPacket::TutorialFlags(_) => "SMSG_TUTORIAL_FLAGS".into(),
|
||||
ServerPacket::SetProficiency { .. } => "SMSG_SET_PROFICIENCY".into(),
|
||||
ServerPacket::InitializeFactions { .. } => "SMSG_INITIALIZE_FACTIONS".into(),
|
||||
ServerPacket::SetFactionStanding { .. } => "SMSG_SET_FACTION_STANDING".into(),
|
||||
|
||||
@@ -15,7 +15,7 @@ use super::{
|
||||
combat_log, death, duel, gameobject, gm_ticket, gossip, group, guild, instance, items, loot,
|
||||
mail, meeting_stone, mirror_timer, monster_move, movement, opcode, page_text, pet, petition,
|
||||
progression, pvp, quest, social, spellbook, spells, stable, summon, taxi, trade, trainer,
|
||||
update_object, vendor, world_state, Character, CreatureQueryInfo, JumpInfo, MoveMode,
|
||||
tutorial, update_object, vendor, world_state, Character, CreatureQueryInfo, JumpInfo, MoveMode,
|
||||
ServerPacket, SpeedKind, SplineMode,
|
||||
};
|
||||
|
||||
@@ -407,6 +407,23 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
opcode::MSG_PVP_LOG_DATA => {
|
||||
ServerPacket::PvpLogData(battlefield::read_pvp_log_data(&mut r)?)
|
||||
}
|
||||
opcode::SMSG_BATTLEFIELD_LIST => {
|
||||
ServerPacket::BattlefieldList(battlefield::read_battlefield_list(&mut r)?)
|
||||
}
|
||||
opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS => {
|
||||
ServerPacket::BattlefieldPositions(battlefield::read_battlefield_positions(&mut r)?)
|
||||
}
|
||||
opcode::SMSG_GROUP_JOINED_BATTLEGROUND => ServerPacket::GroupJoinedBattleground {
|
||||
result: crate::wire::read_u32_le(&mut r)?,
|
||||
},
|
||||
opcode::SMSG_BATTLEGROUND_PLAYER_JOINED => ServerPacket::BattlegroundPlayer {
|
||||
guid: crate::wire::read_u64_le(&mut r)?,
|
||||
joined: true,
|
||||
},
|
||||
opcode::SMSG_BATTLEGROUND_PLAYER_LEFT => ServerPacket::BattlegroundPlayer {
|
||||
guid: crate::wire::read_u64_le(&mut r)?,
|
||||
joined: false,
|
||||
},
|
||||
opcode::SMSG_MEETINGSTONE_SETQUEUE => {
|
||||
let q = meeting_stone::read_meeting_stone_set_queue(&mut r)?;
|
||||
ServerPacket::MeetingStoneSetQueue {
|
||||
@@ -414,6 +431,21 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
status: q.status,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_MEETINGSTONE_SUCCESS => {
|
||||
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::Success)
|
||||
}
|
||||
opcode::SMSG_MEETINGSTONE_IN_PROGRESS => {
|
||||
ServerPacket::MeetingStoneNotice(crate::messages::MeetingStoneNotice::InProgress)
|
||||
}
|
||||
opcode::SMSG_MEETINGSTONE_MEMBER_ADDED => ServerPacket::MeetingStoneNotice(
|
||||
meeting_stone::read_meeting_stone_member_added(&mut r)?,
|
||||
),
|
||||
opcode::SMSG_MEETINGSTONE_JOIN_FAILED => {
|
||||
ServerPacket::MeetingStoneNotice(meeting_stone::read_meeting_stone_join_failed(&mut r)?)
|
||||
}
|
||||
opcode::SMSG_TUTORIAL_FLAGS => {
|
||||
ServerPacket::TutorialFlags(tutorial::read_tutorial_flags(&mut r)?)
|
||||
}
|
||||
opcode::SMSG_PLAYERBOUND => {
|
||||
let bound = binder::read_player_bound(&mut r)?;
|
||||
ServerPacket::PlayerBound {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! The tutorial system's wire (decision 1976; wow-re `system/ui/scratch/tutorial-flags.md` §7):
|
||||
//! one inbound bank of bits and the three sends the client's tutorial code makes.
|
||||
//!
|
||||
//! The client keeps two bit banks filled from the same `SMSG_TUTORIAL_FLAGS` — the fire-once bank
|
||||
//! its C++ trigger reads and sets, and the acknowledged bank Lua's `FlagTutorial` and six C++
|
||||
//! auto-flag sites write, which is the only path that sends. The handler takes **whatever bytes
|
||||
//! are left in the packet** and sizes both banks as `bytes × 8` bits; vmangos sends 32 bytes.
|
||||
|
||||
use std::io::{self, Read};
|
||||
|
||||
/// `SMSG_TUTORIAL_FLAGS` (VERIFIED at the bytes, handler `0x4b5700`): the raw bank, every
|
||||
/// remaining byte of the packet — bit `id` is `bytes[id >> 3] & (1 << (id & 7))`, the same
|
||||
/// `word = id >> 5, bit = 1 << (id & 31)` addressing on little-endian dwords.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct TutorialFlags {
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parse it: the rest of the body, whatever its length.
|
||||
pub(super) fn read_tutorial_flags(r: &mut impl Read) -> io::Result<TutorialFlags> {
|
||||
let mut bytes = Vec::new();
|
||||
r.read_to_end(&mut bytes)?;
|
||||
Ok(TutorialFlags { bytes })
|
||||
}
|
||||
|
||||
/// Body of `CMSG_TUTORIAL_FLAG` (VERIFIED, `0x4b54c0`): one `u32`, the **0-based** id.
|
||||
pub fn tutorial_flag(id: u32) -> Vec<u8> {
|
||||
id.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_bank_is_every_remaining_byte_and_the_flag_is_one_dword() {
|
||||
let body = [0x01u8, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF];
|
||||
let f = read_tutorial_flags(&mut body.as_slice()).unwrap();
|
||||
assert_eq!(f.bytes, body.to_vec());
|
||||
let f = read_tutorial_flags(&mut [].as_slice()).unwrap();
|
||||
assert!(f.bytes.is_empty(), "an empty body is an empty bank");
|
||||
assert_eq!(tutorial_flag(41), vec![41, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,9 @@ pub struct WorldSession {
|
||||
/// was too short for the billing group. Read by [`Self::billing_time_rested`]; see decision
|
||||
/// 1820 for why `0` rather than the client's own uninitialised-global behaviour.
|
||||
billing_time_rested: u32,
|
||||
/// `SMSG_TUTORIAL_FLAGS` if it landed during the login handshake rather than in the world
|
||||
/// stream (decision 1976) — handed to the world entry the way the billing minutes are.
|
||||
tutorial_flags: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl WorldSession {
|
||||
@@ -183,6 +186,7 @@ impl WorldSession {
|
||||
roster_races: Default::default(),
|
||||
chat_language: messages::LANGUAGE_COMMON,
|
||||
billing_time_rested: 0,
|
||||
tutorial_flags: None,
|
||||
};
|
||||
|
||||
// 4. Wait for SMSG_AUTH_RESPONSE. Usually the first encrypted packet, but not always first
|
||||
@@ -257,6 +261,11 @@ impl WorldSession {
|
||||
self.billing_time_rested
|
||||
}
|
||||
|
||||
/// The tutorial bank captured during the login handshake, if any (decision 1976).
|
||||
pub fn take_tutorial_flags(&mut self) -> Option<Vec<u8>> {
|
||||
self.tutorial_flags.take()
|
||||
}
|
||||
|
||||
/// Set a read timeout on the underlying socket (e.g. so a debug read-loop can stop when the
|
||||
/// world goes quiet). `None` clears it (blocking reads).
|
||||
pub fn set_read_timeout(&self, timeout: Option<std::time::Duration>) -> Result<()> {
|
||||
@@ -295,7 +304,13 @@ impl WorldSession {
|
||||
ServerPacket::Other {
|
||||
opcode: opcode::SMSG_WARDEN_DATA,
|
||||
} => return Err(WardenRequired.into()),
|
||||
// The server interleaves account-data / tutorial / cache packets here; skip them.
|
||||
// The tutorial bank, if the server sends it this early (1976): kept for the world
|
||||
// entry — skipped here it would be lost to the roster loop.
|
||||
ServerPacket::TutorialFlags(flags) => {
|
||||
self.tutorial_flags = Some(flags.bytes);
|
||||
continue;
|
||||
}
|
||||
// The server interleaves account-data / cache packets here; skip them.
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,54 @@ impl WorldWriter {
|
||||
&messages::leave_battlefield(map_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Reopen a queued battleground's instance list (`CMSG_BATTLEFIELD_LIST`):
|
||||
/// `ShowBattlefieldList(index)`'s packet, the queued slot's map (decision 1974).
|
||||
pub fn battlefield_list(&mut self, map_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BATTLEFIELD_LIST,
|
||||
&messages::battlefield_list(map_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Join through the battlemaster the list came from (`CMSG_BATTLEMASTER_JOIN`) —
|
||||
/// `JoinBattlefield`'s packet when the cached guid is non-zero (decision 1974).
|
||||
pub fn battlemaster_join(
|
||||
&mut self,
|
||||
battlemaster: u64,
|
||||
map_id: u32,
|
||||
instance_id: u32,
|
||||
as_group: bool,
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BATTLEMASTER_JOIN,
|
||||
&messages::battlemaster_join(battlemaster, map_id, instance_id, as_group),
|
||||
)
|
||||
}
|
||||
|
||||
/// Join without a battlemaster (`CMSG_BATTLEFIELD_JOIN`) — `JoinBattlefield`'s packet when
|
||||
/// the list arrived with a zero guid (decision 1974).
|
||||
pub fn battlefield_join(
|
||||
&mut self,
|
||||
map_id: u32,
|
||||
instance_id: u32,
|
||||
as_group: bool,
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_BATTLEFIELD_JOIN,
|
||||
&messages::battlefield_join(map_id, instance_id, as_group),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask for every queue slot's state (`CMSG_BATTLEFIELD_STATUS`, empty) — the reference's
|
||||
/// world-enter reset sends it once per entry (decision 1974).
|
||||
pub fn battlefield_status(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_BATTLEFIELD_STATUS, &[])
|
||||
}
|
||||
|
||||
/// Ask for the teammates' positions (`MSG_BATTLEGROUND_PLAYER_POSITIONS`, empty) —
|
||||
/// `RequestBattlefieldPositions()`'s packet; the 5000 ms throttle is the caller's (1980).
|
||||
pub fn request_battlefield_positions(&mut self) -> Result<()> {
|
||||
self.send(opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS, &[])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,10 @@ impl WorldWriter {
|
||||
&messages::meeting_stone_leave(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask for the meeting-stone status (`CMSG 0x296`, empty) — the enter-world query the
|
||||
/// reference sends once per world session (decision 1974).
|
||||
pub fn meeting_stone_status_query(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_MEETINGSTONE_STATUS_QUERY, &[])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ mod summon;
|
||||
mod taxi;
|
||||
mod trade;
|
||||
mod trainer;
|
||||
mod tutorial;
|
||||
mod vendor;
|
||||
|
||||
/// Write half of a split [`WorldSession`](super::WorldSession) — owns a cloned socket + the encrypter. Used to send our own
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//! The tutorial system's sends (decision 1976).
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::messages::{self, opcode};
|
||||
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Acknowledge one tutorial (`CMSG_TUTORIAL_FLAG`): the 0-based id — `FlagTutorial`'s and the
|
||||
/// six auto-acknowledge sites' packet, sent only when the acknowledged bank's bit was clear.
|
||||
pub fn tutorial_flag(&mut self, id: u32) -> Result<()> {
|
||||
self.send(opcode::CMSG_TUTORIAL_FLAG, &messages::tutorial_flag(id))
|
||||
}
|
||||
|
||||
/// Mark every tutorial acknowledged (`CMSG_TUTORIAL_CLEAR`, empty) — `ClearTutorials()`.
|
||||
pub fn tutorial_clear(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TUTORIAL_CLEAR, &[])
|
||||
}
|
||||
|
||||
/// Forget every tutorial (`CMSG_TUTORIAL_RESET`, empty) — `ResetTutorials()`.
|
||||
pub fn tutorial_reset(&mut self) -> Result<()> {
|
||||
self.send(opcode::CMSG_TUTORIAL_RESET, &[])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user