Emit customer-provided reagents (and a customerPlayer sub-struct) in the crafting-order
list wire, closing the one part of the flagship that the prior pass parked as
"needs full ItemInstance serialization / server data-model gap".
Both of those blockers were a misattribution. Re-decompiling the reader tree shows the
order's reagents (JamCraftingOrder.reagents) are read by sub_7FF72915FF20 as FLAT
JamCraftingOrderItem records (orderItemID, type, itemGUID, ownerGUID, quantity,
qualityID, flags, reagentBase{itemID,currencyID}, optional slot) - NOT ItemInstances.
The ItemInstance readers (sub_7FF7291CBD40) belong to the JamCliCraftingOrder *recraft*
fields, a different sub-struct. The server's existing OrderReagent {ItemID, CurrencyID,
Quantity, Slot} already holds everything a posting carries; ownerGUID = the customer and
flags = 1 are derived at emit time.
Validated byte-exact against the live 12.0.7 sniff
(C:\sniff\ingame-shop_ordersCrafting_professions.pkt): every order in the 2054-byte
(12 orders) and 1875-byte (11 orders) responses re-encodes to the byte, 0 leftover,
including the reagent blocks and the presence-gated customerNpc sub-struct.
- CraftingOrderPackets.h/.cpp: CraftingOrderReagentData + reagent loop in operator<<
(mirror of sub_7FF72915FF20), presence-byte bit assignment (bit5 customerPlayer,
bit4 customerNpc, bit3 outputOrderItem, bit2 outputItem), customerPlayer emission.
- CraftingOrderHandler.cpp: BuildCraftingOrderData fills reagents from Order::Reagents
(ownerGUID = CustomerGUID) and sets customerPlayer for player orders.
The fulfil wire turned out to be recoverable, not reflection-walled: the client's FULFILL send serializer
(sub_7FF729155000) is BYTE-IDENTICAL to the already-decoded REJECT serializer (sub_7FF7291552B0), so
CraftingOrderFulfill reads the same { u64 OrderID; u8; string note; bit hasContext; [ClientContext] }. No crafted
item rides the wire - the server derives the output from the order's recipe, matching the client's craft-then-fulfil.
Fulfil flow (HandleCraftingOrderFulfill):
- validates the order is Claimed by the sender;
- resolves the recipe output (SkillLineAbility -> spell -> SPELL_EFFECT_CREATE_ITEM) and mails the crafted item
to the customer;
- transitions Claimed -> Fulfilled and releases the escrowed tip to the crafter;
- replies with SMSG_CRAFTING_ORDER_FULFILL_RESULT and broadcasts UPDATE_STATE.
SMSG_CRAFTING_ORDER_FULFILL_RESULT: recovered byte-exact from the client reader sub_7FF7290B9950 (all plain reads -
the "CompressedUInt32" reader 0x7FF72BE6C410 is ReadUInt32): { u8 Result; u64 OrderID; u64; u8; PackedGUID; u32;
u32; u32 }. Result+OrderID are populated; the trailing fields have no offline-resolvable meaning and are sent
0/empty (structurally exact, honest - fulfilment is also signalled via UPDATE_STATE).
Tip escrow (Blizzlike, mirrors the auction deposit; no gold is created or lost):
- CREATE now requires + deducts the tip up front (escrow), failing with MissingCurrency if the customer is short;
- FULFILL pays the escrowed tip to the crafter;
- CANCEL / REJECT / posting-expiry refund the escrowed tip to the customer.
Each order escrows once at create and releases exactly once at its single terminal transition.
Adds the order-state-change notification the client uses to refresh open browse windows in real time.
- CraftingOrderUpdateState packet (SMSG_CRAFTING_ORDER_UPDATE_STATE 0x42033D). Wire is sniff-resolved from
C:\sniff\ingame-shop_ordersCrafting_professions.pkt and cross-checked against the SAME order in the
LIST_ORDERS_RESPONSE capture - OrderID / OrderState / CrafterGUID / SkillLineAbilityID / OrderType all match
byte-exact. Layout: { u64 OrderID; u8(0); u8 OrderState; u16(0); PackedGUID CrafterGUID; u32 SkillLineAbilityID;
u32(0); u8 OrderType; u32 Field30; u32 Field34 }. Field30/Field34 hold small per-order values (~96/~38) that map
to none of the order's known fields; their semantics are not offline-resolvable, so they are sent 0 (honest).
- BroadcastCraftingOrderState(): pushes the update to the online customer and (once assigned) crafter.
- Wired into the claim / release / reject handlers after a successful state change; the freshly-updated order is
re-fetched from the mgr and broadcast. Cancel removes an unclaimed order whose only interested party (the customer)
already receives the CancelResult, so it is intentionally not broadcast.
Turns CMSG_CONTRIBUTION_CONTRIBUTE (was Handle_NULL) into the working war-effort turn-in,
closing the loop over the P1a DB2 stores and the P1 ManagedWorldStateMgr.
ContributionMgr:
- Load(): builds the collector-creature -> contribution index from CreatureXContribution.db2.
- Contribute(player, collectorGuid, contributionId):
1. Validates the collector is an NPC the player can currently interact with
(GetNPCIfCanInteractWith, no flag requirement) and that its creature entry actually
offers this contribution (CreatureXContribution authorization).
2. Resolves Contribution -> ManagedWorldStateInput, gating on the input's
ValidInputConditionID (PlayerCondition).
3. Takes the turn-in cost = the input QuestID's item/currency objectives: a first pass
verifies the player can pay the full cost (nothing is consumed unless everything is
affordable), a second pass consumes items (DestroyItemCount) and currency
(RemoveCurrency, QuestTurnin reason).
4. Feeds the total contributed amount into ManagedWorldStateMgr::AddProgress, which
advances the realm-wide progress world state (and completes an occurrence/stage at the
target). A contribution with no consumable objectives counts as one unit.
Wired: HandleContributionContribute routes the opcode (STATUS_LOGGEDIN, THREADUNSAFE);
sContributionMgr->Load() at startup after the managed world states. Builds clean (game.dll).
Progress increment = total quantity turned in (progress bar == resources contributed);
documented as the modelling choice where a state counts resources rather than turn-ins.
The war-effort Contribution system is now end-to-end: DB2 model -> collector turn-in ->
managed world-state progress. SMSG_CONTRIBUTION_LAST_UPDATE_RESPONSE remains deferred
(factory/thunk reader; no guessed wire).
Adds the realm-wide progress engine TC lacked. For each ManagedWorldState.db2 row the manager
holds a runtime state (progress, stage, occurrences, up/down phase, timers) and:
- Cycles the up/down phase (UpTimeSecs/DownTimeSecs windows); a state with zero windows is
purely contribution-driven and stays in its Up phase.
- Once per elapsed minute, accumulates AccumulationAmountPerMinute toward
AccumulationStateTargetValue during the Up phase, or depletes DepletionAmountPerMinute toward
DepletionStateTargetValue during the Down phase.
- Pushes Progress / CurrentStage / Occurrences through their world states via
WorldStateMgr::SetValueAndSaveInDb (realm-global, persisted), so the client draws the
war-effort/building progress bar and it survives restarts (restored on Load via GetValue).
- Reaching the target completes one occurrence and advances the stage.
- AddProgress(managedWorldStateId, amount) is the seam the Contribute path (P2) feeds into,
clamped to the depletion/target bounds.
Wired into World: Load() after world-state templates load (so persisted progress restores),
Update(diff) in the world tick next to the outdoor-PvP update. Builds clean (game.dll).
Note: values are pushed as realm-global world states, which covers realm-wide managed states;
zone-scoped states would additionally need per-map propagation (documented, future refinement).
Adds the four DB2 stores the Contribution system is built on (TC had the *Meta hotfix
schemas but never loaded these tables): Contribution, CreatureXContribution,
ManagedWorldState, ManagedWorldStateInput.
Field layouts derived from the authoritative TC *Meta structs (DB2Metadata.h) cross-checked
against the 68275 WoWDBDefs layouts (Contribution 167173C3, ManagedWorldState A4E9EA9F,
ManagedWorldStateInput 16683306, CreatureXContribution 29D9C37F). All four have IndexField=-1
(ID first). ManagedWorldState carries the accumulate/deplete progress driver bound to
Current/Progress/Occurrences WorldState IDs; ManagedWorldStateInput carries the QuestID that
is the contribution turn-in cost.
- DB2Structure.h: 4 entry structs.
- DB2LoadInfo.h: 4 load infos (arrays expanded per element, signedness per Meta).
- DB2Stores.h/.cpp: storage globals + LOAD_DB2 registration.
- HotfixDatabase.h/.cpp: hotfix SELECT statements (+ locale for Contribution's two
localized strings) and enum entries.
- sql/updates/hotfixes/master/2026_07_07_00_hotfixes.sql: the five hotfix tables.
Builds clean (game.dll). Next: the ManagedWorldStateMgr runtime (P1) that ticks these
states, then Contribute (P2).
to the client DB2 model (Contribution -> ManagedWorldStateInput -> ManagedWorldState), per
the architecture decision to keep it decoupled from the major-factions branch.
- doc/contribution/CONTRIBUTION_IMPLEMENTATION_PLAN_68275.md: full RE + phased plan.
Wire (CMSG both dirs RE'd byte-exact from client serializers); DB2 model incl. the key
finding that Contribution.db2 has NO FactionID (it feeds a ManagedWorldStateInput, not a
faction) and the cost = the input's QuestID turn-in (no separate cost column).
- ContributionPackets.h/.cpp: readers for CMSG_CONTRIBUTION_CONTRIBUTE
({ObjectGuid CollectorGUID, uint32 ContributionID}) and CMSG_CONTRIBUTION_LAST_UPDATE_REQUEST
({uint32 ContributionID, uint32 Field1}). Included in AllPackets.h.
Opcodes intentionally remain Handle_NULL: routing lands with the real ContributionMgr +
ManagedWorldState runtime (P1/P2) so no stub handler is introduced. SMSG_CONTRIBUTION_LAST_
UPDATE_RESPONSE deferred with a documented reason (reader is a factory/thunk maze; needs a
dispatch decompile or live sniff, no guessed wire).
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.