D2Client exports giving a unit's subtile X/Y position; also discussion of obtaining the 'world view subtile' (where the character stands / camera anchor). Relevant to screen-to-world mapping and viewport offset. | the ones I can get with 1.13c d2client.dll +0x1630 and +0x1660 ... how do I get the world view subtile? Is that where my character stands?

D2Client.dll +0x1630 (GetUnitX), +0x1660 (GetUnitY)


DrlgLevel+0x1CC (1.13c) / +0x21C (1.10) points to the LevelPreset structure {dwDef, dwFiles, dwPreset, pNext*}. Level/DRLG generation data. | [1.13c] DrlgLevel + 0x1CC  [1.10] DrlgLevel + 0x21C ... struct LevelPreset

corpus-sourced; addresses may be 1.10f-era -- rebase to 1.13c + verify before use


Mini-guide to inject a custom UI: hook client-side item right-click handler (find barkscroll item code in D2Client), set a global, open barkscroll UI; hook the barkscroll draw function to load/draw a replacement DC6 when the global is set. A concrete example of hooking a D2Client panel draw routine. | Hook barkscroll draw function, if that global is set, load (and unload the old DC6) and then draw the new DC6

D2Client barkscroll item/draw handlers (not addressed)


Automap cell-range record {int nFirst;+00, int nLast;+04} indexed per level-type; gpMapGfxCells[MAX_LEVELTYPES] gives first/last automap gfx cell for each of the 45 level types (full name table listed). Automap/render data | struct D2AutoMapRecordStrc { int nFirst; //+00 int nLast; //+04 };


D2Common code sites patched to repoint automap cell lookup to a custom gpMapGfxCells array: CommonOffset +0x22C7F/0x22D46 (nFirst), +0x22C4E/0x22C9F/0x22D4D (nLast); +0x22AF5/0x22B32 for map-name strings. Original formula (ID*4)+(GameOffset+0xFF018), asm 6FC6F5E6 | 6FC6F5E6 (ID * 4) + (GameOffset + 0xFF018)


The Esoteric Path (Doug, phrozen_code Discord)

whist's UI notes: input handlers (14) at D2Client.0xDE858; draw the gold drop dialog at D2Client.0xA3840; open-dialog function D2Client.0xC30E0; in-memory dialog struct D2Client.0xFAD60. Panel drawing/UI | draw the gold drop dialog: D2Client.0xA3840

draw=0xA3840, open=0xC30E0, struct=0xFAD60, input=0xDE858


D2gfx 6FA748C0 calls #10004 then 6FA7BCEE; 6FA7BCEE reads [esp+8], on TEST EAX zeroes ecx, writes result to [eax+0x18F46] (a D2gfx window/state flag). Kieran patch subject, Necrolis gave clean naked stub | 6FA748C5 E8 24740000 CALL D2gfx.6FA7BCEE


Cross-version ordinal mapping in D2Cmp (graphics/cell decompression module): #10044 in 1.10 == #10067 in 1.13c | D2Cmp.10044 (1.10) D2Cmp.10067(1.13c)


Draws a Unicode string: ECX=ptr to unicode string, EDX=x, Arg1=y, Arg2=colour, Arg3=always 0. Called from D2Client to print stats on character screen. | Function: Display Text Address: 6F8AAD40 in v1.10 ECX - pointer to Unicode String EDX - x coord Arg1 - y coord Arg2 - colour


Silvermane's on-screen text overlay: calls level-name printout at D2Client.6FACF0D0, fetches Y coord from [6FBB19FC] and X coord from [6FB9A704], SUB EDX,30 to shift X, then D2Win #10171 display text. Shows the screen coord globals used for HUD text placement. | MOV EAX,DWORD PTR DS:[6FBB19FC] ; retrieve Y coordinate ... MOV EDX,DWORD PTR DS:[6FB9A704] ; retrieve X coordinate SUB EDX,30


Patch point in inventory placement: CheckCharmInvLocation(pItem) validates a charm's inventory location (ItemLocation, IsMatchingType 0x0D, pItemPath->dwPosY>3). Directly touches inventory-panel item placement logic. | // Charm Inventory check {D2DLL_D2COMMON, 0x27064, PATCH_JMP...} ... if (pItemPath->dwPosY > 3) return TRUE;


Returns which container/page an item is in: 00 Inventory, 01 BodyLoc, 02 Belt, 03 Ground, 04 Cursor. Core mapping for inventory/equip side-panel item location. | Get the Node Page Offset: 00052100 (6FD92100) ... 00: Inventory 01: BodyLoc 02: Belt 03: Ground 04: Cursor


D2FillArea passes 6 params; in 1.13d the CALL EAX lands in D2Gdi 0x6f877370 (takes all 6), in 1.14d lands at 0x006c8a60 windowed renderer (uses only params 3-6). Area-fill (solid rect) render path. | in 13d the CALL goes to this address 0x6f877370 in D2Gdi.dll that function takes all 6 paramters from D2FillArea


void(nXStart,nYStart,nXEnd,nYEnd,dwColor,nDrawMode) - wrapper to every renderer. Guards x<=xEnd && y<=yEnd then calls [DAT_007c8cc0+0xB8](xEnd,yEnd,color,mode). This is the solid-rectangle fill primitive (relevant to dead-zone/panel-area fills). | void D2GFX_DrawSolidRectEx(int nXStart,int nYStart,int nXEnd,int nYEnd,DWORD dwColor,int nDrawMode)


Windowed-mode renderer solid-rect fill; __fastcall(x1,y1,x2,y2,byte color,int mode). The concrete windowed backend behind D2GFX_DrawSolidRectEx wrapper (0x4f6300). | //0x6c8a60 void __fastcall RENDERER_WINDOWED_DrawSolidRectEx (uint param_1,uint param_2,...)


Resolution mode stored at 0x7c8cb8; the renderer object base DAT_007c8cc0 has draw funcs at +0xB8. Directly governs play-area sizing / resolution. | resolution mode in 1.14d is stored here 0x7c8cb8

0x7c8cb8 (also renderer vtable base DAT_007c8cc0)


Deleted User's D2Client global-offset table: resolution X/Y and the WINDOW START X / negative window-start Y offsets = the play-area origin / screen-shift/camera-offset globals. Highly relevant to viewport positioning beside panels. | ptNegWindowStartY = (DWORD*)(offset_D2Client + 0x3A285C); ptWindowStartX = (DWORD*)(offset_D2Client + 0x3A2858);

ResY +0x311470, ResX +0x31146C, NegWindowStartY +0x3A285C, WindowStartX +0x3A2858, IsLodGame +0x3A04F4, MouseY +0x3A6AAC, MouseX +0x3A6AB0, ptClientChar +0x3A6A70


1.14 (post-DLL-merge) addresses; note subtract 0x400000 to get RVA. Mouse X/Y getters useful for cursor-vs-viewport mapping. | D2GetMouseX ... -> 0x468730 ... D2GetMouseY ... -> 0x468740

isLOD 0x44dcc0, GetDifficulty 0x44dcd0, GetMouseX 0x468730, GetMouseY 0x468740


void __fastcall(wchar_t* pString,int nXpos,int nYpos,int nColour,int nTransTbl). Main HUD unicode draw call. | D2FUNC(D2WIN, D2DrawText, void, __fastcall, (wchar_t* pString, int nXpos, int nYpos, int nColour, int nTransTbl), 0xAD40)


Cel graphic header: +00 bFlip, +04 nWidth, +08 nHeight, +0C nXOffset, +10 nYOffset, +14 dwNodeLength, +18 pCelNode (also offset to next cel in file), +1C dwLength, +20 pPixels[]. Core sprite/floor-tile blit struct. | struct D2GFXCelStrc //sizeof 0x20 + x { BOOL bFlip; //+00 int nWidth; //+04 ... uint8_t pPixels[]; //+20 };


void __fastcall(func1,func2) that initializes two function-pointer globals. Kingpin named it D2SetScrollEndFunc. Named for scroll-end handling (viewport scroll callbacks). | static D2Client6FB1CBD0 D2SetScrollEndFunc = (D2Client6FB1CBD0)(0x7CBD0 + ClientOffset); // D2Client.6FB1CBD0

D2Client.0x6FB1CBD0 (RVA 0x7CBD0); globals 6FBB9F58/6FBB9F5C (1.10), 6FBCBF94/6FBCBF98 (1.13c)


Full DRLG Level struct layout: dwDrlgType@0x00 (1 maze,2 preset,3 wilderness), pRoom2First@0x10, union maze/preset/wilderness@0x14, dwPosX@0x1C, dwPosY@0x20, dwSizeX@0x24, dwSizeY@0x28, pNextLevel@0x1AC, pMisc@0x1B4, dwLevelType@0x1C0, hSeed@0x1C4, pLevelPresets@0x1CC, dwLevelNo@0x1D0, RoomCenterX/WarpX[9]@0x1E0, RoomCenterY/WarpY[9]@0x204, dwRoomEntries@0x228. Crash in D2Common #10322 DRLGInitLevel tied to sizeX/siz | DWORD dwDrlgType; //0x00 1 - maze, 2 - preset, 3 - wilderness ... DWORD dwSizeX; //0x24 DWORD dwSizeY; //0x28


kain_abel_666 (quoting Necrolis) (phrozen_code Discord)

Function responsible for drawing the skill icons in 1.10 | In 1.10 have a look at D2Client.0x6FB18AC0, its responsible for drawing the icons.


Hookable draw-line export; params (nXStart,nYStart,nXEnd,nYEnd,nPaletteIndex,nAlpha); width = end-start. Direct hook point for line rendering | void __stdcall ExpDrawLine(int nXStart, int nYStart, int nXEnd, int nYEnd, uint8_t nPaletteIndex, uint8_t nAlpha)


Core game-UI draw function; hook target for drawing UI (calls #10072). Called from within larger UI routine; also builds a 0x13C stack frame at 6FB21B70 | //D2Client.6FB20A30 D2FUNC(D2CLIENT, CDraw_Game_UI, void, __fastcall, (DWORD Arg01), 0x80A30)


Inside the GUI draw routine: CALL 6FAEF360 draws stat-points buttons, CALL 6FAEF730 draws skill-points buttons (at 6FB22030/6FB22044) | CALL d2client.6FAEF360 ; GUI Buttons -> stat points ... CALL d2client.6FAEF730 ; GUI Buttons -> skillpts


kain_abel_666 / Necrolis (phrozen_code Discord)

The 'Panel\Level' DC6 (character/level side panel gfx) string is at D2Client.6FB79AB0; loaded via DC6 load function D2Client.6FB20100 and the loaded handle stored at global [6FBB5BB4]; corresponding unload function at D2Client.6FB20140. Load MOV ECX,6FB79AB0 at 6FAEEBA0; store MOV [6FBB5BB4],EAX at 6FAEEBAF | 6FAEEBA0 MOV ECX,D2Client.6FB79AB0 ; ASCII "Panel\Level" ... so DC6/DCC loading function is D2Client.6FB20100?

str 6FB79AB0; load 6FB20100; unload 6FB20140; global 6FBB5BB4


Correct sequence to drop from inventory without leaving blank slot: ChangeAnimMode(IMODE_ONGROUND), RemoveItemFromInventory, GetRoom, D2DropItem(pGame,NULL,pItem,pRoom,x,y), then server UpdateInventoryItems + RefreshInventory | D2COMMON_INVENTORY_RemoveItemFromInventory(pUnit->pInventory, pItem); D2GAME_D2DropItem(pGame, NULL, pItem, pRoom, pCoords.nX, pCoords.nY);


Automap uses hardcoded ASCII level-type table in D2Common .rdata; table start at 6FDDB9E0 (offset 0xB9E0), sole reference/call at offset 0x6B30D (live 6FDBC30D) which loads table into EDI. To extend past lvltype 35 relocate table and repoint the call | copies the table from 6FDDB9E0 into the EDI register ... the call to that address at 0x6B30D

table 6FDDB9E0 (0xB9E0); ref 0x6B30D (6FDBC30D)


Automap record {int nFirst;+00, int nLast;+04}; MAX_LEVELTYPES 255; level-type name table gpfMapNamesSize[] (0=None..35 '5 Lava', 36+ Dark Alliance/dummy). Patch points to repoint gpMapGfxCells and name table at D2Common +0x22C7F/22D46 (nFirst), +0x22C4E/22C9F/22D4D (nLast), +0x22AF5/22B32 (names) | struct D2AutoMapRecordStrc { int nFirst; //+00 int nLast; //+04 };


whist's extension: rebuilt automap.txt compile to link LevelName to lvltypes.txt (TXTLINKER_LVLTYPES) so table can be arbitrary size (256+ pairs). AutomapBin record layout: TileName@0x10, Style@0x18, Start/EndSequence@0x19/0x1A, Cel1-4@0x1C/0x20/0x24/0x28. Patch stubs at D2Common 0x6CEF4 (GetLvlTypeFromAutomapRecord), 0x6CE9B (CompileAutomapTables), plus range-table repoints at 0x6CFF3/0x6FAD4/0x6CFB0/0x6D013/0x6FADF | {"TileName", TXTFIELD_ASCII, 7, 0x10, 0} ... {"Cel1", TXTFIELD_DWORD, 0, 0x1C, 0}

D2Common +0x6CEF4/0x6CE9B/0x6CFF3/0x6CFB0/0x6D013/0x6CFD1; record +0x10..0x28


D2Win ordinal 10110 draws a text popup background; helpers 6f8f1fb0 (text->background width, param in EAX) and 6f8f1e80 (text->background height, param in ESI); width/height feed D2GFX_DrawRectangle | 6f8f1fb0, takes text as param1 ... returns background width ... passed into the D2GFX_DrawRectangle function


RoomEx flag bits incl HAS_WARP_0..7 (0x10-0x800), SUBSHRINE_ROW1-4, HAS_WAYPOINT 0x10000, AUTOMAP_REVEAL 0x40000, NO_LOS_DRAW 0x80000 (line-of-sight draw suppression), HAS_ROOM 0x100000, HASPORTAL 0x400000, POPULATION_ZERO 0x800000 (towns/nospawn), TILELIB_LOADED 0x1000000, PRESET_UNITS_ADDED/SPAWNED, ANIMATED_FLOOR 0x8000000 (river of flame, hell act5) | ROOMEXFLAG_NO_LOS_DRAW = 0x00080000, ... ROOMEXFLAG_ANIMATED_FLOOR = 0x08000000, //animated floors (river of flame, hell act5)


Player is always at center of screen; getting window rect and halving mis-places by ~15-20%. Correct approach converts unit world coords to screen via a world_to_screen routine (diablo2::utils::screen::world_to_screen); unit x/y from get_unit_x/y(player->path) | diablo2::utils::screen::world_to_screen(label->x, label->y, mx, my); Does the trick.


kain_abel_666 located where a background is drawn: a D2gfx.#10072 draw call at 04D72AE5 that takes an X (EAX from [EBP-18C]) and a Y value (EDX from [EBP-190]); he identifies the Y push at 04D72AD0 as the value that is 'messing up' his positioning. Directly a render-pipeline / draw-position problem analogous to ours. | so i figured out where it draws the background ... CALL DWORD PTR DS:[4D76AA4] ; D2gfx.#10072 ... is the Y Value that is messing up


Necrolis' reversed inventory item-draw function (draws an item cel inside the inventory/side panel, with overlay support). Shows the full draw path: UNITDRAW_CacheGFXData -> D2LoadCelFile -> GFXUTIL_CellWithinScreen -> D2DrawImage -> UNITDRAW_DrawOverlays. Directly the side-panel render pipeline. | Function: ITEMS_InventoryDraw Address: D2Client.0x6FB5A320 ... BOOL __fastcall ITEMS_InventoryDraw(D2UnitStrc* pItem, int nXpos, int nYpos)


Per-room tile container holding wall/floor/shadow tile data; sizeof 0x2C. Fields: pMapLinks+00, pAnimTiles+04, nWalls+08, nFloors+0C, nShadows+10, pTiles+14. SwineFlu asks whether it can be surgically removed and rebuilt from the room seed without client/server desync. Core of floor/tile rendering data. | struct DLRGTileGrid //sizeof 0x2C { DRLGTileLink* pMapLinks; //+00 ... int nFloors; //+0C int nShadows; //+10 DRLGRoomTiles pTiles; //+14 };


kain_abel_666 has labelled the client screen-dimension globals: gScreenWidth @ 6FB740EC and gScreenHeight @ 6FB740F0. Relevant to viewport/play-area sizing. | i have the screen width and height labeled as D2Client.gScreenWidth @ 6FB740EC and .gScreenHeight @ 6FB740F0

gScreenWidth 6FB740EC, gScreenHeight 6FB740F0


D2Client.6FB57BD0 reads global at 6FBC1AE8; Harvest identifies it as GetMouseYCoordinate (RVA 0xB7BD0, __stdcall, no args). Screen-space coordinate accessor. | D2FUNC(D2CLIENT, GetMouseYCoordinate, DWORD, __stdcall, (), 0xB7BD0) //D2Client6FB57BD0

D2Client.0x6FB57BD0 (RVA 0xB7BD0); global 6FBC1AE8


Kingpin: the easy way to add new buttons is to hook the code that draws the bottom UI panel. Points at the panel-draw routine as the injection site. | add new buttons is simple, easiest way is hook part where it draw the bottom ui panel


kain_abel_666 repositioning D2BuffIcons: text via D2Win.#10117 D2DrawText(pString,nX,nY,nColour,nTransTbl); icon images via GFX.#10073. Asks what -1 as the transtbl arg means. UI-panel overlay positioning. | D2DrawText(wchar_t* pString, int nXpos, int nYpos, int nColour, int nTransTbl); ... the only called to GFX.#10073 for the images


oli: changing set-item color in 1.13c. Ground color patch at 6FB6FF1D works; the inventory (side-panel) copy is a separate address 6FB4408F (changed 02000000 -> 08000000). Concrete evidence that inventory-panel drawing uses a distinct code path from world/ground drawing. | this is the adress for inv 6FB4408F changed 02000000 to 08000000 ... 6FB6FF1D is for the ground and it works


Client-side handler run when clicking an item; takes inventory pointer, mouse X/Y and inventory page. Relevant to side-panel/inventory hit-testing. | //D2Client.0x475C0 D2FUNC(D2CLIENT, Item_Click, int32_t, __fastcall, (D2UnitStrc* pPlayer, D2InventoryStrc* pInventory, int pMouse_X, int pMouse_Y, ui


Elmegaard working through a broken gfx-frame bug: proposal to split pUnit->dwGfxFrame from a new dwGfxGameFrame = pGame->dwGameFrame << 8, and where the frame bonus (D2Common_10031_GetFrameBonus << 8) is applied. Animation-frame timing for unit rendering. | pUnit->dwGfxGameFrame = D2Common_10031_GetFrameBonus(pUnit) << 8; pUnit->dwGfxFrame = pGame->dwGameFrame << 8;


Gets the inventory record id for a unit/inv page (Inventory.txt). Used for inventory/panel geometry. | //D2Common.0x6FDBEBE0 (#10409) int __stdcall UNITS_GetInventoryRecordId(D2UnitStrc* pUnit, int nInvPage, BOOL bLoD)


Two state-flag accessors returning DWORD* into a unit's state bitfield: #10492 GetGfxStateFlags @0x6FDB4760, #10494 GetStatFlags @0x6FDB48F0. GfxStateFlags governs which overlay/graphic states are active on a unit. | //D2Common.0x6FDB4760 (#10492) DWORD* __stdcall STATES_GetGfxStateFlags(D2UnitStrc* pUnit) ... //D2Common.0x6FDB48F0 (#10494)


Static path struct (used by items/objects on ground): pRoom@0x00, nTargetX@0x04, nTargetY@0x08, nXPos@0x0C, nYPos@0x10, unk[2]@0x14, nDirection(BYTE)@0x1C. (Canight variant puts dwHeight@0x50.) Ground-position data feeding unit placement. | struct D2StaticPathStrc { D2RoomStrc* pRoom; //0x00 int nTargetX; //0x04 ... int nXPos; //0x0C int nYPos; //0x10 ... BYTE nDirection; //0x1C };

pRoom+00, nXPos+0C, nYPos+10, nDirection+1C


Necrolis: void __fastcall PLAYERMODE_Change(pGame,pPlayer,pSkill,BYTE nMode,int nUnitType,DWORD dwGUID,BOOL bAllowReEnter) at D2Game.0x6FC81A00 (RVA 0x51A00). Changes a unit's animation/mode. | Function: PLAYERMODE_Change Address: D2Game.0x6FC81A00 ... void __fastcall PLAYERMODE_Change(D2GameStrc* pGame, D2UnitStrc* pPlayer, D2SkillStrc* pSki


Skill-description decimal precision fix; hook points in D2Client at 0x73438 (capture value), 0x73495 (fix), 0x73458 (exit-if-zero), with returns 0x7343D/0x734A1/0x73472. This is on the skill-description UI text render path. | { D2DLL_D2CLIENT, 0x73438, (DWORD)PATCH_JMP, FALSE, 0x1 }, { D2DLL_D2CLIENT, 0x73439, (DWORD)SkillDescCaptureValue, TRUE, 0x0 }


Global holding the item the cursor is hovering (inventory/ground); compared at D2Client 6FAE10A1 as CMP ESI,[gpCurrentMouseOverItem] (ptr at 0x1732BC4 in the BlackSSE build). Directly tied to inventory/panel item interaction. | 6FAE10A1 |> 3B35 C42B1703 |CMP ESI,DWORD PTR DS:[BlackSSE.gpCurrentMouseOverItem

6FAE10A1 / gpCurrentMouseOverItem (172BC4)


v1.10 automap.txt loader compares level names against a hardcoded 36-entry table at D2Common offset 0x8E2D0 (from lvltypes.txt), which breaks level types past 36; fix substitutes a lvltypes record-index field. Relevant to floor/tile/automap level handling. | The existing loader function in v1.10 for automap.txt compares the level names against a hardcoded table at d2common offset 8E2D0 ... has only 36 entr


D2Client.0x6FAA8870 was thought to control rain but actually just reflects the result of D2COMMON_10632_GetRainFromLevelsTxt (weather from levels.txt) - a client display value. | D2Client.0x6FAA8870 ... it's actually just updated to reflect the results of D2COMMON_10632_GetRainFromLevelsTxt


D2Common_10177 (returns 32-bit) is consumed in D2Client.0x6FB49D70; lack of sign-extension when moving from D2PathPointStrc implies D2PathPointStrc::X is unsigned. Path point coords feed positional/camera math. | looking at the usage of D2Common_10177 in D2Client.0x6FB49D70 ... D2PathPointStrc::X is unsigned


whist: 0xC39E0 is the main ui loop where all panels are rendered; it calls 0x58FB0 which draws the boxes shown when ALT is pressed to show ground items; hookable via d2template | at D2Client.0xC39E0 is the main ui loop where all the different panels are rendered ... a call to D2Client.0x58FB0, which is the function that handles

D2Client.0xC39E0 (main UI loop); D2Client.0x58FB0 (ALT item box draw)


A patch that intercepts panel positioning at 0xC39F7 (one past the 0xC39E0 UI loop region) | { D2DLL_D2CLIENT, 0xC39F7, 38, (int)HD::PanelPosition_Interception, TRUE }

D2Client 0xC39F7 (size 38, PATCH... TRUE)


Conqueror (orig Araksson) (phrozen_code Discord)

Master click/message dispatcher for all menu panels; found by locating the 4 adjacent Storm#414-417 Input* calls, the function just before them handles all menu clicks; called ~40 times, one callback per panel type; callbacks share globals with the drawing function | the function immediately before is the one that controls all the Clicks of the Menus ... called about 40 times ... one for each type of Panel


Per-panel message table entry: param, key, __stdcall callback taking D2WinMsgStrc* | struct D2ClientFnMsg { uint32_t dwPar; DWORD dwKey; D2WinMSGS_CLT pCallback; };


Client panel draw offset globals used to compute inventory/stash/cube item positions; X offset scales with resolution width (simple), Y offset value (e.g. 196) source is unclear and larger than expected — used for resolution-height support of an item mover | D2VAR(D2CLIENT, UIPanelDrawYOffset, signed int, 0x124958)


Conqueror (Araksson code) (phrozen_code Discord)

Panel button hit-test computes coords from screen width/height minus WindowStartX/Y plus fixed offsets (e.g. -0x12e..-0x10e X, -0x60..-0x40 Y); Conqueror advises deriving positions from screen-width/window-width constants and half-screen center + cursor offset for dragging | (BLSS_CLIENT_GetScreenWidth() - BLSS_CLIENT_WindowStartX()) + -0x12e <= mouse_x


Conqueror (Araksson/D2Moo) (phrozen_code Discord)

Builds each room's ppRoomsNear list by scanning level rooms within tile distance <10 in X and Y (nTileXPos/YPos/Width/Height), sorts by position; D2Moo raises near cap 6->10 (ppNearRooms[1024]) | Function: DRLGROOM_AllocRoomNears Adress: D2Common.0x6FD77BB0 ... Improvement 6 --> 10


To fill missing tiles at higher resolution: iterate nearby rooms excluding those the client already drew, and do additional drawing for rooms visible from client's perspective. FLOOR tiles are drawn by a SEPARATE function from wall/roof tiles and units — two distinct functions, both called recursively. Directly relevant to filling the black play-area/dead-zone with world tiles | Floor tiles are handled separately from wall/roof tiles and units - hence why there are two distinct functions ... These two functions are also called

n/a (based on Necrolis reversed multires code, d2mods.info t=59615)


LAZ: to add a NEW screen/UI shift without disturbing existing behavior, you must extend the shift struct | if you want a new shift that doesn't affect other stuff, you might have to extend that struct


eezstreet: first two palshifts are ignored; changing 0x6FB27A23 to 'mov edi,0' shifts back the Translvl column so must add 2. Item draw uses the color index (one of 21) as offset into the transform/invtransform shift | Even if I change 0x6FB27A23 to be mov edi, 0 it will just shift back the Translvl column


Draws multi-item ground hover boxes; second half is the relevant part; uses FONTS_GetTextDimensions then nWidth += 8 for box sizing | Function: ITEMDESC_DrawGroundMultiHover Address: D2Client.0x6FB09F60


Automap graphics cell first/last per level type; level-type name table (0=None..44) drives automap tile ranges; two dword patches point the engine at an enlarged name table | D2AutoMapRecordStrc gpMapGfxCells[MAX_LEVELTYPES]; ... struct { int nFirst; int nLast; }

MAX_LEVELTYPES 255; patch CommonOffset+0x22AF5 and +0x22B32


Single top-level handler that draws the entire bottom control panel and the exp bar. NOPing its no-arg calls reveals what each sub-call draws. | that's the function I mentioned ... D2Client.0xC39E0 (top level handler necrolis mentioned)


Patch sites (NOPBLOCK, 0x15 bytes) that disable the bottom-panel input handlers; input handlers are separate from the draw code. | Disable bottom panel input handlers


Patch sites (NOPBLOCK 0x15) that disable the mini-panel input handlers. | Disable mini panel input handlers


Control panel + exp bar draw from one top-level handler; NOP the no-arg calls to see what draws what. Click/input handlers are NOT part of draw code and can be found via storm ordinal #511. | You can NOP out the calls with no args to quickly see what draws what ... find any input handler via that ordinal


Renders the orb text (life/mana orb display) on the control panel. | Function: ControlPanel::OrbTextDisplay Address: D2Client.0x6FAFF0B0


To locate draw code for an asset, find READS of the asset's data pointer, not the load code. Example pointer 6FBCBC80. | to find what draws it you need to look for reads of 6FBCBC80


Sets bit 8 of pTileData->dwFlags from the sign bit (0x80000000) of nTileFlags; floor/room tile flag handling. | 0x6FD88AC0 ... if (nTileFlags & 0x80000000) pTileData->dwFlags |= 8


Unnamed RoomEx flag bit; room-tile relevant, asked but unnamed. | anybody has a name for RoomExFlag 0x80000000 ?


Vendor gamble/inventory chain, size 0xC: +0x00 dwPlayerGUID, +0x04 bGamble (whether sold item appears in vendor inventory), +0x08 pPrev. | struct D2VendorChainStrc //sizeof 0xC { DWORD dwPlayerGUID; BOOL bGamble; D2VendorChainStrc* pPrev; }


whist's old notes point here for higher-level inventory/charm-requirement handling. | old notes pointing to D2Client.0x95AB8


Pushes RGB values and calls draw (0000D390) to set inventory item cell background colors: red-requirement bg, hover bg, default blue bg. Bytes stored at 6FBBB330-334. | PUSH 80 -> red color (red requirement background) ... default blue background


When charm reqs are met, jump gives the charm a blue background (Firehawk 2019 note). Cited by our own Doug. | reqs met, give charm a blue background ... JMP D2Client.6FB45803


Plan to decompile d2expres in Ghidra to load a high-res DC6 for a new stat page and make its button work. | decompile d2expres in ghidra, and see if anything there seems like it may let us load a high res dc6 for new stat page and make the button work


Custom stats interface: loads a DC6 and blits frames via D2PrintImage at panel coordinates (0,256),(256,256),(0,512),(256,512),(0,552) then fillRect(0,0,400,552). Extra frames 6/7 don't render. Uses setImage/setFrame/D2PrintImage/fillRect helpers; palette index 5. | NewInterface_Stats.cpp is containing this code, where it loads the dc6 ... D2PrintImage(&data, 0, 256, -1, 5, 0);


Doug (Esoteric Path/Sophia) notes porting functions/hooks from 1.10 to 1.13c requires shifting all offsets and re-hooking. | It would require too much work to shift offsets from 1.10, hook those functions. Etc.


Item-on-ground nameplate rendering. When Alt held, MOV DWORD PTR [EBP+118] sets draw mode: 6FB0933F sets 1 (regular items), 6FB09306 sets 5 (hovered item). Can hook 6FB0946E CALL D2Win.#10013 to override palette/draw mode. Single nameplate (alt not held) at 6F8F356C PUSH 2. | else you can just hook: 6FB0946E CALL <JMP.&D2Win.#10013> and replace the values for palette and draw mode yourself


1.13c: forced a conditional jump to unconditional JMP at D2Client 6FB28092 -> 6FB281BF to eliminate the 'Skill Tab 1' hover text plus its black background box. | 6FB28092 E9 28010000 JMP D2Client.6FB281BF ... eliminated it


Whilst's edit controlling the level-name DC6 shown on level change. At D2Client BF33F (6FB6F337): reads [6FB8BC4C], CDQ/SUB/SAR, PUSH 1 (palshift), PUSH 5 (draw mode), SUB EAX,8C (vertical position), PUSH 1 (horizontal position). Routine begins at 6FB6F0D0 (reads [6FBCBBFC]); called from 6FB73AA1. | 6FB6F345 2D 8C000000 SUB EAX,8C ;vertical position 6FB6F34A 6A 01 PUSH 1 ;horizontal position

6FB6F337 (0xBF33F); routine start 6FB6F0D0; caller 6FB73AA1


CALL D2Client.6FB6F0D0 at 6FB73AA1 is the sole call site of the level-entry display routine; nearby CMP [6FBCC414],3 and CALL 6FB12680 / 6FB0C5C0 / CMP [6FBAADCC]. | 6FB73AA1 |> E8 2AB6FFFF CALL D2Client.6FB6F0D0 <--- here


1.13c D2DrlgStrc fields: +0x098 (=1.10 +0x3E0, DWORD added-active-rooms counter, never decremented), +0x45C (=1.10 +0x3DC, BYTE counter, can overflow), +0x008 (=1.10 +0x3DD, set to 5 or 7 per +0x24 dwFlags, can be decremented). Client-side active-room processing; see D2Common.#10715 (1.13c) / #10003 (1.10). | offsets for these fields in 1.13c are D2DrlgStrc +0x098 +0x45C +0x008 ... 0x3E0 is the amount of added active rooms


void __fastcall UNITDESC_DrawInformation(D2UnitStrc* pUnit) at D2Client.0x6FB20A30 (1.10). Draws the unit info/nameplate; contains a large per-unit-type switch that looks up the display name. Render-relevant (unit nameplate drawing). | Function: UNITDESC_DrawInformation Address: D2Client.0x6FB20A30 ... large switch inside it for each unit type


Client monster-description block (immunities/minion text) around 6FB2100D. NOP the TEST EAX/JNZ at 6FB2100D to append monstats to minions/uniques; also adjust MOV EAX,13 at 6FB22167 and ADD EAX,31 at 6FB221D0. Underlying draw call at 6FB20600. D2Client based at 6FAA0000. | 6FB2100D ... 90 NOP ... 6FB22167 B8 13000000 MOV EAX,13 ... 6FB221D0 83C0 31 ADD EAX,31


1.13c D2Client attack/cast speed bars for left/right click. Draw routine at 6FAEE7F0 (SUB ESP,124; MOV EAX,[6FB8BC4C]; ADD EAX,-56 vertical position). Horizontal position set by PUSH 50 (Arg2) at 6FAEEC24 — that section holds left bar, right bar, and mouseover icon values. | 6FAEEC24 |. 6A 50 PUSH 50 ; |Arg2 = 00000050 ... values for the icons for the left skill bar, right skill bar

6FAEE7F0 (0xEE7F0); horiz PUSH 50 at 6FAEEC24


Boolean set TRUE in most cases (solo) that governs the game's sleep; main loop otherwise spins with no vsync, which is why removing the frame limit is easy | it does have a sleep but D2Client.0x6FB747C0 is set to TRUE in most cases


Function so hot that profiling it makes the game slow to boot (likely main-loop / per-frame) | why profiling 0x6FAA9AF0 made the game so slow to boot


HP/mana/stamina sent as 15-bit fields in one packet; change the 15 to 16 at three server + three client sites to raise the ~65k limit (1.13c) | just gotta change the 15 to 16 at these three locations (1.13c) D2Game.0x8B81B D2Game.0x8B82D D2Game.0x8B83F

D2Game 0x8B81B/0x8B82D/0x8B83F; D2Client 0xAC1CF/0xAC1DB/0xAC1EB


Function responsible for drawing the stamina bar; 0x7BEFBA is an array of palette indexes picking bar color by remaining stamina | 0x4975B0. that is the func responsible for drawing the stamina bar ... 0x7BEFBA is an array of pallette indexes

draw 0x4975B0, palette-index array 0x7BEFBA


Function that draws the experience bar | try looking at 0x498EA0 for xp bar


Environment lighting cycle tables: {angle, phase, R,G,B, 0} keyframes driving the screen ambient color tint per act | const D2EnvironmentCycleStrc gNormalEnvironmentCycle[6] = { { 320, 3, 125, 144, 243, 0 }, ...

gNormalEnvironmentCycle / gAct4EnvironmentCycle / gEclipseEnvironmentCycle [6]


Hover-object hook draws custom DC6 frames (hp/res frames) at hardcoded screen coords via D2GFX_DrawShiftedImage when hovered unit is an enemy monster matching a txt id | D2GFX_DrawShiftedImage(&ChesRes, 90, 17, -1, 5, 0);// hp frame


D2RoomStrc* __stdcall DUNGEON_StreamRoomAtCoords(D2DrlgActStrc* pAct, int nX, int nY) — resolves the room at world coords | D2RoomStrc* __stdcall DUNGEON_StreamRoomAtCoords(D2DrlgActStrc* pAct, int nX, int nY)


Allocates a DRLG level from mempool; sets pDrlg, nLevelId, nLevelType, nDrlgType from DATATBLS_GetLevelDefRecord | D2DrlgLevelStrc* __fastcall DRLG_AllocLevel(D2DrlgStrc* pDrlg, int nLevelId)


Walks Room2->pPreset PresetUnit chain; sets cellNo into Maximap.dc6 to draw custom automap icons for preset units/shrines (shrine list by dwTxtFileNo, Type 0x0F) | void Miniicons::RevealRoom(Room2* room) { for (PresetUnit* preset = room->pPreset; ...) ... cellNo = 1499;


Full tile struct layout for floor/tile rendering: +00 nDirection, +04 nRoofHeight, +06 fMaterial, +08 nHeight, +0C nWidth, +10 nHeightToBottom, +14 nType, +18 nStyle, +1C nSequence, +20 nRarity/nFrame union, +24 dw24To8BitFilterMask, +28 fTileFlags[26] (first byte index, 25 subtile flags), +42 wCacheIndex, +44 dwUnk44, +48 dwBlockOffset, +4C nBlockSize, +50 nBlocks, +54 pBlock, +58 pLibraryName (runtime), +5C pLRUCac | struct D2TileStrc //sizeof 0x60 { int32_t nDirection; //+00 ... int32_t nHeightToBottom; //+10 ... D2TileBlockStrc* pBlock; //+54


The callback that fills the Units renderer structure to later draw; invoked from D2Common.#10049 DUNGEON_CallRoomCallback. Central to the play-area unit render pipeline. Signature: BOOL __stdcall INTERFACE_FillRender(D2RoomStrc* pRoom, D2GameViewRenderDataCallBackStrc* pArgs). Hazel hit a crash here throwing many-CtC items into a crowd. | This is a callback from D2Common.#10049 (DUNGEON_CallRoomCallback). It is the function that is responsible for filling the Units renderer structure to


Args struct for room culling during render: +00 D2ViewStrc* pView, +04 RECT pCullingRect, +14 DRLGRoom* pClientRoom, +18 BOOL bTilesOnly. Directly about the viewport/culling rectangle that decides what tiles/rooms get drawn -- relevant to play-area vs panel clipping. | struct D2RoomCullArgsStrc { D2ViewStrc* pView; //+00 RECT pCullingRect; //+04 DRLGRoom* pClientRoom; //+14 BOOL bTilesOnly; //+18 };


Hover/tooltip text draw. Clamps X/Y against screen size via D2GFX_GetScreenSize(&nGameWidth,&nGameHeight); positions box so it stays on screen (nXpos > nGameWidth-nWidth -> nGameWidth-nWidth-5). Draws background via D2GFX_DrawSolidRectEx. Shows how the game reads viewport dimensions for UI placement -- the nGameWidth/nGameHeight globals bound the drawable area. | D2GFX_GetScreenSize(&nGameWidth, &nGameHeight); ... if ((uint32_t)nXpos > nGameWidth - nWidth) nXpos = nGameWidth - nWidth - 5;


NOP-ing CMP DWORD PTR DS:[6FBBA6FC],EBX / JE at 6FB22062-6FB22068 makes a UI panel always displayed, but its buttons stop working when 'closed'. 6FBBA6FC is the panel-open global flag. Relevant to forcing side panels on. | nooping the following will make it always displayed, but the buttons won't work if u "close" the panel. 6FB22062 |. 391D FCA6BB6F CMP DWORD PTR DS:[6F


6FB00CD0 is the handler for when you click that (panel) button; NOP everything up to the retn 4 to disable the click action. | 6FB00CD0 i think is the handler for when u click that button. if u basically noop everything up to the retn 4 it'll disable what happens when u click


Function that draws the waypoint panel in 1.13c. Active tab is a D2Client global variable (no server round-trip to switch tabs). Good entry point for locating panel-tab state globals. | the function that draws the waypoint panel (D2Client.0xAAEC0 on 1.13c)


Client-local simplified struct used to draw the waypoints panel: +00 int levelId, +04 byte activated. Client keeps its own copy separate from server PlayerData waypointData. | the client has it's own local copy, and also uses a separate simplified struct to draw the waypoints panel // D2Client.0xFCD8C struct WaypointClientDa


Draws the selected-unit name over the play area. Two names for same addr: D2FUNC CDraw_Game_UI (0x80A30) and Araksson's INTERFACE_DrawUnitSelectedName (void __fastcall(D2UnitStrc*)). Silvermane long wanted to rewrite this hover-name function. | Function: INTERFACE_DrawUnitSelectedName Adress: D2Client.0x80A30 void __fastcall INTERFACE_DrawUnitSelectedName(D2UnitStrc* pUnit)


Draws the ground item hover name in the play area: void __fastcall UI_DrawGroundHoverItem(D2UnitStrc* pItem, wchar_t* pBuffer, int nSize). | //D2Client.6FB20740 void __fastcall UI_DrawGroundHoverItem(D2UnitStrc* pItem, wchar_t* pBuffer, int nSize)


D2Client.0x6FADD360 handles item names only; D2Client.0x6FB297F0 works for everything but players and vistiles (items always return base name). Used to resolve hovered-unit display names. | D2Client.0x6FADD360 is purely for item names, D2Client.0x6FB297F0 works for everything but players and vistiles


OOG (out-of-game) menu draw/input entry points (1.13c): MENUS_DrawRegisteredControls (D2Win.#10024), INPUT_HandleMenuMouseInput (0x180F0), INPUT_HandleMenuKeyDownInput (0x182A0), INPUT_HandleMenuKeyCharInput (0x18370). | Function: MENUS_DrawRegisteredControls Address: D2Win.#10024 ... INPUT_HandleMenuMouseInput D2Win.0x180F0


Char/stat-screen string layout table array at D2Client.6FB78010. Struct: int nXstart, int nY, int nXend, short nString, int nArg. Governs stat-panel text positioning. | //D2Client.6FB78010 CharScreenStringTable gpCharScreenStringTable[] ... struct CharScreenStringTable { int nXstart; int nY; int nXend; short nString;


INTERFACE_StatScreenClickDown (D2Client.0x31290) and INTERFACE_StatScreenClickUp (0x31470), both void __stdcall(D2WinMsgStrc*). Handle input on the character/stat side panel. | Function: INTERFACE_StatScreenClickDown Adress: D2Client.0x31290 ... INTERFACE_StatScreenClickUp Adress: D2Client.0x31470


D2Tweaks hooks two D2Client call sites: +0x1641B = handle dropped items, +0x81BF5 = draw dropped item names (the ground-label render call). Useful anchors for the ground-item name draw in the play area. | m_draw_dropped_items_names_original = ... get_call(diablo2::d2_client::get_base() + 0x81BF5)); ... set_call(...get_base() + 0x1641B, handle_dropped_it

base+0x1641B (handle_dropped_items), base+0x81BF5 (draw_dropped_items_names)


For windowed/background running: NOP D2GFX 0x4D11 and 0x4932 (VA 6FA74932/6FA74D11) to prevent minimizing; NOP D2Win 0xDC9E (6F8ADC9E) to keep the game running while unfocused. D2Gfx multiclient fix at d2Gfx+0x446A (write 0xEB). | //D2GFX.6FA74932/6FA74D11 prevent minimizing { D2DLL_D2GFX, 0x4D11, 0x90, FALSE, 11 }, { D2DLL_D2GFX, 0x4932, 0x90, FALSE, 11 }, //D2Win.6F8ADC9E Stay


Full room struct layout -- the world floor/tile geometry a floor renderer walks. nSubtileX 0x00, nSubtileY 0x04, nSubtileWidth 0x08, nSubtileHeight 0x0C, nTileXPos 0x10, nTileYPos 0x14, nTileWidth 0x18, nTileHeight 0x1C; pRoomTiles 0x20; ppRoomList 0x24; nNumRooms 0x28; pUnitFirst 0x2C; pUnitUpdate 0x30; pCollisionGrid 0x34; pRoomEx 0x38; pSeed 0x3C; dwFlags 0x48; ppClients 0x4C; nTileCount 0x58; pAct 0x78; pRoomNext | int nSubtileX;//0x00 ... int nTileXPos;//0x10 ... D2RoomCollisionGridStrc* pCollisionGrid;//0x34


Disassembly of D2Common.0x6F68D570 charm-requirement/inventory-slot bounds check. pItem is [ESP+4]; pItem->pDynamicPath/pStaticPath union at +0x2C; grid posX at pPath+0x0C; posY at pPath+0x10. This is the inventory PANEL grid coordinate model used by CharmZone. | 6F68D57B .  8B40 2C  MOV EAX,DWORD PTR DS:[EAX+2C] ; pitem->pPath ... CMP DWORD PTR DS:[EAX+0C],5 ; pPath->posX

pItem->pPath at +0x2C; pPath->posX +0x0C; pPath->posY +0x10


MOV EBX,50 instructions that set the DC6 sprite draw size for the mana and health globes; patch target to enlarge the orb bubbles. | 6FAFFB8B BB 50000000 MOV EBX,50 ; Mana DC6 size ... 6FAFF9F9 ... ; Health DC6 size

D2Client.0x6FAFFB8B (mana), 0x6FAFF9F9 (health)


Four+ CMP/MOV sites that clamp the light radius (CMP EBP,imm / MOV EBP,imm). Patching the immediate (e.g. to 0x1F) forces a large on-screen lit radius regardless of torch/quest state. Verified working in olly. | 6FAA4293 83FD 1F CMP EBP,1F ... 6FAA4298 BD 1F000000 MOV EBP,1F ... This works perfectly


PUSH 4 that supplies the draw-mode/color index for first-line text; color index 9 = white, another index = gold. | 6F8EFA43 6A 04 PUSH 4 ; draw mode first line texts 9 is for white which number is for gold


NOP these (11 bytes at each D2GFX site, 24 bytes at D2Win site) to stop the game minimizing on focus loss and keep it running while unfocused. | //D2GFX.6FA74932/6FA74D11 prevent minimizing ... //D2Win.6F8ADC9E Stays running while unfocused


1.13c inventory-panel left-button handlers, __stdcall taking D2WinMsgStrc*. (1.10 equivalents: down 0x46E60, up 0x48140, InteractClickItem 0x475C0.) | D2FUNC(D2CLIENT, INTERFACE_InventoryLButtonDown, ... 0x98DC0) ... INTERFACE_InventoryLButtonUP ... 0x9C630)


Global UnitAny* for the item currently held on the cursor / selected in inventory (1.13c). | VARPTR(D2CLIENT, SelectedInvItem, UnitAny*, 0x11BC38) // Updated 1.13c


Used to read the hovered inventory item's GUID (only works for inventory items; user seeking equivalent for chest/stash object units). | I use d2client 0x1158F4 to get the hovered item's guid, but that is for inventory items


Hook site for the character-screen shift-click stat allocation; patched to cap shift-click stat input at 5. | //Shift clicking only imputs 5 { D2DLL_D2CLIENT, 0x315D3, (DWORD)PATCH_JMP, FALSE, 0x01 }


BYTE __stdcall taking D2RoomStrc*; returns the level id for a room. Pair with UNITS_GetRoom to get the level a unit is standing in. | //D2Common.0x6FD8C7A0 (#10057) BYTE __stdcall DUNGEON_GetLevelIdFromRoom(D2RoomStrc* pRoom)


D2RoomStrc* __stdcall taking D2UnitStrc*; returns the room a unit occupies. | //D2Common.0x6FDBE270 (#10342) D2RoomStrc* __stdcall UNITS_GetRoom(D2UnitStrc* pUnit)


D2ActiveRoomStrc* getter at a target X/Y within a unit's room; DUNGEON_IsRoomInTown tests town membership. | D2ActiveRoomStrc* pRoom = D2GAME_GetRoom_6FC52070(UNITS_GetRoom(pUnit), nTargetX, nTargetY);


CharmZone technique: rewrite of D2Common.#10840 requirement check restricting charms to an inventory sub-rectangle. Returns TRUE only if item is a charm, unit-type UNIT_ITEM, node page NODEPAGE_STORAGE, and pStaticPath->nYPos >= 6 and reqs met. Hooked from both D2Common (0x5FE70) and D2Client (0x4118B). | { D2DLL_D2COMMON, 0x5FE70, (DWORD)PATCH_JMP, FALSE, 0x01 }, { D2DLL_D2COMMON, 0x5FE71, (DWORD)D2Common_CharmRequirements, TRUE, 0x00 }, { D2DLL_D2CLIE

hook D2Common ordinal #10840 at offset 0x5FE70; D2Client hook 0x4118B


Reported intermittent access-violation in D2Common when entering one particular level -- unresolved in this chunk; possibly level/room load related. No fix given. | c0000005 at 6F622D97 01:00021D97 in D2Common.dll ... when entering one level


Belt UI row count: CALL D2Common.#10751/#11088 get item type, CMP EAX,13 (row 19 belt), JNZ hides extra rows | CMP EAX,13 ; Check for row 19 (belt itemtype) ... no additional rows is show


Full COLLISION_* bitflag table 0x0001-0x8000 (BlockWalk, BlockLOS, Wall, BlockPlayer, Missile, Player, DeadBody etc.) used in path/tile collision map | #define COLLISION_BLOCKWALK 0x0001 ... #define COLLISION_DEADBODY 0x8000


Multibox patch nops D2GFX window-focus check; 1.13c range 0x21-0x66 | { D2Dll_kD2GFX, 0x21, kPatchNop, 0, 0x66 - 0x21 }


Nop JMP at 6FAE2F0E; calls to D2Client.0x6FAE4060 set up the durability desc line | look for the calls to D2Client.0x6FAE4060 which sets up the durability desc line


D2GFX_DrawLine at exe base + offset 0xF6380; pointer-arith cast (uintptr_t/DWORD) needed, hooks confirmed working | D2GFX_DrawLine = ...(hBaseExe) + 0xF6380 ... confirmed my hooks work now


NewAutomapCell returns pCell; +0x04 nCellNo determines the graphic drawn in the automap cell (asks re lookup table) | pCell(returned by NewAutomapCell)->nCellNo(offset +0x04) is what determines what is drawn in the cell


Automap cell drawing routine | See AUTOMAP_DrawCells 1.10f:0x6FACE230


{ CelFile* pCelFile; RECT tRect; DWORD nOffsetX; DWORD nOffsetY; } - describes an automap cell graphic | struct AutomapCellTypeInfo { CelFile *pCelFile; RECT tRect; DWORD nOffsetX; DWORD nOffsetY; }


Lighting interception patch; user reports it fails in Act 5 for 1.13c (may need separate act5 patch) | Patch* lightingPatch = new Patch(Call, D2CLIENT, { 0xA9A37, 0x233A7 }, (int)Lighting_Interception, 6)

Call D2CLIENT {0xA9A37, 0x233A7}, 6 bytes


Sets phase of a storm: phase 0 no rain, 1 wind-up, 2 rain, 3 wind-down; contains act5 level-id switch feeding snow/weather globals | D2Client.0x6FAA6D40 ... sets the phase of a storm (with phase 2 being the rain phase


Act5 snow/rain: per-level switch sets weather DAT_007a89d8/dc/9c/c0 params, RNG FUN_00472280, draw via FUN_004f6300(x,0,y,...); seeds from pPlayer->pSeed | i assume the snow is here ... switch(DVar3) { case LEVEL_HARROGATH ... _DAT_007a89dc = 0x2a


Controls NPC/store tab selection; topmost tab 3, bottom 1 | It's in D2Client.0xE24F8. top most tab is 3. bottom most is 1


Act palette load: D2Client sets act palette via D2Win.#10177 PAL_LoadActData(nActPalette) which calls D2Win.0x6F8AE5E0 with pl2/dat pair; to swap palettes load tile palette each frame, then reload units palette AFTER all tiles rendered (optional 3rd call for UI) | load the tile palette each frame ... make a second call ... just after all tiles have been rendered to load the units palette (and UI


Vendor/store tab struct {int x@0;int y@4;short stringId@8;BOOL isSelected@0xA;BOOL isActive@0xE}; array based at D2Client.0xE1178 | struct VendorTab { int x; int y; short stringId; BOOL isSelected; BOOL isActive; }


Store-tab loop MOV ECX,D2Client.6FB91186 then indexed access [ESP+EAX*4+10] | MOV ECX,D2Client.6FB91186 ... MOV EBP,DWORD PTR SS:[ESP+EAX*4+10]


Weather tick decrements timer, re-rolls via FUN_00472280, and draws overlay via FUN_004f6300 with screen-relative offsets DAT_0071146c/711470 | FUN_004f6300(iVar7,0,iVar5,iVar6,0xff,5); return;


Patch at D2Client+0x9963C adjusts stash-panel close button Y position | {D2DLL_D2CLIENT, 0x9963C, {0xEB}, 0x01}, // Stash close button Y pos


Draws HP bar for spirits, vines and golems (shared); monID check inside failed for user | its offset is 6FADB5F0. But it draws hp bar for spirits, vines and golems


Exp-bar popup init: hit-test uses gnScreenWidth/2-0x92..-0x17 and *D2CLIENT_ScreenHeight-0x2b..-0x22, calls WIN_HoverText; waypoint text via WIN_DrawNormalText(text,x,y,color,transTbl) at 0x6fb2610a | Init here D2Client.0x6faffd58 ... gnScreenWidth / 2 + -0x92 <= nMouseXPos ... WIN_DrawNormalText


{ wchar_t wszDesc[2][30]; DWORD dwStatNo; DWORD dwTimer; DWORD dwColor; BOOL fEnable; } (asks if same in 1.11b/1.13c) | struct StatMonitor { wchar_t wszDesc[2][30]; DWORD dwStatNo; DWORD dwTimer; DWORD dwColor; BOOL fEnable; }


D2Win.#10175 uses pUnit->nGlobalPaletteShift(+0x6C) for the unit's current PL2 shift, taking precedence over D2Client.0x6FAAE3C0; overwrite those calls for custom PL2 shift | D2Win.#10175 which uses pUnit->nGlobalPaletteShift (+6C) ... takes precedence over D2Client.0x6FAAE3C0

D2Win.#10175; pUnit->nGlobalPaletteShift (+0x6C); D2Client.0x6FAAE3C0


Unit palette assignment: 0x6FB27A23 begins it, 0x6FB5BFC0 writes palette index into unit draw data; bosses/champions roll random PL2 shift from unit GUID (source of random color shifts) | D2Client.6FB27A23 is the start of the unit palette assignment (D2Client.0x6FB5BFC0 sets the palette index in the units draw data) for monsters

1.10: 6FB27A23 start; D2Client.0x6FB5BFC0 sets palette index; 1.06 0x71FE7; 1.00 0x9BBE7


The Esoteric Path (Discord corpus mine 2026-07-19)

CORPUS-CONFIRMED floor-shift mechanism -- converges with finding.floor_shift_internal_to_pass1. The FLOOR is a SEPARATE drawer from walls/roofs (Necrolis: MULTIRES_DrawGroundTiles vs DrawLowerWallTiles/DrawUpperWallTiles/DrawRoofTiles; SwineFlu confirms floor is a distinct recursive fn per Necrolis's reversed code at d2mods.info t=59615). Floor horizontal placement = D2FloorTileParamsStrc @ D2Client+0xEA990 (0x6FB9A990, sizeof 0x44): nScreenPanels(+0x20)=which panels open, pView(+0x24), clip nXMin/Max nYMin/Max(+0x2C..0x38), nViewXOffset(+0x3C = D2Client+0xEA9CC)/nViewYOffset(+0x40). Necrolis: 'anyone chasing the D2DX floor -320 shift should hook the FILL of this struct and watch nScreenPanels -> nViewXOffset.' Struct filled INSIDE the floor path (1.10f fill 0x6FAA3330 -- rebase to 1.13c by xref of writes to +0xEA990). EXPLAINS why our lens fails on the floor: pre-PASS1 hook authors the render ctx (walls read it -> fixed) but nViewXOffset is set LATER internally from nScreenPanels; our ledger's 'nViewXOffset already 0' was a PRE-fill timing error. FIX: hook the struct fill / DrawGroundTiles and zero nViewXOffset (or force nScreenPanels=0 for the floor pass only) so floor matches the unshifted walls. Cull rect = D2RoomCullArgsStrc{pView,pCullingRect,pClientRoom,bTilesOnly} (Necrolis).


The Esoteric Path (cross-verify 2026-07-19)

CROSS-VERIFY of all Grimoire addresses vs the on-disk 1.13c DLLs (D2Client 0x6FAB0000-0x6FBE5000, D2gfx ..0x6FAA1000, D2Win 0x6F8E0000, D2Glide 0x6F850000, D2Common 0x6FD50000, D2Game 0x6FC20000, D2CMP 0x6FE10000). 476 addresses / 698 records: 419 (88%) fall in a valid 1.13c module range (222 D2Client code + 117 D2Client data + D2Glide/D2Common/D2gfx/D2Win/D2Game); 57 FLAGGED as outside any 1.13c module = 1.10f-era/wrong. Flagged cluster = corpus.* 0x6FAAxxxx addresses sitting in the GAP between D2gfx (ends 0x6FAA1000) and D2Client (0x6FAB0000) -> they are 1.10f D2Client (lower base). NOTABLY the floor-drawer MULTIRES_DrawGroundTiles 0x6FAA2E60 and tile-setup 0x6FAA3330 ARE FLAGGED 1.10f -> do NOT use for 1.13c; rebase via struct layout + a 1.13c xref/disasm. RULE: any corpus 6FAAxxxx (below 0x6FAB0000) or cross-module-gap address is 1.10f, rebase before use. Assimilation-verification baseline.

cross-verify vs C:/Diablo II/*.dll (1.13c)


The Esoteric Path (5-front disasm assault 2026-07-19, direct from 1.13c D2Client.dll)

ROOT CAUSE PINNED. The floor+roof static -320 shift is ctx[+0x24] (scrollX), NOT ctx[+0x04] and NOT any global we ruled out. TWO PROJECTION FAMILIES: (1) FLOOR+ROOF flush @0x6FB4ED50 (D2Client+0x9ED50): screenX = ctx[+0x04](drawL) - ctx[+0x24](scrollX) + tileAbsX -- NO offX term. (2) WALLS+UNITS project via offX=ScreenXShift 0x6FBCC418 (RVA 0x11C418) which our lens zeroes -> they re-center. ctx = *0x6FBCC3C4 (RVA 0x11C3C4) = the SAME struct the lens writes (confirmed: both callers of tile drawer 0x6FB4EC10 -- 0x6FB3B224 in PASS2 + 0x6FB4EE7A -- push edi=ctx; PASS1 0x6FB3B0C0->DrawRooms 0x6FB2A9F0 on same ctx). So ctx[+0x04]=0 DOES reach the floor -> not the residual. THE 320 ENTERS ctx[+0x24] at DrawGame 0x6FAF4230: scrollX = scrollCenterX(thunk 0x6FABC1D2 on view *0x6FBCBBFC=playerBase) - (R-L)/2 (0x6FAF4270-79), written 0x6FAF42F8 'mov [esi+0x24],ecx' (esi=*0x6FBCC3C4) right before PASS1 call 0x6FAF4302. Lens moves ctx[+0x04]/[+0x0C] SYMMETRICALLY (-320,960->0,1280) so R-L stays 1280, (R-L)/2 stays 640, scrollX UNAFFECTED -> panel camera re-center +DispW/4=320 survives in ctx[+0x24]. Floor pan = 0-(scrollX_nopanel+320) = static -320. DispW/4=1280/4=320. Roof same path. SUPERSEDES finding.screenshiftx_is_floor_shift, finding.survey_floor_is_downstream, finding.d2floortileparams_floor_shift (that struct was 1.10f). FIX: correct ctx[+0x24] to hold no-panel pan before PASS1 (extend lens; ctx[+0x24]-=DispW/4 for open-panel side, sign per ScreenOpenMode 0x6FBCC414). Floor+roof only. Confirm sign/magnitude via one live RPM of ctx[+0x24] panel-open vs closed standing still.

disasm assault vs C:/Diablo II/D2Client.dll (1.13c): 0x6FB4ED50 0x6FB4EC10 0x6FAF4230 0x6FAF42F8 0x6FB3B110 0x6FB2A9F0


The Esoteric Path (live disasm 2026-07-19, SGD2FreeRes.dll in the running process)

DEFINITIVE ROOT CAUSE of the floor/roof panel-shift. It is in SGD2FreeRes.dll (loaded @0x739C0000), NOT D2Client -- D2Client's whole floor path is 100% VANILLA (verified by live-vs-disk patch map: DrawGame 0x44230, scroll write 0x442F8, floor flush 0x9ED50, floor drawer 0x9EC10, DrawRooms 0x7A9F0, PASS1 0x8B0C0, PASS2 0x8B110 all byte-identical to disk). SGD2FreeRes hijacks SetTileCullingBound: D2Client+0x8A4D0 is patched 'jmp 0x739C16B0' (trampoline) -> real fn 0x739D21D0. Inside 0x739D21D0 a PANEL-CONDITIONAL block: 0x739D2212 call 0x739D64A0 (nonzero when a side panel open); 0x739D221A je 0x739D2230 (skip if none); 0x739D221C add [ebx+4],0xFFFFFF60 (top-=160); 0x739D2223 add [ebx],0xFFFFFEC0 (LEFT-=320=DispW/4=0x140); 0x739D2229 add [ebx+8],0x140 (RIGHT+=320). ebx = the tile-cull/draw window (arg+0x14). => on panel-open SGD2 moves the floor's tile window by +/-320. The vanilla flush (screenX = ctx+0x04 - ctx+0x24 + tileAbsX) then faithfully draws the shifted tiles; walls read offX (our lens holds 0) so they don't move -> floor vs walls split by 320. RECONCILES the paradox that render ctx (drawL/scrollX) measured FLAT open-vs-closed (floor_scan same-position: both drawL=0 scrollX=-208, delta 0): the shift rides in tileAbsX collected from the shifted cull window, NOT in the ctx. Other SGD2 hooks confirmed live: SetScreenShift 0xC39F6 -> 0x739D1F80; DrawScreenBg 0xC3AA6 -> 0x739C10B0; stock left/right bg NOP'd @0x271ED/0x270F1. FIX target: neutralize/adjust the panel block at 0x739D221C or its gate je@0x739D221A (NOP-test flips je->jmp). CONFIRMED SHIFT (Doug's 3 screenshots: no-panel centered, inventory=world pushed left, character=pushed right).

live disasm SGD2FreeRes.dll @0x739D21D0 (Game.exe PID 17412)

live-vs-disk patch map of D2Client (scratchpad live_patch_map.py)


The Esoteric Path (Doug insight + method 2026-07-19)

METHOD (Doug's key correction 2026-07-19): the ON-DISK DLL is NOT what runs. PlugY / SGD2FreeRes / d2fps patch D2Client in memory at load; at least 3 mods control frame/UI. Analyzing on-disk VANILLA code produced FOUR dead-end floor-shift hypotheses (see finding.floor_shift_ledger). Correct approach: (1) LIVE-vs-DISK byte diff of the running process reveals every mod patch; (2) disassemble the LIVE (patched) code, following mod redirects; (3) NOP-test via WriteProcessMemory (reversible, elevated) to isolate which mod's patch drives a symptom. D2Client live map: 117 patched regions / 1419 bytes; core floor draw all vanilla; render-controlling mods = SGD2FreeRes (@0x739C0000: SetScreenShift/SetTileCullingBound/DrawScreenBg/disable-stock-bg), PlugY (many), esoteric (ours). Redirect target bases seen: 0x739Cxxxx=SGD2FreeRes; 0x72Exxxx/0x73Dxxxx = mod code; 0x032xxxxx=codecaves(ours); 0x10000000=another module. TOOLKIT built tonight (scratchpad, all elevated RPM/WPM, no x32dbg needed for read/write/NOP): rpm_poll.py, freeze_poke.py, panel_delta.py, floor_scan.py, live_patch_map.py, disasm_sgd2.py, patch_toggle.py.

scratchpad live_patch_map.py vs C:/Diablo II/*.dll


The Esoteric Path (attempt/failure ledger 2026-07-19)

FLOOR-SHIFT HYPOTHESIS LEDGER (chain of superseded theories, each with why it fell): (a) ScreenShiftX D2Client+0x11B9A0 -- FALSIFIED live (WriteProcessMemory froze it to 0, floor did not move; only a border flickered). (b) 'D2DX shifts floor-category downstream' -- FALSIFIED by reading bolrog+Necrolis D2DX source (NO per-category transform; only uniform UnitMotionPrediction, and nomotionprediction=true gave no change). (c) D2FloorTileParamsStrc.nViewXOffset @0x6FB9A990/0x6FB9A9CC -- FALSIFIED: that instance address is 1.10f, ZERO refs in our 1.13c DLL (xref + immediate byte-search); the struct LAYOUT transfers but the 1.13c instance differs. (d) 'ctx+0x24 scrollX carries +320' (5-front on-disk disasm assault) -- FALSIFIED live: floor_scan same-position closed-vs-open showed drawL=0/scrollX=-208 in BOTH (delta 0). (e) CONFIRMED: SGD2FreeRes SetTileCullingBound live patch shifts the tile-cull window +/-320 on panel-open (finding.floor_shift_sgd2_tileculling). META-LESSON: (a)-(d) all came from analyzing the ON-DISK vanilla DLL. The shift is in a MOD's live patch. Doug flagged the layered-mod interaction; the live-vs-disk patch map resolved it. Also: a same-position live read (stand still) is required -- 20-min-apart readings are confounded by camera movement.


The Esoteric Path (live WPM freeze test 2026-07-19, Doug visual+screenshot)

FREEZE-scrollX TEST (hammered ctx+0x24 = *(D2Client+0x11C3C4)+0x24 from -240 to -600, 3439 writes/30s): BOTH floor AND walls flickered/slid (Doug: 'a lot of flickering on the walls and floors'). CONCLUSIONS: (1) the D2Client flush 0x6FB4ED50 / render-ctx path IS the LIVE world render -- SGD2FreeRes does NOT draw the floor via its own renderer; the vanilla screenX = ctx+0x04(drawL) - ctx+0x24(scrollX) + tileAbsX is what shows. Rules IN the D2Client path. (2) CORRECTS finding.floor_shift_is_ctx_scrollx: walls do NOT use offX-not-scrollX -- BOTH floor and walls read ctx+0x24. The world renders as a unit through this ctx. (3) By elimination: drawL+scrollX are FLAT panel-open-vs-closed (floor_scan same-position delta 0), the flush is live, so the panel shift can only ride in tileAbsX (the COLLECTED tile coords). Leading theory: our no-shift lens forces drawL=0 AFTER DrawRooms already collected tiles using the game's shifted drawL (the council-wave-6 'hook too late'). ALSO: NOP of SGD2 SetTileCullingBound panel-block (0x739D221C) had ZERO impact -> that cull-widen is not the shift. NEXT: read tileAbsX/screenX at the flush (codecave probe or hardware BP) panel-open vs closed; and re-check WHEN our lens sets drawL vs when DrawRooms reads it.

live WPM freeze ctx+0x24 (Game.exe PID 17412, ctx 0x214639D4)


The Esoteric Path (Doug's waypoint experiment + template-match 2026-07-19)

OBJECTIVE PIXEL-PRECISE MEASUREMENT of the floor shift. Setup (Doug): Barbarian standing on the Act1 encampment waypoint, no merc, no effects. The waypoint = co-located FLOOR marker (brown cross-diamond) + UNIT markers (blue swirls). Captured game frames closed vs open, template-matched each marker: waypoint brown-cross (FLOOR tile) moved LEFT 480 screen-px (dy 0, conf 0.56); blue swirl (UNIT) 0px (conf 0.82); campfire embers (OBJECT) 0px (conf 0.68); walls 0 (prior overlay). Screen is 1920w upscaled by D2DX from game-render 1280 (DispW) => factor 1.5 => 480 screen-px = 320 GAME-px = exactly DispW/4. DEFINITIVE: the FLOOR-TILE LAYER shifts exactly DispW/4 = 320 game-px LEFT on panel-open; units/objects/walls hold at 0 (offX layer, our lens already fixes them). Confirms Doug's exact claim to the pixel. RECONCILES the whole investigation: the render ctx (drawL/scrollX) reads FLAT open-vs-closed because the 320 rides in the collected floor tileAbsX, NOT the camera scalars; the earlier full-floor overlay aliased the 320 (=~5 tiles) but the waypoint's non-periodic cross revealed it. Method note: capture the game window via PIL ImageGrab (D2DX/D3D content grabs fine); measure in GAME-space = screen/1.5. FIX TARGET: un-shift the floor tile collection (tileAbsX) by DispW/4. Ruled out as the source: ScreenShiftX 0x11B9A0 (poke null), SGD2 SetTileCullingBound cull-block 0x739D221C (NOP null), the render ctx (flat). It is in the floor tile-coordinate computation (DrawRooms / the floor pass tileAbsX).

game-frame capture + template-match (wp_closed.png/wp_open.png, Game.exe PID 17412)


SURVEY/phase-corr min-max envelope is BLIND to a sub-tile-period floor shift

The [SURVEY] probe (grDrawVertexArrayContiguous vertex X min/max, closed vs open) reported dMinX=0 for the floor caller 0x6A55 (x=-32..1296 both states), which I nearly read as 'floor reaches the GPU unshifted'. WRONG: the floor is a periodic full-screen tile grid; a -320 (=DispW/4) shift REFILLS the same min/max envelope, so the envelope is invariant under the exact shift Doug sees. Same aliasing trap that made diff_shift.py phase-correlation return dx=0 on the floor grid. RULE: never use a min/max envelope or a periodic-signal correlation to decide whether a tiled floor shifted; use a NON-periodic landmark (Doug's waypoint template-match gave the true -320) or read a specific tile's abs coord. Doug's direct visual observation (floor+waypoint slide -320, units/walls hold) stands; the probe simply cannot see it.


Floor -320 is baked into tile coords in the DrawGame PROLOGUE via native ctxL=-320, before the lens's PASS1-hook correction

Live [NOSHIFT] log (session PID17412, right panel mode=1): ctxL -320->0 every frame -- the native game sets ctx+0x04 (draw-window left) = -DispW/4 = -320 on right-panel-open, and the NoShift lens zeroes it at the PASS1-call hook 0x6FAF4302. But the floor room-collection reads ctxL a beat EARLIER (DrawGame prologue, before the hook; council-wave-6 note feature_panelworld.c:590), so the -320 is baked into the collected tile abs coords (tileAbsX); the flush 0x6FB4ED50 (screenX=ctxL-scrollX+tileAbsX) then faithfully carries it with ctxL=0. offX held 0 (units/walls safe); ctxL NOT held early enough for the floor. [FLOORPARAMS]/[SCRATCH] channels empty => nViewXOffset & scratch-scroll are NOT the carrier; ctxL-baked-in-prologue is. Source of the -320: view-setup fn 0x6FB29140 computes -DispW/4 and writes it to ctx+0x04 via cull-bound setter 0x6FB3A4D0 (pass-through). TEST IN FLIGHT: esoteric.ini FloorCompX=320 + glidevtx floor-filter 0x6840 -> compensate the floor's vertex X +320 at the D2Glide submit; if the floor snaps into alignment, upstream-baked confirmed and the root fix = author ctxL=0 before the prologue collection (earlier hook) or NOP the 0x6FB29140 -320 write.


DEFINITIVE floor draw path: D2Glide 0xB890 via grDrawVertexArray@12; -320 baked in vertex screen-X upstream

Settled by LIVE execution counters (safe codecave, no guessing): (1) The floor is drawn by D2Glide fn 0x6F85B890 (an FPU polygon rasterizer) via grDrawVertexArray@12 (thunk 0x6F856026 -> IAT 0x6F861250). @12 fires ~54k calls/2s, ONLY from 0xB890 (call sites 0xBC57/0xBEF7). (2) The workflow's 0x6F856290 AND 0x6F856AA0 (both have the pretty DispW/4+neg -320 logic and call @12) are DEAD -- entry execution counters = 0 while walls_6840 counter = 310969. Patching either's mode-1 cmp (0x62B8 / 0x6AC9 : 01->7f) did NOTHING (confirms dead; corroborates the old v1.9.1 KB 'dead 0x6290'). The live dispatch calls 0x6AA0 from 0x8572B2 but 0x6AA0 still never executes -- 'called-from' != 'executed'. (3) Walls are @16 grDrawVertexArrayContiguous (IAT 0x6F8611D0) via 0x6840; the 4 probed callers 0x6A55/0xA344/0x9AF4/0xA555 are ALL @16 (walls) -- which is why FloorCompX on 0x6A55 moved walls not floor (wrong primitive). (4) The @12 vertices are an array of POINTERS (arg3=0x6F866308 -> ptrs 0x6F865F18..) into 0xB890's OWN static scratch buffer at D2Glide 0x6F865F18 (0xB890 opens with 'mov edi,0x6f865f18'); that buffer is READWRITE and holds REAL screen coords (captured GrVertex X=720.0 Y=164.0, 7 floats/28B stride, panel open). So the floor's -320 is baked into the vertex SCREEN-X and rides from D2Client through D2Glide unchanged; 0xB890 and its callers 0xD1A0<-0xDF4F compute NO DispW/4 (the DispW touches there are RGB565 unpack + res-config writes 0x280/0x320). CONCLUSION: the -320 is baked ONCE upstream in D2Client's floor tile abs-coord (tileAbsX at flush 0x6FB4ED50), in a value the NoShift lens does NOT touch (not ctxL/offX/nViewXOffset -- holding all three 0 never moved the floor). The aliasing-blind [SHIFT]/[SURVEY] probes (min/max envelope on a periodic tile grid) could not see it. FIX MUST BE UPSTREAM (static patch), NOT a live per-vertex comp: 3 ACCESS_VIOLATION crashes came from hand-writing the shared 450x/frame vertex buffer live (FPU/accumulation/downstream-D2DX-fragility). Live toolkit built this session: log_drawers.py (entry exec counter+mode histogram, codecave), iat_count.py (draw-primitive call counter), safe_at12_log.py (READ-ONLY arg capture), patch_dg_byte.py (1-byte D2Glide patch). Codecave lesson: stub must NOT overlap its own data buffers; rel8/rel32 = target-(end of jump); verify-disasm-before-fire catches bugs.


D2Client tile flush 0x6FB4ED50 is DEAD -- the floor's live screen-X exists ONLY in the D2Glide @12 vertex buffer

Read-only exec probe at the flush 0x6FB4ED6E (cmp bl,0xff): with the game IN-GAME (worldCtx 0x21FD39D4) and the RIGHT panel open (ScreenOpenMode=1) and the hook confirmed installed (byte 0xE9), the flush counter stayed idx=0 -- it NEVER FIRES. So the tile flush screenX=ctxL-scrollX+tileAbsX @0x6FB4ED50 (the projection the NoShift lens PW_RVA_FLUSHHOOK and the floor-path workflow both analyzed) is DEAD CODE. This is why holding ctxL/offX/nViewXOffset=0 never moved the floor: they feed a dead projection. SAME dead-code trap as the D2Glide twins 0x6290/0x6AA0. CONSEQUENCE: there is NO live D2Client static-patch point of the classic form; the floor's finalized screen coordinate exists ONLY in the D2Glide @12 vertex buffer (0xB890 output at D2Glide 0x6F865F18, GrVertex X=720 panel-open, READWRITE). Likely the vanilla D2Client tile path was redirected by SGD2FreeRes (d2dx_sgd2freeres.dll, the res+sidebar mod that also owns SetTileCullingBound 0x739D221C LEFT-=320/RIGHT+=320) into the D2Glide 0xB890/@12 path, and the -320 sidebar shift is applied on that live path -- NOT in vanilla D2Client. PRAGMATIC FIX: a +320 comp on the @12 (grDrawVertexArray, IAT 0x6F861250) floor vertices, done in crash-tested C in esoteric.dll (masked FPU, skip NaN/inf, gate ScreenOpenMode==1) -- targets the floor primitive only (walls=@16). Live hand-rolled per-vertex comp crashed (ACCESS_VIOLATION) 3x: read-only capture PROVED the vertices are pointer-array into a READWRITE static buffer refilled per-call (no accumulation) with real coords, so the crash was FPU-on-special-value or a downstream D2DX effect from the shifted X -- both handled by the framework's C path. ROOT (deeper): trace 0xB890 coord source or SGD2FreeRes's floor redirect.


@12 CONFIRMED as the live floor path (Doug visual); +320 vertex comp SHEARS the floor (shared-vertex mesh)

BREAKTHROUGH: v1.13 comps the floor's grDrawVertexArray@12 vertex X (+320 right panel, in crash-safe C, retaddr-filtered to 0xB890's submits [0xBC00,0xBF00)). Doug's DIRECT visual confirmation: the comp VISIBLY affects the FLOOR ('you are ABSOLUTELY touching the right place now') -- units/walls unaffected. So @12 / D2Glide 0xB890 IS the live floor draw, settled beyond doubt after a session of dead-code traps (0x6290/0x6AA0 @12 twins dead, D2Client flush 0x6FB4ED50 dead). NEW SYMPTOM: instead of a clean +320 translate, the floor renders DISTORTED/SHEARED (horizontal smear of the ground plane; panel-closed baseline is clean; units undistorted). This matches the safety-review's #1 predicted correctness risk: the floor is a SHARED-VERTEX mesh (grDrawVertexArray@12 = array of POINTERS to GrVertex; D2's isometric floor tiles share edge/corner vertices), so blindly comping every pointer-array entry double-comps shared vertices (+640 on shared, +320 on unique) -> shear. Also possible: the two @12 submit sites in 0xB890 (0xBC57/0xBEF7) reuse the static buffer 0x6F865F18, or per-frame accumulation across the ~450 calls/frame. FIX DIRECTION: comp each UNIQUE vertex exactly once (per-call or per-frame de-dup of vertex pointers), OR shift at a stage upstream of the shared-vertex expansion, OR find where SGD2FreeRes applies the sidebar shift and neutralize it there. Crash-safety now solid (integer NaN/Inf exponent check before any x87 fld -- the review caught that the earlier fld-on-SNaN was the same bug as the 3 asm crashes). Next: research the D2 floor mesh structure + SGD2FreeRes sidebar shift + D2DX grDrawVertexArray, then a shear-free comp.


@12 floor comp de-dup: comp each UNIQUE vertex pointer once per call (shear fix, perspective OFF confirmed)

Doug confirmed he does NOT play with D2 'Perspective' video mode on -> the floor is orthographic, so a FLAT +320 comp is geometrically valid (no per-Y scaling), which makes the shared-vertex de-dup the correct fix for the shear (finding.floor_at12_CONFIRMED_shear). CHANGE (src/probe.c, PK_VTX12 branch): added a per-call seen[256] pointer set; when comp!=0 each vertex POINTER is comped exactly ONCE (linear-scan de-dup, skip duplicates) so shared strip/fan verts get +320 not +640. Crash-safety unchanged (null-ptr skip + integer NaN/Inf exponent check before any x87 fld). Built with portable Zig (tools/build.ps1) + deployed to C:\Diablo II\esoteric.dll (264704 bytes, mtime 2026-07-19 20:50). ini armed: FloorCompX=320, glidefloor=vtx12 D2Glide 0x11250 0xBC00, Survey=0. PREDICTION: floor now translates a clean +320 (fills the panel-open dead-zone) WITHOUT the horizontal smear. PENDING: Doug relaunch (PlugY, non-elevated via explorer) + open right panel + report. If it still shears -> either >256 distinct verts/call (raise cap) or the shear is in D2DX's fan expansion downstream (go upstream: write-BP the SGD2FreeRes floor-shift global). CAVEAT: seen cap 256 -> a single @12 call with >256 distinct verts would leave the tail un-de-duped (observed calls ~12 verts, so unlikely).


@12 floor de-dup v1.14 FAILED live (still shears); count=12000 is an AGGREGATE not per-call; root cause unconfirmed

Doug tested attempt.floor_at12_dedup_v1 (v1.14, per-call seen[256] pointer de-dup) LIVE: the ground tiles STILL show significant distortion/smear -- the de-dup did NOT fix the shear. CORRECTION: the probe log '[PROBE glidefloor] count=12000' is p->cnt, an ACCUMULATED counter summed across many floor calls between dumps (probe.c:254), NOT one call; probe.c:91 bails on any call >4096 verts. So per-call vertex count is UNMEASURED (prior single capture ~12 verts). TWO candidate causes, NOT yet distinguished -- do NOT patch on a guess: (a) seen[256] cap too small -> a call with >256 unique verts leaves the tail double-comped -> shear; (b) the >4096-vert bail -> big floor calls skip the comp entirely (render shifted) while small calls render comped -> a MIX that reads as tearing. Need a per-call count measurement (add per-call log, relaunch). Source read confirmed D2DX reads each source pointer ONCE, so the in-place de-dup MODEL is right; the shear is our comp double-hitting shared pointers within the source array. LESSON: my read of Doug's screenshot as 'no shear' was WRONG -- pixels lie, Doug's eyes are ground truth (he caught it).


Floor -320 = native D2Client width/4 on SGD2FreeRes's 1280, DECOUPLED from offX; D2DX exonerated; scr@0x11C414 + width@0x6FB8BC48

SOURCE READ (4-agent workflow over refsrc/ SGD2FreeRes + d2dx-jarcho + D2MOO): (1) D2DX EXONERATED -- OnDrawVertexArray reads each floor GrVertex.x ONCE, maps linearly to NDC, no X offset; the -320 is baked in BEFORE D2DX -> the de-dup model is correct. (2) SGD2FreeRes has NO literal width/4 or panel-conditional floor shift; it only sets GeneralDisplayWidth=1280 (native width/4 = 320 vs vanilla 640->160) and ScreenShiftX = width/2-320 = 320. So the floor -320 is NATIVE D2Client re-deriving from width at draw time, AMPLIFIED (1280) + DECOUPLED from units by SGD2FreeRes. (3) D2MOO: vanilla shifts floor+units TOGETHER via one camera origin -> a floor-ONLY divergence is a MOD artifact. LIVE+DISASM (same-user ctypes RPM + capstone on on-disk D2Client, base 0x6FAB0000): scr @ D2Client+0x11C414 (abs 0x6FBCC414) = panel flag, LIVE-CONFIRMED 0->1 on inventory open (2=left). Resolution-WIDTH global = [0x6FB8BC48] (800/640 on disk; SGD2FreeRes patches to 1280 live). Vanilla panel handler @ 0x6FACE8B3 / 0x6FACEA3F: mov eax,[0x6FB8BC48]; cdq; and edx,3; add eax,edx; sar eax,2; neg eax (= -(width/4)) -> mov [0x6FBCC418],eax (offX) ; mov [0x6FBCC414],1 (scr). BUT offX (0x6FBCC418) = UNITS/WALLS shift: live offX read 0 with inventory open yet floor still -320 -> FLOOR IS DECOUPLED from offX (matches ledger_v2). Broad open-vs-closed D2Client memory diff found NO clean stored -320 global (camera drift = noise) -> floor shift is INLINE / a SGD2FreeRes hook. D2FloorTileParamsStrc (code-editing KB, Necrolis): nScreenPanels+20, pView+24, nViewXOffset+3C; instance addr 0x6FB9A990 is a 1.10f addr (0 refs in our 1.13c -> confirms ledger_v2 (c)). VERIFIED STACK: Jarcho d2dx 0.103.4 (glide3x, NOT bolrog -- that's the .bak) writes+loads bundled d2dx_sgd2freeres.dll + d2fps 1.0.2; PlugY 14.03.


Floor -320 site still unpinned (D2Client inline vs SGD2FreeRes hook); RE offloaded to ChatGPT via FLOOR_RE_PACKAGE

The floor's exact -320 site is unpinned: either a separate inline -width/4 in the D2Client floor-tile draw loop (gated by scr/nScreenPanels) OR a SGD2FreeRes live patch/hook of that draw. On-disk D2Client search: scr (0x6FBCC414) = 210 refs; filtered to 60 sites reading width AND scr AND computing -(x/4) -- too many to eyeball, and the on-disk floor draw may be vanilla (SGD2FreeRes patches it live). Doug has FREE ChatGPT frontier compute, so the RE is OFFLOADED: assembled FLOOR_RE_PACKAGE_for_ChatGPT.md (esoteric-dll root, ~231KB/56k tok) = findings + DATA A (the 60 D2Client candidate disasm sites, offX handler flagged) + DATA B (SGD2FreeRes D2Client render patches: set_screen_shift / set_tile_culling_bound / set_general_display_width_and_height) + DATA C (D2FloorTileParamsStrc + PATH_WorldToScreen). Asks for the exact site + a minimal version-guarded byte patch (esoteric.dll applies via WPM at load) un-shifting ONLY the floor. AWAITING ChatGPT result -> verify vs live binary -> implement. Toolkit: scratchpad/{live_floor_read,scan_disk,scan_scr_shift,gen_disasm,build_package}.py. OPEN FIX OPTIONS: A=fix the comp (measure per-call count -> hash-set de-dup + handle >4096 calls); B=upstream neutralize (this package).


FLOOR -320 SITE FOUND + byte-verified: D2Client 0x6FB10275 (scr->EBX=+/-width/4); 2-byte patch 0x6027D 74 15->EB 27

ChatGPT (frontier, fed FLOOR_RE_PACKAGE) located the floor shift; I VERIFIED it byte-for-byte vs on-disk D2Client. The recursive ground-tile traversal at D2Client 0x6FB10250 (RVA 0x60250) holds the floor-local panel offset: 0x6FB10275 mov eax,[0x6FBCC414](scr); 0x6FB1027A xor ebx,ebx (EBX=0 default); dec eax; 0x6FB1027D je 0x6FB10294 (scr==1 right); dec eax; jne 0x6FB102A6 (scr!=2); scr==2 LEFT -> EBX=+width/4; scr==1 RIGHT @0x6FB10294 -> mov eax,[0x6FB8BC48](width); sar eax,2; mov ebx,eax; neg ebx = -width/4. At 1280 (SGD2FreeRes-patched width) EBX=-320 right/+320 left; applied to tile coords at 0x6FB102A6+ (movsx eax,[ecx+6]; idiv [0x6FBA16B0]; sub edi,[0x6FBCC1F8]). VERIFIED FIX: patch 0x6FB1027D 74 15 (je 0x6FB10294) -> EB 27 (jmp 0x6FB102A6); EBX pre-cleared by the xor, so ALL panel states reach 0x6FB102A6 with EBX=0 -> NO floor shift, no shear, single site. SIGNATURE @RVA 0x6027C = 48 74 15 48 75 24 A1 48 BC B8 6F, confirmed EXACT on the live binary. Untouched: offX(0x6FBCC418 walls/units), panel state, and the culling block @0x6FB10C52/0x6FB10D12 (commits view window to 0x6FBCC1F0/1F4/1F8/1FC -- DO NOT patch that). NOT a SGD2FreeRes hook (it only sets width=1280, magnifying this vanilla /4). Implement via eso_patch_bytes (verify-then-write). Before test: turn OFF the competing FloorCompX comp (=0) + the PanelWorldNoShift/D2DXWiden shift-features so the clean upstream patch is tested alone. PENDING Doug live test.


FloorPanelShiftFix v1.15 CONFIRMED LIVE: floor shear/divergence GONE (world coherent; whole-world vanilla shift remains, PanelWorld's job)

esoteric.dll v1.15 applied the verified 2-byte patch (esoteric.log: '[patch] OK floor-panel-shift @ 6FB1027C'). Doug live-tested in ISOLATION (FloorPanelShiftFix=1, PanelWorld=0, D2DXWiden=0, FloorCompX=0): the FLOOR SHEAR IS GONE -- the world renders COHERENT (waypoint diamond sharp, ground moves as one piece); Doug: 'everything is shifted now, as if back to vanilla'. INTERPRETATION: the D2Client 0x6FB10275 EBX=+/-(width/4) branch was the floor's EXTRA/DIVERGENT shift ON TOP of the whole-world camera recenter, NOT the world shift itself. Zeroing EBX makes the floor shift the SAME amount as units/walls -> coherent, no divergence, no shear. The multi-day floor shear/divergence bug is SOLVED. What remains is the whole-world vanilla camera shift (a SEPARATE mechanism -- exactly what PanelWorld's NoShift suppresses). Why PanelWorld sheared before: it held units at 0 while the floor's EBX branch kept diverging; with EBX=0 the floor no longer fights the no-shift lens. NEXT (ini only, no rebuild): re-enabled PanelWorld=1 + D2DXWiden=1 alongside FloorPanelShiftFix=1 -> the no-shift + widen stack should now hold the whole world coherently AND fill behind the panel. Pending Doug relaunch + test.


Floor+roof RESIDUAL shift = tile COLLECTION/cull shift @ D2Client 0x6FB10C52 (EBX=+/-width/4 -> sub ecx,ebx @0x6FB10D12 -> view window 0x6FBCC1F0)

After the floor DRAW patch (0x6FB1027D, shear gone) + PanelWorld NoShift+Widen+D2DXWiden ALL applying (log 04:37: [NOSHIFT] offX0/ctxL-320->0, [WIDEN] ctx0C 960->1280, D2DXWiden 1280 asserted), Doug: floor AND roof STILL shift. FOUND the source -- a SECOND width/4 shift in the tile VIEW/CULL setup @ D2Client 0x6FB10C52: mov eax,[0x6FBCC414](scr); xor ebx,ebx; dec eax; je 0x6FB10CBC(right); dec eax; je 0x6FB10C91(left); each branch (gated also by ebp=[0x6FBCC1B0]==1) computes EBX=+/-(width/4) from [0x6FB8BC48], then 0x6FB10D12 'sub ecx,ebx' commits to the VIEW WINDOW 0x6FBCC1F0/1F4/1F8/1FC. The ground-tile traversal (0x6FB10250) READS 0x6FBCC1F8 for bounds -> shifts ALL collected tiles (floor+roof) at COLLECTION, BEFORE PanelWorld's NoShift zeroes the camera = the multi-session 'hook too late', now pinned. ChatGPT flagged 0x6FB10C52/0x6FB10D12 as risky (drives tile CULLING/selection) -> un-shifting may clip margins (mitigated by D2DXWiden=1280). This is the DRAW-shift (patched) vs COLLECTION-shift (this) split. Doug has ~unlimited free ChatGPT frontier compute (promo) -> chose to OFFLOAD: package E:\...\esoteric-dll\FLOOR_RE_PACKAGE2_collection_for_ChatGPT.md (context + cull-block disasm + traversal read + question: un-shift collection WITHOUT breaking culling). AWAITING ChatGPT -> verify vs binary -> patch.


CONVERGENT ANSWER (4 ChatGPT prompts): virtualize scr(0x11C414)=0 TRANSACTIONALLY around the world pass (PASS1@0x6FAF4302..PASS2@0x6FAF436A); scr is the shared SELECTOR, no single derived shift global

Ran 4 frontier prompts (forward-RE, architecture-rethink, adversarial-audit, prior-art-research) + 3 KB-extracts. They CONVERGED: there is NO shared derived panelShiftX global; scr @0x6FBCC414 (ScreenOpenMode) is the shared SELECTOR that MULTIPLE INDEPENDENT consumers each read to recompute +/-(width/4) (floor EBX @0x6FB10275, view/cull @0x6FB10C52, others). Clean fix is NOT per-consumer whack-a-mole -- VIRTUALIZE scr=0 for the WORLD RENDER PASS, RESTORE it for the UI/panel draw (D2R does exactly this scene/UI separation). offX(0x11C418) is NOT the root (separate walls/units offset). Prior-art: SGD2FreeRes is the closest legacy prior art (patches CullingSpec draw_window_rect+tile_culling_window); no known LoD mod does full world-under-panel; a wrapper +320 is too late (can't recover un-collected tiles). Audit MISREAD our draw patch as asymmetric -- it's EB=unconditional jmp so ALL scr reach 0x6FB102A6 with EBX=0 (symmetric, re-verified). KEY: PanelWorld ALREADY has a scr=0 hold (g_hold_mode / ini PanelWorldHoldMode, DEFAULT OFF) but as an ASYNC thread holding scr=0 continuously incl. during UI -> 'diamond'/cull artifacts. FIX = TRANSACTIONAL: DrawGame @0x6FAF4249 loads ctx *[0x6FBCC3C4], calls PASS1(0x6FB3B0C0)@0x6FAF4302 then PASS2(0x6FB3B110)@0x6FAF436A(walls/roofs/units); PanelWorld already hooks the PASS1 call (PW_RVA_PASS1CALL 0x44302). Set scr=0 before PASS1, restore after PASS2. QUICK TEST first (ini PanelWorldHoldMode=1, no rebuild): does async scr=0 un-shift the floor (artifacts OK)? If yes -> implement transactional; then ChatGPT's check: reverting 0x6027D/0x6FB10D12 should still compute 0 (scr=0).


v1.17 TRANSACTIONAL scr=0 world-pass guard: implemented in PanelWorld, CONFIRMED_SAFE on 3 adversarial angles, built + deployed

Implemented ChatGPT's transactional guard as pw_install_scrguard() in feature_panelworld.c, gated by [Features] PanelWorldScrGuard. Mechanism: E9-detour the GATE @0x6FAF4243 (6 bytes 0F857501 0000 = jne 0x6FAF43BE) -> 0x80-byte cave entry stub (pushfd/pushad; if depth==0 save scr@0x6FBCC414; depth++; if saved scr in {1,2} set scr=0; popad/popfd; REPRODUCE the original JNE via preserved EFLAGS -> push 0x6FAF4249 fall-through / push 0x6FAF43BE early-out; ret). E9-detour the EPILOGUE @0x6FAF43BE (5 bytes 83C420 C3 CC = add esp,0x20;ret) -> exit stub (pushfd/pushad; depth-- underflow-guarded; if depth==0 restore scr; popad/popfd; add esp,0x20; ret). Hooking the GATE (not body 0x6FAF4249) puts scr=0 BEFORE the prologue tile-collection reads it; every invocation incl. early-out balances depth. Only scr in {1,2} virtualized (0 and special-3 left alone). Blob is PIC for savedScr(+0x78)/depth(+0x7C) via CALL/POP; 5 absolute D2Client refs relocated to live cb (ASLR-safe). Uses eso_codecave_alloc(0x80) + eso_detour (verify-then-write, auto-refuses double-hook vs PanelWorld2P:reseed-ret which shares 0x6FAF43BE). VERIFICATION: my capstone disasm + byte-compare vs live binary PASSED (gate/epi bytes match, JNE target 0x6FAF43BE, 5 reloc fields correct, push targets correct). Then a 3-agent adversarial workflow ALL returned CONFIRMED_SAFE: (1) blob stack-balanced/EFLAGS-correct/depth-safe/PIC-correct; (2) DrawGame single-entry/single-exit, nothing branches into either E9, ALL exits funnel through 0x6FAF43BE, nothing enters 0x6FAF4249; (3) epilogue-restore scope safe -- scr is a D2Client-PRIVATE global, the 6 post-PASS2 tail calls are world/scene finalization + cross-DLL thunks (D2Win/D2gfx can't read scr), no UI/panel/hit-test runs inside DrawGame's tail. Only caveat (informational): a callee longjmp/throw would strand the guard, but D2's draw path uses neither. BUILT 1.17.0-scrguard (Zig cc x86, LoadLibrary OK), DEPLOYED C:\Diablo II\esoteric.dll (266240 bytes, sha 67728A57). INI clean-test config: PanelWorldScrGuard=1, FloorPanelShiftFix=0 (consumer patches reverted -> should compute 0 under the guard), PanelWorldNoShift=0, SecondPass=1 (A-E off), D2DXWiden=1. AWAITING Doug's live test: does the floor un-shift full-width behind the panel? does the panel still render + clicks stay coherent? log line to confirm install: '[feat] ScrGuard: transactional scr=0 world-pass guard ARMED'.


v1.17 scr-guard LIVE TEST: BLACK DEAD-ZONE RESOLVED -- world now renders FULL-WIDTH BEHIND both panels (the ultimate goal's hard part). Remaining = DispW/4 camera RECENTER (offX), a separate lever.

Doug's live test of the transactional scr=0 world-pass guard (v1.17): the black dead-zone is GONE -- the world renders full-width behind BOTH the inventory (right) and character (left) panels (grass/walls/NPCs continue behind the panel art). This is the 'world behind panels' goal's hardest part, achieved. Mechanism confirmed: guard holds scr=0 across DrawGame -> the native no-panel draw-window computes full width -> nothing scissored to black. NOTE this also proved the widen worker (eso_pw_widen @0x6FAF4302) is NEUTERED by the guard (it early-returns on mode!=1&&!=2, and the guard makes mode=0 inside the pass) -- so the full-width comes from the GUARD's scr=0 (native no-panel draw window), not the widen worker. REMAINING BUG: the world still SHIFTS when a panel opens (right panel -> world slides LEFT; left panel -> world slides RIGHT) = the vanilla DispW/4 play-area RECENTER. This is a SEPARATE lever from the scr-gated width: it rides in offX = GeneralPlayAreaCameraShiftX @0x6FBCC418, a STORED camera offset the panel handler writes ONCE at panel-open, OUTSIDE the world pass -- so the guard's scr=0 never resets it. offX is written-once and is NOT a detection flag (unlike scr), so pinning it 0 is safe + keeps hit-test coherent (screen<->world both use offX=0). NEXT: re-armed the async offX-hold (ini PanelWorldNoShift=1, pw_hold_offx_noshift) to test whether offX is the sole remaining lever. If the world centers -> fold offX into the guard transactionally (entry: save+zero offX@0x6FBCC418 alongside scr; exit: restore) for a clean race-free fix. If still shifted -> the recenter is in scrollX (scroll origin) and hold that instead.


WALL/ROOF SHIFT SOLVED: carrier = render-ctx drawWinL(ctx+0x04), baked -/+DispW/4 at panel-toggle by setter 0x6FB3A4D0; pin ctx+0x04=0/ctx+0x0C=DispW -> world whole behind panels. Live-confirmed.

The last lever. After scr-guard (scr=0 world pass) + offX-hold locked floor/units/objects, WALLS+ROOFS still slid +/-320. A 4-agent workflow (find-wall-lever) traced it: the panel-toggle handlers (0x6FAEB0A0 inventory/skill; the 0x6FB2F0C3/3A9/648 family) compute DispW/4 and do TWO things: (1) write offX(0x6FBCC418)+scr(0x6FBCC414) [already neutralized], AND (2) fold +/-DispW/4 into a viewport CENTER (ecx=[0x6FBA9E14]+/-DispW/4) passed to the generic draw-window setter 0x6FB3A4D0, which BAKES it into the render-context RECT: drawWinL @ ctx+0x04 and drawWinR @ ctx+0x0C (ctx=*[0x6FBCC3C4]). This bake happens at TOGGLE time (not per-frame; 0x6FB3A4D0 has ~110 callers, none per-frame), so no per-frame guard reaches it. FLOOR is immune because DrawGame re-derives scrollX(ctx+0x24) FROM drawWinL each frame -> drawWinL cancels in screenX=drawWinL-scrollX+tileAbsX; UNITS ride offX (held 0). But the wall/roof drawers (0x6FB3AE00/6C0/140) project through the baked drawWinL RAW with no cancelling term -> the +/-320 survives. nViewXOffset(0x6FB9A9CC) is DEAD CODE (0 refs whole-file) -- confirmed why holding it did nothing. VERIFICATION (couldn't be proven on-disk: wall accessors are D2gfx ordinal imports): LIVE same-user RPM read confirmed drawWinL(ctx+0x04): 0 panel-CLOSED -> -320 with RIGHT panel open (drawWinR 1280->960). LIVE WPM poke: forcing drawWinL=0/drawWinR=DispW while the inventory was open SNAPPED the walls into alignment with the floor (Doug confirmed y). FIX v1.18: pw_hold_offx_noshift (the offX-hold thread) now also pins ctx+0x04=0 and ctx+0x0C=DispW when a panel is open (ini PanelWorldHoldDrawWin=1). Race-free: drawWinL is written once at toggle, DrawGame only READS it. This completes 'world whole behind panels': scr-guard (width/black) + offX-hold (units) + drawWin-hold (walls/roofs/floor coherence). Live-read toolkit: scratchpad live_ctx_read.py / live_drawwin_poke.py.


DONE (v1.18 CONFIRMED LIVE): the world renders WHOLE behind the side panels -- black gone, floor+units+objects+walls+roofs all static, full-width, coherent. The hard part is over.

Doug confirmed the v1.18 relaunch: with the inventory/character panels open, the ENTIRE world (ground tiles, NPCs, objects, AND walls/roofs) holds static and full-width behind the opaque panels -- no black dead-zone, no shift, no shear, world coherent, panel drawn on top. This is the goal the esoteric.dll PanelWorld feature was built for. Shipped as C:\Diablo II\esoteric.dll 1.18.0-worldwhole. Achieved by three composed levers (see [[solution.world_behind_panels_ARCHITECTURE]]). Remaining: Doug's cleanup list (config tidy, retire the dead shift-hacks + the ineffective wall/roof patches) -- polish, not the hard part.


CRYSTALLIZED SOLUTION -- world-behind-panels = THREE INDEPENDENT levers (there is NO single shift global). scr-guard (black/width) + offX-hold (units) + drawWin-hold (walls/roofs). All in esoteric.dll PanelWorld.

GOAL: at 1280x720, render the D2 LoD 1.13c world FULL-WIDTH and UN-SHIFTED behind the opaque side panels (inventory=right/scr1, character=left/scr2), instead of vanilla's DispW/4 (=320) play-area recenter that hides the world and leaves a black dead-zone.

KEY INSIGHT (why it took so long): the panel shift is injected in THREE STRUCTURALLY INDEPENDENT places, each read by a different render layer -- there is no one global to zero. Every single-lever attempt fixed one layer and exposed the next.

THE THREE LEVERS (all live, all in PanelWorld, esoteric.dll):
 1. scr-GUARD (kills BLACK / width). Transactional virtualization of scr=ScreenOpenMode(0x6FBCC414)=0 for the ENTIRE DrawGame world pass: E9-detour the gate @0x6FAF4243 (save scr, zero for scr in {1,2}, depth-count, reproduce the JNE) + the epilogue @0x6FAF43BE (restore). Makes the scr-gated draw-window + tile-cull compute the full no-panel width -> nothing scissored to black. ini PanelWorldScrGuard=1. Verified CONFIRMED_SAFE (blob byte-correct, single-entry/single-exit, epilogue-restore scope). Makes the old consumer patches (0x6FB1027D je->jmp, 0x6FB10D12 sub->nop) REDUNDANT.
 2. offX-HOLD (kills UNIT/NPC/OBJECT recenter). Async thread pins offX=GeneralPlayAreaCameraShiftX(0x6FBCC418)=0 while a panel is open. offX is written once at panel-open and read only by the 5 unit/object/overhead-text projections; holding 0 un-recenters them. Race-free (written-once). ini PanelWorldNoShift=1 (arms pw_hold_offx_noshift).
 3. drawWin-HOLD (kills WALL/ROOF shift). Same thread also pins the render-context draw-window drawWinL(ctx+0x04)=0 and drawWinR(ctx+0x0C)=DispW (ctx=*[0x6FBCC3C4]). The panel-toggle setter 0x6FB3A4D0 bakes +/-DispW/4 into that RECT at open time; walls/roofs (PASS2 drawers 0x6FB3AE00/6C0/140) project through it RAW. The floor is immune (DrawGame re-derives scrollX FROM drawWinL each frame so it cancels) and units ride offX -- only walls needed this. Race-free (drawWinL written once at toggle, DrawGame only reads). ini PanelWorldHoldDrawWin=1. LIVE-CONFIRMED: drawWinL 0(closed)->-320(right panel); WPM poke to 0 snapped the walls into alignment.

Supporting: SGD2FreeRes sets DispW=1280 live; D2DXWiden forces D2DX gameSize=1280 so x>=960 isn't scissored (ini D2DXWiden=1). nViewXOffset(0x6FB9A9CC) is DEAD CODE in 1.13c (0 refs) -- ignore it.

WINNING ini [Features]: PanelWorldScrGuard=1, PanelWorldNoShift=1, PanelWorldHoldDrawWin=1, D2DXWiden=1, PanelWorld=1, PanelWorldSecondPass=1 (forces the old A-E shift-hacks off), FloorPanelShiftFix=0 (redundant under the guard). PanelWorldWallFix (the 3 wall/roof view-setup patches) applied but did NOTHING -- retire it.

METHOD that cracked it: read the actual live binary (not assume vanilla), static disasm (capstone) + ChatGPT frontier analysis + a 4-agent workflow to fan out the wall trace, then LIVE same-user RPM/WPM to confirm each lever before baking (esoteric-dll	ools + scratchpad live_ctx_read.py / live_drawwin_poke.py).


BOTH-PANELS (scr=3) all-black FIXED (v1.18.1): D2 SKIPS the world passes when ScreenOpenMode=3 (cmp[scr],3;je); scr-guard now virtualizes ANY non-zero scr {1,2,3}.

With BOTH side panels open (inventory right + character left), ScreenOpenMode(scr @0x6FBCC414)=3 and the whole world went BLACK. LIVE read (both panels open): scr=3, offX=0, drawWinL(ctx+0x04)=0, drawWinR(ctx+0x0C)=1280 -- i.e. the both-panels draw window is ALREADY full+unshifted, so this was NOT a shift; D2 simply SKIPS the world render passes when scr==3 (the DrawGame `cmp [scr],3; je <skip>` gates before the passes). The scr-guard originally virtualized only scr in {1,2} (ChatGPT deliberately left 3 alone), so scr=3 fell through -> passes skipped -> black. FIX: changed the guard's entry-stub condition from `cmp eax,1;je / cmp eax,2;jne` to `test eax,eax; je` (same 10 bytes, no relocation drift) so it virtualizes scr=0 for ANY non-zero scr {1,2,3}; with scr=0 the cmp/je no longer skips -> passes run -> world renders behind both panels. offX/drawWin hold thread condition also widened `mode==1||2` -> `mode!=0` (idempotent for scr=3 since its window is already correct). Shipped v1.18.1-bothpanels, Doug confirmed fixed. Verified the modified blob via capstone before deploy.


MULTIRES_SetResolution

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_viewport). VA 0x6FAC0DF0. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


SetResolutionFromOptions

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_viewport). VA 0x6FB15180. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


GetResolutionRegistry

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_viewport). VA 0x6FB16270. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


SetResolutionRegistry

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_viewport). VA 0x6FB162A0. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


DisableScrollOfInifussRect

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_render). VA 0x6FB2C380. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


DrawSummitBackground

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_render). VA 0x6FB3A6C0. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


SetTileCullingBound

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_viewport). VA 0x6FB3A4D0. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


MULTIRES_DrawGroundTiles

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_render). VA 0x6FAC43D0. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


MULTIRES_GroundTileParamSetup

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_render). VA 0x6FAC4050. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


MULTIRES_DrawRoofTiles

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_render). VA 0x6FAC3A00. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


MULTIRES_DrawLowerWallTiles

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_render). VA 0x6FB4EC10. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


D2SetScrollEndFunc

1.13c render/viewport function name HARVESTED from the Codex D2MOO KB (B2 wave-1, evidence class verified_cfg_translation; subsystem world_viewport). VA 0x6FB2AE80. Provenance: E:\Codex-D2MOO-1.13c-KB\mappings2-wave1-results.csv (read-only). Codex names are cross-version CFG/ABI translations against STOCK 1.13c; verify vs the live patched image before relying on bytes.


TileRoofDraw

1.13c tile-draw routine name HARVESTED from Codex KB (B2 wave-1, dispositioned_verified_split_lineage = verified identity but the 1.10f lineage split/merged, so not a clean 1:1 translation). VA 0x6FB29D30.


TileShadowOrSpecialDraw

1.13c tile-draw routine name HARVESTED from Codex KB (B2 wave-1, dispositioned_verified_split_lineage = verified identity but the 1.10f lineage split/merged, so not a clean 1:1 translation). VA 0x6FB29C80.


TileSpecialOrientationDraw

1.13c tile-draw routine name HARVESTED from Codex KB (B2 wave-1, dispositioned_verified_split_lineage = verified identity but the 1.10f lineage split/merged, so not a clean 1:1 translation). VA 0x6FB29E20.


1.13c tile-draw routine name HARVESTED from Codex KB (B2 wave-1, dispositioned_verified_split_lineage = verified identity but the 1.10f lineage split/merged, so not a clean 1:1 translation). VA 0x6FB2A180.


1.13c tile-draw routine name HARVESTED from Codex KB (B2 wave-1, dispositioned_verified_split_lineage = verified identity but the 1.10f lineage split/merged, so not a clean 1:1 translation). VA 0x6FB2A260.


1.13c tile-draw routine name HARVESTED from Codex KB (B2 wave-1, dispositioned_verified_split_lineage = verified identity but the 1.10f lineage split/merged, so not a clean 1:1 translation). VA 0x6FB2A500.


HARVEST from Codex D2MOO KB: 18 verified 1.13c render/viewport function NAMES land on addresses we had only numerically -- incl. SetTileCullingBound = 0x6FB3A4D0 (the wall-shift viewport-origin setter) and the 0x6FB29C80/D30/E20 tile sub-draws

One-way harvest (Codex repo is READ-ONLY to us, see [[codex-d2moo-kb-readonly]]) of Codex's B2 wave-1: 32 D2Client render/viewport rows -> 12 verified_cfg_translation + 6 verified-but-split-lineage. CROSS-VALIDATION with our own independent RE: (1) **0x6FB3A4D0 = SetTileCullingBound** -- the generic play-area viewport-origin setter our wall-shift workflow proved BAKES +/-DispW/4 into drawWinL(ctx+0x04) at panel-toggle; Codex reached it independently and gives it a name. (2) **0x6FB29C80 / 0x6FB29D30 / 0x6FB29E20 = TileShadowOrSpecialDraw / TileRoofDraw / TileSpecialOrientationDraw** -- exactly the tile-pass sub-draws in our render-pipeline notes, previously unnamed. (3) **0x6FAC0DF0 = MULTIRES_SetResolution** -- our 'FOV setup fn'. (4) 0x6FB3A6C0 = DrawSummitBackground -- NOTE this corrects an earlier working assumption that 0x6FB3A6C0 was a wall/roof projection routine. **OPEN DISCREPANCY: 0x6FB4EC10** -- our render-pipeline notes call it the PASS2 'unit draw'; Codex names it MULTIRES_DrawLowerWallTiles. Both may be partly right (shared/adjacent routine) but this must be resolved against the binary before either name is asserted. CAVEAT: Codex verifies against STOCK 1.13c (VersionChanger set, D2Client base 0x6FAB0000 -- same as ours); live patched bytes differ. Also harvested: Codex's CE-library cross-join DEMOTED two false same-ordinal D2MOO mappings (D2Common+0x31940 = STATS_GetBeltGoldLimit not DUNGEON_CallRoomCallback; D2Common+0x37D50 = STATLISTS_GetSkillId not STATES_CheckStateMaskShatterOnUnit).


MEASURED: same-number PE export ordinal is NOT a valid 1.10f->1.13c semantic mapping method -- 54/56 audited rows FAILED (96.43% raw, 98.54% population-weighted); D2Common failed 16/16

Result of the OA1 stratified audit in the Codex D2MOO KB (READ-ONLY to us, see [[codex-d2moo-kb-readonly]]). Codex had built ~91% of its 1.10f->1.13c 'verified' coverage on the class verified_export_ordinal = 'the same export ordinal exists in both stock versions and lands in a valid section'. A 56-row stratified sample (D2Common 16, D2Game 16, D2Gfx 5, D2Win 14, D2Net 3, D2Lang 2) applied a real exit gate (entry boundary, CFG, semantic constants, ABI, caller/callee neighborhood). **54 of 56 FAILED.** Only D2Game::GAME_CloseAllGames and D2Lang::Unicode::strnicmp survived. D2Common failed 16/16. ROOT CAUSE: the PE export table was reordered between 1.10f and 1.13c, so equal ordinal number != same function; the method was pairing unrelated routines. This measures ordinal-equality-as-semantics, NOT the validity of either version's export table. CONSEQUENCE: Codex's headline coverage fell 1,536 -> 1,482 rows on the sample alone, and the ~1,325 unaudited ordinal-only rows cannot be credited -- applying the measured rate leaves roughly 155-200 genuinely evidence-backed rows (~3% of the 5,749-row parity queue), not ~26%. PRIOR SIGNAL: the harvest of Doug's own CE library had already caught two false same-ordinal mappings (D2Common+0x31940 = STATS_GetBeltGoldLimit not DUNGEON_CallRoomCallback; D2Common+0x37D50 = STATLISTS_GetSkillId not STATES_CheckStateMaskShatterOnUnit) -- those were not anomalies, they were the tip of a ~97% failure rate. **RULE FOR THIS GRIMOIRE: never import a cross-version mapping whose only evidence is a matching export ordinal.** Our harvest of Codex render/viewport names (see [[finding.codex_render_name_harvest_2026_07_20]]) deliberately took only verified_cfg_translation and split-lineage rows, so no ordinal-class mapping ever entered this grimoire. Corollary worth keeping: Doug's hand-verified CE/live RE outperformed ~1,300 machine-inferred rows.