Skip to content

Commit cfbc4f4

Browse files
committed
loot: add C_LootHistory (group-loot roll history backport)
Backport of the MoP C_LootHistory namespace. Vanilla 1.12 has no loot-history store, but it receives the live group-loot roll traffic, which the stock UI only turns into chat. This reconstructs the history client-side (like Aura::Source): four co-hooks on the loot-roll packet handlers accumulate a ring (128) of rolled items with per-player Need/Greed/Pass results and the winner, exposed through the namespace. Handlers, from the opcode dispatch FUN_005E6010: - START_ROLL 0x2A1 FUN_0061B310 (__fastcall(packet), RET 0) - ALL_PASSED 0x29E FUN_0061B640 (msg=EDX, RET 0) - ROLL_WON 0x29F FUN_0061B9E0 (ctx=ECX, msg=EDX, +stack, RET 4) - ROLL 0x2A2 FUN_0061C0B0 (msg=ECX, ctx=EDX, +stack, RET 4) The per-handler ABI (message register, stack arg, RET) is load-bearing: matching it exactly is what keeps the co-hooks from corrupting ESP. Names and class come from the engine NameCache (FUN_0055F080, null- callback read of a loaded entry), captured at roll time — so they resolve even for a roller who has left the group by the time their roll arrives. The item's random suffix is only in SMSG_LOOT_START_ROLL (the result packets send 0), so it's read off the store-head node right after that handler runs, giving a correct item:id:0:suffix:unique link. Surface: C_LootHistory.GetNumItems / GetItem / GetPlayerInfo (name, class token, rollType, roll, isWinner, isMe) + LOOT_HISTORY_ROLL_CHANGED / _ROLL_COMPLETE events (_FULL_UPDATE reserved). The MoP master-loot functions (SetExpiration / GiveMasterLoot / CanMasterLoot) are omitted — they need server support 1.12 lacks. Documented in docs/API.md + README.
1 parent bb9579c commit cfbc4f4

4 files changed

Lines changed: 578 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
4343
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
4444
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
4545
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
46+
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo` |
4647
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
4748
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
4849
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
@@ -121,6 +122,8 @@ when launching with `-console`), not as Lua functions. See the
121122
| `GLOBAL_MOUSE_UP` | `button` |
122123
| `HEARTHSTONE_BOUND` | *(none)* |
123124
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
125+
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
126+
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
124127
| `LOOT_SCAN_COMPLETED` | *(none)* |
125128
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
126129
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |

docs/API.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ build instructions.
9999
- [`EQUIPMENT_SWAP_PENDING` event](#equipment_swap_pending-event)
100100
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
101101
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
102+
- [`LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
102103
- [`LOOT_SCAN_COMPLETED` event](#loot_scan_completed-event)
103104
- [`LOSS_OF_CONTROL_ADDED` / `LOSS_OF_CONTROL_UPDATE` events](#loss_of_control_added--loss_of_control_update-events)
104105
- [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event)
@@ -255,6 +256,11 @@ build instructions.
255256
- [`C_Loot.IsScanInProgress()`](#c_lootisscaninprogress)
256257
- [`C_Loot.GetLastScanResults()`](#c_lootgetlastscanresults)
257258

259+
- [LootHistory](#loothistory)
260+
- [`C_LootHistory.GetNumItems()`](#c_loothistorygetnumitems)
261+
- [`C_LootHistory.GetItem(itemIndex)`](#c_loothistorygetitemitemindex)
262+
- [`C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)`](#c_loothistorygetplayerinfoitemindex-playerindex)
263+
258264
- [LossOfControl](#lossofcontrol)
259265
- [`C_LossOfControl.GetActiveLossOfControlDataCount()`](#c_lossofcontrolgetactivelossofcontroldatacount)
260266
- [`C_LossOfControl.GetActiveLossOfControlData(index)`](#c_lossofcontrolgetactivelossofcontroldataindex)
@@ -2295,6 +2301,22 @@ forwarding, which [`C_Reputation.GetLastStandingChange`](#c_reputationgetlaststa
22952301
exposes — so addons can read the structured payload from inside the
22962302
chat event without re-parsing the localized string.
22972303

2304+
### `LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events
2305+
2306+
Fire as group-loot rolls progress, so a loot-history UI can update without
2307+
polling. Register like any engine event
2308+
(`frame:RegisterEvent("LOOT_HISTORY_ROLL_COMPLETE")`), then read the item via
2309+
[`C_LootHistory.GetItem`](#c_loothistorygetitemitemindex) /
2310+
[`C_LootHistory.GetPlayerInfo`](#c_loothistorygetplayerinfoitemindex-playerindex).
2311+
2312+
| Event | Args | When |
2313+
|---|---|---|
2314+
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex`, `playerIndex` (numbers) | A player rolled or passed on an item. |
2315+
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` (number) | The item was decided — won or all-passed. |
2316+
| `LOOT_HISTORY_FULL_UPDATE` || Reserved for API / `IsEventValid` completeness (the display frame fires it on open); the DLL doesn't dispatch it. |
2317+
2318+
See the [LootHistory](#loothistory) section for the reconstruction and read API.
2319+
22982320
### `LOOT_SCAN_COMPLETED` event
22992321

23002322
Fires (no payload) once a [`C_Loot.ScanNearbyLoot()`](#c_lootscannearbyloot)
@@ -6125,6 +6147,57 @@ tooltip display, extract the payload form via
61256147
requires the literal `"item:"` prefix and rejects full `|cff...|Hitem...|h`
61266148
input.
61276149

6150+
## LootHistory
6151+
6152+
Backport of the MoP `C_LootHistory` namespace — group-loot roll history.
6153+
Vanilla 1.12 has no loot-history store, but it does receive the live
6154+
group-loot roll traffic (`SMSG_LOOT_ROLL` per player, `SMSG_LOOT_ROLL_WON`,
6155+
`SMSG_LOOT_ALL_PASSED`) — which the stock UI only turns into chat lines. This
6156+
reconstructs the history client-side: packet co-hooks accumulate a ring of
6157+
rolled items with per-player Need/Greed/Pass results and the winner, exposed
6158+
through the namespace. Rollers' **name and class come from the engine
6159+
NameCache** captured at roll time, so they resolve even for a player who has
6160+
left the group by the time their roll arrives.
6161+
6162+
History is in-memory (last 128 rolled items) and resets on `/reload` or logout.
6163+
The MoP master-loot management functions (`SetExpiration` / `GiveMasterLoot` /
6164+
`CanMasterLoot`) are intentionally omitted — they drive server support 1.12
6165+
doesn't have.
6166+
6167+
Progress is signalled by the `LOOT_HISTORY_ROLL_CHANGED` /
6168+
`LOOT_HISTORY_ROLL_COMPLETE` events — see the
6169+
[Events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
6170+
section.
6171+
6172+
### `C_LootHistory.GetNumItems()`
6173+
6174+
Returns the number of rolled items currently in the history.
6175+
6176+
### `C_LootHistory.GetItem(itemIndex)`
6177+
6178+
`1`-based. Returns `itemLink, numPlayers, isDone, winnerName`:
6179+
- `itemLink``item:<id>:0:<suffix>:<unique>` (valid for `GetItemInfo` /
6180+
`GameTooltip:SetHyperlink`), carrying the roll's random suffix so
6181+
"of the X" items keep their affix.
6182+
- `numPlayers` — how many players have rolled/passed so far.
6183+
- `isDone``true` once the item is decided (won or all-passed).
6184+
- `winnerName` — the winner's name, or `nil` if undecided / all passed.
6185+
6186+
Returns nothing for an out-of-range index.
6187+
6188+
### `C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)`
6189+
6190+
Both `1`-based. Returns `name, class, rollType, roll, isWinner, isMe`:
6191+
- `name` — the roller's name (from the NameCache).
6192+
- `class` — the class token (`"WARRIOR"`, `"MAGE"`, …) for
6193+
`RAID_CLASS_COLORS`, or `nil` if unknown.
6194+
- `rollType``0` = pass, `1` = need, `2` = greed.
6195+
- `roll` — the `1``100` roll, or `0` if they passed.
6196+
- `isWinner``true` if this player won the item.
6197+
- `isMe``true` if this roller is the local player.
6198+
6199+
Returns nothing for an out-of-range item or player index.
6200+
61286201
## LossOfControl
61296202

61306203
Backports `C_LossOfControl` — the active crowd-control / interrupt effects on

src/Offsets.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,42 @@ enum Offsets {
12181218
// aura that's still inside its computed duration window.
12191219
FUN_ON_AURA_REMOVED = 0x00612320,
12201220

1221+
// Group-loot roll packet handlers (the inner display functions in
1222+
// LootRoll.cpp). Each is `__fastcall`; the deserialized 0x48-byte
1223+
// PendingLootRoll message struct is fully populated at entry and freed
1224+
// at exit, so read its fields BEFORE calling the original. The register
1225+
// the message arrives in differs per handler (matched in
1226+
// loot/History.cpp): ROLL passes it in ECX, WON / ALL_PASSED in EDX.
1227+
// Backs the C_LootHistory backport (loot/History.cpp) — 1.12 has no
1228+
// loot-history store, so we reconstruct one from these packets.
1229+
FUN_LOOT_ROLL_HANDLER = 0x0061C0B0, // SMSG_LOOT_ROLL (msg=ECX)
1230+
FUN_LOOT_ROLL_WON_HANDLER = 0x0061B9E0, // SMSG_LOOT_ROLL_WON (msg=EDX)
1231+
FUN_LOOT_ALL_PASSED_HANDLER = 0x0061B640, // SMSG_LOOT_ALL_PASSED (msg=EDX)
1232+
1233+
// SMSG_LOOT_START_ROLL (opcode 0x2A1) handler — `__fastcall(ECX = packet)`,
1234+
// RET 0. The result packets (ROLL/WON/PASSED) carry 0 for the item's
1235+
// random suffix/seed; only START_ROLL does. It deserializes a fresh
1236+
// PendingLootRoll node (same 0x20/0x24 field offsets as the message) and
1237+
// publishes it as the store head at VAR_LOOTROLL_STORE_HEAD, so we read
1238+
// the suffix straight off that node right after the original runs.
1239+
FUN_LOOT_START_ROLL_HANDLER = 0x0061B310,
1240+
VAR_LOOTROLL_STORE_HEAD = 0x00C4DC00, // PendingLootRoll* — newest node
1241+
1242+
// PendingLootRoll message field offsets, derived from the SMSG_LOOT_ROLL
1243+
// deserializer FUN_0061BFA0 (reads GUID/u32/byte fields off the packet).
1244+
OFF_LOOTROLL_SOURCE_GUID = 0x10, // u64 looted-object GUID (roll identity)
1245+
OFF_LOOTROLL_SLOT = 0x18, // u32 loot slot
1246+
OFF_LOOTROLL_ITEM_ID = 0x1C, // u32 itemID
1247+
// Random-enchant fields for the item link `item:id:enchant:suffix:unique`.
1248+
// +0x20 is the suffix/random-property ID the engine's item-name builder
1249+
// FUN_005D8B00 uses (indexes the random-properties DBC for the "of the X"
1250+
// name); +0x24 is the paired seed/unique value.
1251+
OFF_LOOTROLL_ITEM_SUFFIX = 0x20, // i32 random suffix / property ID
1252+
OFF_LOOTROLL_ITEM_SEED = 0x24, // u32 random seed (link unique field)
1253+
OFF_LOOTROLL_ROLLER_GUID = 0x30, // u64 roller / winner GUID
1254+
OFF_LOOTROLL_ROLL_NUM = 0x38, // u8 roll (1..100); high bit = didn't roll
1255+
OFF_LOOTROLL_ROLL_TYPE = 0x40, // u32 rollType: 0 = need, 2 = greed
1256+
12211257
// CGPlayer-side sub-struct, allocated for any player-controlled
12221258
// unit (local self, target, party, raid, inspect targets — all of
12231259
// them). Holds player-specific data that's *not* in the broadcast

0 commit comments

Comments
 (0)