Implements the full client<->server surface for "Recent Allies" (the players you
recently grouped with, so you can note who they were), turning three Handle_NULL
opcodes into a working, persistent system.
Wire recovered byte-exact by decompiling the client readers/serializers:
- CMSG_SET_ALLOW_RECENT_ALLIES_SEE_LOCATION (sub_7FF72914CCD0) = 1 packed bit.
- CMSG_RECENT_ALLY_REQUEST_DATA (sub_7FF7290805B0) = empty.
- CMSG_RECENT_ALLY_SET_NOTE (sub_7FF7290806C0) = { PackedGuid, bit-length note, bit }.
- SMSG_RECENT_ALLY_DATA_RESPONSE (reader sub_7FF7290BBE20 -> entry sub_7FF729162B00)
= { u32 count, entry[]{ PackedGuid, u64 wowAccount, u32, activity-vector{u8,u64,u32,u32},
11-bit note + 3 flag bits, note bytes } }.
- SMSG_RECENT_ALLY_NOTE_UPDATED (reader sub_7FF7290BBF10) = { u32, PackedGuid, u64, note }.
The note length is an 11-bit bit-length field (2048 = 1<<11 sentinel in the readers);
the 3 entry flag bits and the 24-byte "shared activity" sub-records have no
offline-resolvable semantics, so they are sent 0/empty (structure honest, values zeroed).
System:
- RecentAlliesMgr: query-on-demand, DB-backed (no session cache) - RecordGrouping (mutual,
both directions), GetAllies (most-recent-first, capped 100), SetNote, and the persisted
Set/GetAllowSeeLocation toggle.
- Tracking hook in Group::AddMember: on joining a (non-BG) party/raid, records the new
member and every other online member as mutual recent allies.
- Two tables (character_recent_allies + character_recent_ally_settings) with base schema +
update SQL, and five CharacterDatabase prepared statements.
- RecentAllyHandler: the three CMSG handlers; SET_NOTE echoes SMSG_RECENT_ALLY_NOTE_UPDATED.
On CMSG_ACCEPT_WARGAME_INVITE acceptance, actually start the war game instead of
just echoing the response: create the chosen battleground/arena, queue both premade
groups onto forced opposing teams (challenger ALLIANCE, opponent HORDE), and invite
them in. Players then port via the existing NeedConfirmation -> CMSG_BATTLEFIELD_PORT
-> SendToBattleground handshake, exactly like a normal queue pop.
Built against the rated-arena start path (BattlegroundQueue::Update: CreateNewBattleground
-> AddGroup -> InviteGroupToBG -> StartBattleground), but with no matchmaking since both
groups are known up front. Validates the recorded challenge's BattlemasterListID against
sBattlemasterListStore, resolves the level bracket from the challenger, and derives the
arena team size from the challenging group (0 for battlegrounds).
- BattlegroundQueue: new public AddWargameSide(leader, group, bg, bracketEntry, team) that
queues a whole premade side onto a forced team, registers each member's queue slot, and
fires the enter-confirmation (wraps the private InviteGroupToBG; keeps its visibility).
- BattleGroundHandler: HandleAcceptWargameInvite builds the instance and queues both sides.
War Games (arranged PvP practice matches between two premade groups) were entirely unhandled: all four
CMSG were Handle_NULL and TC never constructed the SMSG, despite the queue infrastructure
(BattlegroundQueueIdType::Wargame, Battleground::Wargame type, ERR_WARGAME_* results) already existing.
This builds the full challenge handshake:
- CMSG_START_WAR_GAME: a party leader challenges another group. Validates leadership, resolves the opposing
group + its leader, records the pending request, and sends SMSG_CHECK_WARGAME_ENTRY to the opposing leader
plus SMSG_WARGAME_REQUEST_SUCCESSFULLY_SENT_TO_OPPONENT back to the challenger.
- CMSG_ACCEPT_WARGAME_INVITE: the opposing leader answers. Correlates against the recorded challenge and
replies to the challenger with SMSG_WARGAME_REQUEST_OPPONENT_RESPONSE (accepted/declined).
Wire recovered byte-exact from the 12.0.7 client:
- CMSG_START_WAR_GAME (send serializer sub_7FF72906F9F0): PackedGUID OpposingPartyMember, uint32, uint16,
uint64 QueueID, trailing bit TournamentRules. (The flat opcode-layout tool under-reported this as guid+u32+bit;
decompiling the serializer recovered the additional uint16 + uint64.)
- CMSG_ACCEPT_WARGAME_INVITE (sub_7FF72906FC80): PackedGUID OpposingPartyMember, uint64 QueueID, trailing bit.
- SMSG_CHECK_WARGAME_ENTRY (client deserializer sub_7FF729086A80): PackedGUID, uint64 QueueID, uint64 Time,
bit7-of-a-byte TournamentRules (written as an explicit MSB-first byte, matching the client's >>7 read).
Field48/52 in the challenge (chosen battleground selection) are named by inference from wire position and are
read to consume the packet; only OpposingPartyMember/QueueID/TournamentRules drive P0 logic. The challenge
registry is in-memory and ephemeral (no persistence), keyed by the opposing leader; handlers run on the world
thread so no locking is needed. P1 (queue both groups into a wargame instance on acceptance) is left documented.
The premade-groups sniff (dump_12.0.7.68275_premandegroups) + the client JOIN serializer
(sub_7FF72914ABE0) + the generated Lua API doc (LfgListingCreateData/LfgEntryData) resolve the
descriptor that was flagged NEEDS-SNIFF. It is BIT-PACKED, not byte-aligned: the previous flat
47-byte read over-ran the real 36-byte packet and would have thrown on live JOIN packets.
- Rewrote ListingDescriptor to the real wire: bit-packed header (5-bit trailing-vector count;
three bit-packed string lengths of 10/11/8 bits; four boolean flags isAutoAccept /
isCrossFactionListing / isPrivateGroup / newPlayerFriendly; presence bits for the nilable numeric
fields) -> FlushBits -> member-requirement block (nested sub_7FF729167840) -> fixed activity
fields (ActivityID u32, RequiredDungeonScore float, trailing u8) -> trailing uint32 vector ->
three strings -> present optional fields. Reads are guarded against over-run (pass-through
descriptor, never fatal). Field names are authoritative (Lua); bit-widths from the client bit
writers (sub_064C20=5b, sub_0649E0=2b, sub_064AA0=3b).
- Added the LFGEntryPlaystyle / LFGEntryGeneralPlaystyle enums (from the generated API docs).
- Search now derives category + activity group from the listing's activity via
GroupFinderActivity.db2 (GroupFinderCategoryID / GroupFinderActivityGrpID) instead of descriptor
fields the wire does not carry - the ActivityID is the authoritative source.
- "crate" listing from the sniff confirms ActivityID 0x7B4 = GroupFinderActivity 1972.
Result-enum values (JOIN_RESULT/APPLY_RESULT) are still open: they are not exposed to the Lua layer
(CreateListing returns bool) and no result packet was in this capture; default-0/success holds.
Implements the crafting-order browse, the system's long-standing blocker. The full
JamCraftingOrder wire was recovered from the client reader tree (sub_7FF7291611C0 ->
scalar head sub_7FF729160490 + sub-readers) and the LIST response body
(sub_7FF7290B9350); field offsets/types are confirmed byte-exact against
jam_reflection_FINAL_68275.json.
- SMSG_CRAFTING_ORDER_LIST_ORDERS_RESPONSE (0x420333): header + order vector.
CraftingOrderData serializes the scalar head byte-exact (version, orderID,
skillLineAbilityID, orderState, orderType, minQuality, endDate, claimEndDate,
tipAmount, houseCutAmount, flags, customer/crafter PackedGUID, npcCraftingOrderSetID,
npcTreasureID, split-encoded notes length + notes) then the wrapper header byte.
Customer-provided reagents and the four presence-gated optionals
(customerPlayer/customerNpc/outputOrderItem/outputItem) are sent absent — byte-exact
for a basic public order. Header fields with offline-unconfirmable semantics are
sent 0 and documented.
- CMSG_CRAFTING_ORDER_LIST_MY_ORDERS (0x3B0118): lists the requester's own orders.
- CMSG_CRAFTING_ORDER_LIST_CRAFTER_ORDERS (0x3B0119): reads the leading
PackedGUID station + skillLineAbilityID, lists claimable orders for that recipe.
Completes the core loop: create -> browse -> claim -> release/reject/cancel.
Closes the client round-trip for the order actions built in P2/P3. The
response SMSGs were previously assumed reflection-walled; reading the
client's real deserializers (sub_7FF7290B92D0 / _9640 / _9A90 / _96E0 /
_9B90, reached from the 0x42-family opcode dispatch) shows they are
hand-written and share one byte-aligned layout:
uint8 Result // ClientCrafting::CraftingOrderResult
uint64 CraftingOrderID
The full 49-value CraftingOrderResult enum was extracted from the client
enum registrar and is now declared so the server can report accurate
outcomes (Ok on success; CannotCreate/CannotClaim/CannotCancel/
CannotRelease/CannotReject when the manager rejects the action).
Adds CraftingOrderActionResult (+ the five opcode-specific subclasses)
and sends the matching result from each handler, so the client's order
UI now confirms and updates instead of silently guessing. FULFILL_RESULT
(which additionally carries a second id, a byte, a PackedGUID and three
uint32s) and CRAFT_RESULT (a nested block) are larger and follow with
the crafting/escrow phase; their layouts are recovered and recorded.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: luis <[email protected]>
Wire the two crafter-side order actions that complete with a real,
correct state change and no dependency on the (reflection-gated) SMSG
responses. Layouts recovered via Ghidra from the client serializers:
- CraftingOrderRelease (0x3B011C, sub_729154E00): u64 orderID + u8 +
optional ClientContext. Routes to CraftingOrderMgr::ReleaseOrder
(order returns to the pool).
- CraftingOrderReject (0x3B011F, sub_7291552B0): u64 orderID + u8 +
reason string (length in the bit block, bytes trailing the context) +
optional ClientContext. New CraftingOrderMgr::RejectOrder transitions
the order to Rejected (crafter-claimed or personal-for-crafter only).
- Register both opcodes STATUS_LOGGEDIN (were Handle_NULL).
All 8 remaining CMSG serializers were decompiled (see plan); FULFILL
(needs craft-system item delivery), LIST_MY/LIST_CRAFTER + GET_NPC_REWARD_INFO
(paired with the reflection-gated SMSG responses), REPORT_PLAYER and
UPDATE_IGNORE_LIST are deferred with documented reasons rather than stubbed.
m+ run12.0.7.pkt (SMSG_CHALLENGE_MODE_START, 45B body) shows the wire is {MapID, MapChallengeModeID,
KeystoneLevel, ...}, but the code mislabeled the first three slots and sent the MapChallengeMode id in
the MapID slot and the keystone level in the challenge-id slot. Added the MapID field (populated from
MapChallengeMode.db2), reordered the writer, and confirmed against the sniff (MapID 2526, challenge
402, level 2). SMSG_MYTHIC_PLUS_CURRENT_AFFIXES (count + N x {AffixID,u32}) is confirmed unchanged by
the same capture. Full decoded wire for START/COMPLETE/RESET/AFFIXES/NEW_WEEK_RECORD/NEW_PLAYER_RECORD:
Completes the conduit collection into a Blizzlike acquisition flow: obtaining
a conduit item now adds it to the collection, so the GM ".conduit add" command
is demoted to an admin/test convenience.
- Add the SoulbindConduitItem.db2 (ItemID -> ConduitID) and
SoulbindConduitRankProperties.db2 (Rank <-> ItemLevel) stores (both Metas
already present in TC, hashes 0xEDC6BB40 / 0x6EFBF7C9).
- DB2Manager: build an ItemID -> ConduitID map (GetConduitForItem) and
GetConduitRankForItemLevel(), which picks the highest rank whose ItemLevel
does not exceed the acquired item's level (a higher-ilvl duplicate upgrades
the collection, as retail does).
- Player::TryCollectConduitFromItem() runs from StoreNewItem alongside the
existing transmog CollectionMgr::OnItemAdded hook (same addToCollection gate),
collecting the mapped conduit at the item-level-derived rank (falls back to
the conduit's lowest defined rank if no properties row qualifies).
Non-destructive: the conduit item is not consumed (retail may consume it, but
that is not verified offline, so items are never destroyed on assumption).
Unblocks covenant P2 by building the conduit sub-system the soulbind
gameplay hangs off of (all garrison-talent opcodes were Handle_NULL).
Data (Phase A): add the SoulbindConduit.db2 store (Meta 0x2632E47D
already present) so conduits can be validated by type/covenant/spec.
Collection (Phase B): the conduit collection is server-authoritative -
Player now tracks owned conduits (conduitId -> RankIndex) and socketed
conduits (GarrTalent node -> conduitId + treeId), both persisted to new
character_soulbind_conduits / character_soulbind_conduit_sockets tables
loaded at login. CollectConduit() grants at the data-driven lowest
RankIndex defined for the conduit (never assumes 0; refuses if there is
no rank row). The "conduit add" GM command is the interim grant trigger;
auto-collection from conduit items needs the item-to-conduit link (TODO).
Socketing (Phase C): CMSG_GARRISON_SOCKET_TALENT now decodes to
{ u32 GarrTalentID, u32 count, count x (SoulbindConduitID, Rank) } - the
pair mirrors this protocol's GarrisonTalentSocketData. The handler routes
to Player::SocketConduit, which validates the conduit exists, is owned,
and its covenant matches, then persists and casts the conduit's
SoulbindConduitRank->SpellID aura. ApplyConduitSpells/RemoveConduitSpells
gate on the active soulbind's tree, re-apply on login (mirrors
_LoadGlyphAuras) and swap on soulbind switch.
Server-authoritative: the client-sent rank is ignored (server uses its
own collection). The socket wire's leading id / pair order carry a
NEEDS-CONFIRM (sniff) flag, but the handler fails closed - an unowned or
invalid id simply no-ops, so a misread can never corrupt state or apply a
wrong spell.
- fix death issues for bot corpses moving and being attackable
- guard hired bots from receiving movement orders - this should not happen for hired bots as they should follow the player
- added BG role re-roll after flag capture
- fix npc flag being removed in BG for hired bots.