NTFS load order, a title-sorted AddOns list, and tooltips at the cursor
- addons load in the order NTFS lists a folder, case-insensitive with an underscore before the letters, the order the vanilla corpus was written against and the one that decides which addon provides a shared library (2178) - GetNumAddOns and the index form of every AddOn verb walk a title-sorted list that hides the Blizzard addons the server says to hide, and the character-select AddOns list is in title order (2175) - SetPoint and SetAllPoints raise the reference's own errors for a frame name that does not exist or an omitted relative frame instead of quietly anchoring to the parent, and an explicit nil means the screen root (2176) - a tooltip owned with ANCHOR_CURSOR follows the mouse, which pfUI's tooltip, xpbar and chat modules, pfQuest's browser and TipBuddy ask for; an omitted or unknown anchor is ANCHOR_LEFT (2176) - a $parent name handed to CreateFrame, CreateTexture or CreateFontString expands as it does in XML, so FonzAppraiser's thirty dropdowns and pfQuest's search icon get the names they asked for (2176) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,12 @@ pub enum SessionEvent {
|
||||
/// 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 addons `SMSG_ADDON_INFO` hid from the Lua index space, or **`None` when the server
|
||||
/// never answered our addon block** (decision 2175) — and the two are different: an empty
|
||||
/// list is a reply that hid nothing, `None` is the state in which the reference's
|
||||
/// `GetNumAddOns()` answers 0. It rides the connect event for the same reason the billing
|
||||
/// minutes do: the reply lands during the handshake and nothing downstream can ask again.
|
||||
addon_info: Option<Vec<String>>,
|
||||
},
|
||||
/// The server **refused** the character we picked (`SMSG_CHARACTER_LOGIN_FAILED`) — we are not
|
||||
/// in the world and never were, whatever [`Self::Connected`] said a moment ago.
|
||||
|
||||
@@ -59,6 +59,26 @@
|
||||
/// are byte-identical to the `modulus`/`tdata` array carried in vmangos and cmangos alike.
|
||||
pub const STANDARD_MODULUS_CRC: u32 = 0x4C1C_776D;
|
||||
|
||||
/// **Which addons `SMSG_ADDON_INFO` hid** — the reply's `status` bytes paired back against the
|
||||
/// records we sent (decision 2175).
|
||||
///
|
||||
/// The reply carries no names: the client re-walks its own `## Secure:` list in the same order it
|
||||
/// sent it, so record *i* is `sent[i]`. A `status` of **2** is what makes the reference set
|
||||
/// `[rec+0x29] = 1` (`0x51db84`) and drop the addon from the Lua index space — on a stock install
|
||||
/// that is all twelve, which is why the AddOns list shows only the player's own addons.
|
||||
///
|
||||
/// A reply with **more** records than we sent is truncated to what we sent, and one with fewer
|
||||
/// simply says nothing about the rest: both are the server disagreeing with us about the block,
|
||||
/// and neither is worth failing a login over.
|
||||
pub fn hidden_from_reply(statuses: &[u8], sent: &[SecureAddon]) -> Vec<String> {
|
||||
statuses
|
||||
.iter()
|
||||
.zip(sent)
|
||||
.filter(|(status, _)| **status == 2)
|
||||
.map(|(_, addon)| addon.name.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One record in the addon block — a secure addon as the client describes it to the server.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SecureAddon<'a> {
|
||||
|
||||
@@ -74,7 +74,7 @@ pub use action_bar::{
|
||||
set_action_button, set_actionbar_toggles, ActionButton, ACTION_KIND_ITEM, ACTION_KIND_MACRO,
|
||||
ACTION_KIND_SPELL,
|
||||
};
|
||||
pub use addons::{SecureAddon, STANDARD_MODULUS_CRC, STOCK_SECURE_ADDONS};
|
||||
pub use addons::{hidden_from_reply, SecureAddon, STANDARD_MODULUS_CRC, STOCK_SECURE_ADDONS};
|
||||
pub use area_trigger::area_trigger;
|
||||
pub use attack::{attack_swing, AttackSwingError, AttackerState};
|
||||
pub use auction::{
|
||||
|
||||
@@ -90,6 +90,9 @@ pub const SMSG_AUTH_RESPONSE: u16 = 0x01EE;
|
||||
/// in the world. benilla does not implement Warden; [`crate::WorldSession::connect`] refuses such
|
||||
/// a server at the handshake rather than entering a 30-second kick/reconnect cycle.
|
||||
pub const SMSG_WARDEN_DATA: u16 = 0x02E6;
|
||||
/// The server's answer to `CMSG_AUTH_SESSION`'s addon block — one record per `## Secure:` addon we
|
||||
/// sent, in the order we sent them, with no count and no names (decision 2175).
|
||||
pub const SMSG_ADDON_INFO: u16 = 0x02EF;
|
||||
pub const SMSG_COMPRESSED_UPDATE_OBJECT: u16 = 0x01F6;
|
||||
/// A zlib envelope holding a **batch of whole movement packets** (763, VERIFIED vmangos
|
||||
/// `Opcodes_1_12_1.h`). Not an edge case: vmangos moves a session onto this carrier the moment it
|
||||
|
||||
@@ -1589,6 +1589,19 @@ pub enum ServerPacket {
|
||||
id: u32,
|
||||
value: u32,
|
||||
},
|
||||
/// `SMSG_ADDON_INFO` (`0x2ef`) — the server's per-addon verdict on the block we sent in
|
||||
/// `CMSG_AUTH_SESSION` (decision 2175).
|
||||
///
|
||||
/// **It carries no count and no names.** The client re-walks its own `## Secure:` list in the
|
||||
/// same order it sent it and reads one record per addon, so record *i* is
|
||||
/// `STOCK_SECURE_ADDONS[i]` (wow-re `system/net/scratch/cmsg-auth-session-addon-block.md` §6).
|
||||
/// Only the `status` byte matters to us: **2** is what makes the client set `[rec+0x29] = 1`
|
||||
/// and drop the addon from the Lua index space, which is why a stock install's AddOns list
|
||||
/// shows the player's addons and none of Blizzard's.
|
||||
AddonInfo {
|
||||
/// One `status` per record, in the order the records arrived.
|
||||
statuses: Vec<u8>,
|
||||
},
|
||||
Other {
|
||||
opcode: u16,
|
||||
},
|
||||
@@ -1934,6 +1947,7 @@ impl ServerPacket {
|
||||
ServerPacket::TradeStatusExtended { .. } => "SMSG_TRADE_STATUS_EXTENDED".into(),
|
||||
ServerPacket::InitWorldStates(_) => "SMSG_INIT_WORLD_STATES".into(),
|
||||
ServerPacket::UpdateWorldState { .. } => "SMSG_UPDATE_WORLD_STATE".into(),
|
||||
ServerPacket::AddonInfo { .. } => "SMSG_ADDON_INFO".into(),
|
||||
ServerPacket::Other { opcode } => format!("opcode {opcode:#06x}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,59 @@ const fn is_movement_relay(o: u16) -> bool {
|
||||
}
|
||||
|
||||
/// Parse a server packet body (already decrypted + sized) by opcode.
|
||||
/// `SMSG_ADDON_INFO`'s record stream — **no count, no names, no trailer** (decision 2175).
|
||||
///
|
||||
/// The grammar is the one `AddOn_ReadAddonInfoReply 0x51da70` reads, per record (wow-re
|
||||
/// `system/net/scratch/cmsg-auth-session-addon-block.md` §6):
|
||||
///
|
||||
/// ```text
|
||||
/// u8 status ; 2 -> [rec+0x29] = 1 (hidden from the Lua index space)
|
||||
/// ; 0 -> [rec+0x24] = 2 (rejected)
|
||||
/// ; else -> verify the .toc/Bindings.xml signature
|
||||
/// u8 infoProvided ; persisted into the .pub and echoed next logon
|
||||
/// if infoProvided:
|
||||
/// u8 keyProvided
|
||||
/// if keyProvided: u8[256] modulus
|
||||
/// u32 revision
|
||||
/// u8 urlProvided
|
||||
/// if urlProvided: u8[256] url
|
||||
/// ```
|
||||
///
|
||||
/// The retail capture is the minimal form — 12 x 8 bytes of `{2, 1, 0, 0u32, 0}`.
|
||||
///
|
||||
/// **A truncated record ends the walk and keeps the records before it**, rather than failing the
|
||||
/// packet. Only a record that parsed whole contributes a status, so `statuses[i]` is always
|
||||
/// record *i*'s and the pairing against what we sent cannot slip. The stream is self-delimiting only if every field is read exactly right, so a grammar
|
||||
/// error here would otherwise turn a working login into an unparseable packet; the statuses we did
|
||||
/// read are still true, and a short read is visible as a count that does not match what we sent.
|
||||
fn read_addon_info(r: &mut &[u8]) -> Vec<u8> {
|
||||
let mut statuses = Vec::new();
|
||||
while !r.is_empty() {
|
||||
let Ok(status) = read_u8(r) else { break };
|
||||
let Ok(info_provided) = read_u8(r) else { break };
|
||||
if info_provided != 0 {
|
||||
let Ok(key_provided) = read_u8(r) else { break };
|
||||
if key_provided != 0 && skip(r, 256).is_none() {
|
||||
break;
|
||||
}
|
||||
if read_u32_le(r).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Ok(url_provided) = read_u8(r) else { break };
|
||||
if url_provided != 0 && skip(r, 256).is_none() {
|
||||
break;
|
||||
}
|
||||
statuses.push(status);
|
||||
}
|
||||
statuses
|
||||
}
|
||||
|
||||
/// Advance `r` by `n` bytes, or `None` when it holds fewer.
|
||||
fn skip(r: &mut &[u8], n: usize) -> Option<()> {
|
||||
(r.len() >= n).then(|| *r = &r[n..])
|
||||
}
|
||||
|
||||
pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
let mut r = body;
|
||||
Ok(match opcode {
|
||||
@@ -1596,6 +1649,9 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
transport: info.transport,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_ADDON_INFO => ServerPacket::AddonInfo {
|
||||
statuses: read_addon_info(&mut r),
|
||||
},
|
||||
other => ServerPacket::Other { opcode: other },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ pub struct WorldSession {
|
||||
/// `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>>,
|
||||
/// `SMSG_ADDON_INFO`'s per-record `status` bytes, in arrival order — `None` until the server
|
||||
/// answers our addon block, which is the state that keeps the Lua index space empty
|
||||
/// (decision 2175). Read by [`Self::take_addon_info`].
|
||||
addon_info: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl WorldSession {
|
||||
@@ -187,6 +191,7 @@ impl WorldSession {
|
||||
chat_language: messages::LANGUAGE_COMMON,
|
||||
billing_time_rested: 0,
|
||||
tutorial_flags: None,
|
||||
addon_info: None,
|
||||
};
|
||||
|
||||
// 4. Wait for SMSG_AUTH_RESPONSE. Usually the first encrypted packet, but not always first
|
||||
@@ -276,7 +281,23 @@ impl WorldSession {
|
||||
|
||||
/// Read + decrypt + parse one server packet.
|
||||
pub fn recv(&mut self) -> Result<ServerPacket> {
|
||||
recv_packet(&mut self.stream, Some(self.crypto.decrypter()))
|
||||
let packet = recv_packet(&mut self.stream, Some(self.crypto.decrypter()))?;
|
||||
// **Captured here rather than in a loop arm**, because it is not one loop's business:
|
||||
// `SMSG_ADDON_INFO` lands somewhere between `CMSG_AUTH_SESSION` and the roster, and which
|
||||
// of the handshake's three read loops sees it is the server's timing, not our contract.
|
||||
// `recv` is the one place all of them go through (decision 2175).
|
||||
if let ServerPacket::AddonInfo { statuses } = &packet {
|
||||
self.addon_info = Some(statuses.clone());
|
||||
}
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
/// The `SMSG_ADDON_INFO` verdict, taken once — `None` when the server never answered our addon
|
||||
/// block, which is a real state and not a failure: vmangos only replies when
|
||||
/// `BuildAddonPacket` accepts the block, and a rejection leaves the session alive and silent
|
||||
/// (`WorldSocket.cpp:447`). The reference behaves the same way — no reply, no Lua index space.
|
||||
pub fn take_addon_info(&mut self) -> Option<Vec<u8>> {
|
||||
self.addon_info.take()
|
||||
}
|
||||
|
||||
/// Send a client packet (encrypted header + plaintext body).
|
||||
|
||||
Reference in New Issue
Block a user