-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOffsets.h
More file actions
3126 lines (2945 loc) · 164 KB
/
Offsets.h
File metadata and controls
3126 lines (2945 loc) · 164 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
enum Offsets {
FUN_FRAME_SCRIPT_INITIALIZE = 0x7039E0,
FUN_INVALID_FUNCTION_PTR_CHECK = 0x42A320,
FUN_LOAD_SCRIPT_FUNCTIONS = 0x490250,
// Master glue Lua init — clean linear caller of all 5 glue batch
// trampolines (60 main + 11 char-select + 24 char-create + 10 realm
// + 4 frame globals = 109 total). Body: 0x0046ABB0..0x0046ABD2,
// 35 bytes, single caller (FUN_0046A7B0). Runs once per glue boot:
// initial launch and every world→glue return (log out). Post-hook
// is the glue analog of `FUN_LOAD_SCRIPT_FUNCTIONS` — by the time
// it returns, `VAR_LUA_STATE` points at the freshly populated glue
// state, so `FrameScript_RegisterFunction` writes land on it.
FUN_LOAD_GLUE_SCRIPT_FUNCTIONS = 0x0046ABB0,
// GameTooltip script-method prologue helpers (used to resolve self → CFrameScriptObject*).
// Underlying Lua C API names per [[docs/LuaCAPI.md]] are `lua_rawgeti`
// (0x6F3BC0) and `lua_touserdata` (0x6F3740). The Set* method prologue's
// call sequence
// PushObject(L, idx, 0); // == lua_rawgeti(L, idx, 0) — pushes the
// // lightuserdata at table[0]
// GetObject(L, -1); // == lua_touserdata(L, -1) — extracts
// // the raw CFrameScriptObject *
// is the canonical "Lua frame table → C++ object" path. The reverse
// (`GameTooltip:GetOwner` etc.) is just `lua_rawgeti(L, REGISTRY,
// [cobj+OFF_COBJECT_LUA_REGISTRY_REF])` — the engine pre-allocates
// the registry handle when the frame is created from Lua.
FUN_FRAMESCRIPT_PUSH_OBJECT = 0x6F3BC0,
FUN_FRAMESCRIPT_GET_OBJECT = 0x6F3740,
// CFrameScriptObject layout: the registry refkey allocated when the
// engine first exposes the C++ object to Lua. Pushing a frame back
// to Lua is `lua_rawgeti(L, LUA_REGISTRYINDEX, [cobj+0x08])`.
OFF_COBJECT_LUA_REGISTRY_REF = 0x08,
// Within a CGFrame, the embedded LayoutFrame sub-object starts here.
// Engine code that wants to invoke LayoutFrame methods polymorphically
// (anchoring, positioning) stores the LayoutFrame* — i.e.
// `(Frame*)obj + 0x24` — instead of the bare Frame*. Reverse
// lookups (e.g. GameTooltip:GetOwner) subtract this back out.
OFF_FRAME_LAYOUT_SUBOBJECT = 0x24,
// Inner spell-tooltip builder, called from Script_GameTooltip_SetSpell at 0x00532E92.
// __thiscall(spellID, 0, 0, isPet, 0, 0, 0); we always pass isPet=0.
FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP = 0x0052E610,
// Existing GameTooltip method-table entries we dispatch to from
// backported convenience methods. Each is `int __fastcall(void *L)`
// expecting the standard self+args layout on the Lua stack.
// (Slot numbers are the method-registry index per `docs/raw_methods.txt`.)
FUN_SCRIPT_GAMETOOLTIP_SET_HYPERLINK = 0x00531FD0, // slot 12
FUN_SCRIPT_GAMETOOLTIP_SET_INVENTORY_ITEM = 0x00532EE0, // slot 19
FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_BUFF = 0x00534AC0, // slot 32
FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_DEBUFF = 0x00534E30, // slot 33
FUN_SCRIPT_GAMETOOLTIP_SET_TALENT = 0x00535170, // slot 34
// Iterator that registers an array of frame-method bindings on a per-frame-type
// method registry (e.g. VAR_GAMETOOLTIP_METHOD_REGISTRY for GameTooltip).
// __fastcall(ecx = MethodEntry table, edx = count, [stack] = context).
FUN_REGISTER_FRAME_METHODS = 0x00701D80,
VAR_GAMETOOLTIP_METHOD_REGISTRY = 0x00C0CF20,
// "Currently displayed thing" state fields on a GameTooltip frame
// instance. Each Set* path writes one of these (and zero or two
// others), and the per-tooltip Clear at FUN_00530050 zeroes all of
// them on Hide/before-redraw. The Get* methods are simple reads —
// whichever field is non-zero tells us what kind of tooltip is up.
//
// Verified by decoding the builder functions:
// - BuildItemTooltip (0x0052B650) writes +0x380/+0x384 (item
// GUID, only when there's a real CGItem) and
// +0x398 (itemID) at 0x0052B6CE / 0x0052B6FE.
// - BuildSpellTooltip (0x0052E610) writes +0x39C (spellID) at
// 0x0052E6D5 (param_7==0 branch — skipped for
// the next-rank tooltip side-build).
// - BuildUnitTooltip (FUN_00529FE0) writes +0x368/+0x36C (unit
// GUID) — see SetUnit block below.
// - BuildGameObjectTooltip (0x0052AA20) writes +0x370/+0x374
// (gameobject GUID) at 0x0052AA52 / 0x0052AA59.
// Only call site is the in-world hover handler
// FUN_00492890; no Lua `SetGameObject` method.
// - Clear (0x00530050) zeroes all of them.
OFF_TOOLTIP_ITEM_GUID_LO = 0x380, // 0 for SetItemByID (no CGItem)
OFF_TOOLTIP_ITEM_GUID_HI = 0x384,
OFF_TOOLTIP_ITEM_ID = 0x398,
OFF_TOOLTIP_SPELL_ID = 0x39C,
// Unit GUID written by the inner unit-tooltip builder
// (FUN_00529FE0) when `tooltip:SetUnit(token)` resolves the token
// to a non-zero GUID. Cleared by the same `FUN_00530050` clear
// that handles the item/spell IDs above, so `(lo|hi) == 0` means
// "no unit currently displayed" — same gating pattern HasSpell /
// HasItem use.
OFF_TOOLTIP_UNIT_GUID_LO = 0x368,
OFF_TOOLTIP_UNIT_GUID_HI = 0x36C,
// GameObject GUID written by the in-world hover tooltip populator
// (FUN_0052AA20) — there is no Lua-callable `SetGameObject` method
// in vanilla, so this slot only ever fills when the player mouses
// over a gameobject in the world (nodes, chests, doors, etc.).
// Same clear/gating semantics as the unit GUID slot.
OFF_TOOLTIP_GAMEOBJECT_GUID_LO = 0x370,
OFF_TOOLTIP_GAMEOBJECT_GUID_HI = 0x374,
// Owner frame stored by `tooltip:SetOwner(frame, anchor)` and
// compared by `IsOwned`. Holds `owner_CObject + OFF_FRAME_LAYOUT_SUBOBJECT`
// (i.e. the LayoutFrame*, not the bare Frame*), or 0 if unowned.
// Verified via the helper at 0x0052FFE0 invoked by SetOwner's tail
// (writes `[this+0x314] = arg+0x24`) and Script_GameTooltip_IsOwned
// at 0x00530FE0 (reads `[edi+0x314]` and compares against the same
// `+0x24`-offset value).
OFF_TOOLTIP_OWNER = 0x314,
// CGItem → fully-dressed item link string. __fastcall(ecx = CGItem *)
// → const char *. Reads the item's itemID, quality, permanent
// enchant ID, random-properties seed/factor, and unique ID off the
// CGItem's instance block + descriptor, builds the dressed name via
// FUN_005D8BC0 (handles random-suffix decoration like "Foo of the
// Bear"), then sprintf's into the global buffer at DAT_00C0CF68 and
// returns its address. The returned pointer is to a long-lived
// engine global, safe to read until the next call.
//
// Same helper Script_GetContainerItemLink (0x004F9930) and
// Script_GetInventoryItemLink (0x004C8C10) call after resolving
// their slot-form args. Bypassing them lets us build dressed links
// for tooltips set via SetMerchantItem / SetLootItem / etc. where
// the item isn't in the player's bag/equipment.
FUN_GAMETOOLTIP_BUILD_ITEM_LINK = 0x0052AE00,
// Engine's inventory swap-and-send. Same primitive
// `Script_EquipCursorItem` (0x00489660) uses after the cursor's
// source location has been resolved. Sends opcode 0x10D
// (CMSG_SWAP_INV_ITEM) for same-container swaps or 0x10C
// (CMSG_AUTOEQUIP_ITEM) for cross-container, then runs the
// packet through the engine's own send pipeline at FUN_005AB630.
//
// Signature:
// void __thiscall(
// CGPlayer *this,
// u32 srcItemGuidLo, u32 srcItemGuidHi,
// u32 srcContainerGuidLo, u32 srcContainerGuidHi,
// u32 srcLinearSlot,
// u32 dstContainerGuidLo, u32 dstContainerGuidHi,
// u32 dstLinearSlot,
// int flag); // 0 = normal path
//
// Linear-slot encoding for sources/dests in player invMgr:
// 0..18 paperdoll (1-based slot - 1)
// 19..22 equipped bag containers (bag IDs 1..4 themselves)
// 23..38 backpack contents (1-based bag-0 slot S → 22 + S)
// For sources in a CGContainer (equipped bag B = 1..4), the
// container GUID is the bag's own GUID and srcLinearSlot is
// 0-based within that bag (1-based Lua slot - 1).
//
// This call neither reads nor writes the cursor-state globals at
// [0xBE0810] / [0xBE0814]; cursor visibility is purely a side
// effect of the cursor-pickup path that normally precedes it.
// Calling this directly produces a server-side swap with no
// client-side cursor manipulation.
FUN_INVENTORY_SWAP = 0x005E0C40,
// Sister helper to FUN_INVENTORY_SWAP — packet builder for
// `CMSG_SPLIT_ITEM` (opcode 0x10E). Same __thiscall ABI shape;
// the item-GUID args (param_1, param_2) are unused padding for
// ABI parity. Packet wire format:
// [0x10E, srcBag, srcSlot, dstBag, dstSlot, count]
// where srcBag/dstBag are byte-converted from container GUIDs by
// the same `FUN_005e13b0` helper the swap function uses
// (`0xFF` = INVENTORY_SLOT_BAG_0 / player, `19..22` = equipped bags).
//
// Server semantics are all-or-nothing: any failure
// (insufficient source, dest has different item, dest would
// overflow maxStack) leaves source untouched and emits
// `SMSG_INVENTORY_CHANGE_FAILURE`. No cursor involvement on send
// or response.
//
// Signature:
// void __thiscall(
// CGPlayer *this,
// u32 unused1, u32 unused2, // ABI padding
// u32 srcContainerLo, u32 srcContainerHi,
// u32 srcLinearSlot, // only low byte hits the wire
// u32 dstContainerLo, u32 dstContainerHi,
// u32 dstLinearSlot, // only low byte
// u32 count); // only low byte
FUN_INVENTORY_SPLIT = 0x005E1210,
// Registers a single global Lua function. __fastcall(name, func).
FUN_FRAMESCRIPT_REGISTER_FUNCTION = 0x00704120,
// `FrameScript_Object::ScriptRegister(this, name)` — `__thiscall`,
// `this` = a `CFrameScriptObject *`. On first call (when `this+0x04`
// is zero) builds a Lua wrapper table `{[0] = lightuserdata(this)}`
// with `_G["__framescript_meta"]` as metatable, `luaL_ref`s it into
// the registry, stores the refkey at `this+0x08`. Always increments
// `this+0x04` (the Lua-side refcount). Optional `name` argument
// installs `_G[name] = wrapper` for engine-named frames.
//
// We call this in `PushNamePlateFrame` so the engine and our own
// C_NamePlate getters operate on the **same** wrapper table —
// every push through `lua_rawgeti(REGISTRY, this+0x08)` (the
// canonical engine path) lands on the same Lua object pfUI
// received in `NAME_PLATE_CREATED`, so addon-set fields
// (`plate.nameplate = decoratedButton`) survive engine-side
// re-fetches. Earlier note in `Info.cpp` warned about pinning the
// refcount; for pool-managed nameplates the engine never
// un-registers them anyway, so the pin is benign.
FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER = 0x00701BD0,
// `this+0x04` Lua refcount, incremented by `ScriptRegister`. We
// read it as a "has the engine ever exposed this CObject to Lua"
// probe — equivalent to checking `this+0x08 > 0` but more direct.
OFF_COBJECT_LUA_REFCOUNT = 0x04,
// Direct cvar lookup — `__fastcall(const char *name) → CVar* | NULL`.
// Hash-table by-name lookup over the CVar registry; same call
// `Script_GetCVar` makes internally before the engine wraps the
// result in lua_pushstring + a "CVar doesn't exist" error path.
// Calling it directly lets us skip both the Lua roundtrip and the
// unknown-cvar error — we coerce NULL to false instead, matching
// modern `C_CVar.GetCVarBool` semantics.
//
// The returned struct holds the value string at `+0x20` (read by
// `Script_GetCVar` at `0x00488BF9` as `mov edx, [eax+0x20]; call
// lua_pushstring`). The value is the raw `char *` the engine
// stores; reading it directly is safe for the lifetime of the
// cvar (vanilla cvars don't get re-allocated outside `/console
// set` flows, which we don't race here).
FUN_FIND_CVAR = 0x0063DEC0,
OFF_CVAR_VALUE_STR = 0x20,
// Engine-side Lua C functions backing the in-game CVar globals.
// Standard `int __fastcall(void *L)` ABI. We don't register them
// ourselves in-game — the engine already does at boot — but we
// re-register the same pointers on the glue Lua state so login /
// char-select GlueXML can read and write CVars too (vanilla 1.12
// exposes none of these in glue by default). CVar storage is
// process-global, so a write from either state is visible to the
// other.
FUN_SCRIPT_REGISTER_CVAR = 0x00488B00,
FUN_SCRIPT_GET_CVAR = 0x00488BA0,
FUN_SCRIPT_SET_CVAR = 0x00488C10,
FUN_SCRIPT_GET_CVAR_DEFAULT = 0x00488CF0,
// `Script_RunScript` — the C function backing `_G.RunScript(code)`
// in-game. `int __fastcall(void *L)`. Mirrored onto the glue Lua
// state in `src/script/Run.cpp` so GlueXML can also `RunScript("...")`
// — useful for slash-command-style debug helpers at the login /
// realm / char-select screens.
FUN_SCRIPT_RUN_SCRIPT = 0x0048B980,
// Game::ResolveUnitToken — __fastcall(ecx = const char *token) → CGUnit_C *.
// Returns the unit pointer for "player", "target", "party1", etc. Use this
// rather than the global at 0x00B41414 — that global holds something
// related (its +0xC0 has the player GUID) but is NOT the same CGPlayer_C
// pointer the inventory routines expect.
FUN_RESOLVE_UNIT_TOKEN = 0x00515940,
// Local-player CGObject-like global. Not the same pointer as
// ResolveUnitToken("player") returns — that one's the canonical
// CGPlayer_C used by inventory etc. This pointer's +0xC0 field
// holds the local player's 64-bit GUID, and the visible-object
// iterator at FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS walks its
// +0xAC list. Useful for "is this GUID me?" checks without
// round-tripping through Lua.
VAR_LOCAL_PLAYER_PTR = 0x00B41414,
OFF_LOCAL_PLAYER_GUID = 0xC0,
// `__fastcall(ecx = const char *token) → uint64_t GUID` — the
// inner token-to-GUID step that `FUN_RESOLVE_UNIT_TOKEN` calls
// before doing the active-object lookup. Returns the unit's
// GUID without depending on the unit being visible / loaded as
// a CGObject, so `partyN` / `raidN` resolve correctly even when
// the member is out of range or on a different continent
// (`Script_UnitName` uses this path too; that's why `UnitName`
// already works for OOR party members in vanilla).
//
// Internal dispatch:
// - "player" → `[localPlayer + 0x08]` (GUID ptr)
// - "target" → `DAT_00B4E2D8`/`DAT_00B4E2DC` globals
// - "mouseover" → `DAT_00B4E2C8`/`DAT_00B4E2CC` globals
// - "partyN" → `FUN_004E81A0(slot)` → `[VAR_PARTY_GUIDS + slot*8]`
// - "raidN" → `FUN_00491940(slot)` → raid GUID array
// - "partypetN", "raidpetN" similar (pet-slot variants)
// - "<arbitrary name>" → raises "Unknown unit name: %s" via
// the engine's error helper (so this
// still errors on bad tokens — same
// semantics as the existing
// `UnitGUID` documented behavior).
//
// Don't use this from code paths that need to handle literal
// character names — see CLAUDE.md "Resolving input to a name"
// for the `lua_pcall(UnitName)` workaround. For pure unit-token
// input it's the right primitive.
FUN_TOKEN_TO_GUID = 0x00515970,
// `SStrCmpI(a, b, n)` — Storm's case-insensitive memcmp-style
// comparator. **`int __stdcall(const char *a, const char *b, int n)`**
// — the function ends with `ret 0xc`, so the callee pops the
// 3-arg stack frame. Declaring it as `__cdecl` and calling makes
// MSVC emit a redundant `add esp, 12` post-call, drifting ESP
// upward by 12 per call and corrupting the caller's stack frame
// — manifested as a deep-Lua crash whose `L` pointer landed in
// `.text`. Returns 0 when the first `n` characters match
// (ignoring case) or both strings end before `n`.
FUN_SSTR_CMP_I = 0x0064A4C0,
// UNIT_FIELD_TARGET within `m_objectFields` — 64-bit GUID at byte
// offsets +0x28 (lo) / +0x2C (hi). Verified by disassembling
// `FUN_TOKEN_TO_GUID`'s suffix walker at `0x00515A1C-A2C`:
// `mov eax, [obj + 0x110]; mov edi, [eax + 0x28]; mov ebx, [eax + 0x2c]`
// — reads the target GUID to chain `targettarget`-style tokens.
// Distinct from the higher field offsets in the
// `OFF_UNIT_FIELD_*` block; UNIT_FIELD_TARGET is one of the few
// 1.12 offsets that matches the CMaNGOS-documented vanilla
// layout.
OFF_UNIT_FIELD_TARGET = 0x28,
// Party / raid roster counts and the party GUID array referenced
// in the `FUN_TOKEN_TO_GUID` dispatch comment above. Used by
// `UnitTokenFromGUID` to cap its candidate iteration — solo
// players skip all 88 group tokens; a 5-person party scans 4
// slots instead of 40.
//
// - `VAR_PARTY_GUIDS` — 4-slot QWORD array (one GUID per party
// member, low+high dwords). Walked by `Script_GetNumPartyMembers`
// (`FUN_004E86D0`) at `0x004E86D0` to compute the count.
// - `VAR_RAID_MEMBER_COUNT` — single int maintained by the
// engine's raid-roster handler. Read directly by
// `Script_GetNumRaidMembers` (`FUN_004BB530`).
VAR_PARTY_GUIDS = 0x00BC6F48,
PARTY_MAX_SLOTS = 4,
VAR_RAID_MEMBER_COUNT = 0x00B713E0,
RAID_MAX_SLOTS = 40,
// Chat-event dispatcher — single choke point through which all
// CHAT_MSG_* events fire after the SMSG_MESSAGECHAT packet handler
// (opcode 0x96 → FUN_0049D560) parses the wire data. Called with
// the sender GUID as stack args 9 and 10 (lo, hi).
//
// Calling convention: `__fastcall` with 10 args — ECX = sender
// name string, EDX = chat type, then 8 stack args ending in the
// GUID pair. Called from:
// - FUN_0049D560 directly for live (non-throttled) chat
// - The pending-chat queue processor (`__AUPENDINGCHAT`) for
// messages buffered via FUN_0049CAE0 when the engine flag at
// 0x008435FC is set
// - Many synthetic chat synthesizers throughout the codebase
// (system notifications, arena team membership changes, etc.)
// which pass 0 / NULL for the GUID args
//
// Hooked by `Chat::CurrentGUID::ChatDispatch_h` to capture the
// GUID into a global for `GetCurrentChatGUID()` to read during an
// addon's CHAT_MSG_* OnEvent.
FUN_CHAT_DISPATCH = 0x0049A870,
// Per-player inventory manager lives at this offset on the player object.
OFF_PLAYER_INVENTORY_MANAGER = 0x1D38,
// ItemMgr::GetItemBySlot — __thiscall(this, slot) → CGItem* (NULL if empty).
// Slot is the engine's linearized slot index, not bagID/slot tuple.
FUN_ITEMMGR_GET_ITEM_BY_SLOT = 0x006228A0,
// CGUnit visible-items helper used by `Script_GetInventoryItemLink` for
// non-player units (target/party/inspect targets). __thiscall(this=unit,
// int 0-based slot) → visible-item entry*, or NULL if slot is out of
// [0, 18]. Reads `[unit + 0xE68]` (visible-items array base for the
// unit) and indexes `base + 0x118 + slot * 0x30`. Each 0x30-byte entry
// holds the itemID at `+0x08`; the engine reads it back exactly that
// way before feeding it to `_GetRecord` for hyperlink construction
// (verified in `Script_GetInventoryItemLink` at `0x004C8D05`-`0x4C8D34`).
//
// **Crash hazard**: `[unit + 0xE68]` is uninitialized for NPCs
// (CGCreature_C objects). The helper has no NULL check — it computes
// `garbage + 0x118 + slot*0x30` and returns that as a valid pointer.
// The engine relies on callers to gate this with `UnitPlayerControlled`
// first; we do the same in `Item::InventoryID`.
FUN_UNIT_GET_VISIBLE_ITEM = 0x005F0D60,
OFF_VISIBLE_ITEM_ITEM_ID = 0x08,
// CGUnit m_objectFields pointer offset. Different from CGItem's
// descriptor at +0x114 — these are sibling classes under CGObject
// with class-specific descriptor offsets.
OFF_UNIT_DESCRIPTOR = 0x110,
// Pointer to the CGUnit's 8-byte GUID at `*(CGUnit + 0x08)`. Verified
// in `Script_GetInventoryItemLink` at `0x004C8CB0`-`0x004C8CB5`:
// `mov eax, [esi+8]; mov edi, [eax]; mov ecx, [eax+4]` reads the
// GUID's lo+hi dwords through this indirection. CGItem uses the same
// offset for its instance block (also containing a GUID + itemID),
// so the layout is consistent across CGObject subclasses — but the
// contents differ per class.
OFF_UNIT_GUID_PTR = 0x08,
// UNIT_FIELD_FLAGS within m_objectFields. Bit 3 (`0x08`) is
// `UNIT_FLAG_PLAYER_CONTROLLED`, which `Script_UnitPlayerControlled`
// (`0x00516410`) tests via `mov eax, [m_objectFields + 0xA0];
// shr eax, 3; test al, 1`.
// CGUnit m_objectFields current/max stat offsets, **verified
// empirically** on Turtle WoW (1.12.1) by `_classicapi_DescDump`
// searching for the live `UnitMana` value at descriptor offsets:
//
// +0x40 HEALTH (current)
// +0x44 POWER1 (current mana — verified at multiple values)
// +0x48 POWER2 (current rage)
// +0x4C POWER3 (current focus)
// +0x50 POWER4 (current energy)
// +0x54 POWER5 (current happiness)
// +0x58 MAXHEALTH (= 807 at full HP in test data)
// +0x5C MAXPOWER1 (max mana — stays at 1435 even when current = 443)
// +0x60..+0x6C MAXPOWER2..5
// +0x70 LEVEL (= 26 in test data)
// +0xA0 FLAGS (verified separately via `Script_UnitPlayerControlled`)
//
// **The 1.12.1 layout is offset 0x18 (= 6 fields) earlier than the
// CMaNGOS-documented vanilla layout** (which puts HEALTH at field
// 0x16 = +0x58). My initial implementation read MAXHEALTH/MAXMANA
// when I wanted current values — caused `IsUsableSpell` to falsely
// return usable even when mana was below cost. Trust the binary
// (and `_classicapi_DescDump` if you ever need to re-verify), not
// external emulator field tables.
OFF_UNIT_FIELD_HEALTH = 0x40,
OFF_UNIT_FIELD_POWER1 = 0x44,
// POWER1..5 are 4 bytes each, contiguous from +0x44:
// +0x44 mana / +0x48 rage / +0x4C focus / +0x50 energy / +0x54 happiness.
OFF_UNIT_FIELD_MAXHEALTH = 0x58,
OFF_UNIT_FIELD_MAXPOWER1 = 0x5C,
// MAXPOWER1..5 are 4 bytes each, contiguous from +0x5C (same
// mana/rage/focus/energy/happiness order as POWER1..5). Vanilla
// 1.12 has 5 power types; the WotLK additions (Runes / Runic Power)
// don't exist in this descriptor layout.
UNIT_POWER_MIN_TYPE = 0,
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
// Per-power-type display divisor. Raw descriptor values get
// divided by this before they're surfaced through Lua. Vanilla
// 1.12 stores rage as 0..1000 (internally) and divides by 10 to
// show 0..100; happiness is stored at 1000x scale and divides
// by 1000. Engine reads from `Script_UnitMana` at
// `0x006E7130 + type * 4` (= `0x0086F978`). Same trick the
// 3.3.5/4.x clients use; the table is per-client, the design is
// stable.
//
// [0] = 1 MANA
// [1] = 10 RAGE
// [2] = 1 FOCUS
// [3] = 1 ENERGY
// [4] = 1000 HAPPINESS
VAR_UNIT_POWER_DIVISOR_TABLE = 0x0086F978,
OFF_UNIT_FIELD_LEVEL = 0x70,
// UNIT_FIELD_BYTES_0 dword at +0x78; byte 3 (= +0x7B) is the unit's
// primary power type (one of UNIT_POWER_MIN_TYPE..MAX_TYPE).
// Verified at `0x005179E6` in vanilla's `Script_UnitPowerType`:
// mov edx, [eax + 0x110]
// movzx eax, byte ptr [edx + 0x7B]
OFF_UNIT_DESCRIPTOR_POWER_TYPE_BYTE = 0x7B,
OFF_UNIT_FIELD_FLAGS = 0xA0,
UNIT_FLAG_PLAYER_CONTROLLED = 0x08,
// Bit 19 of UNIT_FIELD_FLAGS — `Script_UnitAffectingCombat` at
// `0x00517E4A`-`0x517E5C` tests it via `mov eax, [fields+0xA0];
// shr eax, 19; test al, 1`. We use it for `InCombatLockdown`,
// which in 1.12 collapses to "is the local player in combat" — no
// secure-frame system here, so the modern lockdown semantics
// reduce to a plain combat flag check.
UNIT_FLAG_IN_COMBAT = 0x00080000,
// Bit 29 of UNIT_FIELD_FLAGS — set by the engine when the player is
// feigning death (Hunter's `Feign Death`). Standard vanilla
// `UNIT_FLAG_FEIGN_DEATH = 0x20000000` per emulator source. Tested
// by `UnitIsFeignDeath(unit)` — works on any unit since
// UNIT_FIELD_FLAGS is broadcast in object updates.
UNIT_FLAG_FEIGN_DEATH = 0x20000000,
// Bit 24 of UNIT_FIELD_FLAGS — set by the engine when a player is
// possessed (priest's `Mind Control`, warlock's `Subjugate Demon`).
// Standard vanilla `UNIT_FLAG_POSSESSED = 0x01000000` per emulator
// source (CMaNGOS/TrinityCore). Read by `UnitIsPossessed(unit)`.
UNIT_FLAG_POSSESSED = 0x01000000,
// `UNIT_FIELD_CHANNEL_OBJECT` lo/hi (64-bit GUID of the object the
// unit is currently channeling onto — bobber for fishing, lock for
// lockpicking, etc.). CMaNGOS vanilla field 20 (= 0x50) minus the
// 1.12.1 -0x18 unit-fields shift = 0x38. Verified empirically by
// diffing the player descriptor during fishing: 0x38/0x3C went
// `0 → bobber GUID` (high half `0xF110xxxx` = the vanilla
// GameObject-GUID prefix), back to 0 on cast end.
//
// Note: not every channel populates this. The warlock's Ritual of
// Summoning channel leaves it at 0 — that channel binds to the
// participants via the spell, not via UNIT_CHANNEL_OBJECT.
OFF_UNIT_FIELD_CHANNEL_OBJECT = 0x38,
// `UNIT_FIELD_CHANNEL_SPELL` (u32 spell ID the unit is currently
// channeling, or 0). CMaNGOS vanilla field 144 (= 0x240) minus the
// -0x18 shift = 0x228. Verified empirically: the warlock's diff
// shows `0x228 = 0 → 0x2BA` (698 = Ritual of Summoning) for the
// duration of his channel; same value appears on every clicker
// participating in the ritual; fishing populates it with 7620
// (Fishing spell ID). Broadcast UpdateField, so it's readable for
// any visible unit, not just the local player.
OFF_UNIT_FIELD_CHANNEL_SPELL = 0x228,
// Aura arrays in the unit's `m_objectFields` descriptor (at `unit
// + OFF_CGUNIT_OBJECT_FIELDS`). 48 total auras packed as two
// parallel sub-ranges (32 buffs, then 16 debuffs) sharing the
// flags / levels / applications side-arrays indexed by absolute
// slot (0..47). Derived by decompiling `Script_UnitBuff`
// (`0x00519500`) and `Script_UnitDebuff` (`0x005198F0`):
//
// Script_UnitBuff iterates desc[+0xA4 .. +0x124) step 4 (slots 0..31)
// Script_UnitDebuff iterates desc[+0x124 .. +0x164) step 4 (slots 32..47)
// Both read flag nibble at desc[+0x164 + slot/2] >> ((slot&1)*4) & 0xF
// and gate on `(nibble & 0x0E) != 0` (visibility)
// Both read stacks at desc[+0x1AC + slot] and display as `byte + 1`
//
// For our purposes we don't decode individual flag bits — we
// distinguish helpful vs harmful by absolute slot range, and we
// derive `dispelName` from `Spell.dbc[+0x10]` (see
// `OFF_SPELL_DISPEL_TYPE` below), not from the flags nibble.
OFF_UNIT_FIELD_AURA = 0xA4, // u32 spell ID per slot, 48 slots total
OFF_UNIT_FIELD_AURAFLAGS = 0x164, // 4 bits per aura, 2 per byte, covers all 48
OFF_UNIT_FIELD_AURALEVELS = 0x17C, // u8 caster level per aura, 48 bytes
OFF_UNIT_FIELD_AURAAPPLICATIONS = 0x1AC, // u8 (stacks-1) per aura, display value = byte+1
UNIT_AURA_BUFF_COUNT = 32, // slot range 0..31 (helpful)
UNIT_AURA_DEBUFF_COUNT = 16, // slot range 32..47 (harmful)
UNIT_AURA_TOTAL = 48,
UNIT_AURA_VISIBLE_MASK = 0x0E, // nibble mask used by the engine's visibility gate
// Reusable "is this spell record a user-visible aura" predicate.
// `__fastcall(spellRecord*) -> bool`. Checks Spell.dbc Attributes
// (high bit of byte at +0x18 must be clear), AttributesEx bit
// 0x10000000 must be clear, has a non-trivial effect (effects at
// +0x16C..+0x174 must contain at least one not in {0x2C, 0x2D,
// 0x97}), and a mechanic-vs-player-immunity-bitmap gate via
// `+0x2B0`. Both `Script_UnitBuff` and `Script_UnitDebuff` call
// it to filter their aura iteration; we call it the same way.
FUN_SPELL_IS_VISIBLE_AURA = 0x00519860,
// Player-only aura timing tables. Unlike UNIT_FIELD_AURA (which is
// populated for any unit but has no timing info), these are the
// local player's own buffs/debuffs with full duration data — the
// server broadcasts cast/duration only for the player's own auras.
//
// VAR_PLAYER_BUFF_TABLE: 48-entry array, 16 bytes each.
// +0x00 (int) slotCode: -1 = empty; 0..31 = buff entry;
// 32..47 = debuff entry. Doubles as the index
// into the expiration table.
// +0x04 (int) spellID — verified via `Script_GetPlayerBuffTexture`
// at `0x004E4740`, which reads `[entry+0x4]` and
// bounds-checks against `[VAR_SPELL_RECORD_COUNT]`.
// +0x0A (byte) flags: bit 0x1 = cancelable
// +0x0C (int) untalented rank — the value `GetPlayerBuff`
// returns as its second result. Not the spellID.
//
// VAR_PLAYER_BUFF_EXPIRATION_TABLE: parallel `int[]` of absolute
// expiration timestamps in ms. Same epoch as `FUN_OS_TICKCOUNT_MS`
// and (verified) Lua's `GetTime()` (which multiplies tickcount by
// 0.001) — so converting `expirationMs * 0.001` gives an absolute
// seconds-value that's directly comparable to `GetTime()` on the
// Lua side. No epoch reconciliation needed.
//
// Sourced from `Script_GetPlayerBuff` (`0x004E45D0`) /
// `Script_GetPlayerBuffTimeLeft` (`0x004E4930`); the
// `FUN_004E4450(buffID)` inner helper does the
// `expirationTable[entry.slotCode] - currentMs` math we mirror.
VAR_PLAYER_BUFF_TABLE = 0x00BC6040,
VAR_PLAYER_BUFF_EXPIRATION_TABLE = 0x00BC5F68,
PLAYER_BUFF_TABLE_COUNT = 48,
PLAYER_BUFF_ENTRY_STRIDE = 16,
OFF_PLAYER_BUFF_SLOT_CODE = 0x00,
OFF_PLAYER_BUFF_SPELL_ID = 0x04,
// CMSG_CANCEL_AURA sender — `__fastcall(int spellID)`. Builds an
// opcode-0x136 packet (`{opcode=0x136, spellID}`) and ships it.
// Used directly by `CancelSpellByID` / `CancelSpellByName` to
// skip `Script_CancelPlayerBuff`'s client-side gates (the
// per-entry cancelable flag at `[+0x0A]` and the fallback
// AttributesEx `0x04` check) and let the server be the source
// of truth for what's cancelable.
//
// **Crash hazard**: bounds-checks `spellID` against Spell.dbc
// count and sets the record pointer to NULL on OOR, then
// unconditionally dereferences `[record + 0x1C]`. Callers MUST
// pre-validate via `Spell::Lookup::RecordForID(spellID)` (which
// also catches empty record slots) before calling.
FUN_CANCEL_AURA_SEND = 0x006E7040,
// `GetTickCount`-style millisecond counter the engine uses as its
// time source. Same value Lua's `GetTime()` reads (scaled by
// 0.001 to seconds). `__fastcall void → uint32_t` (no args, ms
// tick count in EAX).
FUN_OS_TICKCOUNT_MS = 0x0042B790,
// Spell.dbc `m_durationIndex` field — pointer into SpellDuration.dbc.
// Verified via `FUN_004E44B0` (`0x004e44b0`) and `FUN_006EA000`
// (`0x006ea000`), both of which read `[spellRec + 0x78]` and use
// the result as a SpellDuration.dbc index.
OFF_SPELL_DURATION_INDEX = 0x78,
// Spell.dbc `m_baseLevel` — the spell level used as the anchor in
// `(effLevel - baseLevel) * perLevel + base` scaling. Same offset
// `0x70` as CGUnit's UNIT_FIELD_LEVEL but in a different struct.
OFF_SPELL_BASE_LEVEL = 0x70,
// SpellDuration.dbc — class instance at `0x00C0D820`, records at
// `0x00C0D828`, count at `0x00C0D82C`. Record layout matches the
// SpellCastTimes.dbc shape:
// +0x00 (int) id
// +0x04 (int) base duration ms (signed; negative + perLevel=0
// means "no real duration" / infinite aura)
// +0x08 (int) per-level duration scaling ms
// +0x0C (int) max duration ms (cap)
VAR_SPELLDURATION_RECORDS = 0x00C0D828,
VAR_SPELLDURATION_COUNT = 0x00C0D82C,
OFF_SPELLDURATION_BASE_MS = 0x04,
OFF_SPELLDURATION_PER_LEVEL_MS = 0x08,
OFF_SPELLDURATION_MAX_MS = 0x0C,
// CGPlayer-side sub-struct, allocated for any player-controlled
// unit (local self, target, party, raid, inspect targets — all of
// them). Holds player-specific data that's *not* in the broadcast
// UpdateField descriptor:
//
// +0x08 uint32 PLAYER_FLAGS (AFK = bit 1, DND = bit 2,
// RESTING = bit 5)
// +0x118 + slot*0x30 visible-items table (slots 0..18,
// walked by FUN_UNIT_GET_VISIBLE_ITEM)
//
// Same offset works for any player; the visible-items helper and
// `UnitIsAFK`-style flag readers share the same `[unit + 0xE68]`
// pointer. NPCs / CGCreature_C objects have this slot
// *uninitialized* — reading without a `UNIT_FLAG_PLAYER_CONTROLLED`
// gate is a known crash path (helper does `garbage + 0x118 +
// slot*0x30` then derefs).
//
// Flag bits verified by:
// - `Script_IsResting` (`0x00516EA0`): `[CGPlayer + 0xE68] +
// 0x08`, `shr 5; test 1` → bit 5 = RESTING (0x20).
// - Nameplate AFK renderer (`0x005EC9E0`): `[unit + 0xE68] +
// 0x08`, `test [+8], 2` → bit 1 = AFK (0x02). Works for ANY
// unit, not just local player — confirmed by user testing
// `<AFK>` rendering above other players' heads on stock 1.12.
// - DND bit by symmetry with chat-flag protocol; confirmed in-game.
//
// PLAYER_FLAGS being out-of-band (in this sub-struct rather than a
// broadcast UpdateField) is a vanilla-only quirk — modern WoW
// (3.0+) added PLAYER_FLAGS as a UpdateField at descriptor +0x228.
// In 1.12, descriptor +0x228 is repurposed as UNIT_CHANNEL_SPELL
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
// exists on the +0xE68 sub-struct here.
OFF_CGPLAYER_INFO = 0xE68,
// CGObject's `GetPosition` virtual — vtable slot 5 (offset 0x14).
// Signature: `__thiscall(this, float outBuf[3]) → float *position`.
// The returned float* may equal outBuf (the object filled it
// directly) or point at a cached position field on the object
// (CGPlayer keeps its own copy). Either way, dereference the
// pointer for x/y/z. Used by `Script_CheckInteractDistance` for
// its squared-distance compute; same vtable layout for CGUnit,
// CGPlayer, CGGameObject, etc. since they share the CGObject
// base. Bytes verified via the call-site disassembly at
// `0x0048BACF` / `0x0048BADC`.
OFF_CGOBJECT_VTBL_GET_POSITION = 0x14,
// Quest list array inside the `+0xE68` sub-struct. 20 fixed
// slots of 0xC (12) bytes each — `questID` at slot+0x00, and
// some flags/state in the remaining 8 bytes. Walked by
// `Script_IsUnitOnQuest` (`0x004DFE10`) which iterates by byte
// offset over `[0, 0xF0)` with `0xC` stride and compares each
// entry's `+0` against the target questID. The engine writes
// this from `SMSG_QUESTGIVER_QUEST_DETAILS` / quest sync packets,
// so it covers any synced player-controlled unit (self + nearby
// party / raid in sync range), not just the local player.
//
// **Crash hazard**: same as visible items at `+0x118` — for NPCs
// (CGCreature_C objects) the `+0xE68` slot is uninitialized.
// Callers MUST gate on `UNIT_FLAG_PLAYER_CONTROLLED` before
// dereferencing. The engine's `Script_IsUnitOnQuest` is protected
// because its GUID-by-type filter (`FUN_00468460(0x10, ...)`)
// rejects non-player GUIDs upstream; `FUN_RESOLVE_UNIT_TOKEN`
// doesn't have that filter, so we gate explicitly.
OFF_CGPLAYER_INFO_QUEST_LIST = 0x28,
CGPLAYER_INFO_QUEST_LIST_STRIDE = 0xC,
CGPLAYER_INFO_QUEST_LIST_MAX = 20,
OFF_PLAYER_INFO_FLAGS = 0x08,
PLAYER_FLAG_AFK = 0x02,
PLAYER_FLAG_DND = 0x04,
// Guild-key field on the CGPlayer sub-struct. Verified by
// disassembling `Script_GetGuildInfo` at `0x004C9330`: after
// resolving the unit and checking the GUID-is-player bit, the
// function reads `mov ecx, [edi+0xE68]; mov ecx, [ecx+0x0C]` —
// if the value is 0, returns nil; otherwise passes it as the
// first arg to the guild cache lookup at `0x00560D30`. Two
// players in the same guild end up with the same value here
// (the cache key uniquely identifies a guild record), so
// `UnitIsInMyGuild` can compare this field between `"player"`
// and the input unit when the data is loaded — sidestepping the
// roster fetch requirement entirely for visible units.
//
// Populated immediately for the local player on guild join, and
// for any other player-controlled unit whose `+0xE68` sub-struct
// the engine has synced (party members, raid members, the
// current target, etc.). For unsynced units it reads 0.
OFF_PLAYER_INFO_GUILD_KEY = 0x0C,
// Guild roster — the per-character cache populated by
// SMSG_GUILD_ROSTER. `[VAR_GUILD_ROSTER_PTR]` is `GuildMember **`
// (a heap-allocated array of pointers), indexed `[0..total-1]` where
// total = `[VAR_GUILD_ROSTER_TOTAL_COUNT]`. Includes offline
// members — the "show offline" UI toggle controls what
// `GetNumGuildMembers()` returns by default and how the roster
// panel renders, but the underlying array always holds every
// member the server sent. Entries can still be NULL for other
// reasons (e.g. pending refresh), so null-check per entry.
//
// Verified by disassembling `Script_GetGuildRosterInfo` at
// `0x004D1200`:
// mov ecx, [0x00B73118] ; count
// cmp eax, ecx; jae bail
// mov edx, [0x00B72704] ; roster base
// mov edi, [edx+eax*4] ; entry = roster[idx]
// test edi, edi; je bail
// lea edx, [edi+8]; call lua_pushstring ; name at +0x08 (inline char[])
//
// Other GuildMember fields the engine pushes from the same loop:
// +0x38 int level
// +0x3C int classIndex
// +0x44 int rankIndex
// (We don't read these, but they're verified in the same function.)
//
// The companion `[0x00B7311C]` global is the online-only count
// (returned by `GetNumGuildMembers()` when called with no args).
// The roster array spans the full total — both counts share the
// same backing storage; online vs. total only affects the iteration
// bound the Lua API uses, not the underlying array.
VAR_GUILD_ROSTER_PTR = 0x00B72704,
VAR_GUILD_ROSTER_TOTAL_COUNT = 0x00B73118,
OFF_GUILD_MEMBER_NAME = 0x08,
// PackBagSlot — __fastcall(L, void **outInvMgr, int *outLinearSlot, int *outUnused) → bool.
// Reads bagID at Lua stack[1] and slot at stack[2], validates them, and
// returns the inventory manager + linear slot ready to feed into GetItemBySlot.
FUN_PACK_BAG_SLOT = 0x004F9820,
// Player inventory manager layout — used for direct GUID-array reads
// that bypass `GetItemBySlot`'s bank gate at `0x006228C1`.
// +0x00 uint32 max slot count
// +0x04 uint64* pointer to a flat GUID array, indexed by linear slot
// (8 bytes per slot — low dword + high dword)
// +0x10 uint8 "bank-aware mode" flag (engine-internal; gates the
// slot-range checks in `GetItemBySlot`'s validation
// path)
// Slot ranges, matching `PackBagSlot`'s linearization:
// 0..22 equipment / paperdoll
// 23..38 backpack (16 slots)
// 39..62 main bank (24 slots)
// 63..68 bank bag SLOTS (the bag items themselves; the bag CONTENTS
// live in each equipped bag's own invMgr, reachable via the
// bag's vtable +0x10)
// 81+ keyring
//
// **Bank slots are populated from server data at login** (verified
// empirically on Turtle WoW: fresh login + cleared WDB folder shows
// bank GUIDs already populated in slots 39..62, with the gate at
// `VAR_BANK_GATE_GUID` = 0). The gate doesn't hide data missing —
// it hides data that's present from boot. Reading the GUID array
// directly recovers it without ever opening the bank window.
OFF_INVMGR_GUID_ARRAY = 0x04,
INVMGR_BANK_MAIN_FIRST_SLOT = 39,
INVMGR_BANK_MAIN_LAST_SLOT = 62,
INVMGR_BANK_BAG_FIRST_SLOT = 63,
INVMGR_BANK_BAG_LAST_SLOT = 68,
// Engine's `ObjectMgr::Get`-style resolver — given a type and GUID,
// returns the resolved CGObject pointer (or null). Same function the
// engine itself uses inside `GetItemBySlot` (called at `0x00622904`)
// and `PackBagSlot` (called at `0x004F98E2`). We invoke it directly
// for bank slots so we sidestep the banker-GUID gate that
// `GetItemBySlot` applies for slots 39..68 — the GUIDs in
// `invMgr+0x04` are populated at login, only the gate gets toggled
// by bank-window state.
//
// __fastcall(int type, const char *debugName, u32 guidLo,
// u32 guidHi, int priority) → void *
//
// Type values: 2 = item (returns CGItem*), 4 = container/bag
// (returns CGContainer*). Engine passes `"ItemMgr"` as debugName
// and `0x172` as priority for both call sites we've decoded.
FUN_OBJECT_RESOLVE_BY_GUID = 0x00468460,
// The type arg is a bitmask of object-type bits, not an enum
// index — `1<<1` for items, `1<<2` for containers, `1<<3` for
// units, matching what `FUN_00529FE0` passes for SetUnit (`ECX=8`).
OBJ_TYPE_ITEM = 2,
OBJ_TYPE_CONTAINER = 4,
OBJ_TYPE_UNIT = 8,
OBJ_TYPE_GAMEOBJECT = 0x20, // 1<<5; passed by FUN_0052AA20 (hover-tooltip populator).
// `CGObject::GetName` — returns the display-name `const char *` for
// a resolved CGObject (CGUnit / CGPlayer / CGCreature). Internally
// does a NameCache lookup for players and reads the creature-info
// cache for NPCs; falls back to `"UNKNOWNOBJECT"` / `"Unknown Being"`
// when neither resolves. The same path `FUN_00609370` (the wrapped
// "name + PvP-rank decoration" helper used by the SetUnit tooltip
// builder) calls internally.
//
// __thiscall const char *(void *obj, int *outFlags /* may be 0 */)
//
// Caller-owned `outFlags` receives metadata about the lookup; we
// pass `nullptr` since we just want the name.
FUN_OBJECT_GET_NAME = 0x00609210,
// `CGGameObject_C::GetName` — gameobject-specific name getter.
// Returns `*(obj+0x214)+0x08` (the cached GameObjectStats_C record's
// name field) if the record has loaded, or the engine's empty-string
// sentinel `DAT_00882748` otherwise. Used by the hover tooltip
// populator at FUN_0052AA20.
//
// __fastcall const char *(void *gameObject)
//
// The polymorphic `FUN_OBJECT_GET_NAME` above is unit-cache-only
// (routes through NameCache / creature cache) and returns the
// "UNKNOWNOBJECT" sentinel for gameobjects.
FUN_GAMEOBJECT_GET_NAME = 0x005F8100,
// Bank gate. The engine writes the active banker NPC's GUID here
// when the bank window opens (8-byte qword) and zeroes it on close.
// `GetItemBySlot` returns null for slots 39..68 if this GUID is zero,
// even though the slot data in `invMgr+0x04` remains populated.
// Bypassed in the direct-read bank path; informational only otherwise.
VAR_BANK_GATE_GUID = 0x00BDD038,
// `Script_UseContainerItem` Lua C function — `__fastcall(void *L)`.
// Reads bagID at Lua stack[1] and slot at stack[2], dispatches to the
// engine's item-use machinery (same path the secure
// `tooltip:Click()` / `UseContainerItem(...)` flow takes from
// Lua). We invoke this from `C_Container.UseHearthstone` after
// locating the hearthstone in bags.
FUN_SCRIPT_USE_CONTAINER_ITEM = 0x004FA0E0,
// Vanilla 1.12 hearthstone is always itemID 6948 and uses spell 8690
// ("Hearthstone" - 10-second cast, teleport to bind). Modern WoW
// recognizes many hearthstone-equivalent toys (Garrison, Dalaran,
// etc.) but those items don't exist in 1.12. Custom servers
// (Turtle WoW and similar) sometimes ship alternate hearthstone
// items that reuse spell 8690 as their on-use, so the hearthstone
// matcher OR's `itemID == HEARTHSTONE_ITEM_ID` against
// `OnUseSpellIDForItemID(itemID) == HEARTHSTONE_SPELL_ID` to catch
// both shapes.
HEARTHSTONE_ITEM_ID = 6948,
HEARTHSTONE_SPELL_ID = 8690,
// `Script_PickupContainerItem` Lua C function — `__fastcall(void *L)`.
// Reads bagID at Lua stack[1] and slot at stack[2]; if an item is
// present there, it goes onto the cursor (or, if cursor already holds
// an item, swaps with the bag slot). Used by `EquipItemByName` to
// pick up the source item before dispatching to the equip helpers.
FUN_SCRIPT_PICKUP_CONTAINER_ITEM = 0x004F9B30,
// `CGItem::UseItem` — the engine's actual "use this item" primitive.
// It's the fallback dispatch in `Script_UseContainerItem` (the call
// site at 0x004FA430 after every special-cursor-mode branch is
// skipped), and the function `Script_UseContainerItem` ends up at
// for normal hearthstone-style / potion / food / scroll use. We
// call it directly from `UseItemByName` so we don't pay a
// Lua-stack roundtrip just to re-find the item the engine already
// points us at.
//
// Internally dispatches by item flags / fields:
// - off-target-pickup items (flag 0x200) → FUN_005EDEA0/FUN_005EDD60
// - bind-on-pickup confirmation needed → FUN_004E32E0
// - food items (flag 0x4) → FUN_005EDC80
// - quiver/ammo (flag 0x2000) → FUN_005EEF40
// - default (potions, hearthstone, scrolls, …) → FUN_006E5A90
//
// __thiscall uint(CGItem *item, const uint64_t *targetGuid,
// int flag)
//
// `targetGuid` is a pointer to a 64-bit GUID — for self-use items
// the engine substitutes the player itself even if a different
// target is passed, so passing zero (no target) is fine for most
// items. `flag` is 0 in every Script_UseContainerItem call site.
FUN_ITEM_USE = 0x005D8D00,
// `CGPlayer::AutoEquipCursorItem` — `__thiscall(CGPlayer *this, int flag)`.
// The engine-internal helper that `Script_AutoEquipCursorItem`
// (`0x0048A040`) is a thin wrapper around: that wrapper just
// resolves the local player and calls this with `flag = 0`.
// Equips the cursor item to its natural slot (engine picks based
// on inventory type) and clears the cursor. No-op if cursor is
// empty.
FUN_AUTO_EQUIP_CURSOR_ITEM = 0x005E1480,
// Per-item descriptor block (object/item-field array) lives at this offset
// on the CGItem instance.
OFF_ITEM_DESCRIPTOR = 0x114,
// Within the descriptor, ITEM_FIELD_FLAGS is at +0x3C (a single dword).
// Bit 0 = soulbound, bit 3 = broken (see GetInventoryItemBroken at 0x4C8626
// which tests `[descriptor+0x3C] & 0x08`). Confirmed empirically by dumping
// descriptor bytes for a worn-and-bound item.
OFF_DESCRIPTOR_FLAGS = 0x3C,
ITEM_FLAG_SOULBOUND = 0x01,
// Client-side "in-transaction" lock — set when the engine puts an
// item on the cursor / mail-attaches it / trade-attaches it, cleared
// when the SMSG_UPDATE_OBJECT post-processor (`FUN_005D8440`)
// confirms the transaction OR a cursor-cancel sweep
// (`FUN_00495460`) clears all locks.
//
// This is NOT in m_objectFields — it's a per-CGItem instance flag
// at byte offset 0x314, lowest bit (mask 0x01). Bit 1 of the same
// dword is a separate "cache-coherence" flag set by the SMSG
// post-processor. We only expose bit 0 here.
//
// The matching setter / clearer the engine uses:
// FUN_004953E0(guidLo, guidHi) — set: `[item+0x314] |= 1` + fire ITEM_LOCK_CHANGED
// FUN_00495420(guidLo, guidHi) — clear: `[item+0x314] &= ~1` + fire ITEM_LOCK_CHANGED
// FUN_00495460() — sweep: walk all bags/items, clear all locks
//
// Vanilla fires `ITEM_LOCK_CHANGED` (engine event 0xBC = 188) on
// every set / clear; no payload.
OFF_ITEM_CLIENT_LOCK = 0x314,
ITEM_CLIENT_LOCK_BIT = 0x01,
// `__stdcall(uint guidLo, int guidHi)` — sets bit 0 at `[item+0x314]`
// for the resolved item and fires ITEM_LOCK_CHANGED. Mirror of
// FUN_ITEM_UNLOCK_BY_GUID below. Engine's normal trigger is the
// optimistic-lock step inside pickup/equip/attach paths (cursor
// packets are sent immediately after).
FUN_ITEM_LOCK_BY_GUID = 0x004953E0,
// `__stdcall(uint guidLo, int guidHi)` — clears bit 0 at
// `[item+0x314]` for the resolved item and fires ITEM_LOCK_CHANGED.
// Resolves the item by GUID internally via FUN_00468460; safe to
// call with the GUID of an item that's already gone (event still
// fires but no-op on state). The engine's normal trigger for this
// is the SMSG_UPDATE_OBJECT post-processor (`FUN_005D8440`) when
// the server confirms a transaction.
FUN_ITEM_UNLOCK_BY_GUID = 0x00495420,
// `__cdecl()` — walks every CGItem the engine knows about (bag
// containers + their contents) and clears the lock bit on each.
// Fires a single ITEM_LOCK_CHANGED at the end. The engine itself
// only calls this from the PLAYER_LEAVING_WORLD cleanup path
// (`FUN_005FEF70` → fires event 0x112 right after); we expose it
// to addons as a stuck-lock recovery primitive.
FUN_ITEM_UNLOCK_ALL = 0x00495460,
// ITEM_FIELD_STACK_COUNT — single dword. Verified by decoding
// `Script_GetContainerItemInfo` (`0x004F9670`): after resolving the
// descriptor, `mov eax, [esi+0x114]; fild [eax+0x20]` is the count
// return. Field index 0x8 in 1.12's item-field layout, which puts
// STACK_COUNT *before* the contained/creator GUIDs (0x9..0xE) —
// different from the more common documented layout that places
// STACK_COUNT after them. Trust the binary, not external docs.
OFF_DESCRIPTOR_STACK_COUNT = 0x20,
// ITEM_FIELD_SPELL_CHARGES[0] — first of five signed dwords. Field
// indices 10..14 (fields 8..9 are STACK_COUNT and DURATION; field 15
// is FLAGS at +0x3C, which gives a tight sandwich around the
// SPELL_CHARGES range). Derived from the descriptor-field name table
// builder at `FUN_0047f840`, which consumes a list of `{ name_ptr,
// ?, count, ?, ? }` entries starting at `0x0083a328` (14 items) —
// summing the `count` field in declaration order gives each name's
// starting dword index. STACK_COUNT/FLAGS/DURABILITY/MAX_DURABILITY
// landing on their known offsets cross-checks the derivation. The
// tooltip "X Charges" text in `FUN_0052b650` reads from the
// *enchantment*-charges range at +0x48 (slot N + 0x8) which is a
// different field (ITEM_FIELD_ENCHANTMENT, fields 16..36).
//
// Value is signed: positive = rechargeable (wand recovers charges
// on use? no — they decrement; positive means "doesn't destroy on
// last use"). Negative = single-use, destroyed when count hits 0.
// `GetItemCount(includeUses=true)` uses abs() for the multiplier.
OFF_DESCRIPTOR_SPELL_CHARGES_0 = 0x28,
// ITEM_FIELD_DURABILITY (current) and ITEM_FIELD_MAXDURABILITY (max) live
// adjacent to each other in the descriptor as plain dwords. Verified in
// `Script_GetInventoryItemBroken` (`0x004C8590`): after resolving the
// descriptor, it reads `[ecx+0xA4]` for max and `[eax+0xA0]` for cur,
// and treats the item as broken when `max > 0 && cur == 0`. Items with
// no durability concept (consumables, materials) have both fields 0.
OFF_DESCRIPTOR_DURABILITY = 0xA0,
OFF_DESCRIPTOR_MAX_DURABILITY = 0xA4,
// Per-item repair-cost helper used by Script_GameTooltip_SetInventoryItem
// (3rd return value). __fastcall(ecx = CGItem *) -> int copperCost.
// Returns 0 for null/broken/no-durability/fully-repaired items, otherwise
// the cost in copper, with the merchant discount applied IFF a merchant
// window is currently open.
//
// Internals: FUN_005DA330 is the raw calculator; FUN_004FAF30 is the
// discount wrapper that calls it. The raw side:
// 1. Look up the item's ItemStats record via DBCache_ItemStats_C_GetRecord
// (callback=NULL → returns NULL for uncached items).
// 2. Index DurabilityCosts.dbc by `subClass` to get the per-class row.
// 3. Branch on inventoryType: weapon (2) uses cols at row+4..row+0x54,
// armor (4) uses cols at row+0x58..row+0x78.
// 4. Multiply by DurabilityQuality.dbc's quality multiplier (indexed
// by `quality * 2 + 1`).
// 5. Round to nearest int; clamp 0 → 1 if there was any cost at all.
//
// The discount wrapper:
// - Resolves the local player object via FUN_00468550 (returns the
// player's GUID from [0x00B41414 + 0xC0]).
// - Resolves the current-merchant object via the GUID stored in
// DAT_00BDDFA0 / DAT_00BDDFA4. Those globals are set by
// FUN_004FACF0 on SMSG_LIST_INVENTORY (merchant frame opens) and
// zeroed by FUN_004FAC50 when the merchant frame closes.
// - If both lookups succeed, calls FUN_00612B80(merchant, player)