-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathOffsets.h
More file actions
6210 lines (5856 loc) · 345 KB
/
Copy pathOffsets.h
File metadata and controls
6210 lines (5856 loc) · 345 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 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 General Public License for more details.
//
// You should have received a copy of the GNU 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,
// Fatal-error dispatcher. `__fastcall(uint code)`. Writes `code`
// to `DAT_00882738` and chains into the process-teardown path;
// `FUN_00403BC0` reads the code at exit and shows a localized
// popup keyed off it. Code `10` = "interface files are corrupt"
// — the engine fires this from FrameXML's `FUN_0048FBF0` hash
// mismatch and per-addon `FUN_0051F240` hash mismatch (plus
// other call sites we haven't pinned down by byte pattern).
// Hooked by `Security::FrameXMLBypass` to suppress code 10 so
// our in-memory file modifications (Bindings.xml splice +
// embedded `!!!ClassicAPI` addon) don't trigger termination.
FUN_FATAL_ERROR = 0x00401560,
// 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,
// CGameTooltip line pool (see TODO #97). The engine caps AddLine
// (FUN_00530270) at `numLinesAllocated`, which the XML-OnLoad scan
// (FUN_00529650, vtable slot 9) sets to the count of contiguous
// `<name>TextLeftN`/`TextRightN` FontStrings the template declares
// (30). Tooltip::LinePool grows it: it appends its own C++-created
// FontStrings to the three parallel arrays and bumps this count. Each
// array is a {count@+0x0, cap@+0x4, data@+0x8} descriptor; the
// reallocators realloc `data` to newCount*4 and copy `cap` old elements.
VAR_GAMETOOLTIP_VTABLE = 0x00808F60, // type guard for a resolved tooltip object
OFF_GAMETOOLTIP_NUM_LINES_ALLOC = 0x320, // int — pool size AddLine gates on
OFF_GAMETOOLTIP_TEXTLEFT_DESC = 0x324, // {count,cap,data}; data (+0x8) = CSimpleFontString*[]
OFF_GAMETOOLTIP_TEXTRIGHT_DESC = 0x330, // symmetric right column
OFF_GAMETOOLTIP_WRAPFLAG_DESC = 0x33C, // per-line wrap int[]
FUN_TOOLTIP_REALLOC_PTR_ARRAY = 0x00536C80, // __thiscall(desc, newCount) — pointer arrays
FUN_TOOLTIP_REALLOC_INT_ARRAY = 0x004368C0, // __thiscall(desc, newCount) — wrap-flag array
// GameTooltip:SetHyperlinkCompareItem (Tooltip::Compare) — builds the
// equipped item's tooltip natively (no Lua roundtrip) and appends
// colored stat deltas. FUN_GAMETOOLTIP_BUILD_ITEM is the engine's own
// item-tooltip builder that SetInventoryItem/SetHyperlink call:
// __thiscall(self, itemID, guid*, guid*, a4, a5, a6, headerFlag, a8, a9)
// Proven arg shape from FUN_005353b0/FUN_00535700 (all flags 0 → a clean
// item tooltip). The builder's own headerFlag (param_7) path merges the
// "Currently Equipped" text into the name line and force-wraps it, so we
// build clean (flag 0) and prepend the grey header ourselves by shifting
// the line FontStrings — see FUN_FONTSTRING_SET_* / OFF_FONTSTRING_*.
// FUN_GAMETOOLTIP_ADD_LINE is the raw add-line body (see LinePool notes);
// its color args are pointers to a 4-byte packed color {b,g,r,a} — the
// Lua AddLine handler (FUN_00531630) builds it as 0xFF<rr><gg><bb>, i.e.
// uint32 `0xFF000000 | r<<16 | g<<8 | b` in little-endian memory.
FUN_GAMETOOLTIP_BUILD_ITEM = 0x0052B650,
// OnTooltipSetItem support. FUN_GAMETOOLTIP_SCRIPT_RESOLVER is the
// CGGameTooltip vtable method (vtable 0x00808F60) that maps a script name
// to its handler slot; __thiscall(self, const char *name) -> int* slot,
// 0 if the name isn't a tooltip script. It first delegates to the base
// frame resolver, then checks OnTooltipSetDefaultAnchor(+0x444) /
// OnTooltipCleared(+0x44c) / OnTooltipAddMoney(+0x454) — each an 8-byte
// {handler, context} slot. Vanilla has no OnTooltipSetItem, and the object
// (alloc size 0x460) has no free 8-byte slot, so we co-hook this to hand
// out a C-side per-tooltip cell for that name. FUN_FRAME_INVOKE_SCRIPT is
// the engine's real script invoker __fastcall(handler /*ecx = slot[0]*/,
// frame /*edx*/): it binds the global `this` = frame and runs the handler
// under its own protected lua_pcall (0 args). We call it directly from the
// item-builder co-hook to fire OnTooltipSetItem.
// NOTE: the clear fires OnTooltipCleared via the wrapper FUN_00702690
// (self, &slot), which additionally stamps the global exec-context
// (DAT_00ceeac0) from slot[1]. That context mutation is only valid from
// the engine's top-level frame-script dispatch — firing it from deep in a
// nested Lua->C->Lua stack (e.g. an addon's SetAuctionItem wrapper)
// corrupts the outer script's context and faults. So we skip the wrapper
// and call the inner invoker, which is self-contained.
FUN_GAMETOOLTIP_SCRIPT_RESOLVER = 0x005295D0,
FUN_FRAME_INVOKE_SCRIPT = 0x00704D50,
// The arg-passing sibling of FUN_FRAME_INVOKE_SCRIPT — every frame
// script that carries values (OnClick(button), OnMouseWheel(delta),
// OnUpdate(elapsed), OnValueChanged(value), OnKeyDown(key), OnEvent's
// args, …) funnels through here. __cdecl(int handlerRef, void *frame,
// const char *fmt, void *vaPtr): fmt is a printf-style spec string
// (`%d`/`%u`/`%f`/`%s`), vaPtr points at the packed vararg buffer
// (4-byte stride for d/u/s, 8 for f). It sets the `arg1..argN` globals
// from the format (saving/restoring the previous values), sets the
// `this` global to `frame`, then runs the handler under a protected
// lua_pcall with **zero Lua args** and the engine error handler
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF) as errfunc. FUN_FRAME_INVOKE_SCRIPT
// is the same shape for the no-arg scripts (OnShow/OnHide/OnEnter/…).
// `Frame::ScriptArgs` co-hooks both to additionally pass the handler
// modern positional args (self, arg1..argN). The frame's own Lua object
// ref lives at frame+OFF_COBJECT_LUA_REF (refcount at
// OFF_COBJECT_LUA_REFCOUNT); FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER
// lazily creates it when the refcount is 0.
FUN_FRAME_RUN_SCRIPT_ARGS = 0x00704F10,
// The base ScriptObject's OnEvent handler slot — an 8-byte
// {handler, context} pair at frame+0x0C (from FUN_00702590, the base
// resolver every frame-type resolver chains through, and confirmed by
// the event dispatchers FUN_00703E50 / FUN_00703F50 passing `frame+0xC`
// as the slot). `*(int*)(frame + OFF_FRAME_ONEVENT_SLOT)` is the OnEvent
// handler ref; `Frame::ScriptArgs` compares it against the handler ref
// the runner is invoking to detect an OnEvent dispatch (exact — each
// SetScript makes a distinct ref, so a function bound to two scripts
// still differs per slot) and prepend the `event` positional.
OFF_FRAME_ONEVENT_SLOT = 0x0C,
// Registry ref (an int, not a pointer) to the frame-script message
// handler the engine passes as the `errfunc` to every script pcall —
// read as `*(int*)VAR_FRAMESCRIPT_ERROR_HANDLER_REF`, pushed via
// lua_rawgeti(REGISTRY, ref). Set once at engine init.
VAR_FRAMESCRIPT_ERROR_HANDLER_REF = 0x008722C8,
// The base Frame script-name resolver — __thiscall(frame, const char *name)
// -> int* slot, 0 for an unknown name. Maps the standard base-frame scripts
// to their 8-byte {handler, context} slots on the frame (OnLoad@+0x118,
// OnUpdate@+0x128, OnEnter@+0x140, … OnKeyUp@+0x190) after delegating to the
// ScriptObject base FUN_00702590 (OnEvent@+0xC). Every frame TYPE's resolver
// chains through this (the tooltip resolver above calls it first), so
// co-hooking it here lets `Frame::Attributes` make `OnAttributeChanged`
// SetScript/GetScript/HookScript-able on ALL frames — for that one name the
// co-hook hands back an external per-frame cell (frames are immortal in 1.12,
// so a pointer-keyed cell never goes stale). Same pattern + ABI modeling as
// FUN_GAMETOOLTIP_SCRIPT_RESOLVER.
FUN_FRAME_SCRIPT_RESOLVER = 0x0076A0D0,
// The other per-object tooltip builders, co-hooked the same way as
// FUN_GAMETOOLTIP_BUILD_ITEM to back OnTooltipSetSpell / OnTooltipSetUnit /
// OnTooltipSetGameObject (see Tooltip::SetEvents). Each is the single funnel
// its Set*/mouseover paths converge on and clears the tooltip
// (FUN_00530050) before repopulating, so firing after it covers every way
// that object type gets set:
// - Spell FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP (0x0052E610, 7 stack args,
// RET 0x1c) — SetSpell/SetSpellByID/SetTalent/SetShapeshift funnel.
// - Unit (0x00529fe0, __thiscall(self, guid*), RET 4) — SetUnit +
// the two engine mouseover paths (FUN_004919d0 / FUN_00492890).
// Returns an int the caller (Script_GameTooltip_SetUnit) tests, so the
// co-hook must forward the original's return value.
// - GameObject (0x0052aa20, __thiscall(self, guid*), RET 4) — the GO
// hover populator; resolves the GO (TYPEMASK_GAMEOBJECT) and writes the
// GO field at +0x370.
FUN_GAMETOOLTIP_BUILD_UNIT = 0x00529FE0,
FUN_GAMETOOLTIP_BUILD_GAMEOBJECT = 0x0052AA20,
// Pointer to the GameTooltip CFrameScriptObject the engine's mouseover
// setter builds into (`DAT_00b4b3c4`; the value at this address is the
// tooltip object). `Frame::Attributes` reads it to build the unit tooltip
// for offline/out-of-range party & raid members, which FUN_00492890 skips.
VAR_GAMETOOLTIP_OBJECT_PTR = 0x00B4B3C4,
// The tooltip's OnTooltipSetDefaultAnchor handler slot (the `{handler,
// context}` pair the script resolver maps that name to). The engine
// mouseover setter fires this before building to (re)establish the
// tooltip owner/anchor via FrameXML's GameTooltip_SetDefaultAnchor —
// without it a rebuild after the tooltip was hidden has no owner and
// stays invisible. `Frame::Attributes` fires it (via the self-contained
// FUN_FRAME_INVOKE_SCRIPT) before the offline roster build.
OFF_TOOLTIP_SET_DEFAULT_ANCHOR_HANDLER = 0x444,
FUN_GAMETOOLTIP_ADD_LINE = 0x00530270, // __thiscall(self, left, right, lColorBGRA*, rColorBGRA*, wrap)
OFF_GAMETOOLTIP_NUM_LINES = 0x31C, // int — live line count (AddLine index; +0x320 is the cap)
// The displayed item's identity is read via the existing
// OFF_TOOLTIP_ITEM_ID (+0x398) / OFF_TOOLTIP_ITEM_GUID_LO (+0x380) —
// link paths set the itemID, CGItem paths (SetInventoryItem/…) set only
// the GUID (see Tooltip::Compare::TooltipItemID and item/Tooltip.cpp).
// Line-shift primitives for prepending the grey "Currently Equipped"
// header. The per-line CSimpleFontString objects live in the parallel
// arrays at OFF_GAMETOOLTIP_TEXTLEFT/RIGHT_DESC (+8 = data). Each stores
// its text buffer pointer at +0xF0 and its 4-byte {b,g,r,a} color behind
// the pointer at +0xB8 (both verified from the setters below). We read a
// line's text/color and re-apply them one slot down, then set line 0.
FUN_FONTSTRING_SET_TEXT = 0x00771D80, // __thiscall(fs, text, flag=0)
FUN_FONTSTRING_SET_COLOR = 0x0077F750, // __thiscall(fs, colorBGRA*)
OFF_FONTSTRING_TEXT = 0xF0, // char* — current text buffer
OFF_FONTSTRING_COLOR_PTR = 0xB8, // ptr → 4-byte {b,g,r,a} color storage
// A line's FontString is only positioned once shown: set the desired
// flag at +0xC4 then call SHOW (FUN_0077fcb0) / HIDE (FUN_0077fc60) —
// the exact pair AddLine (FUN_00530270) and the clear use. Moving text
// into a previously-empty (hidden) cell without this leaves it
// unpositioned, so the line-shift must replicate it.
FUN_FONTSTRING_SHOW = 0x0077FCB0, // __fastcall(fs) — realizes +0xC4
FUN_FONTSTRING_HIDE = 0x0077FC60, // __fastcall(fs)
OFF_FONTSTRING_SHOWN_FLAG = 0xC4, // int — desired-shown flag
// The per-tooltip clear only HIDES line FontStrings (sets +0xC8 = 0)
// and clears the left text; right-column text buffers are left intact.
// So a shift must decide a cell's content by its actually-shown flag
// (+0xC8), not by whether +0xF0 still holds (stale) text.
OFF_FONTSTRING_VISIBLE = 0xC8, // int — actually-shown flag
// FontString creation primitives used by Tooltip::LinePool to build the
// extra lines entirely in C++ (no addon Lua). Calling conventions and
// offsets mirrored from Script_CreateFontString (0x00773C30),
// Script_SetFontObject (0x0079D1A0 → 0x00770C60), and Script_SetPoint
// (0x007A2540 → 0x00767C70) — see TODO #97's investigation update.
FUN_GAMETOOLTIP_SETUP_LINES = 0x00529650, // vtable slot 9 — the pool scan we co-hook
VAR_SIMPLEFONTSTRING_POOL = 0x00CF4D10, // CSimpleFontString free-list pool (allocator `this`)
VAR_SIMPLEFONTSTRING_CLASS_TAG = 0x00846544, // ".?AVCSimpleFontString@@" (alloc debug tag)
FUN_REGION_POOL_ALLOC = 0x00760450, // __thiscall(pool, zeroInit=0, tag, line=-2) -> raw mem
FUN_SIMPLEFONTSTRING_CTOR = 0x00770D30, // __thiscall(mem, parent, layer, sublayer) -> fs
FUN_REGION_SET_NAME = 0x0076C650, // __thiscall(region, name) — registers _G[name]
FUN_FONTSTRING_SET_FONT = 0x00770C60, // __thiscall(fs + OFF_FONTSTRING_FONT_HOLDER, fontObject)
FUN_REGION_SET_POINT = 0x00767C70, // __thiscall(region+OFF_REGION_ANCHOR, point, relAnchor, relPoint, x, y, flag=1)
OFF_FONTSTRING_FONT_HOLDER = 0xCC, // font-reference sub-object (SetFont `this`)
OFF_FONTSTRING_FONT_OBJECT = 0xD0, // font object pointer (holder + 4)
OFF_REGION_ANCHOR = 0x24, // LayoutFrame anchor sub-object (SetPoint `this` / relativeTo base)
DRAWLAYER_ARTWORK = 2,
FRAMEPOINT_TOPLEFT = 0,
FRAMEPOINT_LEFT = 3,
FRAMEPOINT_RIGHT = 5,
FRAMEPOINT_BOTTOMLEFT = 6,
// FUN_REGION_SET_POINT stores offsets in *internal* coordinates, not
// pixels. Script_SetPoint converts: `internal = pixel * [0x00832A44] /
// ([0x00832A4C] * 1024)` (FUN_0041ae60's factor × input ÷
// (FUN_0041ad70's return × DAT_007ffd68)). Both globals are runtime
// UI-scale floats; passing raw pixels makes offsets ~1000× too large.
VAR_UI_COORD_SCALE_MUL = 0x00832A44, // float numerator
VAR_UI_COORD_SCALE_DIV = 0x00832A4C, // float denominator base
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
// "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 (FUN_GAMETOOLTIP_CLEAR) zeroes unit(+0x368)/GO(+0x370)/
// itemID(+0x398)/spell(+0x39c) — but NOT the item GUID +0x380/+0x384.
// That omission means the item GUID goes stale across a switch to a
// tooltip that shows no item (SetUnitAura, SetEquipmentSet, …), so
// GameTooltip:GetItem would resolve the *previous* item. We co-hook
// the clear (Tooltip::ClearItemGuid) to zero +0x380/+0x384 too, so
// every fresh tooltip resets it and only the item builder re-sets it.
// All 14 Set*/builder paths route through this clear, so the co-hook
// covers them uniformly. __fastcall(self).
FUN_GAMETOOLTIP_CLEAR = 0x00530050,
OFF_TOOLTIP_ITEM_GUID_LO = 0x380, // 0 for SetItemByID (no CGItem); stale-safe via the clear co-hook
OFF_TOOLTIP_ITEM_GUID_HI = 0x384,
OFF_TOOLTIP_ITEM_ID = 0x398,
OFF_TOOLTIP_SPELL_ID = 0x39C,
// Auction/compare descriptor. Script_GameTooltip_SetAuctionItem
// (0x00535810) has no CGItem instance, so it stashes the listing's
// random-property (suffix) id at tooltip+0x424 and marks the
// compare-descriptor valid by writing the builder's compare flag to
// this[0x110] (tooltip+0x440) = 1 (SetAuctionItem passes param_6=1;
// every other Set* path passes 0, and the builder writes it on each
// param_7==0 build — so +0x440 is a reliable per-build gate, not
// stale). This is 1.12's analog of 3.3.5's `tooltip[0x130]`-gated
// `piVar5[0x2d]` random-property read in GameTooltip:GetItem. GetItem
// reads +0x424 (only when +0x440 is set) to put the suffix in the link.
OFF_TOOLTIP_COMPARE_FLAG = 0x440,
OFF_TOOLTIP_COMPARE_SUFFIX = 0x424,
// 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,
// CGItem → decorated instance display name. __thiscall(ecx = CGItem *,
// char *outBuf, uint outSize). Reads the instance's random-suffix ID
// off the descriptor (+0x98, gated on the broken flag) and writes the
// plain — uncolored, unbracketed — display name into outBuf:
// "Ethereum Torque of the Sorcerer" when a suffix is present, the base
// ItemStats name otherwise (via the item cache + ItemRandomProperties
// localized suffix at record +0x1c). This is the inner name builder
// FUN_GAMETOOLTIP_BUILD_ITEM_LINK wraps in brackets, so the name it
// produces matches GetItemLink's bracketed name and modern
// C_Item.GetItemName exactly. Inner formatter FUN_005D8B00.
FUN_ITEM_BUILD_INSTANCE_NAME = 0x005D8BC0,
// Inner name formatter behind FUN_ITEM_BUILD_INSTANCE_NAME, callable
// WITHOUT a CGItem: `__fastcall(char *out /*ecx*/, uint outSize /*edx*/,
// uint32 itemID, int suffixID)`. Fetches the ItemStats record for itemID
// and, when suffixID is a valid ItemRandomProperties row with a localized
// suffix name (record +0x1c + locale*4), formats base + suffix via
// ITEM_SUFFIX_TEMPLATE; otherwise the base name. Lets the by-STRING item
// paths (GetItemNameByID, C_Item.GetItemInfo) apply an item link's random
// suffix without a live item instance.
FUN_ITEM_BUILD_NAME_FROM_ID = 0x005D8B00,
// 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,
// `this+0x08` — the CObject's Lua-registry ref (an int key into the
// registry table). `lua_rawgeti(REGISTRY, this[+0x08])` pushes the
// frame's Lua-side object. Lazily populated by ScriptRegister when the
// refcount above is 0.
OFF_COBJECT_LUA_REF = 0x08,
// 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,
// Internal CVar registrar — what `Script_RegisterCVar` calls after a
// `FindCVar` miss (the call at `0x00488B8A`). `__fastcall`; ECX=name,
// EDX is a second string slot the script path leaves 0, then six
// stack args: flags, defaultValue, changeCallback, categoryId,
// hiddenBool, userData. Dedups by name (re-registering OR-merges
// flags + updates the callback), so it's idempotent across `/reload`.
// The new-cvar branch forces flags bit 0 on (`puVar3[7] = flags | 1`),
// which is what makes script-registered cvars persist to Config.wtf.
// See `CVar::Factory` for the wrapper.
FUN_REGISTER_CVAR = 0x0063DB90,
// Internal "apply value" — `__fastcall(cvar /*ecx*/, value, a3, a4,
// a5, a6)`, called by `Script_SetCVar` at `0x00488CA8`. Fires the
// change callback at `[cvar+0xBC]` as
// `char __fastcall(cvar /*ecx*/, prevValue /*edx*/, newValue /*stack*/,
// userData /*stack*/)` — return 0 to REJECT the change, nonzero to
// accept (verified at `0x0063DF7D`).
FUN_SET_CVAR_VALUE = 0x0063DF50,
// CVar struct fields (beyond the value string at +0x20 above).
OFF_CVAR_FLAGS = 0x1C,
OFF_CVAR_CALLBACK = 0xBC,
OFF_CVAR_USERDATA = 0xC0,
// The categoryId the script path passes as the registrar's 6th stack
// arg (the `PUSH 9` in `Script_RegisterCVar`). Cosmetic console
// grouping; we reuse it so our cvars behave like script-registered ones.
CVAR_SCRIPT_CATEGORY = 9,
// 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,
// Set the player's current target to a GUID —
// `__fastcall(uint64_t *guid)`. Validates the GUID resolves to a unit
// (typemask 8) then commits the selection (CMSG_SET_SELECTION +
// client-side target). This is what `Script_TargetUnit` (0x004899D0)
// calls after resolving its token→GUID; we call it directly with a
// GUID we already have (e.g. a totem creature). NOTE: reads `guid[0]`
// (lo) and `guid[1]` (hi) through the pointer.
FUN_TARGET_BY_GUID = 0x00489A40,
// Tab-targeting internals, shared with our backported TargetNearest* /
// TargetDirection* family (`target/Nearest.cpp`).
//
// `FUN_TARGET_CANDIDATE_VALID` (0x00493E40) — the engine's per-mode
// candidate validity predicate used by the native `TargetNearestEnemy`
// / `TargetNearestFriend` cycle. `__fastcall(CGUnit *player /*ecx*/,
// CGUnit *candidate /*edx*/, int mode) → int` (nonzero = valid). Modes:
// 1 = enemy (attackable + alive + not-critter), 2 = friend (assistable +
// alive), 3 = party, 4 = raid. Wraps the reaction cores
// `FUN_00606980` (can-attack) / `FUN_006066f0` (can-assist), so calling
// it gives us the engine's exact hostility semantics for free.
FUN_TARGET_CANDIDATE_VALID = 0x00493E40,
// `FUN_SET_SELECTION_BY_GUID` (0x00493540) — canonical target setter the
// native tab-cycle commits through. `__cdecl(uint32_t guidLo, uint32_t
// guidHi)`. Validates + fires CMSG_SET_SELECTION + client target +
// event 0xC3, and maintains the current-selection globals below.
// Passing {0,0} clears the target.
FUN_SET_SELECTION_BY_GUID = 0x00493540,
// Current target-selection GUID (lo/hi), written by
// `FUN_SET_SELECTION_BY_GUID`. We read it to detect whether the player's
// target changed out from under an in-progress tab-cycle.
VAR_CURRENT_SELECTION_GUID_LO = 0x00B4E2D8,
VAR_CURRENT_SELECTION_GUID_HI = 0x00B4E2DC,
// 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. Also a NULL-check proxy for "is
// the object manager initialised" — engine iterators deref this
// unconditionally on entry, so callers must skip on the glue /
// character-select screen.
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,
// `__fastcall(const uint64_t *guid /*ecx*/, int *outCount /*edx*/) ->
// char** tokenArray` — the engine's GUID → unit-token REVERSE map. Fills
// a reused static buffer array (the returned pointer) with the name
// string of every ENGINE-NATIVE token currently pointing at `guid` and
// writes the count to `*outCount`. Checks, in order: player, pet, target,
// party1..4, partypet1..4, raid1..40, raidpet1..40, npc, mouseover — the
// same slots the per-token unit-event broadcast `FUN_00515e50` fans out
// to (that function is just this map + a fire-per-token loop). Does NOT
// know ClassicAPI's synthetic `focus` / `nameplateN` tokens (the engine
// has no concept of them) — callers add those separately. Non-throwing:
// returns count 0 pre-world (resolves the active player internally and
// bails if absent). The returned array is a shared static reused on the
// next call, so snapshot the strings before doing anything that could
// re-enter. nampower names this `GetNamesFromGUID` (left as an unfilled
// TODO in its offsets.hpp). Used by `Unit::Identity::TokensForGUID` for
// the phase-2 `UNIT_SPELLCAST_*` remote-unit fan-out.
FUN_UNIT_TOKENS_FROM_GUID = 0x00515C50,
// Mouseover-unit GUID globals — the `"mouseover"` token resolves to
// these (see the token-dispatch list above). Written ONLY by
// `FUN_SET_MOUSEOVER_UNIT`; zero when nothing is moused over. Used by
// `Unit::Mouseover` to detect the loss transition.
VAR_MOUSEOVER_GUID_LO = 0x00B4E2C8,
VAR_MOUSEOVER_GUID_HI = 0x00B4E2CC,
// The engine's single mouseover set/clear chokepoint —
// `__stdcall(guidLo, guidHi, arg3, arg4)` (early returns are all
// `ret 0x10`, and the caller pushes 4 without cleaning). It is the
// ONLY writer of `VAR_MOUSEOVER_GUID_*`, so every mouseover
// gain/loss/change flows through it. On a unit gain it fires
// `UPDATE_MOUSEOVER_UNIT` itself (via `FUN_FIRE_EVENT_NO_ARGS(0x150)`
// in the friendly/hostile-unit tooltip case); on loss it clears the
// GUID but fires nothing — the gap `Unit::Mouseover` closes.
// Change-gated by its callers (if it ran per-frame the gain fire
// would spam every frame while hovering, which vanilla doesn't), so
// it's a cool hook target.
FUN_SET_MOUSEOVER_UNIT = 0x00492890,
// Event ID of `UPDATE_MOUSEOVER_UNIT` (name-table slot
// `0x00BE16D8` → `(0x16D8 - 0x1198) / 4`). Fired with no args via
// `FUN_FIRE_EVENT_NO_ARGS`; Lua handlers read `UnitExists("mouseover")`.
EVENT_UPDATE_MOUSEOVER_UNIT = 0x150,
// `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,
// Owner/controller GUID fields within `m_objectFields`, each a 64-bit
// GUID (lo/hi). Both byte-verified from engine readers:
// CHARMEDBY +0x10 — `Script_UnitIsCharmed`
// (`mov ecx,[eax+0x10]; or ecx,[eax+0x14]; jz`).
// CREATEDBY +0x20 — the unit owner/title builder `FUN_0052FD30`
// reads `[desc+0x20]`/`[+0x24]` as the summoner GUID.
// A pet/guardian/totem's owner lives in CREATEDBY; a charmed unit's in
// CHARMEDBY. `Unit::Pet` reads these to find a minion's player owner.
// (The engine uses only these two for ownership — there's a GUID slot
// at +0x18 too, but nothing reads it as an owner, so it's left out.)
OFF_UNIT_FIELD_CHARMEDBY = 0x10,
OFF_UNIT_FIELD_CREATEDBY = 0x20,
// Pet-vs-minion discriminator for an owned unit: `int __fastcall(unit)`.
// `0x00605570` is really the engine's **creature-type resolver** — it's
// `Script_UnitCreatureType`'s (`0x0051A280`) inner helper and returns the
// `CreatureType.dbc` id (1=Beast, 3=Demon, 7=Humanoid, …), via three
// paths in order: a display-override at descriptor `+0x212`, then the
// creature-cache type at `[unit+0xB30]+0x18`, then the player-race type
// at `ChrRaces[[unit+0x110]+0x78]+0x24`. `FUN_UNIT_CREATURE_TYPE` is the
// canonical name; `FUN_UNIT_PET_MINION_CLASS` is a historical alias for
// the same address.
//
// The pet/minion use: the unit-title builder `FUN_0052FD30` calls it as
// `(fn(unit) != 1) + 1` → 1 = "X's Pet" title, 2 = "X's Minion". That
// works because it's really testing "is this a Beast (type 1)": hunter
// pets are Beasts, warlock minions are Demons (type 3) — so `!= 1`
// cleanly separates them, which neither the GUID (pets and guardians
// share the 0xF14… prefix) nor UNIT_FLAG_PLAYER_CONTROLLED reliably does.
FUN_UNIT_PET_MINION_CLASS = 0x00605570,
FUN_UNIT_CREATURE_TYPE = 0x00605570,
// 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.
// +0x00 = u32 slot count (OFF_INVMGR_SLOT_COUNT), +0x04 = u64* GUID
// array (see OFF_INVMGR_GUID_ARRAY below for the slot-range map).
OFF_PLAYER_INVENTORY_MANAGER = 0x1D38,
OFF_INVMGR_SLOT_COUNT = 0x00,
// --- Descriptor-field observer system --------------------------------
// The engine's generic "watch a descriptor field range for changes"
// mechanism (nodes tagged "__AUCMirrorHandler__", 0x30 bytes). The
// registrar attaches a node to the object's per-field observer anchors;
// each node keeps a private MIRROR of the watched bytes, seeded from
// the live value at registration. During SMSG_UPDATE_OBJECT processing
// the dispatcher FUN_00465570 memcmps live vs mirror and invokes the
// callback only on a real change (the mirror is then re-synced by
// FUN_004667A0). Firing engine events from the callback is sanctioned —
// the engine's own bag observer does exactly that.
//
// Registrar — __fastcall(int bank /*ecx*/, uint32_t fieldOffset /*edx*/,
// uint32_t guidLo, uint32_t guidHi, int size, const void *callback,
// void *userArg1, void *userArg2). `guid` selects the watched OBJECT
// (player, item, container, …); `bank` selects its field bank
// (1 = ITEM, 2 = CONTAINER, 4 = PLAYER — the engine's per-bag
// manager FUN_004F91A0 registers bank-2 observers on each equipped
// bag's CGContainer); `fieldOffset` is BANK-relative (container
// SLOT_1 = 8, not its descriptor-absolute 0x20). The engine's
// inventory setup FUN_004F8CC0 registers the player's bag/backpack/
// bank/keyring GUID ranges this way — but NOT equipment.
FUN_DESC_OBSERVER_REGISTER = 0x00467E70,
// Unregister — __fastcall(int bank /*ecx*/, uint32_t fieldOffset
// /*edx*/, uint32_t guidLo, uint32_t guidHi, const void *callback,
// void *userArg1). Removes the node matching (callback, userArg1)
// from that object+field's observer list; harmless no-op when
// nothing matches. What FUN_004F91A0 calls for an unequipped bag.
FUN_DESC_OBSERVER_UNREGISTER = 0x00467FB0,
// Observer callback ABI (verified from the dispatcher's call site at
// 0x004655DC + the engine callback FUN_004F8DB0's RET 0x10):
// int __fastcall cb(uint32_t fieldOffset /*ecx, as registered*/,
// uint32_t size /*edx*/,
// uint32_t guidLo, uint32_t guidHi,
// const uint32_t *oldValue /*the node's mirror*/,
// void *userArg1) → return 1, callee cleans 0x10.
// The live (new) value is NOT passed — read it from the object (the
// engine's bag callback reads the invMgr GUID array).
// ---- Unit-event token observers (Unit::TokenObserver) ---------------
// The engine makes target/party/raid/pet/mouseover units fire per-token
// unit events (UNIT_HEALTH, UNIT_MANA, UNIT_AURA, …) by watching bank-UNIT
// descriptor fields: FUN_0051bbb0 loops event index i in
// [0, EVENT_NAME_UNIT_MAX); for each whose static name slot is non-null it
// registers a bank-3 observer on field i*4 (per-index size below) with
// callback FUN_0051bd50 → the broadcast FUN_00515e50(guid, i), which fires
// event i once per token referencing the guid. The FIELD INDEX IS THE EVENT
// ID (health field +0x40 → event 16 = UNIT_HEALTH). Unit::TokenObserver
// replicates that exact loop with a caller's own callback to make synthetic
// tokens (focus, nameplateN) first-class; the registrar always appends a
// fresh node (FUN_00467f00, no dedup), so ours coexists with the engine's —
// a unit that is also the target fires both "target" and our token. The
// broadcast always fires with format "%s" + the token (DAT_0082e280).
// Per-index watched size (FUN_0051bbb0's switch): i∈{0,2,4,10}=8,
// 0x29=0xD8 (UNIT_FIELD_AURA), 0x6B=0x30, 0xA7/0xAE=0x1C, else 4.
VAR_EVENT_NAME_TABLE_STATIC = 0x00BE1198, // char*[] boot name array; slot i (base+i*4) non-null ⇒ real event i
EVENT_NAME_UNIT_MAX = 0xB6, // FUN_0051bbb0's loop bound
// The engine's inventory observer setup — registers the bag-slot
// GUID-field observers above, once per enter-world (sole caller is the
// enter-world initializer FUN_004908C0, latched by DAT_00B4B424).
// Player::Equipment co-hooks it to register the equipment-slot
// observers the engine never installs; the co-hook inherits the exact
// engine timing + lifetime (nodes die with the player object).
FUN_INV_OBSERVER_SETUP = 0x004F8CC0,
// PLAYER_FIELD_INV_SLOT_HEAD — first of the 19 equipment-slot u64 GUID
// fields in the player descriptor (0x4A8..0x538, stride 8; 0x540 is
// the first equipped-bag slot). 0-based slot = (offset - 0x4A8) >> 3;
// Lua inventory slot (GetInventoryItemLink etc.) = that + 1.
OFF_DESC_PLAYER_EQUIP_FIRST = 0x4A8,
DESC_PLAYER_EQUIP_SLOTS = 19,
DESC_OBSERVER_BANK_PLAYER = 4,
DESC_OBSERVER_BANK_UNIT = 3, // CGUnit descriptor bank (FUN_0051bbb0's watch loop)
// ITEM_FIELD_DURABILITY, ITEM-bank-relative: descriptor-absolute
// +0xA0 (field index 0x28, which counts the 6 OBJECT fields) minus
// the 0x18-byte OBJECT prefix. u32.
//
// DEAD END — bank-1 (ITEM field) observers register without error but
// are NEVER invoked: item values-updates demonstrably arrive
// (verified by probing the values pre-pass FUN_00465330) and
// player-bank observers fire in the same session, yet item-bank
// callbacks stay silent. Registration arithmetic was fully verified
// (bank base FUN_00465690(1)=0x18, next-bank walk FUN_004656E0
// item: 0→1→2, anchors match dispatch), so the suspect is the
// registrar's internal wrapper lookup (FUN_00464890 — the observer
// registry, NOT the object manager) silently no-op'ing for item
// objects. UPDATE_INVENTORY_DURABILITY is instead backed by
// FUN_INVENTORY_ALERTS_RECOMPUTE below. Constants kept for the
// investigation trail.
DESC_OBSERVER_BANK_ITEM = 1,
OFF_DESC_ITEM_DURABILITY = 0x88,
// The engine's inventory-alerts recompute — walks the equipment
// slots, reads each item's ITEM_FIELD_DURABILITY / MAXDURABILITY /
// broken flag from the descriptor, fills the per-slot alert table at
// [DAT_00B71F60], and UNCONDITIONALLY fires UPDATE_INVENTORY_ALERTS
// (event 501) at the end. The engine calls it whenever an owned
// item's fields change (the item post-update handler FUN_005D9A90 —
// which also fires UNIT_INVENTORY_CHANGED and the bag-update chain —
// routes every item values-update through it) plus equip and
// enter-world paths. That makes it the engine's own "durability may
// have changed" signal: Player::Equipment co-hooks it and diffs a
// {guid, durability} snapshot per slot to back
// UPDATE_INVENTORY_DURABILITY. void __fastcall(), no args.
FUN_INVENTORY_ALERTS_RECOMPUTE = 0x004C7EE0,
// 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,
// CGCreature client-side creature-data cache row. Populated by the
// engine when an NPC GUID becomes visible (the same row the
// `CreatureCache` SMSG fills with the templated NPC's name,
// subname, etc.). NULL for unsynced units or for player CGUnits
// (players don't have a row in this cache — their name comes from
// the SMSG_NAME_QUERY_RESPONSE-fed name cache at `0x00C0E228`).
//
// Layout of the cache row:
// +0x00 ?
// +0x0C name C-string ptr (alias of UNIT_NAME for creatures)
// +0x10 subName C-string ptr ("Innkeeper", "<Master Trainer>", etc.)
// +0x1C CreatureFamily id (→ CreatureFamily.dbc), 0 = none
//
// Used by `Script_UnitName`'s creature path and by VanillaMinimapTracking's
// subname-based blip filter (`src/Blips.cpp::ExtractUnitSubName`).
OFF_UNIT_CREATURE_CACHE_ROW = 0xB30,
OFF_CREATURE_CACHE_SUB_NAME = 0x10,
// CreatureFamily id within the +0xB30 cache row — the value the engine's
// `Script_UnitCreatureFamily` (`0x0051A310`) reads via its family helper
// `FUN_006055e0` (`[unit+0xB30] ? [row+0x1C] : 0`) before mapping it
// through `CreatureFamily.dbc` for the localized name. `UnitCreatureFamilyID`
// returns the raw id. Same field as `OFF_CREATURE_FAMILY` in the creature
// cache data block, reached through the unit here.
OFF_CREATURE_CACHE_FAMILY = 0x1C,
// 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_MOD_CAST_SPEED — the cast-time multiplier float (1.0 = normal,
// <1.0 = faster). The server folds it into `SpellEntry::GetCastTime`
// (`castTime *= GetFloatValue(UNIT_MOD_CAST_SPEED)`); the client reads it
// in `FUN_006e3340` at descriptor `+0x22c`. Backs `UnitSpellHaste`
// (`haste% = (1/mult - 1) * 100`). nampower surfaces the raw float as its
// `modCastSpeed` unit field.
OFF_UNIT_MOD_CAST_SPEED = 0x22C,
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,
// UNIT_FIELD_STAT0..4 — the fully-computed (base+item+buff) primary
// stats, broadcast in the descriptor. Order Str/Agi/Sta/Int/Spirit;
// STAT4 (Spirit) = fieldIndex 0x94 → +0x250 (from the UpdateField name
// table; cross-checked in-game: descriptor +0x250 read 173→182 when a