the auction house opens, the Honor tab lands, and a day of fixes
- the auction house end to end: right-clicking an auctioneer opens Browse/Bids/Auctions on the server's reply — search, the category tree from your own DBCs, the reference's 50-a-page pager, multi-key sorting (directions corrected at the bytes), bid/buyout, the sell slot with deposit and duration, cancel with its fee popup; prices sit on the reference's own money-frame kit in lined-up columns, the twenty auction messages split refusals-red / outcomes-to-chat, and a won or sold auction reads as a proper mail receipt — the window's Lua re-worded in our own terms after the overlap ratchet caught the first cut leaning on the reference's text - the PvP honor system whole: the Honor tab and the inspect page over a real wire model, with current rank and highest-ever rank correctly two different fields - a day of reported fixes: WASD survives a /logout re-entry, addon settings save on a window close and Cmd+Q (exactly once — a mac close could fire the tail twice), clams open again, random-enchant items name themselves on every surface, the loot window's rows hold their seats with legible counts, idle players stop sinking through building floors, the shooter re-pulls between Auto Shots, portraits loosen ~19% to the client's own aspect-1.0 matrix, and big creatures thump the camera off the CameraShakes table - character select clicks like 1.12 — captions whiten on hover, buttons fire on release, Humans face straight ahead off the stage bone's real yaw, the facing is per-character, and hunter/warlock pets stand beside you at their family-ramp size with their own emitters (the imp's flames), the Voidwalker's sequence-hidden shackles culled - the smaller player-facing set: nameplate on/off rows in Options with the CanAttack category gate (emissaries stop plating), the bonus bar slides up with igBonusBarOpen, the camera's framing height glides on a shapeshift, the open letter evicts the centre seat, the far-band fog hull stops printing a pale horizon stripe at low view distance, the guide arrow is gold with per-source rim art, error text fades through its shadow too, and /convertraid converts the party - README: the auction house and PvP honor move out of "not built yet" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
//! Coordinates stay **raw WoW** (the `benilla` boundary applies `bevy = (-y, z, -x)`).
|
||||
|
||||
use crate::messages::{
|
||||
ActionButton, AttackerState, ChannelNoticeTail, Character, CreateSpline, DamageShield,
|
||||
ActionButton, AttackerState, AuctionBidderNotification, AuctionCommandTail, AuctionListEntry,
|
||||
AuctionOwnerNotification, ChannelNoticeTail, Character, CreateSpline, DamageShield,
|
||||
EnvironmentalDamageLog, ExplorationXp, FriendEntry, FriendStatusUpdate, GossipOption,
|
||||
GroupLootInfo, GroupMemberEntry, GuildCommandResult, GuildEventNotice, GuildInfo,
|
||||
GuildQueryResponse, GuildRoster, InspectHonorStats, ItemInfo, ItemPushResult, JumpInfo,
|
||||
@@ -1142,6 +1143,52 @@ pub enum SessionEvent {
|
||||
/// mail waiting, `-86400.0` = none. Drives `HasNewMail()`/`UPDATE_PENDING_MAIL` (decision 0544
|
||||
/// P3).
|
||||
NextMailTime { seconds: f32 },
|
||||
/// `MSG_AUCTION_HELLO`'s reply — the auctioneer we greeted plus its `AuctionHouse.dbc` row
|
||||
/// (1..7, the deposit/cut rates). This packet, not our send, is what opens the auction window
|
||||
/// (decision 1511 P0's wire layer; the window itself is a later phase).
|
||||
AuctionHello { auctioneer: u64, house_id: u32 },
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT` — the verdict on a sell/cancel/bid, keyed by `action`
|
||||
/// ([`crate::messages::auction_action`]) and `error` ([`crate::messages::auction_error`]);
|
||||
/// `tail` is the conditional payload the error selects. `auction_id` is `0` on most failures,
|
||||
/// and several refusals send no packet at all (decision 1511).
|
||||
AuctionCommandResult {
|
||||
auction_id: u32,
|
||||
action: u32,
|
||||
error: u32,
|
||||
tail: AuctionCommandTail,
|
||||
},
|
||||
/// `SMSG_AUCTION_LIST_RESULT` — a Browse page (max 50 rows). `total_count` is the pre-cap
|
||||
/// match count the pager needs, and it rides at the END of the wire body.
|
||||
AuctionListResult {
|
||||
auctions: Vec<AuctionListEntry>,
|
||||
total_count: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_OWNER_LIST_RESULT` — the Auctions tab page (our own listings). Same frame
|
||||
/// and record as [`Self::AuctionListResult`].
|
||||
AuctionOwnerListResult {
|
||||
auctions: Vec<AuctionListEntry>,
|
||||
total_count: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_BIDDER_LIST_RESULT` — the Bid tab page. Same frame and record as
|
||||
/// [`Self::AuctionListResult`]; the server emits the explicitly-refreshed ids first and then
|
||||
/// our live bids, so one row can appear twice in a page.
|
||||
AuctionBidderListResult {
|
||||
auctions: Vec<AuctionListEntry>,
|
||||
total_count: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_BIDDER_NOTIFICATION` — we won, or we were outbid (`bid_or_zero == 0` means
|
||||
/// **won**, not "no bid").
|
||||
AuctionBidderNotification(AuctionBidderNotification),
|
||||
/// `SMSG_AUCTION_OWNER_NOTIFICATION` — our own auction sold or took a bid. A deliberately
|
||||
/// different shape from the bidder notice: no house id, guid fourth, and an all-zero
|
||||
/// `bidder_guid` is the "sold" signal.
|
||||
AuctionOwnerNotification(AuctionOwnerNotification),
|
||||
/// `SMSG_AUCTION_REMOVED_NOTIFICATION` — an auction we had bid on was cancelled by its seller.
|
||||
AuctionRemovedNotification {
|
||||
auction_id: u32,
|
||||
item_entry: u32,
|
||||
random_property_id: i32,
|
||||
},
|
||||
/// `SMSG_TRADE_STATUS` — one pulse of the player-trade state machine (open/accept/cancel/
|
||||
/// complete + the refusal reasons). The app drives the trade window and the auto-`BEGIN_TRADE`
|
||||
/// reply off this (decision 0592; the window itself is P1).
|
||||
|
||||
@@ -824,6 +824,60 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
ServerPacket::NextMailTime { seconds } => {
|
||||
vec![SessionEvent::NextMailTime { seconds }]
|
||||
}
|
||||
ServerPacket::AuctionHello {
|
||||
auctioneer,
|
||||
house_id,
|
||||
} => vec![SessionEvent::AuctionHello {
|
||||
auctioneer,
|
||||
house_id,
|
||||
}],
|
||||
ServerPacket::AuctionCommandResult {
|
||||
auction_id,
|
||||
action,
|
||||
error,
|
||||
tail,
|
||||
} => vec![SessionEvent::AuctionCommandResult {
|
||||
auction_id,
|
||||
action,
|
||||
error,
|
||||
tail,
|
||||
}],
|
||||
ServerPacket::AuctionListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
} => vec![SessionEvent::AuctionListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
}],
|
||||
ServerPacket::AuctionOwnerListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
} => vec![SessionEvent::AuctionOwnerListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
}],
|
||||
ServerPacket::AuctionBidderListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
} => vec![SessionEvent::AuctionBidderListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
}],
|
||||
ServerPacket::AuctionBidderNotification(n) => {
|
||||
vec![SessionEvent::AuctionBidderNotification(n)]
|
||||
}
|
||||
ServerPacket::AuctionOwnerNotification(n) => {
|
||||
vec![SessionEvent::AuctionOwnerNotification(n)]
|
||||
}
|
||||
ServerPacket::AuctionRemovedNotification {
|
||||
auction_id,
|
||||
item_entry,
|
||||
random_property_id,
|
||||
} => vec![SessionEvent::AuctionRemovedNotification {
|
||||
auction_id,
|
||||
item_entry,
|
||||
random_property_id,
|
||||
}],
|
||||
ServerPacket::TradeStatus { status } => vec![SessionEvent::TradeStatus { status }],
|
||||
ServerPacket::TradeStatusExtended { state } => {
|
||||
vec![SessionEvent::TradeStatusExtended { state }]
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
//! The auction house arc's wire layer (decision 1511 phase P0): the auctioneer hello, the three
|
||||
//! list pages (browse / your bids / your auctions), the sell/bid/cancel verbs, the one command
|
||||
//! verdict, and the three notifications (vmangos `Handlers/AuctionHouseHandler.cpp`,
|
||||
//! `AuctionHouse/AuctionHouseMgr.{h,cpp}`, `Server/Packets/AuctionHouse.{h,cpp}`,
|
||||
//! `Opcodes_1_12_1.h`, VERIFIED — read twice independently at decision time and re-read against
|
||||
//! the source while writing this module).
|
||||
//!
|
||||
//! **There is no `*_LIST_PENDING_SALES` opcode on 5875** — the symbol does not exist anywhere in
|
||||
//! vmangos's opcode tables (0x264/0x265 sit between `SMSG_SPELLORDAMAGE_IMMUNE` and
|
||||
//! `SMSG_SET_FLAT_SPELL_MODIFIER` with no gap for one), and the matching mail answer
|
||||
//! `AUCTION_SALE_PENDING(6)` is dead in the tree. The same shape of recorded fact as `mail`'s
|
||||
//! missing `SMSG_SHOW_MAILBOX`: a pane that exists in later clients has no wire here to build.
|
||||
//!
|
||||
//! Two things hold across every packet below. **Every guid is a plain 8-byte LE `u64`, never a
|
||||
//! packed guid** — vmangos's `ObjectGuid` stream operators write and read `GetRawValue()` whole
|
||||
//! (`ObjectGuid.cpp`), and `PackedGuid` never appears on the auction path. And **the auctioneer
|
||||
//! guid rides at the head of every CMSG** because the server re-validates the 5 yd interact
|
||||
//! distance on each one independently (`GetCheckedAuctionHouseForAuctioneer`): the hello is not a
|
||||
//! session token.
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::wire::{read_i32_le, read_u32_le, read_u64_le};
|
||||
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT`'s `action` field — which verb the verdict answers (VERIFIED
|
||||
/// vmangos `AuctionHouseMgr.h:53-58`, `AuctionAction`).
|
||||
pub mod auction_action {
|
||||
/// Answers [`super::auction_sell_item`] — a new listing was created.
|
||||
pub const STARTED: u32 = 0;
|
||||
/// Answers [`super::auction_remove_item`] — a listing was cancelled.
|
||||
pub const REMOVED: u32 = 1;
|
||||
/// Answers [`super::auction_place_bid`]. A *buyout* rides this action too: buyout is inferred
|
||||
/// server-side (`price >= buyout && buyout != 0`), never flagged on the wire.
|
||||
pub const BID_PLACED: u32 = 2;
|
||||
}
|
||||
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT`'s `error` field (VERIFIED vmangos `AuctionHouseMgr.h:40-51`,
|
||||
/// `AuctionError`). **6, 8, 9, 11 and 12 have no name**: vmangos has no enumerator for them and
|
||||
/// never sends them, and the real client's `ERR_AUCTION_*` table falls through to a generic
|
||||
/// failure branch for anything it doesn't recognize — so an unknown code here is a generic
|
||||
/// refusal to report, not a parse failure.
|
||||
pub mod auction_error {
|
||||
/// Success — read [`super::auction_action`] to know *what* succeeded.
|
||||
pub const OK: u32 = 0;
|
||||
/// The only error carrying an [`super::AuctionCommandTail::Inventory`] tail: an
|
||||
/// `InventoryResult` (`EQUIP_ERR_*`) code, the same table the bag/equip refusals use.
|
||||
pub const INVENTORY: u32 = 1;
|
||||
/// `ERR_AUCTION_DATABASE_ERROR` — also vmangos's catch-all refusal (a bad `etime` lands here).
|
||||
pub const DATABASE: u32 = 2;
|
||||
pub const NOT_ENOUGH_MONEY: u32 = 3;
|
||||
pub const ITEM_NOT_FOUND: u32 = 4;
|
||||
/// Someone outbid us mid-flight; carries the [`super::AuctionCommandTail::HigherBid`] tail.
|
||||
pub const HIGHER_BID: u32 = 5;
|
||||
/// The bid did not clear the minimum increment (5% of the current bid, floored, min 1 copper).
|
||||
pub const BID_INCREMENT: u32 = 7;
|
||||
/// You cannot bid on your own auction.
|
||||
pub const BID_OWN: u32 = 10;
|
||||
pub const RESTRICTED_ACCOUNT: u32 = 13;
|
||||
}
|
||||
|
||||
/// The "don't filter" sentinels [`auction_list_items`]'s browse filters use (VERIFIED vmangos
|
||||
/// `AuctionHouseObject::BuildListAuctionItems`, which compares each filter against exactly these
|
||||
/// before applying it — and takes a whole-table fast path when *all* of them are unset).
|
||||
pub mod auction_filter {
|
||||
/// `slot_id` / `main_category` / `sub_category` / `quality`: `0xFFFF_FFFF` = no filter.
|
||||
pub const ANY: u32 = 0xFFFF_FFFF;
|
||||
/// `level_min` / `level_max`: `0` = no filter. The pair gates `RequiredLevel`, **not** item
|
||||
/// level, and `level_max` is only consulted when `level_min` is set.
|
||||
pub const ANY_LEVEL: u8 = 0;
|
||||
/// `usable`: `0` = no filter. Nonzero drops everything `CanUseItem` refuses (and recipes we
|
||||
/// already know).
|
||||
pub const ANY_USABILITY: u8 = 0;
|
||||
}
|
||||
|
||||
/// The three `etime` values `CMSG_AUCTION_SELL_ITEM` may carry, in **minutes** (VERIFIED vmangos
|
||||
/// `HandleAuctionSellItem`, `AuctionHouseHandler.cpp:281-296`: `etime * MINUTE` is switched
|
||||
/// against 1/4/12 × `MIN_AUCTION_TIME` — and `MIN_AUCTION_TIME` is `2*HOUR`). Anything else is
|
||||
/// refused with [`auction_error::DATABASE`]. The same 1/4/12 multiple also scales the deposit.
|
||||
pub mod auction_duration {
|
||||
pub const SHORT_MINUTES: u32 = 120;
|
||||
pub const MEDIUM_MINUTES: u32 = 480;
|
||||
pub const LONG_MINUTES: u32 = 1440;
|
||||
}
|
||||
|
||||
/// Records per list page. Every server list builder stops at 50 (`count < 50`), so paging is
|
||||
/// `list_from = page * AUCTION_PAGE_SIZE` and the pager's "of N" comes from the list result's
|
||||
/// trailing `total_count`, which is counted *before* this cap.
|
||||
pub const AUCTION_PAGE_SIZE: u32 = 50;
|
||||
|
||||
/// One list-result record's fixed width: **64** bytes — twelve 4-byte fields plus the two 8-byte
|
||||
/// guids (`owner_guid`, `bidder_guid`). Decision 1511's prose says 60; that is an arithmetic slip
|
||||
/// in the summary, not in its field list, which sums to 64 (VERIFIED by re-reading the single
|
||||
/// producer, `AuctionEntry::BuildAuctionInfo`, `AuctionHouseMgr.cpp:811-842`, whose writes are
|
||||
/// `7×u32, u64, 4×u32, u64, u32`). Load-bearing rather than decorative — it is the bound
|
||||
/// [`read_auction_list_result`] uses to survive a body shorter than its own `count` claims.
|
||||
pub const AUCTION_RECORD_BYTES: usize = 64;
|
||||
|
||||
/// One row of any of the three auction list results. `SMSG_AUCTION_LIST_RESULT`,
|
||||
/// `SMSG_AUCTION_OWNER_LIST_RESULT` and `SMSG_AUCTION_BIDDER_LIST_RESULT` share **one** record
|
||||
/// layout and one frame — all three call the same producer (VERIFIED vmangos
|
||||
/// `AuctionEntry::BuildAuctionInfo`). Fixed width ([`AUCTION_RECORD_BYTES`]), no cstrings: the
|
||||
/// item's *name* is not on the wire at all, and comes from the ordinary item-template cache.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AuctionListEntry {
|
||||
/// The auction's server id — the handle [`auction_place_bid`]/[`auction_remove_item`] name.
|
||||
pub auction_id: u32,
|
||||
pub item_entry: u32,
|
||||
/// `PERM_ENCHANTMENT_SLOT` only. 1.12 carries no other enchant information on an auction row:
|
||||
/// vmangos's own `[-ZERO] no other infos about enchantment in 1.12 [?]` comment sits on the
|
||||
/// commented-out loop that would have written the durations and charges a later client reads.
|
||||
pub perm_enchant: u32,
|
||||
/// **Signed** — negative is a random *suffix* id, positive a random *property* id.
|
||||
/// `Item::GetItemRandomPropertyId` returns `int32` and the server casts it through `uint32`,
|
||||
/// so the wire bytes are a two's-complement `i32` and reading them unsigned turns every
|
||||
/// suffixed item ("of the Bear") into a ~4.29-billion property id that resolves to nothing.
|
||||
pub random_property_id: i32,
|
||||
pub suffix_factor: u32,
|
||||
/// The stack size.
|
||||
pub count: u32,
|
||||
/// **Signed** — a negative charge count means "N charges, then destroy the item".
|
||||
/// `Item::GetSpellCharges` returns `int32` and the server casts it through `uint32` (its own
|
||||
/// comment on the line reads `item->charge FFFFFFF`).
|
||||
pub spell_charges: i32,
|
||||
pub owner_guid: u64,
|
||||
/// The seller's **opening** price, captured at listing time and constant for the auction's
|
||||
/// life. NOT the current bid ([`Self::current_bid`]) and not the minimum next bid — vmangos's
|
||||
/// own field comment calls it "maybe useless", but it *is* what the first bid must meet.
|
||||
pub start_bid: u32,
|
||||
/// How much above [`Self::current_bid`] the next bid must go: 5% of the current bid, floored,
|
||||
/// minimum 1 copper. **`0` while nobody has bid** — the server writes
|
||||
/// `bid ? GetAuctionOutBid() : 0`, so a UI must fall back to [`Self::start_bid`] for the
|
||||
/// opening bid rather than reading a `0` here as "any amount will do".
|
||||
pub min_increment: u32,
|
||||
/// `0` = no buyout. Buyout is *inferred*, never flagged: a bid of `>= buyout` on a nonzero
|
||||
/// buyout is a buyout.
|
||||
pub buyout: u32,
|
||||
/// **Milliseconds** remaining, and **unclamped**. The server writes
|
||||
/// `(expireTime - now) * IN_MILLISECONDS` with no floor and only sweeps expiry on its own
|
||||
/// timer, so an expired-but-still-listed auction wraps to a value near 4.29 billion. Read
|
||||
/// faithfully here — a consumer must treat an implausibly large value as **expired**, not as
|
||||
/// "very long".
|
||||
pub time_left_ms: u32,
|
||||
/// `0` = nobody has bid.
|
||||
pub bidder_guid: u64,
|
||||
/// `0` = no bid yet.
|
||||
pub current_bid: u32,
|
||||
}
|
||||
|
||||
/// The conditional tail `SMSG_AUCTION_COMMAND_RESULT` carries after its three dwords — which
|
||||
/// shape follows is decided by `error` first and `action` only inside the `OK` arm (VERIFIED
|
||||
/// vmangos `WorldSession::SendAuctionCommandResult`, `AuctionHouseHandler.cpp:70-96`, whose
|
||||
/// `switch` is on the error code).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuctionCommandTail {
|
||||
/// Every other (error, action) pair — the three dwords are the whole packet. This is the
|
||||
/// common case: a plain `OK` + `STARTED`/`REMOVED`, and every named error but two.
|
||||
Empty,
|
||||
/// [`auction_error::OK`] **and** [`auction_action::BID_PLACED`]: the outbid step the *next*
|
||||
/// bid must clear, recomputed from the bid just accepted.
|
||||
BidPlaced { new_min_outbid: u32 },
|
||||
/// [`auction_error::INVENTORY`]: an `InventoryResult` (`EQUIP_ERR_*`) code.
|
||||
Inventory { result: u32 },
|
||||
/// [`auction_error::HIGHER_BID`]: we were outbid between reading the page and bidding — who
|
||||
/// holds it now, at what bid, and the step the next bid must clear.
|
||||
HigherBid {
|
||||
new_bidder_guid: u64,
|
||||
new_bid: u32,
|
||||
new_min_outbid: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// `SMSG_AUCTION_BIDDER_NOTIFICATION` — "you won" / "you were outbid", pushed to the *bidder*
|
||||
/// (VERIFIED vmangos `AuctionBidderNotification::AppendBodyTo`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:78-87`). Deliberately **not** the same shape as
|
||||
/// [`AuctionOwnerNotification`]: this one leads with the house id and puts the guid third, the
|
||||
/// owner one has no house id and puts the guid fourth. Two structs and two readers, on purpose —
|
||||
/// sharing either would silently mis-decode one of them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AuctionBidderNotification {
|
||||
/// `AuctionHouse.dbc` row (1..7) — the same id [`read_auction_hello`] answers with.
|
||||
pub house_id: u32,
|
||||
pub auction_id: u32,
|
||||
/// The bidder the notice is *about* — us.
|
||||
pub bidder_guid: u64,
|
||||
/// **`0` means WON**, not "no bid" (vmangos writes `won ? 0 : auction->bid`, and the real
|
||||
/// client branches `ERR_AUCTION_WON_S` vs `ERR_AUCTION_OUTBID_S` on exactly this). Nonzero is
|
||||
/// the bid that beat us.
|
||||
pub bid_or_zero: u32,
|
||||
/// The step the next bid must clear (`GetAuctionOutBid()`).
|
||||
pub out_bid: u32,
|
||||
pub item_entry: u32,
|
||||
/// **Signed**, as on [`AuctionListEntry::random_property_id`] — the same
|
||||
/// `Item::GetItemRandomPropertyId()` value, cast through vmangos's `uint32` packet field.
|
||||
/// `0` when the item is gone.
|
||||
pub random_property_id: i32,
|
||||
}
|
||||
|
||||
/// `SMSG_AUCTION_OWNER_NOTIFICATION` — "your auction sold" / a bid landed, pushed to the *seller*
|
||||
/// (VERIFIED vmangos `AuctionOwnerNotification::AppendBodyTo`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:89-97`). **No `house_id`, and the guid sits fourth** — see
|
||||
/// [`AuctionBidderNotification`] for why the two are kept apart.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AuctionOwnerNotification {
|
||||
pub auction_id: u32,
|
||||
/// The current bid.
|
||||
pub bid: u32,
|
||||
/// The step the next bid must clear (`GetAuctionOutBid()`).
|
||||
pub out_bid: u32,
|
||||
/// Who bid. **All-zero on a sale**: vmangos fills this field only when the auction has *not*
|
||||
/// sold yet (`if (!sold)`), so a zero guid here is the "sold" signal, not a missing bidder.
|
||||
pub bidder_guid: u64,
|
||||
pub item_entry: u32,
|
||||
/// **Signed**, as on [`AuctionListEntry::random_property_id`]. `0` when the item is gone.
|
||||
pub random_property_id: i32,
|
||||
}
|
||||
|
||||
/// Body of `MSG_AUCTION_HELLO` (VERIFIED vmangos `AuctionHello::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:3-6`): one full 8-byte auctioneer guid. **Same opcode both
|
||||
/// directions** — the reply ([`read_auction_hello`]) echoes the guid and adds the house id, and it
|
||||
/// is the *reply* that opens the window, not our send.
|
||||
pub fn auction_hello(auctioneer: u64) -> Vec<u8> {
|
||||
auctioneer.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUCTION_SELL_ITEM` (VERIFIED vmangos `AuctionSellItem::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:42-49`): `u64 auctioneer, u64 itemGuid, u32 bid, u32 buyout,
|
||||
/// u32 etime`.
|
||||
///
|
||||
/// `etime_minutes` may only be one of [`auction_duration`]'s three values; anything else is
|
||||
/// refused with [`auction_error::DATABASE`]. A `bid` or `etime_minutes` of `0` is answered with
|
||||
/// **nothing at all** (`HandleAuctionSellItem`'s silent `return`), as is a bid the player cannot
|
||||
/// pay the deposit for — a UI that blocks waiting for an ack will hang.
|
||||
pub fn auction_sell_item(
|
||||
auctioneer: u64,
|
||||
item_guid: u64,
|
||||
bid: u32,
|
||||
buyout: u32,
|
||||
etime_minutes: u32,
|
||||
) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8 + 8 + 4 + 4 + 4);
|
||||
body.extend_from_slice(&auctioneer.to_le_bytes());
|
||||
body.extend_from_slice(&item_guid.to_le_bytes());
|
||||
body.extend_from_slice(&bid.to_le_bytes());
|
||||
body.extend_from_slice(&buyout.to_le_bytes());
|
||||
body.extend_from_slice(&etime_minutes.to_le_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUCTION_REMOVE_ITEM` (VERIFIED vmangos `AuctionRemoveItem::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:36-40`): `u64 auctioneer, u32 auctionId` — the seller's
|
||||
/// cancel. Answered by `SMSG_AUCTION_COMMAND_RESULT` (action [`auction_action::REMOVED`]) *except*
|
||||
/// when the auction already carries a bid and the seller cannot pay the 5% cut out of pocket, in
|
||||
/// which case the server refuses **silently**.
|
||||
pub fn auction_remove_item(auctioneer: u64, auction_id: u32) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8 + 4);
|
||||
body.extend_from_slice(&auctioneer.to_le_bytes());
|
||||
body.extend_from_slice(&auction_id.to_le_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUCTION_LIST_ITEMS` — the Browse tab's search (VERIFIED vmangos
|
||||
/// `AuctionListItems::ReadFromWorldPacket`, `Server/Packets/AuctionHouse.cpp:51-63`): `u64
|
||||
/// auctioneer, u32 listfrom, cstr searchedname, u8 levelmin, u8 levelmax, u32 auctionSlotID, u32
|
||||
/// auctionMainCategory, u32 auctionSubCategory, u32 quality, u8 usable`.
|
||||
///
|
||||
/// **Ten fields and nothing else: no sort columns, no sort count, no trailing padding.** That is
|
||||
/// the field an implementer invents, because later expansions do carry sort bytes here — on 5875
|
||||
/// *filtering* is entirely server-side and *sorting* is entirely client-side. (Corroborated
|
||||
/// beyond the reader: vmangos runs `VerifyPacketWasCorrectlyRead` after every `ClientPacket`
|
||||
/// parse and error-logs a body it did not consume to the end, which a real 5875 client appending
|
||||
/// sort bytes would trip on every search.)
|
||||
///
|
||||
/// The sentinels are in [`auction_filter`]: `0xFFFF_FFFF` for the four u32 filters, `0` for the
|
||||
/// three small ones, and an empty `searched_name` is just the lone NUL. `quality` is a
|
||||
/// **minimum**, not an equality (`proto->Quality < query.quality` is what drops a row).
|
||||
/// `level_min`/`level_max` gate `RequiredLevel`, not item level. The name match is a
|
||||
/// case-insensitive substring against the localized name *with the random-property suffix
|
||||
/// appended*. `list_from` pages by [`AUCTION_PAGE_SIZE`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn auction_list_items(
|
||||
auctioneer: u64,
|
||||
list_from: u32,
|
||||
searched_name: &str,
|
||||
level_min: u8,
|
||||
level_max: u8,
|
||||
slot_id: u32,
|
||||
main_category: u32,
|
||||
sub_category: u32,
|
||||
quality: u32,
|
||||
usable: u8,
|
||||
) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8 + 4 + searched_name.len() + 1 + 1 + 1 + 4 + 4 + 4 + 4 + 1);
|
||||
body.extend_from_slice(&auctioneer.to_le_bytes());
|
||||
body.extend_from_slice(&list_from.to_le_bytes());
|
||||
body.extend_from_slice(searched_name.as_bytes());
|
||||
body.push(0);
|
||||
body.push(level_min);
|
||||
body.push(level_max);
|
||||
body.extend_from_slice(&slot_id.to_le_bytes());
|
||||
body.extend_from_slice(&main_category.to_le_bytes());
|
||||
body.extend_from_slice(&sub_category.to_le_bytes());
|
||||
body.extend_from_slice(&quality.to_le_bytes());
|
||||
body.push(usable);
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUCTION_LIST_OWNER_ITEMS` — the Auctions tab (VERIFIED vmangos
|
||||
/// `AuctionListOwnerItems::ReadFromWorldPacket`, `Server/Packets/AuctionHouse.cpp:23-27`): `u64
|
||||
/// auctioneer, u32 listfrom`. Answered by `SMSG_AUCTION_OWNER_LIST_RESULT`. The server matches on
|
||||
/// the **account**, then filters to this character's guid.
|
||||
pub fn auction_list_owner_items(auctioneer: u64, list_from: u32) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8 + 4);
|
||||
body.extend_from_slice(&auctioneer.to_le_bytes());
|
||||
body.extend_from_slice(&list_from.to_le_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUCTION_PLACE_BID` (VERIFIED vmangos `AuctionPlaceBid::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:29-34`): `u64 auctioneer, u32 auctionId, u32 price`. The
|
||||
/// buyout verb too — send `price = buyout` and the server takes it as one; there is no separate
|
||||
/// opcode and no flag. A bid the player cannot afford is refused **silently**.
|
||||
pub fn auction_place_bid(auctioneer: u64, auction_id: u32, price: u32) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8 + 4 + 4);
|
||||
body.extend_from_slice(&auctioneer.to_le_bytes());
|
||||
body.extend_from_slice(&auction_id.to_le_bytes());
|
||||
body.extend_from_slice(&price.to_le_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUCTION_LIST_BIDDER_ITEMS` — the Bid tab (VERIFIED vmangos
|
||||
/// `AuctionListBidderItem::ReadFromWorldPacket`, `Server/Packets/AuctionHouse.cpp:8-21`): `u64
|
||||
/// auctioneer, u32 listfrom, u32 count`, then `count` × `u32 auctionId`.
|
||||
///
|
||||
/// The trailing id list is a **refresh set**, not a filter: the server emits a record for each id
|
||||
/// named here *first* and then appends every auction this player is the current bidder on, both
|
||||
/// feeding one `count`/`total_count` — so an id that is also still ours appears twice in the page.
|
||||
/// Send it empty (`&[]`) for a plain page fetch; the reference client fills it with the auctions
|
||||
/// it has been outbid on so their rows re-sync.
|
||||
pub fn auction_list_bidder_items(auctioneer: u64, list_from: u32, auction_ids: &[u32]) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8 + 4 + 4 + 4 * auction_ids.len());
|
||||
body.extend_from_slice(&auctioneer.to_le_bytes());
|
||||
body.extend_from_slice(&list_from.to_le_bytes());
|
||||
body.extend_from_slice(&(auction_ids.len() as u32).to_le_bytes());
|
||||
for id in auction_ids {
|
||||
body.extend_from_slice(&id.to_le_bytes());
|
||||
}
|
||||
body
|
||||
}
|
||||
|
||||
/// Read `MSG_AUCTION_HELLO`'s **reply** (VERIFIED vmangos `AuctionHelloResponse::AppendBodyTo`,
|
||||
/// `Server/Packets/AuctionHouse.cpp:72-76`): `u64 auctioneerGuid, u32 houseId`. Nothing follows
|
||||
/// the house id on 5875.
|
||||
///
|
||||
/// `house_id` indexes `AuctionHouse.dbc` (rows 1..7: the six faction houses, then Blackwater at 7)
|
||||
/// and is the field the sell pane needs — the deposit and cut rates are read out of that row, so a
|
||||
/// window that assumes a house shows the neutral 25% where it should show 5%. It is also the id
|
||||
/// the auction invoice mails arrive under (`MailListEntry::sender_id` on a
|
||||
/// [`super::mail_message_type::AUCTION`] mail).
|
||||
pub(super) fn read_auction_hello(r: &mut &[u8]) -> io::Result<(u64, u32)> {
|
||||
Ok((read_u64_le(r)?, read_u32_le(r)?))
|
||||
}
|
||||
|
||||
/// One [`AuctionListEntry`] — [`AUCTION_RECORD_BYTES`] of fixed-width fields, 14 reads, in
|
||||
/// `AuctionEntry::BuildAuctionInfo`'s order.
|
||||
fn read_auction_list_entry(r: &mut &[u8]) -> io::Result<AuctionListEntry> {
|
||||
Ok(AuctionListEntry {
|
||||
auction_id: read_u32_le(r)?,
|
||||
item_entry: read_u32_le(r)?,
|
||||
perm_enchant: read_u32_le(r)?,
|
||||
random_property_id: read_i32_le(r)?,
|
||||
suffix_factor: read_u32_le(r)?,
|
||||
count: read_u32_le(r)?,
|
||||
spell_charges: read_i32_le(r)?,
|
||||
owner_guid: read_u64_le(r)?,
|
||||
start_bid: read_u32_le(r)?,
|
||||
min_increment: read_u32_le(r)?,
|
||||
buyout: read_u32_le(r)?,
|
||||
time_left_ms: read_u32_le(r)?,
|
||||
bidder_guid: read_u64_le(r)?,
|
||||
current_bid: read_u32_le(r)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read the body all three list results share — `SMSG_AUCTION_LIST_RESULT`,
|
||||
/// `SMSG_AUCTION_OWNER_LIST_RESULT`, `SMSG_AUCTION_BIDDER_LIST_RESULT` (VERIFIED vmangos's async
|
||||
/// `AuctionHouseClientQueryTask`, `AuctionHouseHandler.cpp:651-701`, which builds all three and
|
||||
/// differs only in `SetOpcode`): `u32 count`, then `count` × [`AuctionListEntry`], then `u32
|
||||
/// totalCount`.
|
||||
///
|
||||
/// **`total_count` rides at the very END, after the records.** The leading `count` is written as
|
||||
/// a placeholder and patched with `put<uint32>` once the records are in; the total is appended
|
||||
/// last. It is the match count *before* the [`AUCTION_PAGE_SIZE`] cap — the pager's "of N" — so
|
||||
/// it is routinely larger than the number of records, and reading it in the wrong place silently
|
||||
/// yields a record field.
|
||||
///
|
||||
/// Returns `(records, total_count)`.
|
||||
pub(super) fn read_auction_list_result(r: &mut &[u8]) -> io::Result<(Vec<AuctionListEntry>, u32)> {
|
||||
let count = read_u32_le(r)?;
|
||||
// Bound the allocation by what the buffer could actually hold, not by a `count` we do not
|
||||
// trust (see the loop guard below).
|
||||
let mut auctions = Vec::with_capacity((count as usize).min(r.len() / AUCTION_RECORD_BYTES));
|
||||
for _ in 0..count {
|
||||
// `count` is an UPPER BOUND, not a record count. vmangos's *browse fast
|
||||
// path* (the no-filter branch of `BuildListAuctionItems`, `AuctionHouseMgr.cpp:716-735`)
|
||||
// does `BuildAuctionInfo(data); if ((++count) >= 50) break;` — discarding a `false`
|
||||
// return. A stale auction whose item row is gone therefore writes ZERO bytes and still
|
||||
// counts, so a real server sends `count = N` with N-k records. Stop cleanly on a short
|
||||
// buffer and return what we did get: the layout is fixed-width, so a dropped record
|
||||
// shifts nothing, and every record we read is intact.
|
||||
if r.len() < AUCTION_RECORD_BYTES {
|
||||
break;
|
||||
}
|
||||
auctions.push(read_auction_list_entry(r)?);
|
||||
}
|
||||
// A body truncated past the last record has no trailing total either; fall back to what we
|
||||
// actually read rather than failing the whole page over the pager's hint.
|
||||
let total_count = if r.len() >= 4 {
|
||||
read_u32_le(r)?
|
||||
} else {
|
||||
auctions.len() as u32
|
||||
};
|
||||
Ok((auctions, total_count))
|
||||
}
|
||||
|
||||
/// Read `SMSG_AUCTION_COMMAND_RESULT` (VERIFIED vmangos `WorldSession::SendAuctionCommandResult`,
|
||||
/// `AuctionHouseHandler.cpp:70-96`): `u32 auctionId, u32 action ([`auction_action`]), u32 error
|
||||
/// ([`auction_error`])`, then the conditional [`AuctionCommandTail`] the error selects.
|
||||
///
|
||||
/// `auction_id` is **`0`** whenever the server had no `AuctionEntry` to name — which is most of
|
||||
/// the failure paths, so it is not a usable correlation handle on an error. The
|
||||
/// remaining-length guard on each tail mirrors `read_send_mail_result`: a body that doesn't carry
|
||||
/// the tail its fields predict decodes as [`AuctionCommandTail::Empty`] rather than erroring.
|
||||
///
|
||||
/// Returns `(auction_id, action, error, tail)`.
|
||||
pub(super) fn read_auction_command_result(
|
||||
r: &mut &[u8],
|
||||
) -> io::Result<(u32, u32, u32, AuctionCommandTail)> {
|
||||
let auction_id = read_u32_le(r)?;
|
||||
let action = read_u32_le(r)?;
|
||||
let error = read_u32_le(r)?;
|
||||
// The server switches on `error`; only its OK arm then looks at `action`. Mirror that order —
|
||||
// an OK with any other action carries no tail at all.
|
||||
let tail = match error {
|
||||
auction_error::OK if action == auction_action::BID_PLACED && !r.is_empty() => {
|
||||
AuctionCommandTail::BidPlaced {
|
||||
new_min_outbid: read_u32_le(r)?,
|
||||
}
|
||||
}
|
||||
auction_error::INVENTORY if !r.is_empty() => AuctionCommandTail::Inventory {
|
||||
result: read_u32_le(r)?,
|
||||
},
|
||||
auction_error::HIGHER_BID if !r.is_empty() => AuctionCommandTail::HigherBid {
|
||||
new_bidder_guid: read_u64_le(r)?,
|
||||
new_bid: read_u32_le(r)?,
|
||||
new_min_outbid: read_u32_le(r)?,
|
||||
},
|
||||
_ => AuctionCommandTail::Empty,
|
||||
};
|
||||
Ok((auction_id, action, error, tail))
|
||||
}
|
||||
|
||||
/// Read `SMSG_AUCTION_BIDDER_NOTIFICATION` (VERIFIED vmangos
|
||||
/// `AuctionBidderNotification::AppendBodyTo`, `Server/Packets/AuctionHouse.cpp:78-87`): `u32
|
||||
/// houseId, u32 auctionId, u64 bidderGuid, u32 bidOrZero, u32 outBid, u32 itemEntry, i32
|
||||
/// randomPropertyId`. **Not interchangeable with [`read_auction_owner_notification`]** — see
|
||||
/// [`AuctionBidderNotification`].
|
||||
pub(super) fn read_auction_bidder_notification(
|
||||
r: &mut &[u8],
|
||||
) -> io::Result<AuctionBidderNotification> {
|
||||
Ok(AuctionBidderNotification {
|
||||
house_id: read_u32_le(r)?,
|
||||
auction_id: read_u32_le(r)?,
|
||||
bidder_guid: read_u64_le(r)?,
|
||||
bid_or_zero: read_u32_le(r)?,
|
||||
out_bid: read_u32_le(r)?,
|
||||
item_entry: read_u32_le(r)?,
|
||||
random_property_id: read_i32_le(r)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read `SMSG_AUCTION_OWNER_NOTIFICATION` (VERIFIED vmangos
|
||||
/// `AuctionOwnerNotification::AppendBodyTo`, `Server/Packets/AuctionHouse.cpp:89-97`): `u32
|
||||
/// auctionId, u32 bid, u32 outBid, u64 bidderGuid, u32 itemEntry, i32 randomPropertyId` — a
|
||||
/// different field order from the bidder notification, and no house id.
|
||||
pub(super) fn read_auction_owner_notification(
|
||||
r: &mut &[u8],
|
||||
) -> io::Result<AuctionOwnerNotification> {
|
||||
Ok(AuctionOwnerNotification {
|
||||
auction_id: read_u32_le(r)?,
|
||||
bid: read_u32_le(r)?,
|
||||
out_bid: read_u32_le(r)?,
|
||||
bidder_guid: read_u64_le(r)?,
|
||||
item_entry: read_u32_le(r)?,
|
||||
random_property_id: read_i32_le(r)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read `SMSG_AUCTION_REMOVED_NOTIFICATION` (VERIFIED vmangos
|
||||
/// `AuctionRemovedNotification::AppendBodyTo`, `Server/Packets/AuctionHouse.cpp:65-70`): `u32
|
||||
/// auctionId, u32 itemEntry, i32 randomPropertyId` — pushed to a *bidder* whose auction the seller
|
||||
/// cancelled (the real client shows `ERR_AUCTION_REMOVED_S`).
|
||||
///
|
||||
/// Returns `(auction_id, item_entry, random_property_id)`.
|
||||
pub(super) fn read_auction_removed_notification(r: &mut &[u8]) -> io::Result<(u32, u32, i32)> {
|
||||
Ok((read_u32_le(r)?, read_u32_le(r)?, read_i32_le(r)?))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ mod action_bar;
|
||||
pub mod addons;
|
||||
mod area_trigger;
|
||||
mod attack;
|
||||
mod auction;
|
||||
mod bank;
|
||||
mod binder;
|
||||
mod channel;
|
||||
@@ -66,6 +67,12 @@ pub use action_bar::{
|
||||
pub use addons::{SecureAddon, STANDARD_MODULUS_CRC, STOCK_SECURE_ADDONS};
|
||||
pub use area_trigger::area_trigger;
|
||||
pub use attack::{attack_swing, AttackerState};
|
||||
pub use auction::{
|
||||
auction_action, auction_duration, auction_error, auction_filter, auction_hello,
|
||||
auction_list_bidder_items, auction_list_items, auction_list_owner_items, auction_place_bid,
|
||||
auction_remove_item, auction_sell_item, AuctionBidderNotification, AuctionCommandTail,
|
||||
AuctionListEntry, AuctionOwnerNotification, AUCTION_PAGE_SIZE, AUCTION_RECORD_BYTES,
|
||||
};
|
||||
pub use bank::{
|
||||
autobank_item, autostore_bank_item, bank_slot_result, banker_activate, buy_bank_slot,
|
||||
};
|
||||
|
||||
@@ -989,6 +989,43 @@ pub const MSG_QUERY_NEXT_MAIL_TIME: u16 = 0x0284; // 644
|
||||
/// timer's expiry otherwise).
|
||||
pub const SMSG_RECEIVED_MAIL: u16 = 0x0285; // 645
|
||||
|
||||
// The auction house arc (decision 1511 P0; VERIFIED vmangos `Opcodes_1_12_1.h`: 597-607, 612-613,
|
||||
// 653 — every value re-read against that table). Bodies in [`super::auction`]. Two family facts
|
||||
// belong here: **there is no `*_LIST_PENDING_SALES` on 5875** — the symbol exists in no vmangos
|
||||
// opcode table at all, so the pending-sales pane is a later client's and has no wire to build —
|
||||
// and every CMSG below carries the auctioneer guid at its head because the server re-validates
|
||||
// the 5 yd interact distance on each one independently; the hello is not a session token.
|
||||
/// Same opcode both directions (597): our request is one auctioneer guid; the reply echoes it and
|
||||
/// adds `u32 houseId` (`AuctionHouse.dbc`, rows 1..7, which carries the deposit/cut rates). It is
|
||||
/// the **reply** that opens the window, not our send.
|
||||
pub const MSG_AUCTION_HELLO: u16 = 0x0255; // 597
|
||||
pub const CMSG_AUCTION_SELL_ITEM: u16 = 0x0256; // 598
|
||||
pub const CMSG_AUCTION_REMOVE_ITEM: u16 = 0x0257; // 599
|
||||
/// The Browse search — ten fields and **no sort bytes** on 5875: filtering is server-side,
|
||||
/// sorting entirely client-side ([`super::auction::auction_list_items`]).
|
||||
pub const CMSG_AUCTION_LIST_ITEMS: u16 = 0x0258; // 600
|
||||
pub const CMSG_AUCTION_LIST_OWNER_ITEMS: u16 = 0x0259; // 601
|
||||
/// Bid *and* buy out: buyout is inferred from the price (`price >= buyout && buyout != 0`), never
|
||||
/// flagged, and there is no separate opcode for it.
|
||||
pub const CMSG_AUCTION_PLACE_BID: u16 = 0x025A; // 602
|
||||
/// The one verdict opcode for sell/cancel/bid, with a conditional tail keyed on its error
|
||||
/// ([`super::auction::AuctionCommandTail`]). Several refusals send **nothing at all** instead.
|
||||
pub const SMSG_AUCTION_COMMAND_RESULT: u16 = 0x025B; // 603
|
||||
/// This and the two list results below share ONE frame and ONE 64-byte record layout
|
||||
/// ([`super::auction::read_auction_list_result`]): `u32 count`, the records, then `u32 totalCount`
|
||||
/// at the very end — the match count *before* the 50-row page cap.
|
||||
pub const SMSG_AUCTION_LIST_RESULT: u16 = 0x025C; // 604
|
||||
pub const SMSG_AUCTION_OWNER_LIST_RESULT: u16 = 0x025D; // 605
|
||||
/// Pushed to the bidder: won, or outbid — `bidOrZero == 0` means **won**, not "no bid".
|
||||
pub const SMSG_AUCTION_BIDDER_NOTIFICATION: u16 = 0x025E; // 606
|
||||
/// Pushed to the seller. A **different field order** from the bidder notification, and no
|
||||
/// `houseId`; the two never share a struct or a reader.
|
||||
pub const SMSG_AUCTION_OWNER_NOTIFICATION: u16 = 0x025F; // 607
|
||||
pub const CMSG_AUCTION_LIST_BIDDER_ITEMS: u16 = 0x0264; // 612
|
||||
pub const SMSG_AUCTION_BIDDER_LIST_RESULT: u16 = 0x0265; // 613
|
||||
/// Pushed to a bidder whose auction the seller cancelled.
|
||||
pub const SMSG_AUCTION_REMOVED_NOTIFICATION: u16 = 0x028D; // 653
|
||||
|
||||
// The world-state table (VERIFIED both ways: vmangos `Opcodes_1_12_1.h` 706-707, and wow-re's read
|
||||
// of the reference's own handler `0x48f690`, whose registration at `0x48f515-0x48f52f` selects these
|
||||
// two arms as the ONLY writers of the table setter `0x4c5870`). Bodies in
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
use crate::wire::Vector3d;
|
||||
|
||||
use super::{
|
||||
ActionButton, AttackerState, CastOutcome, ChannelNotify, Character, ChatMessage,
|
||||
CorpseLocation, DamageShield, EnvironmentalDamageLog, ExplorationXp, FriendEntry,
|
||||
FriendStatusUpdate, GameObjectQueryInfo, GossipOption, GossipPoi, GroupLootInfo,
|
||||
GroupMemberEntry, GuildCommandResult, GuildEventNotice, GuildInfo, GuildQueryResponse,
|
||||
GuildRoster, InitWorldStates, InspectHonorStats, ItemInfo, ItemPushResult, JumpInfo,
|
||||
LevelUpInfo, LootAllPassed, LootItem, LootRoll, LootRollWon, LootStartRoll, MailListEntry,
|
||||
MirrorTimerStart, MoveMode, Object, PartyMemberStatsInfo, PeriodicAuraLog, PetMode, PetSpells,
|
||||
PvpCredit, QuestComplete, QuestDetails, QuestGiverList, QuestOfferReward, QuestOption,
|
||||
QuestRequestItems, QuestTemplate, ResurrectRequestBody, SpeedKind, SpellChainTargets,
|
||||
SpellCooldown, SpellDamageLog, SpellEnergizeLog, SpellGo, SpellHealLog, SpellLogMiss,
|
||||
SpellStart, TaxiMask, TradeStatus, TradeStatusExtended, TrainerSpell, TransportPose,
|
||||
VendorItem, WhoResults, XpGain,
|
||||
ActionButton, AttackerState, AuctionBidderNotification, AuctionCommandTail, AuctionListEntry,
|
||||
AuctionOwnerNotification, CastOutcome, ChannelNotify, Character, ChatMessage, CorpseLocation,
|
||||
DamageShield, EnvironmentalDamageLog, ExplorationXp, FriendEntry, FriendStatusUpdate,
|
||||
GameObjectQueryInfo, GossipOption, GossipPoi, GroupLootInfo, GroupMemberEntry,
|
||||
GuildCommandResult, GuildEventNotice, GuildInfo, GuildQueryResponse, GuildRoster,
|
||||
InitWorldStates, InspectHonorStats, ItemInfo, ItemPushResult, JumpInfo, LevelUpInfo,
|
||||
LootAllPassed, LootItem, LootRoll, LootRollWon, LootStartRoll, MailListEntry, MirrorTimerStart,
|
||||
MoveMode, Object, PartyMemberStatsInfo, PeriodicAuraLog, PetMode, PetSpells, PvpCredit,
|
||||
QuestComplete, QuestDetails, QuestGiverList, QuestOfferReward, QuestOption, QuestRequestItems,
|
||||
QuestTemplate, ResurrectRequestBody, SpeedKind, SpellChainTargets, SpellCooldown,
|
||||
SpellDamageLog, SpellEnergizeLog, SpellGo, SpellHealLog, SpellLogMiss, SpellStart, TaxiMask,
|
||||
TradeStatus, TradeStatusExtended, TrainerSpell, TransportPose, VendorItem, WhoResults, XpGain,
|
||||
};
|
||||
|
||||
/// The **final facing** a `SMSG_MONSTER_MOVE` dictates (its `moveType`): the unit snaps to face this
|
||||
@@ -1155,6 +1155,56 @@ pub enum ServerPacket {
|
||||
NextMailTime {
|
||||
seconds: f32,
|
||||
},
|
||||
/// `MSG_AUCTION_HELLO`'s **reply** (same opcode as our one-guid request, decision 1511 P0):
|
||||
/// the auctioneer we greeted plus the `AuctionHouse.dbc` row (1..7) that names its deposit and
|
||||
/// cut rates. It is this packet, not our send, that opens the window.
|
||||
AuctionHello {
|
||||
auctioneer: u64,
|
||||
house_id: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT` — the verdict on a sell/cancel/bid, keyed by `action`
|
||||
/// ([`super::auction::auction_action`]) and `error` ([`super::auction::auction_error`]);
|
||||
/// `tail` is the conditional payload the error selects (layout in
|
||||
/// [`super::auction::read_auction_command_result`]). `auction_id` is `0` on most failures.
|
||||
AuctionCommandResult {
|
||||
auction_id: u32,
|
||||
action: u32,
|
||||
error: u32,
|
||||
tail: AuctionCommandTail,
|
||||
},
|
||||
/// `SMSG_AUCTION_LIST_RESULT` — a Browse page, answering `CMSG_AUCTION_LIST_ITEMS`.
|
||||
/// `total_count` is the match count *before* the 50-row cap (the pager's "of N"), and rides
|
||||
/// at the END of the body (layout in [`super::auction::read_auction_list_result`]).
|
||||
AuctionListResult {
|
||||
auctions: Vec<AuctionListEntry>,
|
||||
total_count: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_OWNER_LIST_RESULT` — the Auctions tab page (your own listings), answering
|
||||
/// `CMSG_AUCTION_LIST_OWNER_ITEMS`. Same frame and record as [`Self::AuctionListResult`].
|
||||
AuctionOwnerListResult {
|
||||
auctions: Vec<AuctionListEntry>,
|
||||
total_count: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_BIDDER_LIST_RESULT` — the Bid tab page, answering
|
||||
/// `CMSG_AUCTION_LIST_BIDDER_ITEMS`. Same frame and record as [`Self::AuctionListResult`];
|
||||
/// the server emits the explicitly-refreshed ids first and then our own live bids, so a row
|
||||
/// can legitimately appear twice in one page.
|
||||
AuctionBidderListResult {
|
||||
auctions: Vec<AuctionListEntry>,
|
||||
total_count: u32,
|
||||
},
|
||||
/// `SMSG_AUCTION_BIDDER_NOTIFICATION` — we won, or we were outbid (`bid_or_zero == 0` means
|
||||
/// **won**). A different shape from [`Self::AuctionOwnerNotification`], deliberately.
|
||||
AuctionBidderNotification(AuctionBidderNotification),
|
||||
/// `SMSG_AUCTION_OWNER_NOTIFICATION` — our own auction sold or took a bid. No house id, and
|
||||
/// the bidder guid sits fourth rather than third; an all-zero guid is the "sold" signal.
|
||||
AuctionOwnerNotification(AuctionOwnerNotification),
|
||||
/// `SMSG_AUCTION_REMOVED_NOTIFICATION` — an auction we had bid on was cancelled by its seller.
|
||||
AuctionRemovedNotification {
|
||||
auction_id: u32,
|
||||
item_entry: u32,
|
||||
random_property_id: i32,
|
||||
},
|
||||
/// `SMSG_TRADE_STATUS` — one pulse of the trade state machine (open/accept/cancel/complete/
|
||||
/// the refusal reasons); the tail-carrying statuses hold their payload in [`TradeStatus`]
|
||||
/// (layout in [`super::trade::read_trade_status`], decision 0592 P0).
|
||||
@@ -1393,6 +1443,18 @@ impl ServerPacket {
|
||||
ServerPacket::ItemTextQueryResponse { .. } => "SMSG_ITEM_TEXT_QUERY_RESPONSE".into(),
|
||||
ServerPacket::ReceivedMail { .. } => "SMSG_RECEIVED_MAIL".into(),
|
||||
ServerPacket::NextMailTime { .. } => "MSG_QUERY_NEXT_MAIL_TIME".into(),
|
||||
ServerPacket::AuctionHello { .. } => "MSG_AUCTION_HELLO".into(),
|
||||
ServerPacket::AuctionCommandResult { .. } => "SMSG_AUCTION_COMMAND_RESULT".into(),
|
||||
ServerPacket::AuctionListResult { .. } => "SMSG_AUCTION_LIST_RESULT".into(),
|
||||
ServerPacket::AuctionOwnerListResult { .. } => "SMSG_AUCTION_OWNER_LIST_RESULT".into(),
|
||||
ServerPacket::AuctionBidderListResult { .. } => {
|
||||
"SMSG_AUCTION_BIDDER_LIST_RESULT".into()
|
||||
}
|
||||
ServerPacket::AuctionBidderNotification(_) => "SMSG_AUCTION_BIDDER_NOTIFICATION".into(),
|
||||
ServerPacket::AuctionOwnerNotification(_) => "SMSG_AUCTION_OWNER_NOTIFICATION".into(),
|
||||
ServerPacket::AuctionRemovedNotification { .. } => {
|
||||
"SMSG_AUCTION_REMOVED_NOTIFICATION".into()
|
||||
}
|
||||
ServerPacket::TradeStatus { .. } => "SMSG_TRADE_STATUS".into(),
|
||||
ServerPacket::TradeStatusExtended { .. } => "SMSG_TRADE_STATUS_EXTENDED".into(),
|
||||
ServerPacket::InitWorldStates(_) => "SMSG_INIT_WORLD_STATES".into(),
|
||||
|
||||
@@ -11,10 +11,10 @@ use crate::wire::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
action_bar, area_trigger, attack, bank, binder, channel, chat, combat_log, death, duel,
|
||||
gameobject, gossip, group, guild, items, loot, mail, mirror_timer, monster_move, movement,
|
||||
opcode, page_text, pet, progression, pvp, quest, social, spellbook, spells, taxi, trade,
|
||||
trainer, update_object, vendor, world_state, Character, CreatureQueryInfo, MoveMode,
|
||||
action_bar, area_trigger, attack, auction, bank, binder, channel, chat, combat_log, death,
|
||||
duel, gameobject, gossip, group, guild, items, loot, mail, mirror_timer, monster_move,
|
||||
movement, opcode, page_text, pet, progression, pvp, quest, social, spellbook, spells, taxi,
|
||||
trade, trainer, update_object, vendor, world_state, Character, CreatureQueryInfo, MoveMode,
|
||||
ServerPacket, SpeedKind,
|
||||
};
|
||||
|
||||
@@ -1103,6 +1103,65 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
opcode::MSG_QUERY_NEXT_MAIL_TIME => ServerPacket::NextMailTime {
|
||||
seconds: mail::read_query_next_mail_time(&mut r)?,
|
||||
},
|
||||
// The auction house arc (decision 1511 P0). MSG_AUCTION_HELLO is two-way — this is the
|
||||
// REPLY (our request is the same opcode with just the guid), and it is what opens the
|
||||
// window. The three list results share one frame and one 64-byte record; their reader is
|
||||
// bounded by the buffer as well as by the wire `count`, because vmangos's browse fast path
|
||||
// can count a record it then fails to write.
|
||||
opcode::MSG_AUCTION_HELLO => {
|
||||
let (auctioneer, house_id) = auction::read_auction_hello(&mut r)?;
|
||||
ServerPacket::AuctionHello {
|
||||
auctioneer,
|
||||
house_id,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_AUCTION_COMMAND_RESULT => {
|
||||
let (auction_id, action, error, tail) = auction::read_auction_command_result(&mut r)?;
|
||||
ServerPacket::AuctionCommandResult {
|
||||
auction_id,
|
||||
action,
|
||||
error,
|
||||
tail,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_AUCTION_LIST_RESULT => {
|
||||
let (auctions, total_count) = auction::read_auction_list_result(&mut r)?;
|
||||
ServerPacket::AuctionListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_AUCTION_OWNER_LIST_RESULT => {
|
||||
let (auctions, total_count) = auction::read_auction_list_result(&mut r)?;
|
||||
ServerPacket::AuctionOwnerListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_AUCTION_BIDDER_LIST_RESULT => {
|
||||
let (auctions, total_count) = auction::read_auction_list_result(&mut r)?;
|
||||
ServerPacket::AuctionBidderListResult {
|
||||
auctions,
|
||||
total_count,
|
||||
}
|
||||
}
|
||||
// The two notifications carry different field orders and the owner one has no house id —
|
||||
// two readers, never one.
|
||||
opcode::SMSG_AUCTION_BIDDER_NOTIFICATION => ServerPacket::AuctionBidderNotification(
|
||||
auction::read_auction_bidder_notification(&mut r)?,
|
||||
),
|
||||
opcode::SMSG_AUCTION_OWNER_NOTIFICATION => ServerPacket::AuctionOwnerNotification(
|
||||
auction::read_auction_owner_notification(&mut r)?,
|
||||
),
|
||||
opcode::SMSG_AUCTION_REMOVED_NOTIFICATION => {
|
||||
let (auction_id, item_entry, random_property_id) =
|
||||
auction::read_auction_removed_notification(&mut r)?;
|
||||
ServerPacket::AuctionRemovedNotification {
|
||||
auction_id,
|
||||
item_entry,
|
||||
random_property_id,
|
||||
}
|
||||
}
|
||||
// The player-trade arc (decision 0592 P0): the state-machine status pulse and the
|
||||
// item/gold snapshot for one window side.
|
||||
opcode::SMSG_TRADE_STATUS => ServerPacket::TradeStatus {
|
||||
|
||||
@@ -63,9 +63,10 @@ pub struct CharEnumItem {
|
||||
}
|
||||
|
||||
/// A character from `SMSG_CHAR_ENUM` — the full roster entry the character-select screen renders
|
||||
/// (decision 0465): identity, the five appearance bytes, zone/map/position, the display flags, and
|
||||
/// the visible equipment display ids. Only the guild id, first-login byte, pet triple, and first-bag
|
||||
/// pair are parsed for alignment and discarded (nothing at select renders them).
|
||||
/// (decision 0465): identity, the five appearance bytes, zone/map/position, the display flags, the
|
||||
/// visible equipment display ids, and the **pet triple** (the hunter/warlock companion standing
|
||||
/// beside the selected character). Only the guild id, first-login byte and first-bag pair are
|
||||
/// parsed for alignment and discarded (nothing at select renders them).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Character {
|
||||
pub guid: u64,
|
||||
@@ -92,6 +93,16 @@ pub struct Character {
|
||||
/// The 19 visible equipment slots (vanilla order: head, neck, shoulders, shirt, chest, waist,
|
||||
/// legs, feet, wrists, hands, finger×2, trinket×2, back, main hand, off hand, ranged, tabard).
|
||||
pub equipment: [CharEnumItem; 19],
|
||||
/// The pet's `CreatureDisplayInfo.dbc` id — the companion the select screen stands beside the
|
||||
/// character. **0 = no pet**, and that one test is the whole client-side gate: the server
|
||||
/// already suppresses the triple for anything but a living hunter/warlock (vmangos
|
||||
/// `Player::BuildEnumData` — `!GHOST && (CLASS_WARLOCK || CLASS_HUNTER)`), so no class or
|
||||
/// ghost check belongs here.
|
||||
pub pet_display_id: u32,
|
||||
/// The pet's level, and its `CreatureFamily.dbc` id. Carried because they are the record's own
|
||||
/// fields; **nothing renders them today** — the select screen's pet is display-id-driven.
|
||||
pub pet_level: u32,
|
||||
pub pet_family: u32,
|
||||
}
|
||||
|
||||
impl Character {
|
||||
@@ -113,9 +124,9 @@ impl Character {
|
||||
let _guild_id = read_u32_le(r)?;
|
||||
let flags = read_u32_le(r)?;
|
||||
let _first_login = read_u8(r)?;
|
||||
let _pet_display_id = read_u32_le(r)?;
|
||||
let _pet_level = read_u32_le(r)?;
|
||||
let _pet_family = read_u32_le(r)?;
|
||||
let pet_display_id = read_u32_le(r)?;
|
||||
let pet_level = read_u32_le(r)?;
|
||||
let pet_family = read_u32_le(r)?;
|
||||
let mut equipment = [CharEnumItem::default(); 19];
|
||||
for slot in &mut equipment {
|
||||
slot.display_id = read_u32_le(r)?;
|
||||
@@ -140,6 +151,9 @@ impl Character {
|
||||
position,
|
||||
flags,
|
||||
equipment,
|
||||
pet_display_id,
|
||||
pet_level,
|
||||
pet_family,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,19 @@ impl ObjectFields {
|
||||
.flatten()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
/// `ITEM_FIELD_RANDOM_PROPERTIES_ID` (field 44 — vmangos `UpdateFields_1_12_1.h`
|
||||
/// `OBJECT_END + 0x26`, chain-locked here by the tested `ITEM_TEXT_ID` 45 / `DURABILITY` 46
|
||||
/// anchors on either side): the **random-suffix roll** this item instance carries, an
|
||||
/// `ItemRandomProperties.dbc` id. `0` = unrolled.
|
||||
///
|
||||
/// Its one consumer is the NAME: the client joins the row's suffix through
|
||||
/// `ITEM_SUFFIX_TEMPLATE` in its single name formatter `0x5d8b00`, so the item reads "Chipped
|
||||
/// Claw of the Bear" everywhere it is named. The roll's **enchants** never come from here on
|
||||
/// an owned item — the server writes them into `ITEM_FIELD_ENCHANTMENT` slots 2..6, which
|
||||
/// [`Self::item_enchant`] already reads.
|
||||
pub fn item_random_properties_id(&self) -> u32 {
|
||||
self.get_u32(44).unwrap_or(0)
|
||||
}
|
||||
/// `ITEM_FIELD_DURABILITY` — an item's current durability (field 46: chain-locked between the
|
||||
/// tested STACK_COUNT 14 and CONTAINER_NUM_SLOTS 48 = ITEM_END anchors).
|
||||
pub fn item_durability(&self) -> Option<u32> {
|
||||
@@ -132,6 +145,22 @@ impl ObjectFields {
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
/// `PLAYER_VISIBLE_ITEM_<slot>_PROPERTIES` — the **broadcast random-suffix roll** of the item
|
||||
/// worn in equipment slot `i`. The 12-dword per-slot record is `creator(2) · entry ·
|
||||
/// enchant[7] · PROPERTIES · PAD`, and PROPERTIES is a `TWO_SHORT` whose **low half** is the
|
||||
/// `ItemRandomProperties` id — the reference reads it with a `movzx WORD` (`0x53339f`, the
|
||||
/// inspect leg's `+0x424` source; wow-re `tooltip-content-law.md` §E7).
|
||||
///
|
||||
/// This is the only roll feed a client has for anybody else's gear, and its one consumer is
|
||||
/// the NAME: vmangos fills only the PERM/TEMP enchant slots, so an inspected item's suffix
|
||||
/// *lines* stay empty on both sides of the wire — the same thing the reference shows.
|
||||
pub fn player_visible_item_properties(&self, i: u8) -> u32 {
|
||||
(i < 19)
|
||||
.then(|| self.get_u32(FIELD_PLAYER_VISIBLE_ITEM_1_CREATOR + 10 + 12 * u16::from(i)))
|
||||
.flatten()
|
||||
.unwrap_or(0)
|
||||
& 0xffff
|
||||
}
|
||||
/// `PLAYER_FIELD_INV_SLOT_HEAD + 2i` — our equipment (0–18) and equipped-bag (19–22) guids.
|
||||
pub fn player_inv_slot(&self, i: u8) -> Option<u64> {
|
||||
(i < 23).then(|| self.get_guid(FIELD_PLAYER_INV_SLOT_HEAD + 2 * u16::from(i)))?
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//! The auction house's `WorldWriter` sends (decision 1511 P0): the auctioneer greeting, the three
|
||||
//! list pages (browse / your bids / your auctions), and the three market verbs — list an item,
|
||||
//! bid or buy out, cancel. Bodies in [`crate::messages`]'s `auction` builders (layout VERIFIED
|
||||
//! against vmangos `Handlers/AuctionHouseHandler.cpp` + `Server/Packets/AuctionHouse.cpp`). Split
|
||||
//! out per `writer/mod.rs`'s rule — the module a body builder lives in names the module its send
|
||||
//! lives in.
|
||||
//!
|
||||
//! Two things every caller here has to know. **The auctioneer guid rides on every send**, not
|
||||
//! just the hello: the server re-validates the 5 yd interact distance on each one independently
|
||||
//! (`GetCheckedAuctionHouseForAuctioneer`), so there is no session to hold and a walk-away
|
||||
//! invalidates the next verb rather than the window. And **several refusals are silent** — a sell
|
||||
//! with a zero bid or duration, a bid the player cannot afford, a cancel whose 5% cut they cannot
|
||||
//! pay, and any list request while one is already in flight all get *no packet at all*. A UI that
|
||||
//! blocks waiting for an ack hangs.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::messages::{self, opcode};
|
||||
|
||||
use super::WorldWriter;
|
||||
|
||||
impl WorldWriter {
|
||||
/// Greet an auctioneer (`MSG_AUCTION_HELLO`, layout in [`messages::auction_hello`]) — one
|
||||
/// auctioneer guid. **Two-way opcode**: the reply carries the guid back plus the
|
||||
/// `AuctionHouse.dbc` house id (an `AuctionHello` event), and it is that reply which opens the
|
||||
/// window.
|
||||
pub fn auction_hello(&mut self, auctioneer: u64) -> Result<()> {
|
||||
self.send(
|
||||
opcode::MSG_AUCTION_HELLO,
|
||||
&messages::auction_hello(auctioneer),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask a Browse page (`CMSG_AUCTION_LIST_ITEMS`, layout in
|
||||
/// [`messages::auction_list_items`]) — the search's filters as the server reads them, with
|
||||
/// [`messages::auction_filter`]'s sentinels for the ones left unset. **No sort rides the
|
||||
/// wire**: sorting the returned page is entirely ours. `list_from` pages by
|
||||
/// [`messages::AUCTION_PAGE_SIZE`]. Answered by `SMSG_AUCTION_LIST_RESULT`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn auction_list_items(
|
||||
&mut self,
|
||||
auctioneer: u64,
|
||||
list_from: u32,
|
||||
searched_name: &str,
|
||||
level_min: u8,
|
||||
level_max: u8,
|
||||
slot_id: u32,
|
||||
main_category: u32,
|
||||
sub_category: u32,
|
||||
quality: u32,
|
||||
usable: u8,
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUCTION_LIST_ITEMS,
|
||||
&messages::auction_list_items(
|
||||
auctioneer,
|
||||
list_from,
|
||||
searched_name,
|
||||
level_min,
|
||||
level_max,
|
||||
slot_id,
|
||||
main_category,
|
||||
sub_category,
|
||||
quality,
|
||||
usable,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask the Auctions tab page (`CMSG_AUCTION_LIST_OWNER_ITEMS`, layout in
|
||||
/// [`messages::auction_list_owner_items`]) — our own listings. Answered by
|
||||
/// `SMSG_AUCTION_OWNER_LIST_RESULT`.
|
||||
pub fn auction_list_owner_items(&mut self, auctioneer: u64, list_from: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUCTION_LIST_OWNER_ITEMS,
|
||||
&messages::auction_list_owner_items(auctioneer, list_from),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ask the Bid tab page (`CMSG_AUCTION_LIST_BIDDER_ITEMS`, layout in
|
||||
/// [`messages::auction_list_bidder_items`]). `auction_ids` is a **refresh set**, not a filter:
|
||||
/// the server prepends a record for each id named and then appends every auction we are the
|
||||
/// current bidder on. Pass `&[]` for a plain page. Answered by
|
||||
/// `SMSG_AUCTION_BIDDER_LIST_RESULT`.
|
||||
pub fn auction_list_bidder_items(
|
||||
&mut self,
|
||||
auctioneer: u64,
|
||||
list_from: u32,
|
||||
auction_ids: &[u32],
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUCTION_LIST_BIDDER_ITEMS,
|
||||
&messages::auction_list_bidder_items(auctioneer, list_from, auction_ids),
|
||||
)
|
||||
}
|
||||
|
||||
/// List an item for auction (`CMSG_AUCTION_SELL_ITEM`, layout in
|
||||
/// [`messages::auction_sell_item`]) — the Create Auction pane's Create button.
|
||||
/// `etime_minutes` must be one of [`messages::auction_duration`]'s three values (120/480/1440);
|
||||
/// the deposit is charged immediately out of the player's purse. Answered by
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT` (action [`messages::auction_action::STARTED`]) — or by
|
||||
/// silence, if the bid or duration is zero or the deposit is unaffordable.
|
||||
pub fn auction_sell_item(
|
||||
&mut self,
|
||||
auctioneer: u64,
|
||||
item_guid: u64,
|
||||
bid: u32,
|
||||
buyout: u32,
|
||||
etime_minutes: u32,
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUCTION_SELL_ITEM,
|
||||
&messages::auction_sell_item(auctioneer, item_guid, bid, buyout, etime_minutes),
|
||||
)
|
||||
}
|
||||
|
||||
/// Bid on — or buy out — an auction (`CMSG_AUCTION_PLACE_BID`, layout in
|
||||
/// [`messages::auction_place_bid`]). One verb for both: the server reads a `price` at or above
|
||||
/// a nonzero buyout *as* a buyout; there is no separate opcode and no flag. Answered by
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT` (action [`messages::auction_action::BID_PLACED`]), or by
|
||||
/// silence if the player cannot afford the price.
|
||||
pub fn auction_place_bid(
|
||||
&mut self,
|
||||
auctioneer: u64,
|
||||
auction_id: u32,
|
||||
price: u32,
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUCTION_PLACE_BID,
|
||||
&messages::auction_place_bid(auctioneer, auction_id, price),
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel one of our own auctions (`CMSG_AUCTION_REMOVE_ITEM`, layout in
|
||||
/// [`messages::auction_remove_item`]) — the Auctions tab's Cancel button. The deposit is
|
||||
/// **forfeit**, and if the auction already carries a bid the 5% cut comes out of the seller's
|
||||
/// pocket (and the cancel is refused *silently* when they cannot pay it). Answered by
|
||||
/// `SMSG_AUCTION_COMMAND_RESULT` (action [`messages::auction_action::REMOVED`]).
|
||||
pub fn auction_remove_item(&mut self, auctioneer: u64, auction_id: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUCTION_REMOVE_ITEM,
|
||||
&messages::auction_remove_item(auctioneer, auction_id),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
//! | [`trade`] | the player-trade dance |
|
||||
//! | [`duel`] | accept and cancel |
|
||||
//! | [`mail`] | the mailbox |
|
||||
//! | [`auction`] | the auction house: the hello, the three list pages, sell/bid/cancel |
|
||||
//! | [`player_flags`] | the empty-bodied `PLAYER_FLAGS` toggles: PvP, show-helm, show-cloak |
|
||||
//! | [`social`] | friends, ignores, and `/who` |
|
||||
//! | [`area_trigger`] | the one "I walked into trigger N" report |
|
||||
@@ -55,6 +56,7 @@ use super::send_packet;
|
||||
mod action_bar;
|
||||
mod area_trigger;
|
||||
mod attack;
|
||||
mod auction;
|
||||
mod bank;
|
||||
mod binder;
|
||||
mod channel;
|
||||
|
||||
Reference in New Issue
Block a user