# Where the 1.13c life bar gets its screen coordinates
Ground-truth binaries: `C:\Diablo II\*.dll`, Diablo II LoD 1.13c, 32-bit.
Every address below is a **file coordinate** taken from the on-disk image. For D2Client and D2Win
that equals the live address only because both load at their preferred base and both have
`RVA == file offset` in `.text` / `.rdata` / `.data` (measured, see the base table). D2Game and
D2Lang relocate; no D2Game address is cited anywhere in this document.
| Module | Preferred base | Status |
|---|---|---|
| `D2Client.dll` | `0x6FAB0000` | loads at preferred; `.text` VA `0x6FAB1000` raw `0x1000`, `.rdata` VA `0x6FB7E000` raw `0xCE000`, `.data` VA `0x6FB8B000` raw `0xDB000` — RVA==raw measured for all three |
| `D2Win.dll` | `0x6F8E0000` | loads at preferred; `.text` va `0x1000` ro `0x1000`, `.rdata` va `0x1B000` ro `0x1B000`, `.data` va `0x1F000` ro `0x1F000` |
| `D2gfx.dll` | `0x6FA80000` | assumed at preferred (ground truth) |
| `D2Common.dll` | `0x6FD50000` | **live load address NOT verified.** Every D2Common address here is a FILE coordinate. |
| `D2CMP.dll` | — | referenced by ordinal only |
| `D2Game.dll` | `0x6FC20000` | **RELOCATES EVERY LAUNCH** (Storm occupies `0x6FBF0000..0x6FC50000`). Not cited below. |
| `D2Lang.dll` | `0x6FC00000` | relocates (inside the Storm window). Referenced only through D2Win IAT slots, never by address. |
---
## 1. The question
Diablo II 1.13c draws a monster life bar as part of the hover-label box. The question is where that
box's X and Y come from: which routine computes them, in which coordinate space, out of which
globals, and at what point in the frame they are fixed — so that a runtime hook can either reposition
the vanilla bar or draw an independent bar over every monster on screen without redoing the
world-to-screen projection from scratch.
## 2. THE ANSWER
The life bar has **no coordinate source of its own**. It is drawn by the generic D2Win text-box
routine, whose position comes from a **single-entry parameter cache** in D2Win BSS, and that cache is
filled from **two D2Client screen-space globals** that were computed earlier in the same frame by the
hover-label dispatcher.
The chain, measured end to end:
```
D2Common #10651 / #11142 unit world-pixel X / Y (cached in the unit's Path struct)
|
| screenX = worldX - ( [D2Client 0x6FBC9960] - [D2Client 0x6FBCC418] )
| screenY = worldY - ( [D2Client 0x6FBC995C] - 8 )
v
D2Client 0x6FB8BC54 (X) / 0x6FB8BC58 (Y) <- hovered/targeted unit, SCREEN pixels
|
| Y is then biased by a literal -0x48 (72 px up) at three of the four live sites
v
D2Win #10070 (0x6F8F2730) -> #10085 (0x6F8F18F0)
|
v
D2Win cache @ 0x6F9A9E58 : { wchar text[0x400]; int X; int Y; int color; int bCentre; int fill }
| 0x6F9A9E58 +0x800 +0x804 +0x808 +0x80C +0x810
| =A658 =A65C =A660 =A664 =A668
v
D2Win #10110 (0x6F8F33A0) -> D2gfx #10014 x2 (bar) + D2gfx #10019 per glyph (text)
```
Three consequences that matter more than the chain itself:
1. **The cache holds exactly one entry.** There is no array, no stride arithmetic, no second
instance. A full resync sweep of D2Win `.text` (37,625 instructions) finds two references each to
`+0x800`/`+0x804`/`+0x808`/`+0x80C`, four to `+0x810`, eight to the text buffer, and every reader
lies inside `#10110`. A raw 4-byte literal scan for all six addresses across every `.dll`/`.exe`
in `C:\Diablo II` hits **D2Win only** — zero hits in D2Client, D2Common, D2Game, D2gfx, D2CMP,
Fog, Storm or PlugY. So the vanilla mechanism cannot draw more than one bar per flush, and a
floating-bar-over-every-monster feature cannot be built by feeding this cache once per frame.
2. **`0x6FB8BC58` is a moving cursor, not an anchor.** It is written with the unit's screen Y, then
mutated in place by the consumers: `-0x10` per stacked line at `0x6FB6FED4`, and minus a
per-monster sprite height at `0x6FB70916` and `0x6FB70CA4`. Its value is only meaningful at the
instant of a given call site.
3. **The `-0x48` is real and it is vertical — CONFIRMED.** It appears as an 8-bit literal
`83 C6 B8` / `83 C0 B8` / `83 C2 B8` at `0x6FB707E6`, `0x6FB70E80`, `0x6FB70F48`, always applied to
`[0x6FB8BC58]` immediately before the `#10070` call. 72 px above the unit anchor is where the
vanilla name+bar box lands. The prompt's provisional reading ("fixed vertical offset from a screen
anchor") is half right: the offset is fixed, the base is not.
---
## 3. The five `#10070` call sites
All five reach `D2Win #10070` through D2Client thunk `0x6FABD360` = `FF 25 B0 EB B7 6F`
(`jmp dword ptr [0x6FB7EBB0]`), whose IAT slot resolves through D2Client's import descriptors to
`D2Win.dll` ordinal 10070. A resync-safe sweep of D2Client `.text` finds exactly these five.
| # | Call site | Enclosing fn | Resolved arguments | What it draws |
|---|---|---|---|---|
| 1 | `0x6FB6E710`
`E8 4B 4C F5 FF` (via shim) | `0x6FB6E700` — `__usercall` shim, `ret 0xC`. **Zero direct callers**; no 4-byte literal `0x6FB6E700` anywhere in the file. Reached through a computed pointer that was not traced. | Marshalling established by stack arithmetic (shim pushes 4, `#10070` pops 4 via `ret 0x10`, shim's own `ret 0xC` pops 3): `ECX` = shim stack arg1, `EDX` = shim stack arg2, `a1` = shim stack arg3, `a2` = incoming `EDX`, `a3` = incoming `ECX`, `a4` = incoming `EAX`. | **Not established** — the caller is unknown, so the drawn element is unknown. |
| 2 | `0x6FB70836`
`E8 25 CB F4 FF` | `0x6FB707C0` (callers `0x6FB70F9A`, `0x6FB714A5`, `0x6FB714E3`) | `ECX` = `EAX` (text ptr); `EDX` = `EBX` = `[0x6FB8BC54]`; `a1` = `ESI` = `[0x6FB8BC58] - 0x48` (`0x6FB707D4 8B 35 58 BC B8 6F`, `0x6FB707E6 83 C6 B8`); `a2` = `0` (`6A 00`); `a3` = `1` (`6A 01`); `a4` = `EBP` = return of `0x6FB70590`, an integer 0..0x80. Push block `0x6FB7082C: 55 6A 01 6A 00 56`, then `8B D3 8B C8`. | Hover name **plus life bar**, self-centred on the unit's screen X, 72 px above the unit Y cursor. This is the canonical life-bar site. |
| 3 | `0x6FB70B5F`
`E8 FC C7 F4 FF` | `0x6FB70850` (sole caller `0x6FB70F6A`) | `ECX` = `lea ecx,[esp+0x444]` (text buffer, `0x6FB70B58 8D 8C 24 44 04 00 00`); `EDX` = `(rect.left+rect.right)/2 - textWidth/2` (`0x6FB70B46 03 D1`, `0x6FB70B4B D1 FA`, `0x6FB70B4D 2B D0`); `a1` = `0x20` literal (`0x6FB70B56 6A 20`); `a2` = `[esp+0x14]` = the colour slot (`0x6FB70B4F 8B 44 24 14`); `a3` = `0` (`0x6FB70B53 6A 00`); `a4` = `ESI` (`0x6FB70B4A 56`), **value not established**. | A hand-centred line at a hard-coded screen Y of 32. It composes D2Lang `#10003` string indices `0xF9B` and `0x90B` with literal wide `'('` `0x28` and `')'` `0x29` — consistent with a `Name (qualifier)` line. Whether that paints as a top-of-screen block was **not confirmed**. |
| 4 | `0x6FB70E88`
`E8 D3 C4 F4 FF` | `0x6FB70D30` (single reference in the whole module: `0x6FB73A57 E8 D4 D2 FF FF`) | `a1` = `EAX` = `[0x6FB8BC58] - 0x48` (`0x6FB70E71 A1 58 BC B8 6F`, `0x6FB70E80 83 C0 B8`); `a3` = `1` (push at `0x6FB70E77`). Remaining args not established. | Lies between jump-table targets `0x6FB70E38` (dwType 0) and `0x6FB70E9A` (dwType 4), so it is on the **player** arm — *inferred from jump-table address ordering, not from a taken branch.* |
| 5 | `0x6FB70F57`
`E8 04 C4 F4 FF` | `0x6FB70D30`, same function | `a1` = `EDX` = `[0x6FB8BC58] - 0x48` (`0x6FB70F3F 8B 15 58 BC B8 6F`, `0x6FB70F48 83 C2 B8`); `a3` = `1` (push at `0x6FB70F46`). Remaining args not established. | Lies after jump-table target `0x6FB70EBC` (dwType 1) and before `0x6FB70FC1` (dwType 2), so it is on the **monster** arm — *same inference.* Note `0x6FB70EBF` tests `dwTxtFileNo` against `0x1A1`/`0x1A2` and diverts to `0x6FB70F7C`, which resolves the unit owner and returns **before** this block. |
The dwType jump table is at `0x6FB71328` (24 bytes, `.data`-in-`.text`), dispatched at
`0x6FB70E25 8B 45 00` / `0x6FB70E28 83 F8 05` / `0x6FB70E2B 0F 87 E7 04 00 00` /
`0x6FB70E31 FF 24 85 28 13 B7 6F`. Raw table bytes:
```
6FB71328 38 0e b7 6f | bc 0e b7 6f | c1 0f b7 6f | 18 13 b7 6f | 9a 0e b7 6f | 24 12 b7 6f
0 player 1 monster 2 object 3 missile 4 item 5 tile/warp
6FB70E38 6FB70EBC 6FB70FC1 6FB71318 6FB70E9A 6FB71224
```
The two anchor globals are populated **before** the type split, so a hook placed immediately after
`0x6FB70E25` sees valid coordinates for every unit type.
---
## 4. D2Win `#10070` — the contract
**Export identity.** D2Win's export directory is ordinal-only (207 entries, ordinal base 10000, zero
named exports — so `CacheLifeBarParams` and `DrawCachedText` are community conventions, not names
recoverable from the binary). Ordinal 10070 → RVA `0x00012730` → `0x6F8F2730`, and `0x6F8F2730` is
exported as ordinal 10070 only. Ordinal 10085 → `0x6F8F18F0`. Ordinal 10110 → `0x6F8F33A0`.
**Size and shape.** `#10070` is **33 bytes** (`0x21`), `0x6F8F2730..0x6F8F2750` inclusive, 10
instructions, first `int3` pad at `0x6F8F2751`. One exit, no branches.
```
6F8F2730 8B 44 24 0C mov eax,[esp+0x0C] ; a3
6F8F2734 50 push eax
6F8F2735 8B 44 24 0C mov eax,[esp+0x0C] ; a2
6F8F2739 50 push eax
6F8F273A 8B 44 24 0C mov eax,[esp+0x0C] ; a1
6F8F273E 50 push eax
6F8F273F E8 AC F1 FF FF call 0x6F8F18F0 ; ECX and EDX flow through untouched
6F8F2744 8B 4C 24 10 mov ecx,[esp+0x10] ; a4 (valid because the callee is ret 0xC)
6F8F2748 89 0D 68 A6 9A 6F mov [0x6F9AA668],ecx
6F8F274E C2 10 00 ret 0x10
```
**Calling convention: `__fastcall` + 4 callee-cleaned stack dwords — six arguments total.**
`ECX` and `EDX` are untouched to the call (`EAX` is the only scratch) and the callee consumes them
immediately (`0x6F8F18F2 8B F1` `mov esi,ecx`, `0x6F8F18F6 8B DA` `mov ebx,edx`). The `ret 0x10` pop
is corroborated independently by the shim at `0x6FB6E700`, which pushes 4 dwords, calls the thunk,
then executes `ret 0xC` with no stack fixup — only a callee pop of exactly `0x10` balances that
frame.
**It draws nothing.** Its only call reaches `#10085`, whose only two calls are indirect through D2Win
IAT slots `0x6F8FB054` and `0x6F8FB050` = `D2Lang.dll ?strlen@Unicode@@SIHPBU1@@Z` and
`?strcpy@Unicode@@SIPAU1@PAU1@PBU1@@Z`.
**What `#10085` does** (`0x6F8F18F0`, `ret 0xC`, 33 + body):
```
6F8F18F0 53 56 8B F1 85 F6 8B DA 74 1C push ebx/esi; esi=ECX; test esi,esi; ebx=EDX; je 6F8F1916
6F8F18FA FF 15 54 B0 8F 6F call [D2Lang Unicode::strlen]
6F8F1900 3D 00 04 00 00 / 7D 0F cmp eax,0x400 / jge 6F8F1916
6F8F1907 8B D6 / B9 58 9E 9A 6F edx=esi / ecx=0x6F9A9E58
6F8F190E FF 15 50 B0 8F 6F call [D2Lang Unicode::strcpy]
6F8F1916 57 B9 00 02 00 00 33 C0 BF 58 9E 9A 6F F3 AB 5F ; NULL or >=0x400: zero 0x200 dwords = 0x800 bytes
6F8F1926 8B 44 24 0C 8B 4C 24 10 8B 54 24 14
6F8F1932 5E
6F8F1933 89 1D 58 A6 9A 6F mov [0x6F9AA658],ebx ; = incoming EDX
6F8F1939 A3 5C A6 9A 6F mov [0x6F9AA65C],eax ; = a1
6F8F193E 89 0D 60 A6 9A 6F mov [0x6F9AA660],ecx ; = a2
6F8F1944 89 15 64 A6 9A 6F mov [0x6F9AA664],edx ; = a3
6F8F194A C7 05 68 A6 9A 6F 00 00 00 00 mov [0x6F9AA668],0
6F8F1954 5B
6F8F1955 C2 0C 00 ret 0xC
```
So **`#10070` is `#10085` plus one extra parameter that `#10085` defaults to zero.** That is the
entire functional difference between the two exports. `#10085` is itself exported and D2Client calls
it directly through thunk `0x6FABD384` (`FF 25 C8 EB B7 6F`, IAT slot `0x6FB7EBC8`) at **53** sites —
so `#10070` is a thin wrapper, not a gatekeeper. All 53 direct `#10085` callers leave the fill slot at 0.
**The cache — one contiguous singleton, 0x814 bytes, at D2Win `0x6F9A9E58` (BSS: `.data` vsize
`0xACC58`, rawsize `0x2000`, so no file backing):**
| Offset | Address | Field | Evidence |
|---|---|---|---|
| `+0x000` | `0x6F9A9E58` | `wchar_t text[0x400]` (0x800 bytes) | ctor loop `0x6F8FA220 BE 58 9E 9A 6F / BF 00 04 00 00`, calling `[0x6F8FB044]` = D2Lang `??_FUnicode@@QAEXXZ` (default-ctor closure, **not** a destructor); corroborated by `cmp eax,0x400` and `mov ecx,0x200 / rep stosd` |
| `+0x800` | `0x6F9AA658` | `int nX` — **P0**, the incoming `EDX` | read `0x6F8F33F2 A1 58 A6 9A 6F` |
| `+0x804` | `0x6F9AA65C` | `int nY` — **P1**, stack `a1` | read `0x6F8F3407 8B 15 5C A6 9A 6F` |
| `+0x808` | `0x6F9AA660` | `int nColor` — **P2**, stack `a2`. **Not a pointer.** | read `0x6F8F3516 8B 15 60 A6 9A 6F`, forwarded by value as the 6th arg of the per-glyph blit |
| `+0x80C` | `0x6F9AA664` | `int bCentre` — **P3**, stack `a3`, only ever tested `== 1` | `0x6F8F33E9 83 3D 64 A6 9A 6F 01` |
| `+0x810` | `0x6F9AA668` | `int nFill` — **P4**, stack `a4`, bar fill numerator over **128** | `0x6F8F3495 8B 0D 68 A6 9A 6F`, `0x6F8F34C1` |
`0x6F9A9E58 + 0x800 = 0x6F9AA658` exactly, so this is one struct, not six unrelated globals.
`0x6F9AA66C` and `0x6F9AA670` begin their own separate 2-element Unicode arrays with their own ctors
(`0x6F8FA280`, `0x6F8FA252`) and are **not** part of the cache.
**What `#10110` (`0x6F8F33A0`) does with them:**
*Position.* `0x6F8F33E9`: if `bCentre == 1`, `X = P0 - (textWidth+8)/2` (`8B CF D1 F9 2B C1`);
otherwise `X = P0`. `0x6F8F3407`: `Y = P1 + 2` (`83 C2 02`). Both are clamped against a screen rect
obtained from **D2gfx `#10080`** (`0x6FA87F70`, called at `0x6F8F341E` through thunk `0x6F8E7804`):
`0x6F8F345F 8B 44 24 1C / 33 DB / 2B C7 / 85 F6 / 0F 9E C3 / 4B / 23 DE / 3B D8 / 7D 06` gives
`X = min(max(X,0), screenW - (textW+8))`; `0x6F8F347F 8D 71 FB / 8B 4C 24 20 / 8D 45 FB / 3B C8 / 7E 02 / 8B C1 / 3B C6 / 7D 02 / 8B F0`
gives `bottomY = min(max(P1+2, textH-5), screenH-5)`.
*The bar.* `0x6F8F3495 8B 0D 68 A6 9A 6F / 85 C9 / 0F 84 C9 00 00 00` — if `P4 == 0`, jump to
`0x6F8F356C`, which draws a **single black box** (`6A 02 6A 00 56 8D 04 3B 50 8B CE 2B CD 51 53 E8 A2 42 FF FF`)
and rejoins the text draw at `0x6F8F3511`. Otherwise:
```
6F8F34CC 8B 54 24 24 / 0F AF CF imul ecx,edi ; P4 * (textWidth+8)
6F8F34D5 C1 F9 07 sar ecx,7 ; /128
6F8F34DF 8D 0C 2B lea ecx,[ebx+ebp] ; X + filled
6F8F34EE E8 2F 43 FF FF call 0x6F8E7822 -> D2gfx #10014 ; filled part, trans=1
6F8F3508 E8 15 43 FF FF call 0x6F8E7822 -> D2gfx #10014 ; remainder, color=0, trans=2
```
The two rects share top and bottom (`a2 = ebx = esi - textHeight`, `a4 = esi`) and abut at
`X + filled`. That is one percentage bar, not a frame plus a bar. **No separate box/frame fill was
found in `#10110`.**
*Primitives reached.* D2gfx `#10019` (`0x6FA8B050`) once per character via the text drawer
`0x6F8F2940` (`0x6F8F3550 E8 EB F3 FF FF`); D2gfx `#10014` (`0x6FA8BA30`) at three static sites
`0x6F8F34EE` / `0x6F8F3508` / `0x6F8F357B` (at most two execute — the third is on the mutually
exclusive `P4 == 0` branch); D2gfx `#10080` (`0x6FA87F70`) once, unconditionally. Also in the
recursive closure: D2CMP `#10049` (`0x6FE19D30`, called directly at `0x6F8F34BC`, its `AL` result
cached to `0x6F9A9D10` at `0x6F8F34C7` and pushed as the colour operand of the `#10014` fill),
D2CMP `#10065`/`#10006`/`#10070`, and nine Fog.dll ordinals (10024, 10029, 10042, 10043, 10102,
10103, 10104, 10105, 10265).
**D2gfx `#10014` contract** (`0x6FA8BA30`, `ret 0x18`, 6 args): `ECX` = `a1` = x1, `EDX` = `a2` = y1,
stack `a3` = x2, `a4` = y2, `a5` = colour, `a6` = transparency mode. Two early-outs:
`3B F1 / 7C 25` (skip when `x2 < x1`) and `3B C2 / 7C 19` (skip when `y2 < y1`). Dispatches through
`mov eax,[0x6FA91268]` / `call [eax+0xB8]`.
**D2gfx `#10019` contract** (`0x6FA8B050`, `ret 0x18`, 6 args): `ECX` = `a1`, `EDX` = `a2`, stack
`a3..a6`; forwards all six to `[eax+0x88]` off the same table.
**The `#10110` flush inside D2Client.** Exactly one caller: `0x6FB73D91 E8 D0 95 F4 FF` via thunk
`0x6FABD366` (`FF 25 B4 EB B7 6F`, IAT slot `0x6FB7EBB4` = D2Win ordinal 10110). No indirect `FF 15`
call to that slot exists and the dword `0x6FABD366` appears in no pointer table — so this is a safe
single-site hook. It sits 6 calls / `0x26` bytes before the `ret 4` epilogue at `0x6FB73DB7` of
function `0x6FB739E0`, whose single caller `0x6FAF437B` brackets it with D2Win `#10062`
(`0x6FAF4357`) and `#10024` (`0x6FAF4380`). The cache is filled earlier in the same function via
`0x6FB73A57 E8 D4 D2 FF FF` → `0x6FB70D30`.
**The fill fraction's producer.** D2Client `0x6FB70590..0x6FB70675`, called at `0x6FB707CF`:
```
6FB705E6 BE 80 00 00 00 mov esi,0x80
6FB70633 57 / E8 5D BB F4 FF push edi(unit) / call 0x6FABC196 -> D2Common #10913 ; ebx = max HP
6FB7063F 6A 00 / 6A 06 / 57
6FB70644 E8 7D BB F4 FF call 0x6FABC1C6 -> D2Common #10973 ; stat 6 = hitpoints
6FB70649 C1 E0 07 shl eax,7 ; *128
6FB7064C 99 / F7 FB cdq / idiv ebx
6FB70651 83 FE 02 / 7E 13 cmp esi,2 / jle -> return 2 ; visible floor
6FB70656 81 FE 80 00 00 00 cmp esi,0x80
6FB70660 B8 80 00 00 00 mov eax,0x80 ; clamp high
6FB70672 33 C0 xor eax,eax ; early-out = 0
```
Result flows straight to the call: `0x6FB707E0 8B E8` `mov ebp,eax`; `0x6FB7082C 55` `push ebp`.
**Numeric contract: integer 0..128, where 0 suppresses the bar entirely and 2 is the visible floor.**
An injected bar should use the same scale to look native.
**P2 (colour) selection in D2Client**, `0x6FB70A6E..0x6FB70B1D` — measured as the set `{0,1,3,4}`:
```
6FB70A84 8B 4B 14 / 85 C9 / 74 0A pMonsterData = [pUnit+0x14]
F6 41 16 08 / 74 04 test byte [+0x16],8 (unique/boss) -> slot 4
83 F8 01 / 75 14
84 50 16 / 74 08 test byte [+0x16],4 (champion) -> slot 3
C7 44 24 10 03 00 00 00
6FB70AAE 8B 7C 24 1C / 85 FF / 74 08 hostile flag -> slot 1
C7 44 24 10 01 00 00 00
6FB70ABE dwTxtFileNo in {0xF3,0x14D,0xF2,0xD3,0x220,0x23A,0xE5,0x2C0..0x2C5} -> slot 4
```
P2 is never used in arithmetic and has **zero effect on bar geometry**. It can be set to 0 and ignored
when the goal is only the bar.
---
## 5. The globals
### D2Win cache slots (base `0x6F8E0000`, all BSS)
Counts are from a full resync sweep of D2Win `.text` (37,625 instructions) plus a raw 4-byte literal
scan across every `.dll`/`.exe` in `C:\Diablo II`.
| Address | Meaning | Refs in D2Win `.text` | Writers | Readers | Refs in any other module |
|---|---|---|---|---|---|
| `0x6F9A9E58` | text buffer (1024 wchars) | 8 | `0x6F8F1909` (strcpy dest), `0x6F8F191E` (`rep stosd`), ctor `0x6F8FA222` | `0x6F8F3549` (`68 58 9E 9A 6F`) | **0** |
| `0x6F9AA658` | P0 / X | 2 | `0x6F8F1933` | `0x6F8F33F2` | **0** |
| `0x6F9AA65C` | P1 / Y | 2 | `0x6F8F1939` | `0x6F8F3407` | **0** |
| `0x6F9AA660` | P2 / colour | 2 | `0x6F8F193E` | `0x6F8F3516` | **0** |
| `0x6F9AA664` | P3 / bCentre | 2 | `0x6F8F1944` | `0x6F8F33E9` | **0** |
| `0x6F9AA668` | P4 / fill/128 | 4 | `0x6F8F194A` (writes 0), `0x6F8F2748` (writes a4) | `0x6F8F3495`, `0x6F8F34C1` | **0** |
Initializer `0x6F8F3332`, destructor `0x6F8FA222`. There is no third party competing for this buffer
and no mod on disk touching it.
### D2Client anchors (base `0x6FAB0000`, `.data`)
Whole-file 4-byte literal scan and full `.text` resync disassembly (291,932–292,069 instructions,
resync-policy variance only) **agree exactly**, so these enumerations are complete.
| Address | File offset | Meaning | Total refs | Stores |
|---|---|---|---|---|
| `0x6FB8BC48` | `0x0DBC48` | screen width — file initialiser `80 02 00 00` = 640 | — | 4 writers (`0x6FAC0E09`, `0x6FAC0E29`, +2) |
| `0x6FB8BC4C` | `0x0DBC4C` | screen height — file initialiser `E0 01 00 00` = 480 | — | 4 writers |
| `0x6FB8BC54` | `0x0DBC54` | hovered/targeted unit **screen X** (a centre) — file initialiser `FF FF FF FF` = -1 sentinel | 14 | 4: `0x6FB70DEA`, `0x6FB70E11`, `0x6FB71499`, `0x6FB714CB` |
| `0x6FB8BC58` | `0x0DBC58` | hovered/targeted unit **screen Y** (a running cursor) — file initialiser `FF FF FF FF` | 18 | 7: `0x6FB6FED4`, `0x6FB70916`, `0x6FB70CA4`, `0x6FB70DEF`, `0x6FB70E20`, `0x6FB7149F`, `0x6FB714DA` |
Eleven stores across **four** functions: `0x6FB6FEC0`, `0x6FB70850`, `0x6FB70D30`, `0x6FB71340`.
Three of them are decrements, which is what makes `...58` a cursor:
```
6FB6FEC0 8B 0D 58 BC B8 6F mov ecx,[0x6FB8BC58]
6FB6FECF 83 E9 10 sub ecx,0x10 ; 16 px per stacked line
6FB6FED4 89 0D 58 BC B8 6F mov [0x6FB8BC58],ecx
6FB70C97 E8 C4 05 F4 FF call 0x6FAB1260 ; MonStats2 record fetch
6FB70CA0 0F B6 48 0D movzx ecx, byte [eax+0x0D] ; per-monster sprite height
6FB70CA4 29 0D 58 BC B8 6F sub [0x6FB8BC58],ecx ; same shape at 0x6FB70912/0x6FB70916
```
`0x6FAB1260` is verified as a MonStats2 getter: `6FAB1264 8B 0D 74 E4 B7 6F` (`D2Common!sgptDataTables`),
`6FAB127A 69 C0 A8 01 00 00` (MonStats stride `0x1A8`), `6FAB1280 0F BF 44 02 18`,
`6FAB1291 8B 91 90 0A 00 00` (`+0xA90`), `6FAB1297 69 C0 34 01 00 00` (MonStats2 stride `0x134`),
`6FAB129D 03 C2`, `6FAB129F C3`.
X is proven to be a **centre** in the callee: when `bCentre == 1` (which every D2Client site passes)
`#10110` subtracts half the box width at `0x6F8F33FF 8B CF / D1 F9 / 2B C1`. D2Client's own deferred
drawer does the identical thing at `0x6FB6E45D D1 F9 / 0x6FB6E462 2B D1`.
### D2Client deferred-draw triple (`.data` BSS, immediately after the 80-wchar buffer at `0x6FBCC980`)
| Address | Meaning | Total refs | Writers | Readers |
|---|---|---|---|---|
| `0x6FBCCA20` | deferred centre X | 3 | `0x6FB7081C` (live, `89 1D 20 CA BC 6F`), `0x6FB6E744` (in dead fn `0x6FB6E720`) | 1: `0x6FB6E455 8B 15 20 CA BC 6F` |
| `0x6FBCCA24` | deferred Y | 3 | `0x6FB7080E` (live, `89 35 24 CA BC 6F`), `0x6FB6E749` (dead) | 1: `0x6FB6E464 8B 0D 24 CA BC 6F` |
| `0x6FBCCA28` | deferred fill fraction | 2 | `0x6FB70815` (live, `89 2D 28 CA BC 6F`), `0x6FB6E74F` (dead) | **0** |
The three live writes are one contiguous commit inside `0x6FB707C0`, taking the same `ebx`/`esi`/`ebp`
the immediate branch passes to `#10070`. `0x6FB6E720` is genuinely dead: 0 direct call/jmp sites in a
291,932-instruction sweep, 0 raw dword references anywhere in the file, not among D2Client's 4 exports.
The reader `0x6FB6E430` (single caller `0x6FB73D9B`) treats CA20 as a centre and CA24 as a top-ish Y,
then clamps: `0x6FB6E45D D1 F9 / 0x6FB6E462 2B D1` (left = X - w/2), `0x6FB6E46A 8D 49 02` (Y + 2),
`0x6FB6E471 8B 3D 48 BC B8 6F` (screen width), `0x6FB6E48C A1 4C BC B8 6F` / `0x6FB6E491 83 C0 E1`
(screen height - 0x1F).
### D2Client camera / viewport
| Address | Meaning | Writers | Readers |
|---|---|---|---|
| `0x6FBC9960` | camera **OriginX** (world pixels) | 3: `0x6FAEF6A0` (dead stub), `0x6FAEF8D3`, `0x6FAEF8F0` | 38 read sites |
| `0x6FBC995C` | camera **OriginY** (world pixels) | 3: `0x6FAEF6A5` (dead stub), `0x6FAEF8D9`, `0x6FAEF8F6` | 39 read sites |
| `0x6FBCC418` | viewport half-panel X shift: `{0, -(width/4), +(width/4)}` | switch at `0x6FAEFF90` (`0x6FAEFFA4`, `0x6FAEFFBB`, `0x6FAEFFD0`), body inlined 147 times (e.g. `0x6FAEB0B0`) | — |
| `0x6FBCC414` | draw/panel mode, compared against 1, 2, 3 | — | gates `0x6FAEFF90`, `0x6FB70D46` |
| `0x6FBCBEFC` | screen-shake gate | — | `0x6FAEF8D1` |
| `0x6FBCBF00` / `0x6FBBB9DC` | screen-shake X / Y deltas | — | `0x6FAEF8E1` / `0x6FAEF8E7` |
| `0x6FBCBBFC` | `pPlayerUnit` | — | `0x6FAEF89B` |
| `0x6FBC9958` | third viewport global written by `0x6FAEF860` | `0x6FAEF860` | **consumers not traced** |
There are four distinct "origins" in the binary and callers pick among them — the compiler emitted
four accessors, all now dead code but all diagnostic:
```
6FAEF6B0 A1 5C 99 BC 6F / 83 C0 F8 / C3 ; OriginY - 8
6FAEF6C0 A1 60 99 BC 6F / 2B 05 18 C4 BC 6F / C3 ; OriginX - panelShift
6FAEF6D0 ... ; OriginY raw
6FAEF6E0 A1 60 99 BC 6F / C3 ; OriginX raw
```
---
## 6. World-to-screen projection
### 6.1 The isometric transform
`D2Common #11087` = **`0x6FD9DB70` (FILE coordinate, preferred base `0x6FD50000`; D2Common's live
load address was NOT verified)**. `void __stdcall (int* pX, int* pY)`, in-place, `ret 8`.
```
6FD9DB70 8B 54 24 04 mov edx,[esp+4] ; pX
6FD9DB74 8B 02 mov eax,[edx] ; x (WHOLE SUBTILES, not 16.16)
6FD9DB76 56 push esi
6FD9DB77 8B 74 24 0C mov esi,[esp+0xC] ; pY
6FD9DB7B 8B 0E mov ecx,[esi] ; y
6FD9DB7D 57 push edi
6FD9DB7E 8B F8 mov edi,eax
6FD9DB80 2B F9 sub edi,ecx ; x - y
6FD9DB82 C1 E7 04 shl edi,4 ; *16
6FD9DB85 03 C8 add ecx,eax ; x + y
6FD9DB87 89 3A mov [edx],edi ; *pX = (x-y)*16
6FD9DB89 C1 E1 03 shl ecx,3 ; *8
6FD9DB8C 5F pop edi
6FD9DB8D 89 0E mov [esi],ecx ; *pY = (x+y)*8
6FD9DB8F 5E pop esi
6FD9DB90 C2 08 00 ret 8
```
**`isoX = (x - y) * 16`, `isoY = (x + y) * 8`.** 16 px horizontally and 8 px vertically per subtile is
the whole projection. D2Client reaches it through thunk `0x6FABC2F8` = `FF 25 44 E5 B7 6F`.
### 6.2 The projection is already cached in the unit
**Dynamic Path** (moving units): `+0x00` X 16.16 subtile, `+0x04` Y 16.16 subtile, `+0x08` isoX world
pixel, `+0x0C` isoY world pixel.
**StaticPath** (objects/items/tiles): `+0x04` isoX, `+0x08` isoY, `+0x0C` tile X, `+0x10` tile Y.
`pPath` / `pStaticPath` is at `UnitAny+0x2C`.
Proven at the StaticPath initializer `D2Common #10574` = **`0x6FD7FED0` (FILE coordinate)**,
`__stdcall(pUnit, pRoom, int x, int y)`, `ret 0x10`:
```
6FD7FEF7 8B 40 2C mov eax,[eax+0x2C] ; pStaticPath
6FD7FF07 89 30 mov [eax+0x00],esi
6FD7FF0B 89 48 0C mov [eax+0x0C],ecx ; tile X
6FD7FF0E 2B F2 sub esi,edx ; x-y
6FD7FF10 C1 E6 04 shl esi,4 ; *16
6FD7FF13 03 CA add ecx,edx ; x+y
6FD7FF15 C1 E1 03 shl ecx,3 ; *8
6FD7FF18 89 70 04 mov [eax+0x04],esi ; isoX
6FD7FF1F 89 50 10 mov [eax+0x10],edx ; tile Y
6FD7FF22 89 48 08 mov [eax+0x08],ecx ; isoY
```
**A bar routine never has to run the transform itself — one pointer deref gets the projected pixels.**
### 6.3 The four accessors
All `int __stdcall (UnitAny*)`, `ret 4`. Each dispatches on `dwType` at `UnitAny+0x00`:
`type == 2`, or `4 <= type <= 5`, takes the StaticPath branch; everything else takes the dynamic branch.
Dispatch bytes, identical in all four, at `0x6FD8034F`:
`8B 08 / 83 F9 02 / 74 35 / 83 F9 03 / 7E 05 / 83 F9 05 / 7E 2B`.
| Ordinal | Address (**FILE coord**, D2Common base `0x6FD50000`) | D2Client thunk | Returns | Tail bytes |
|---|---|---|---|---|
| `#11080` GetUnitX (16.16 subtile) | `0x6FD80340` | `0x6FABC1AE` | dyn `[path+0x00]`; static `[sp+0x0C] << 16` | `@0x6FD80386 8B 00 C2 04 00 \| 8B 40 2C 8B 40 0C C1 E0 10 C2 04 00` |
| `#11168` GetUnitY (16.16 subtile) | `0x6FD802E0` | `0x6FABC232` | dyn `[path+0x04]`; static `[sp+0x10] << 16` | `@0x6FD80326 8B 40 04 C2 04 00 \| 8B 40 2C 8B 40 10 C1 E0 10` |
| `#10651` GetUnitIsoX (world pixel) | `0x6FD80290` | `0x6FABC1D2` (`FF 25 80 E4 B7 6F`) | dyn `[path+0x08]`; static `[sp+0x04]` | `@0x6FD802C8 8B 41 2C 8B 40 08 C2 04 00 \| 8B 49 2C 8B 41 04 C2` |
| `#11142` GetUnitIsoY (world pixel) | `0x6FD80240` | `0x6FABC1F6` (`FF 25 98 E4 B7 6F`) | dyn `[path+0x0C]`; static `[sp+0x08]` | `@0x6FD80278 8B 41 2C 8B 40 0C C2 04 00 \| 8B 49 2C 8B 41 08 C2` |
The `shl eax,0x10` on the static branches of `#11080`/`#11168` is what proves `+0x0C`/`+0x10` are whole
subtiles while `+0x00`/`+0x04` are 16.16.
### 6.4 The camera origin
`D2Client 0x6FAEF860`, sole caller `0x6FAF435C`. Head at `0x6FAEF860`:
`A1 14 C4 BC 6F / 83 F8 02 / C7 05 58 99 BC 6F 60 FF FF FF / 75 16 / A1 48 BC B8 6F / 99 / 2B C2 / D1 F8 / 2D A0 00 00 00 / A3 58 99 BC 6F`.
Body:
```
6FAEF89B 8B 3D FC BB BC 6F mov edi,[0x6FBCBBFC] ; pPlayerUnit
6FAEF8A2 E8 2B C9 FC FF call 0x6FABC1D2 -> #10651 ; esi = player isoX
6FAEF8AA E8 47 C9 FC FF call 0x6FABC1F6 -> #11142 ; ecx = player isoY
6FAEF8B1 A1 48 BC B8 6F / 99 / 2B C2 / D1 F8 ; width/2
6FAEF8BB 2B F0 ; OriginX = isoX - width/2
6FAEF8BD A1 4C BC B8 6F / 99 / 2B C2 / D1 F8 ; height/2
6FAEF8C7 2B C8
6FAEF8CE 83 C1 10 ; OriginY = isoY - height/2 + 16 (unconditional)
6FAEF8D3 89 35 60 99 BC 6F mov [0x6FBC9960],esi
6FAEF8D9 89 0D 5C 99 BC 6F mov [0x6FBC995C],ecx
6FAEF8E1 (if [0x6FBCBEFC]) esi += [0x6FBCBF00]; ecx += [0x6FBBB9DC]; rewrite both
```
The origin is a live per-frame value centred on the player — **a bar must read it fresh each frame,
never cache it.**
### 6.5 The formula the life-bar path actually uses
```
screenX = D2Common#10651(pUnit) - ( [0x6FBC9960] - [0x6FBCC418] ) -> stored to [0x6FB8BC54]
screenY = D2Common#11142(pUnit) - ( [0x6FBC995C] - 8 ) -> stored to [0x6FB8BC58]
```
Byte-verified at `0x6FB70DF7` (in `0x6FB70D30`, feeding sites 4 and 5) and repeated byte-for-byte at
`0x6FB714B1`:
```
6FB70DF7 8B 35 60 99 BC 6F mov esi,[0x6FBC9960]
6FB70DFD 2B 35 18 C4 BC 6F sub esi,[0x6FBCC418] <-- panel shift, up to +/-200 px at 800x600
6FB70E03 E8 CA B3 F4 FF call 0x6FABC1D2 -> #10651
6FB70E08 2B C6 sub eax,esi
6FB70E11 A3 54 BC B8 6F mov [0x6FB8BC54],eax
6FB70E0A 8B 35 5C 99 BC 6F mov esi,[0x6FBC995C]
6FB70E16 83 C6 F8 add esi,-8 <-- fixed -8 on Y
6FB70E19 E8 D8 B3 F4 FF call 0x6FABC1F6 -> #11142
6FB70E1E 2B C6 sub eax,esi
6FB70E20 A3 58 BC B8 6F mov [0x6FB8BC58],eax
```
**Omitting `[0x6FBCC418]` puts a bar a quarter of the screen width off whenever a side panel is open.**
D2Client's own inverse transform (screen mouse → world) at `0x6FB010F0` uses the identical two
corrections, which is the independent confirmation that these are part of the canonical conversion and
not a local fudge:
```
6FB01149 8B 4D 00 / A1 60 99 BC 6F / 2B 05 18 C4 BC 6F / 03 C8 / 89 4D 00 ; world X
6FB0115C 8B 0D 5C 99 BC 6F / 8B 03 / 83 C1 F8 / 03 C1 / 89 03 ; world Y
```
### 6.6 The perspective fork
There is a second branch of the same computation, selected by **D2gfx `#10013`** (thunk `0x6FABD07E`),
which is a perspective-enabled flag pair, **not** the video mode:
```
6FA8B290 A1 E4 0B A9 6F / 8B 0D E0 0B A9 6F / F7 D8 / 1B C0 / 23 C1 / C3
return ( [0x6FA90BE4] ? [0x6FA90BE0] : 0 )
```
```
6FB70D76 E8 03 C3 F4 FF call 0x6FABD07E ; D2gfx #10013
6FB70D7B 85 C0 / 74 77 ; false -> classic branch at 0x6FB70DF7
6FB70D80 E8 29 B4 F4 FF call 0x6FABC1AE ; D2Common #11080 (X, 16.16)
6FB70D88 E8 A5 B4 F4 FF call 0x6FABC232 ; D2Common #11168 (Y, 16.16)
6FB70D91 E8 06 C3 F4 FF call 0x6FABD09C ; D2gfx #10012 visibility guard -- zero ABORTS the fn
6FB70DAB E8 D4 C2 F4 FF call 0x6FABD084 ; D2gfx #10023 (x, y, 0, &outX, &outY)
6FB70DBB A1 48 BC B8 6F / C1 F8 02 / F7 D8 ; +/- width/4 by [0x6FBCC414]
6FB70DDB 01 44 24 10
6FB70DE7 83 C1 10 ; outY + 0x10
6FB70DEA A3 54 BC B8 6F mov [0x6FB8BC54],eax
6FB70DEF 89 0D 58 BC B8 6F mov [0x6FB8BC58],ecx
```
**Any floating bar must reproduce this fork or it detaches from the sprite the moment the player
enables perspective.** Note the third exit: when `#10012` returns 0 the function aborts rather than
falling back to the software branch.
---
## 7. Implementation path — a floating bar over every monster
The vanilla mechanism **cannot** be reused as-is: the D2Win cache holds exactly one entry (section 4)
and the single `#10110` flush at `0x6FB73D91` draws that one entry. A per-monster bar therefore needs
its own enumeration and its own draw calls.
### Step 0 (mandatory, before writing any byte) — MEASURED constraint
Every address in this document is on-disk. `C:\Diablo II` contains `PlugY.dll`, `SGD2FreeRes*.dll`,
`d2fps.dll`, `BaseMod.dll` and `esoteric.dll`, which patch in memory. The on-disk `D2Client.dll` is
**already patched**: its `.text` VirtualSize is `0x000CCDFB` but its RawSize is `0x000CD000`, and the
`0x205`-byte tail at `0x6FB7DDFB..0x6FB7E000` contains live jump-thunk code, not padding
(`0x6FB7DE11 83 F8 78 74 0C 3D 94 00 00 00 74 05 E9 DD D3 FB FF`, and 44 more `E9 rel32` stubs down to
`0x6FB7DFFC`). **Re-read the live bytes at every address before patching.** Resolve exports with
`GetProcAddress(GetModuleHandleA("D2Win.dll"), MAKEINTRESOURCE(10070))` rather than hard-coding
`0x6F8F2730`.
### Step 1 — MEASURED. Hook the per-frame room-driven unit enumeration.
The frame's per-unit visit is **room-driven, not hash-driven**. D2Client registers callback
`0x6FB2A5D0` with a D2Common room-iteration service at `0x6FB2AC5A`
(`68 D0 A5 B2 6F / 50 / E8 B5 1E F9 FF`, target thunk `0x6FABCB1A`). Inside the callback,
`0x6FB2A87D 56 / E8 85 22 F9 FF` (thunk `0x6FABCB08`) fetches the room's first unit, and the loop walks
`UnitAny+0xE8`.
**Two clean detour sites, both instruction-aligned, both 6 bytes, both with `ESI = UnitAny` live.**
Verified against the file:
```
6FB2A890 56 e8 3c 19 f9 ff 56 89 44 24 40 e8 56 19 f9 ff
^^^^^^^^^^^^^^^^^ push esi ; call 0x6FABC1D2 (#10651) <- 6 bytes, loop HEAD
6FB2A992 8b b6 e8 00 00 00 85 f6 0f 85 f0 fe ff ff 5b 5f
^^^^^^^^^^^^^^^^^ mov esi,[esi+0xE8] <- 6 bytes, loop TAIL
```
`0x6FB2A99A 0F 85 F0 FE FF FF` targets `0x6FB2A9A0 + 0xFFFEFEF0 = 0x6FB2A890` — confirming the two
addresses bracket the same loop.
Prefer the **tail** at `0x6FB2A992`: `ESI` still holds the unit that was just processed, and the
relocated `mov esi,[esi+0xE8]` in the trampoline advances it afterwards, so the hook runs after the
unit's own vanilla draw work. A `jmp rel32` is 5 bytes; overwrite all 6 and relocate the single
instruction.
**Alternative single-site hook, MEASURED:** `0x6FB70E25` in the hover dispatcher, immediately after
`8B 45 00` (`mov eax,[ebp]` = `dwType`) and before the jump table. Both anchor globals are already
populated there and it is type-agnostic — but it fires only for the one hovered unit, not for every
monster. Use it to reposition the *vanilla* bar, not to draw many.
### Step 2 — MEASURED. Identify monsters.
`dwType` is `UnitAny+0x00`; `dwTxtFileNo` is `UnitAny+0x04`. Monster is `dwType == 1`, asserted by the
engine itself at `0x6FB713FD` (`83 3E 01`). Skip `dwTxtFileNo == 0x1A1` and `0x1A2` if you want to
match vanilla's diversion at `0x6FB70EBF`.
If you would rather enumerate independently of the draw loop, the client keeps **two** unit hash
tables, 6 types x 128 buckets of dword pointers (`0x300` dwords each, per-type stride `0x200`):
```
6FB54DE0 57 33 C0 B9 00 03 00 00 BF 08 A6 BB 6F F3 AB
B9 00 03 00 00 BF 08 9A BB 6F F3 AB 5F C3
6FB54E60 C1 E0 09 / 05 08 9A BB 6F / C3 ; Table A base 0x6FBB9A08
6FB54E70 C1 E0 09 / 05 08 A6 BB 6F / C3 ; Table B base 0x6FBBA608
```
Monster buckets: `0x6FBB9C08` (Table A) or `0x6FBBA808` (Table B), 128 buckets each.
`bucket = dwUnitId & 0x7F`; **`dwUnitId` is at `UnitAny+0x0C`, not `+0x08`**; hash chain link is
`UnitAny+0xE4` (`0x6FB54E20 8B 04 82 / 85 C0 / 74 0F / 39 48 0C / 74 0F / 8B 80 E4 00 00 00`).
The per-room chain link is a **different** field, `UnitAny+0xE8`, headed at `pRoom1+0x74`
(`0x6FB12457 8B 71 74`, `0x6FB12467 8B B6 E8 00 00 00`). A hook that walks the wrong one silently
enumerates the wrong set. The full-table sweep template is `0x6FB57060..0x6FB570B3` with the 6-dword
type-order array `{1,0,2,3,4,5}` at `.rdata 0x6FB8412C`
(`01000000 00000000 02000000 03000000 04000000 05000000`).
### Step 3 — MEASURED. Get the unit's world pixels.
`isoX = D2Common#10651(pUnit)`, `isoY = D2Common#11142(pUnit)`, through D2Client thunks `0x6FABC1D2`
and `0x6FABC1F6`. Both are `__stdcall(pUnit)`, `ret 4`, and both are simple reads out of the unit's
cached Path — no arithmetic, no camera math (section 6.2/6.3).
### Step 4 — MEASURED. Convert to screen, both branches.
```
if ( D2gfx#10013() ) // thunk 0x6FABD07E
D2gfx#10023( #11080(u), #11168(u), 0, &sx, &sy ); // thunk 0x6FABD084
if ( !D2gfx#10012() ) skip this unit; // thunk 0x6FABD09C
if ( [0x6FBCC414] == 1 ) sx -= width/4;
if ( [0x6FBCC414] == 2 ) sx += width/4;
sy += 0x10;
else
sx = isoX - ( [0x6FBC9960] - [0x6FBCC418] );
sy = isoY - ( [0x6FBC995C] - 8 );
```
`width` is `[0x6FB8BC48]`, `height` is `[0x6FB8BC4C]`. Read all four globals fresh every frame.
### Step 5 — MEASURED. Bias vertically.
Vanilla puts the name+bar box at `sy - 0x48` (72 px up). A custom bar that must sit clear of the
vanilla one should offset from the same base rather than re-deriving it. If you want it hugging the
sprite instead, subtract the per-monster sprite height: `MonStats2[+0x0D]`, fetched by
`0x6FAB1260(dwTxtFileNo)` (MonStats stride `0x1A8`, `sgptDataTables+0xA90`, MonStats2 stride `0x134`).
*That `+0x0D` is the field vanilla itself subtracts at `0x6FB70CA4`; the MonStats2.txt column NAME
behind it is an **inference** from column position, not verified against the .txt/.bin.*
### Step 6 — MEASURED. Compute the fill.
`fill = clamp( curHP * 128 / maxHP, 2, 128 )`, `0` to suppress the bar. Reproduce
`D2Client 0x6FB70590` or call it: `push pUnit; call 0x6FB70590` returns the value in `EAX`
(`0x6FB707CF E8 BC FD FF FF`). Its inputs are `D2Common #10913` (max, thunk `0x6FABC196`) and
`D2Common #10973(pUnit, 6, 0)` (stat 6 = hitpoints, thunk `0x6FABC1C6`).
### Step 7 — MEASURED primitive, INFERRED geometry. Draw.
**Do not** route a per-monster bar through the D2Win cache — it is a singleton (section 4). Call the
rectangle primitive directly:
```
D2gfx #10014 @ D2gfx 0x6FA8BA30 __fastcall + 4 stack args, ret 0x18
ECX = x1, EDX = y1, stack: x2, y2, colorByte, transMode
early-out if x2 < x1 (3B F1 / 7C 25) or y2 < y1 (3B C2 / 7C 19)
```
Two calls, exactly as `#10110` does it:
```
split = x1 + (fill * boxWidth) >> 7
#10014( x1, top, split, bottom, colorByte, 1 ) // filled
#10014( split, top, x1+boxWidth, bottom, 0, 2 ) // remainder
```
*The pairing of `trans=1`/`trans=2` and `color`/`0` to "filled"/"empty" is an **inference** from the
measured geometry — the two rects share top and bottom and abut at `x1 + filled`. Which one reads as
which colour on screen was not established (see section 9, palette).*
Resolve `#10014` at runtime via `GetProcAddress(GetModuleHandleA("D2gfx.dll"), MAKEINTRESOURCE(10014))`.
### Step 8 — optional, PARTLY NOT ESTABLISHED. Reuse the vanilla text box instead.
`#10110` is an ordinary export and draws immediately when called, so per unit you could write the six
cache slots directly (`0x6F9A9E58` and `0x6F9AA658..0x6F9AA668`) and then call `#10110`, once per
monster. Nothing else in the process contends for the buffer. **Blocker: `#10110`'s own parameter
list and stack cleanup were not measured.** Disassemble `0x6F8F33A0`'s prologue and its terminating
`ret` before attempting this; the D2Client call at `0x6FB73D91` has a push setup that was not captured
here.
### Step 9 — MEASURED. Where the hook body lives.
**There is no usable code cave inside `D2Client.dll`.** Measured on the shipped file:
- `.text` = `0x6FAB1000..0x6FB7DDFB` (VirtualSize `0xCCDFB`), raw `0x1000..0xCE000`.
- 2,080 `int3` runs of >= 8 bytes; the **largest is 16 bytes**, at exactly six addresses:
`0x6FAD5480`, `0x6FB02190`, `0x6FB19900`, `0x6FB381A0`, `0x6FB38430`, `0x6FB55AF0`.
- **Zero** runs of 32+ null bytes anywhere in `.text`.
- Nearest padding to the enumeration hook: `0x6FB2A9E3` (13 bytes), `0x6FB2ACF1` (15), `0x6FB2A5C1` (15).
- Nearest padding to the life-bar family: `0x6FB70842` (14 bytes), `0x6FB707B3` (13), `0x6FB70D23` (13),
`0x6FB6E718` (8), `0x6FB71602` (14).
- The `.text` mapped tail `0x6FB7DDFB..0x6FB7E000` (517 bytes) has **22 free `0x90` bytes** at
`0x6FB7DDFB..0x6FB7DE10` and **3 free `0x00` bytes** at `0x6FB7DFFD`; everything between is already
occupied by the existing patch stubs.
**Conclusion: the detour target must be memory owned by the injected DLL, not a cave.** The 16-byte
`int3` runs are enough for a `jmp rel32` trampoline and nothing more.
---
## 8. Ruled out
These were asserted at some point during the trace and are **false**. They are listed so they are not
re-adopted.
- **"`#10070` is a 14-byte wrapper."** It is 33 bytes (`0x21`), 10 instructions. The mechanism is
correct; the size figure was wrong.
- **"`0x6F8F18F0` is called only from `#10070`."** False. `#10085` is itself an export and D2Client
calls it directly through thunk `0x6FABD384` (IAT slot `0x6FB7EBC8`) at 53 sites, including
`0x6FB700B5`, `0x6FB70159`, `0x6FB70CC4`, `0x6FB71136`, `0x6FB711EC`, `0x6FB71212`, `0x6FB71313` —
interleaved with the `#10070` sites in the same life-bar / hover functions. Within D2Win itself the
single-call-site claim does hold (1 `E8` hit, 0 dword references).
- **"Cache `+0x808` is `void* pPalette`."** It is an `int` colour index in `0..0x0C`, passed by value,
never dereferenced. `0x6F8F2A10 0F B7 28 / 83 ED 30 / 83 FD 0D / 0F 8C DA 00 00 00 / 33 ED` builds it
from an ASCII digit and zeroes it on overflow. A pointer is never range-checked against `0x0D`.
- **"`0x6F8FA220` is a CRT static destructor."** `[0x6F8FB044]` is D2Lang `??_FUnicode@@QAEXXZ` —
MSVC `??_F` is the default-**constructor** closure.
- **"`+0x810` is `nPercent` (out of 100)."** The denominator is 128 (`0F AF CF / C1 F9 07`).
- **"The `#10110` flush is the last draw of the frame, which is why the box draws on top."** Six calls
follow it in the same function: `0x6FB73D96`→`0x6FB71A20`, `0x6FB73D9B`→`0x6FB6E430`,
`0x6FB73DA0`→`0x6FB719B0`, `0x6FB73DA5`→`0x6FB6F990`, `0x6FB73DAA`→`0x6FB6EE60` (death overlay,
strings `youdiedhardcore`/`youdiedinst`), `0x6FB73DAF`→`0x6FB02D90`. Five of six reach D2Win draw
primitives at depth 1–2. Critically, `0x6FB6E430` runs **after** the flush and consumes the same
anchor globals `0x6FBCCA20`/`0x6FBCCA24`. Z-order was never measured; the single-caller property of
`#10110` says nothing about it.
- **"`#10110` bottoms out in exactly two D2gfx primitives."** Three: `#10019`, `#10014`, and `#10080`
(`0x6F8F341E E8 E1 43 FF FF`), the last dispatching through the same renderer table at
`[0x6FA91268]+0x50`. Plus D2CMP `#10049` directly at `0x6F8F34BC`, and Fog ordinals in closure.
- **"The deferred drawer discards the fraction and passes literal 0."** It calls a *different* export:
thunk `0x6FABD37E` → **D2Win `#10150`** (`0x6F8F2FA0`, `ret 0xC`, 3 stack args), not `#10070`
(`ret 0x10`, 4 stack args). `#10150` has no fraction parameter at all, so nothing is discarded into
it. The `6A 00` at `0x6FB6E49F` is the same colour slot that the life-bar call passes 0 in.
- **"`screenX = GetUnitX(u) - [0x6FBC9960]`, a plain subtraction."** Wrong by `[0x6FBCC418]`
(up to +/-200 px at 800x600) and by 8 px in Y on the very path it was scoped to. See 6.5.
- **"`0x6FBC995C` = playerIsoY - height/2."** There is an unconditional `+0x10` (`0x6FAEF8CE 83 C1 10`),
plus two conditional screen-shake terms.
- **"`0x6FB8BC54`/`0x6FB8BC58` are written in only two functions."** Four: `0x6FB6FEC0`, `0x6FB70850`,
`0x6FB70D30`, `0x6FB71340`; eleven stores, not ten. `0x6FB6FED4` was missing from the original
enumeration entirely.
- **"`0x6FB8BC58` is a fixed screen anchor."** It is a mutable cursor (three decrement sites).
- **"A regular hovered monster does not get its name drawn above the sprite."** `0x6FB70850` has two
mutually exclusive paths. The sprite-relative one (`0x6FB70C70..0x6FB70CC4`) draws at
`X = [0x6FB8BC54]`, `Y = [0x6FB8BC58] - MonStats2[+0x0D]` via `#10085`, and is taken whenever the
three MonStats gates at `0x6FB708B0` / `0x6FB708B9` / `0x6FB708C4` fall through to it. The
top-centre path (`Y = 0x20`, hand-centred X, `#10070`) is the guarded exception, additionally gated
on `0x6FB6EA10` returning nonzero (`0x6FB70B2A 85 C0 / 0F 84 97 01 00 00`).
- **"The dispatcher `0x6FB70D30` is called exactly once per frame."** At most once per invocation of
`0x6FB739E0`, and zero times when either early-out fires: `0x6FB739EB 0F 85 C4 03 00 00`
(`[0x6FBAADC8] != 0`) or `0x6FB70D46 0F 84 CF 05 00 00` (`[0x6FBCC414] == 3`).
- **"`0x6FB1AF20` is the function holding both projection branches."** That address is mid-instruction
(`cmp eax,4` spans `0x6FB1AF1E..0x6FB1AF20`) with zero xrefs. The function starts at `0x6FB1AEE0`.
- **"The software formula appears verbatim at three sites."** Nine paired sites: X at `0x6FB09139`,
`0x6FB165E9`, `0x6FB1A92C`, `0x6FB1AF9B`, `0x6FB1B7F5`, `0x6FB1E6FF`, `0x6FB6309C`, `0x6FB70E03`,
`0x6FB714BD`; Y at `0x6FB09150`, `0x6FB165FC`, `0x6FB1A941`, `0x6FB1AFB0`, `0x6FB1B821`,
`0x6FB1E714`, `0x6FB630B7`, `0x6FB70E19`, `0x6FB714D3`. They are not byte-identical — register
choice and whether `[0x6FBCC418]` is loaded separately or subtracted from memory both vary. The
quoted encoding `8B 35 60 99 BC 6F A1 18 C4 BC 6F` is unique to `0x6FB1AF8D`.
- **"The projection fork is selected by video mode."** It is D2gfx `#10013`, a flag pair at
`[0x6FA90BE0]`/`[0x6FA90BE4]`. The video-mode variable is `[0x6FA91258]`, read inside `#10012` and
`#10023`, never by the selector.
- **"D2Win exports have names."** Ordinal-only, `nName = 0`, base 10000. `CacheLifeBarParams` and
`DrawCachedText` are community labels.
---
## 9. Not established
Everything in this list is a gap, not a guess.
1. **What reaches the shim `0x6FB6E700`** (call site 1). Zero direct callers, zero raw literal
references. It is a genuine `__usercall` — a caller must set `EAX`/`ECX`/`EDX` *and* push three
arguments — reached through a computed pointer that was not traced.
2. **What call site 3 (`0x6FB70B5F`) paints on screen.** Y is a measured literal `0x20` and X is
viewport-centred, but the D2Lang string indices `0xF9B` and `0x90B` were not resolved against the
`.tbl`. Its `a4` (`ESI`) was not traced either.
3. **Whether the life bar itself is drawn top-centre in the `0x6FB70850` path.** `0x6FB70B5F` is the
only `#10070` site in that function; the bar there goes through `0x6FB6E6D0` (called at
`0x6FB70C28`, `0x6FB70C58`) and `0x6FB6FCA0`, neither of which receives the centred X computed at
`0x6FB70B4D`. `0x6FB6E6D0`'s positioning was not traced.
4. **The numeric-to-hue binding for P2.** `{0,1,3,4}` selection is measured; that they mean
white/red/blue/gold is known-behaviour inference, read out of no palette table in these binaries.
`#10019` was not followed past `[vtbl+0x88]`.
5. **The component order of the palette at D2Win `0x6F922338`.** D2CMP `#10049` is passed
`(0x50, 0, 0)` and compares them against bytes `[0]`,`[1]`,`[2]` of each 4-byte entry. Whether
byte 0 is R (dark red bar) or B (dark blue) cannot be settled from the file: `0x6F922338` is at
D2Win `.data` offset `0x023338`, past the `0x2000` raw size, so it is uninitialised on disk. Every
other D2Win caller of thunk `0x6F8E7762` passes grey (`0x40,0x40,0x40` / `0x3F,0x3F,0x3F`) or
runtime bytes. **A single live read of `0x6F922338` settles it.**
6. **The D2gfx dispatch-table slot names** (`+0x50`, `+0x84`, `+0x88`, `+0xB8`). The table is not in
D2gfx — it is returned by ordinal `0x2710` of whichever backend loads (`D2Gdi.dll` / `D2DDraw.dll` /
`D2Glide.dll` / `D2Direct3D.dll`; only `D2Gdi.dll` and `D2Glide.dll` are present in
`C:\Diablo II`). `[0x6FA91268]` is D2gfx BSS written at runtime by `0x6FA8B49C`; no static
resolution is possible.
7. **The byte-granularity quirk at `0x6F8F34A3`** (`mov al,[0x6F9A9D10]`, leaving the box-bottom Y in
the upper 24 bits of the `dwColor` passed to `#10014`). The quirk is measured; whether it is
visible depends on whether the backend masks `dwColor` to 8 bits, i.e. on item 6.
8. **`#10110`'s own parameter list and stack cleanup.** Blocks step 8 of the implementation path.
9. **The exact D2gfx `#10023` contract.** Its two out-params land in `[0x6FB8BC54]`/`[0x6FB8BC58]` and
the first is adjusted by +/- width/4, which makes it X — but it was not disassembled past its
forward to `[vtbl+0x64]`.
10. **The dynamic Path's `+0x08`/`+0x0C` writer.** The field layout is proven from the StaticPath
initializer `#10574` and the dynamic accessors are proven to read `+0x08`/`+0x0C`, but the code
that *writes* the cached iso pixels for a moving monster was not located. It is presumably in the
movement/path engine and may call `#11087` through an indirection a linear call index misses.
11. **D2Common's live load address.** Every D2Common address here is a FILE coordinate. D2Common was
not flagged as relocating, but that was not confirmed against a running process.
12. **Whether the perspective backend's transform matches `(x-y)*16` / `(x+y)*8`.** `#10023`
dispatches to `[0x6FA91268]+0x64` in a backend DLL that was not read. What *is* measured: its
inputs are 16.16 subtiles from `#11080`/`#11168` and its outputs are consumed identically to the
software branch's.
13. **The automap's drawing code.** The established result is the stronger negative: no second
`(x-y)`/`(x+y)` transform with any 2:1 scale pair, and no 16.16-form (shift 12/13) transform,
exists anywhere in D2Client, D2Common, D2gfx, D2Game or D2CMP.
14. **The consumer of the per-cell list head at `cell+0x18`** — the list `0x6FB2A500` appends UNIT
nodes to. No instruction in D2Client `.text` reads a cell's `+0x18` as a list head. Consequence:
the exact instruction where a monster **sprite** is blitted with its final screen coordinates was
not found. The two blit paths that were nailed (`0x6FAC3A00`→`0x6FABD1C8`, and
`0x6FB4EC10`→`0x6FABD0D8`/`0x6FABD0D2`) take a cel-record payload, not a `UnitAny`, so they are the
tile/wall/light layers.
15. **Whether the per-animation-frame sprite bounding box** (`UnitAny+0x54` → `+0x28` list →
`node+0x08` → `+0x14` → `{+8,+0xC,+0x10,+0x14}`) is populated for monsters that are never hovered.
Builder `0x6FB1A2F0` has nine call sites (`0x6FB1A46D`, `0x6FB1A5ED`, `0x6FB1A76C`, `0x6FB1ACD5`,
`0x6FB1B917`, `0x6FB1BD8A`, `0x6FB1C697`, `0x6FB1E39F`, `0x6FB1E7CF`); only `0x6FB1A5ED` (the hit
test) was confirmed. If the box is built only on hover, a frame-accurate bar sized to the sprite
would have to force the build.
16. **The semantics of `0x6FBCC414`.** Measured as a 0..3 draw/panel mode compared against 1, 2 and 3.
That it is specifically the inventory/skill panel open-state is an inference from the +/- width/4
shift and the `== 3` bail.
17. **`0x6FBC9721`** — an unaligned `moffs32` global read at `0x6FB707C3` (`A1 21 97 BC 6F`) that gates
call site 2's entire function. Confirmed a genuine unaligned read into `.data`; its writer was not
found.
18. **`0x6FBC9958`** — a third viewport global written by `0x6FAEF860` (to `-0xA0`, or
`width/2 - 0xA0`, or 0, per `[0x6FBCC414]`). Consumers not traced, so whether a bar routine needs
it is unknown.
19. **Bit 9 (`0x200`) of `UnitAny+0xC4`**, which at `0x6FB70ED5` selects site 2 over sites 3/5 for
monsters. Measured as a branch condition only.
20. **`dwTxtFileNo` `0x164`** (site 5's gate) and `0x1A1`/`0x1A2` (the diversion at `0x6FB70EBF`) were
not resolved against MonStats.
21. **MonStats byte `+0x0D` bits `0x02` and `0x80`**, which decide whether a monster gets the vanilla
top-of-screen name+bar or an over-the-head name. Masks confirmed from Fog.dll `gdwBitMasks`
(`0x6FF76798`); the flag semantics are not derivable from code alone. Likewise the MonStats2.txt
column name behind `+0x0D` — measured only as a per-monster subtrahend at `0x6FB70912` and
`0x6FB70CA0`, with no third consumer to cross-check. (Neighbouring byte `+0x0C` is a 1/2/3 selector
at `0x6FB7867C`, consistent with a size enum beside a pixel measure — not proof.)
22. **`0x6FBCC418`'s writers were not traced individually** beyond the switch at `0x6FAEFF90` and the
inlined copies (`0x6FACE7E0`, `0x6FACE940`, `0x6FAEA730`, `0x6FAEB300`, `0x6FAEB0B0`).
23. **Whether `0x6FAF435C`** (sole caller of the camera setter) **is on the per-frame path**, and
whether the dead stub `0x6FAEF6A0` ever executes. Likewise "`0x6FB739E0` is the per-frame overlay
pass" is unproven: its only caller `0x6FAF4230` has zero direct callers and is reached solely via a
code pointer at `.data 0x6FBA4308` (a triple with `0x6FB21B10` and `0x6FB21A70`).
24. **`0x6FB01A80` is not verified as "GetSelectedUnit".** Measured mechanism only: it reads
`[0x6FBCC2F4]` (gate), `[0x6FBC964C]` (type), `[0x6FBC9638]` (id), indexes the table at
`0x6FBBA608` via `0x6FB54E20`, validates with `0x6FB568E0`, and clears the globals on failure. No
mouse hit-test writing those globals was found, so "hovered" is inference.
25. **`0x6FBCCA28` having zero readers is scoped to D2Client's own code.** It is BSS in D2Client's
private `.data` and never address-taken, so an external reader would need a pointer it never
receives — but no cross-module sweep for the literal was run, and D2Game relocates so such a check
would have to be done live.
26. **Attribution of the `0x6FB714xx` anchor stores.** One measurement places them in a function
`0x6FB71340` (call target, sole caller `0x6FB73A64`); another places `0x6FB70D30`'s body as running
through to a `ret` at `0x6FB714EE`, which would make them a second copy of the pattern inside
`0x6FB70D30`. Unresolved; it does not change the store addresses or their effect.
27. **D2Common ordinal names are behavioural, not symbolic.** `#10651`, `#11142`, `#11080`, `#11168`,
`#10331` (pRoom1 from unit), `#10826` (level id from room), `#10383` (adjacent-room array),
`#10913` (max stat), `#10973` (stat get), `#10175`, the room-iteration registrar (`0x6FABCB1A`)
and first-unit-in-room (`0x6FABCB08`) were identified by argument shape and effect, not by symbol.