Skip to content

D2LoD Vanilla+0.0.7-R67

Pre-release
Pre-release

Choose a tag to compare

@VTSTech VTSTech released this 01 Nov 04:19
· 191 commits to main since this release
image image image

D2LoD Vanilla+ — Release 0.0.7

Release notes — R59 → R66

Release: D2LoD-Vanilla-Plus-0.0.7 — the first 0.0.7 build, cut at round R66 (build self-ID Vanilla+0.0.7-R66, confirmed via line 1 of d2boot.log).

Headlines:

  1. The 1-page shared stash (track N round 1, R63–R66) — the first new gameplay feature of the Vanilla+ line: one retail-shaped stash page shared by every single-player character on the PC, reached from a "Shared" tab on the stash panel (no hotkey), carrying both items and gold, stored in the pool's own side file _d2vp_shared.sss. Character saves stay retail-compatible by construction.
  2. The save-and-re-enter crash is fixed (issue #19, R60–R61) — "Saving a game and then trying to enter a game again crashes the game": root-caused, fixed, and field-verified by the owner in one day.
  3. Upstream sync completed through Ghidra 1783 (Rev 74–78) — the 1776–1783 regen absorbed with Vanilla+ intact, field-proven on the first post-sync build (R62: "Builds. Runs. No obvious regressions." plus a full SMOKE pass, items 0–20 green).

Everything ships under the standing invariants: .d2s saves remain byte-retail (the engine gains zero new blocks; fork content rides side files and in-memory snapshots), and every change is opt-in via d2vp.ini — with all keys at their defaults the game behaves as retail.

For the rounds before this window (R46→R52 and R52→R59 — the 720p follow-ups, Den of Evil #13, alt-toggle #15, the item-ilvl feature itself), see the 0.0.6-era round notes and the release table in README.md. R59 is included below because its field verdict and its docs round landed inside this window.


Build index (rounds covered by this release)

Round Date What shipped Track / issue Field verdict (owner, verbatim) Shipped as
R59 2026-10-31 item-ilvl polish: the gap before (N) is always exactly one space track K "Great job. iLvl spacing appears fixed." tag R59; overlay …R59-release-notes-r52-r59.zip (docs)
R60 2026-11-01 issue #19 instrumentation: roster probes + save-path guards, zero behavior change issue #19 probe log delivered; exit state provably clean overlay vanilla-plus-0.0.6-R60-issue-19-instrumentation.zip
R61 2026-11-01 issue #19 root-caught + fixed (the VP Resolution row mutated the static esc-menu table) issue #19 "R61 Confirmed fixes #19" overlay vanilla-plus-0.0.6-R61-issue-19-rootcause-fix.zip
Rev 74–78 2026-11-01 upstream sync: Ghidra 1776–1783 regen absorbed + two sync fixes (compile, link) upstream proven via R62 below merges 3d645e7, 19f9791, d6d4b53
R62 2026-11-01 first post-1783 build stamp; full SMOKE checklist (items 0–20) green — "Good job. Builds. Runs. No obvious regressions." overlay vanilla-plus-0.0.6-R62-sync-stamp.zip
R63 2026-11-01 shared stash round 1 CODED — 1 page, stash-panel tab, gold + items, _d2vp_shared.sss track N — overlay vanilla-plus-0.0.6-R63-shared-stash-1p.zip
R64 2026-11-01 build fix: mingw32 uint32_t* → LPDWORD in the .sss read out-param track N owner build green (13/14 TUs were already clean) overlay vanilla-plus-0.0.6-R64-stash1p-fix.zip
R65 2026-11-01 Shared tab anchor fix (0x1f8→0x180); mechanism field-proven blind track N owner toggled the page blind: page swap, gold + item deposit, character switch — both returned overlay vanilla-plus-0.0.6-R65-stash1p-tabfix.zip
R66 2026-11-01 Shared tab visibility fixed (0x180→0x130, out of the right-panel art's rectangle) track N "Great job. I see it. It works." overlay vanilla-plus-0.0.6-R66-stash1p-tabseam.zip

Docs rounds inside the window (zero source changes): Rev 80 — the pre-R61 round-by-round record offloaded to HISTORY.md (H29); Rev 81 — the R62 full-SMOKE pass recorded + SHARED-STASH-PLAN.md drafted; Rev 85 — the R64→R66 tab geometry story preserved as a comment block on the anchor constant.


Added

1-page shared stash — [features] shared_stash (track N round 1, R63 + fixes R64–R66)

Owner spec (verbatim, 2026-11-01): "Give us 1 page of retail-shaped shared stash to start. Accessible from Stash Panel. No hotkey. Gold and Items can be deposited. If that works we'll do 10 pages. Then 100." — all four plan design calls resolved in one message.

What it does in game. With shared_stash=1 in d2vp.ini, opening any stash in town shows a "Shared" text tab on the same row as the stash gold label (right of center, above the grid's rightmost columns). White = your personal page, green > Shared = the shared page. Clicking the tab swaps the grid between the two sets; the retail stash grid, buttons and hit-testing are untouched. There is no hotkey — the tab is the only affordance, per spec.

  • Items: deposit/withdraw works exactly like the personal stash — drag, shift-click, everything retail.
  • Gold: with the shared page active, the stash panel's normal gold deposit/withdraw buttons move shared gold (the display reads the shared amount); tab back and the display returns to your own banked gold.
  • Cross-character: everything on the shared page survives Save & Exit and is there for the next character on the same PC. Field-proven end to end: "The stash function behind the art - works. … i put gold and an item in there. switched chars. clicked around, and the item and gold were there." (owner, R65 field run).

How it stays retail-safe (the architecture, zero new packets, zero new item bytes, zero server container code):

  • While the shared page is active, the player's retail page-4 stash inventory holds the shared set — real item units materialized from the side file by the retail save-stream loader (the same call the trade-cancel restore uses). Every retail stash flow works unchanged.
  • The personal set waits in a fork-owned 'JM' memory snapshot written by the retail per-item encoder. Toggling = snapshot → dematerialize → materialize the other set → push the retail 0x9C/0x9D adds/removes so the client grid redraws.
  • Shared gold borrows the retail UNITSTAT_goldbank stat while the page is active; the save flush absorbs whatever the retail buttons moved.
  • I1 invariant by construction: the flush sits at the top of the single save dispatcher — before any .d2s is built, the shared set is parked into <save dir>\_d2vp_shared.sss (own 20-byte header: magic VPS1, version 1, pageCount 1, gold u32, additive checksum) and the personal set comes back. The .d2s itself never sees a shared item.
  • Durability: atomic write (temp → MoveFileExA replace), one-generation .bak, checksum-guarded reads — a corrupt file degrades to an empty pool with a log mark, and the file on disk is never touched by a misread (six degrade marks; drill = SMOKE §H item 27).
  • Scope guards: the server handles the toggle packet only when [features] shared_stash=1 and the game is single-player; other game types (and unpatched retail servers) see an out-of-range button id and reject it harmlessly. Per-game client reset rides the wholesale UI reset.

Config: new documented key [features] shared_stash — 0 (default) = vanilla, 1 = the tab. The template the exe regenerates next to d2exe carries the key with the comment; the 2-second poll means flipping the ini mid-session is picked up without a restart.

Limits of v1 (all deliberate): one page (10/100 pages are a version-2 header extension — the header already carries pageCount), single-player only, and the tab is drawn for the 800×600-class panel set (the 1280×720 check follows the 800×600 pass, per the owner's scope call — the hit-testing math is panel-relative and already verified there).

(nothing else new in this window)

R59's item-ilvl polish is a fix to the R55–R57 feature, listed under Fixed.


Fixed

Save → re-enter crash (issue #19, owner-filed 2026-10-31) — R60 probes, R61 root cause + fix, field-verified same day

Owner symptom (verbatim): "Saving a game and then trying to enter a game again crashes the game" — a second-session defect: the first game entry after boot was fine, the next one died ~47 ms after the first server tick with HALT line=1281 (0x501).

  • R60 (instrumentation, zero behavior change): three-candidate static audit (regression window clean; .d2s save/load engine byte-symmetric across all ten blocks; the party regroup provably correct on clean data), then shipped probes + guards: roster exit/join state probes (roster: clear lists … / roster state: … [R60]), a SaveMercData null-guard so a data-less merc is omitted from the save instead of crashing it, a roll-fail mark naming the merc site, and corpse-flags determinism (01 00 00 00, what real 1.10+ files carry — was uninitialized stack garbage written into every corpse record).
  • R61 (the catch): the owner's R60 field log killed the stale-roster candidate (exit state provably all-NULL) and pinned the crash. The save was never the trigger — the Vanilla+ Resolution row was. The 720p feature mutates a decomp-era static esc-menu table at runtime (adds a third video mode: count 2→3 with an empty stop path); the retail esc-menu loader re-initializes on every game entry, hit the empty stop path, and took the retail fatal 0x501. Classic decomp-port hazard: "static" tables are live data when a fork writes to them.
  • The fix: loader guards degrade to marks (an empty stop path keeps the prior DC6 — log: escmenu: video item 1 stop 2 path empty - prior dc6 kept [R61], exactly once at the second entry) + a sub-count clamp + a draw-site NULL guard. Guards, not a halt.
  • Field: "R61 Confirmed fixes #19" — the second entry loads and plays; no halt report:, no d2crash-*.txt. Issue #19 CLOSED; the re-enter check is now a standing per-build regression item (SMOKE §G).

Shared tab invisible (R63 feature, fixed R65 + R66) — the word was drawn, then painted over

After R63–R64 the shared stash worked but the "Shared" word could not be seen anywhere ("i don't see any 'Shared' text tab when I open Stash").

  • R65 (anchor): the tab's right-edge anchor had been derived from a wrong panel-width read (0x1f8 — the 2×256 px background tiling); the real 800-class panel content stops at +0x190. Anchor pulled to 0x180, inside the panel under every width reading. Decisive side effect: with the tab still invisible, the owner clicked where it should be — and the page swapped, gold and items round-tripped across characters. The entire mechanism (client toggle, 0x4F/0x7D packet, page-4 materialization, .sss write/read, cross-character persistence, shared gold) was thereby field-proven blind; the hit test is draw-order-immune.
  • R66 (the paint-over): the owner's hypothesis — "if anything, it is drawing, and then the art is drawing on top of it" — was exactly right. Tracing the frame: the tab draws inside DRAW_UI_Inventory()'s stash branch, and ~30 lines later the same function blits the right panel art (Panel::InvChar frames 4–7) at ScreenSizeX - gnPanelOffsetX - 0x140 — its rectangle starts at panel-relative +0x140 (abs x=400 at 800×600) and vertically spans the gold-label row. Both shipped anchors (0x1f8, 0x180) right-aligned the word inside that rectangle: drawn, then repainted by the art's opaque pixels. Fix: right edge 0x130 — 16 px left of the art rect — after auditing everything else the frame paints after the tab site (nothing repaints panel-relative x < 0x140 on that row at any supported width). Hit band re-centered on the word (+0x0c8..+0x130), and the whole R64→R66 geometry story is now a comment block on the constant so the next edit can't re-walk the seam.
  • Field: "Great job. I see it. It works." — tab visible, page toggle working, on the R66 build.

item-ilvl one-space trim (R59)

"Sometimes there is more than one space between Item Name and (#)" — data-driven, hence "sometimes": the tooltip name line is composed from string-table entries (base name, magic affix words, unique/set/rare names), and several of those entries end with their own trailing space, so the (N) suffix landed after one. Worst case: magic items whose suffix affix word is the last component. Fix (fork TU only, zero decomp edits): ITEMILVL_AppendNameSuffix now strips the caller name's trailing spaces/tabs before appending — the gap before (N) is always exactly the one space the format owns. OFF still never writes the buffer. Field: "Great job. iLvl spacing appears fixed."


Internals

  • Upstream sync to Ghidra 1783 (Rev 74–78, three merges: 3d645e7 1776–1781, 19f9791 1782, d6d4b53 1783). The regen waves re-typed register parameters, dropped phantom params and normalized switches across the tree; all three absorbed with Vanilla+ intact (markers 0 repo-wide, ghidra_version.cpp = 1783). Two merge casualties caught and fixed immediately: the 1781 resolution dropped D2Game/GAME/SCmd.cpp's d2_boot.h include (restored, Rev 76) and took the regen-header side of the DrawSolidRectEx BYTE-vs-DWORD fix, silently overloading it until d2.exe failed to link (definition realigned to BYTE + paired signature tripwires, Rev 77). The whole chain field-run on the first post-1783 build (R62).
  • Tripwire gate suite: 240 → 283 rows, all green. +6 in R60 (probes/guards), +6 in R61 (escmenu degrade/clamp/retired-halts), +2 sync signature pairs, +25 in R63 (the stash architecture: sites, includes, ini keys, VPS1 magic, marks, atomic write, .bak, goldbank borrow, retail encoder/loader/remove/0x9C, SP guard), +1 per fix round after (R64 DWORD dwGot, R65 tab anchor, R66 the must-not-match "out of right-art cover" tripwire). Suite: tools/check-vanilla-plus.sh, manual-only by charter since Rev 58.
  • Save-path hardening that ships even if you never touch the shared stash (R60, kept): the merc petdata guard and the corpse-flags determinism init make the .d2s writer defensive and deterministic on data the retail path used to trust or leave uninitialized.
  • Docs: pre-R61 journal offloaded verbatim to HISTORY.md H29 (Rev 80); SHARED-STASH-PLAN.md drafted and maintained as track N's design record (Rev 81); SMOKE-CHECKLIST gained §G (re-enter regression, items 18–20) and §H (shared stash, items 21–27).
  • Self-ID: d2boot.log line 1 reads Vanilla+0.0.7-R66 for this release (D2Client/Engine/Application.cpp:66).

Known issues (open at the 0.0.7 cut)

  • #8 — Necro skeleton spawn tint: R51 spawn-polarity fix is in, field re-verify pending. Symptom: own raised skeletons spawn yellow. One useful datum for the next round: note whether a skeleton ever re-tints (e.g. after a roster-update event) or stays yellow forever — "never re-tints" would falsify the R51 theory. Listed as known-fail in SMOKE (skip, don't burn time).
  • The roster-heal corruptor watch stays ARMED. roster heal: cap marks in d2boot.log mean real roster corruption — the regroup/flatten walks are provably correct on clean data (re-scoped R60). Two marks on the R60 log's fresh session remain unexplained; the crash fix does not depend on them (the regroup spin terminates on a healthy chain), but the watch closes only on a confirmed root cause. If you see roster heal: in the log, send the log.
  • Shared stash v1 boundaries (by design, not defects): 1 page / SP-only / 800×600-first (720p tab check owed, then a hardening round: the retail .d2s round-trip proof — SMOKE §H item 25 — and the checksum degrade drill — item 27 — are still owed on the R66 build).

Upgrade notes

  • From 0.0.6 (any round): drop-in. No save conversion, no packet changes, no ini keys removed. New key [features] shared_stash=0 appears in the regenerated d2vp.ini template — the game stays vanilla until you opt in.
  • First boot: line 1 of d2boot.log must read Vanilla+0.0.7-R66 (R66 or lower with a 0.0.6 prefix = wrong exe, don't test further — SMOKE item 0).
  • Using the shared stash: set shared_stash=1, save/exit once (or wait for the 2 s ini poll), open a stash in town, click Shared. The pool file _d2vp_shared.sss appears in the save folder on the first tab-back — that file is the shared page; your .d2s files are untouched by it and remain fully retail-loadable.
  • Rollback: set shared_stash=0 (or delete the key) — the tab disappears and the stash is retail again. The pool file is ignored, not deleted.
  • Smoke test: SMOKE-CHECKLIST.md — §A/§B for any new build, §G as the #19 regression check, §H items 21–27 for the shared stash.
  • Source users: apply the 0.0.7 overlay over the Ghidra-1783-synced tree (idempotent over R66), or fast-forward with the git bundle: git fetch vanilla-plus-1776-1783-R66-0007-release.bundle main:main. Then rebuild on Win11 (tools/build-local-win11.cmd) and verify the self-ID.

Full changelog (Rev 69 → Rev 86)

Rev Round Summary
69 R59 item-ilvl: exactly one space before (N) — trailing-space trim in ITEMILVL_AppendNameSuffix; gates 239→240
70 — docs: track K field verdict recorded; consolidated release notes R52→R59
71 R60 issue #19 instrumentation: roster probes, merc petdata guard, corpse-flags determinism; gates 240→246
72 R61 issue #19 root-caused + fixed: VP Resolution row vs the static esc-menu table; degrade guards; gates 246→252
73 — docs: track M close record (ISSUE-19-BODY.md), issue #19 CLOSED
74 sync upstream Ghidra 1776–1781 absorbed (merge 3d645e7)
75 sync upstream Ghidra 1782 absorbed (merge 19f9791)
76 sync sync compile-fix: d2_boot.h include restored in D2Game/GAME/SCmd.cpp; gates 253
77 sync sync link-fix: DrawSolidRectEx definition realigned to BYTE + signature tripwires; gates 255
78 sync upstream Ghidra 1783 absorbed (merge d6d4b53); markers 0, stamp 1783, gates 255/255
79 R62 stamp: first post-1783 build field-proven ("Builds. Runs. No obvious regressions."); self-ID R62
80 — docs: handoff trim — pre-R61 journal → HISTORY.md H29
81 — docs: R62 full SMOKE pass recorded; SHARED-STASH-PLAN.md drafted
82 R63 shared stash round 1 coded: page-4 materialization, 'JM' snapshot, _d2vp_shared.sss (VPS1, atomic write, .bak, checksum), goldbank borrow, Shared tab (0x4F/0x7D, no hotkey), SP guard; gates 255→280
83 R64 build fix: mingw32 uint32_t* → LPDWORD (DWORD dwGot) in the .sss read; gates 280→281
84 R65 tab anchor 0x1f8→0x180; mechanism field-proven blind (page swap, cross-char gold + item); gates 281→282
85 R66 tab right edge 0x180→0x130 (out of the right-panel art rect), hit band re-centered +0x0c8..+0x130, geometry story commented on the constant; gates 282→283; field: "I see it. It works."
86 — this release: 0.0.7 cut — self-ID Vanilla+0.0.7-R66, release notes R59→R66, README/VANILLA-PLUS release rows, SMOKE build lines, track N field status

Tag: D2LoD-Vanilla-Plus-0.0.7 (cut on the R67 content state). Between tags, work continues as revision rounds (-Rxx); the shipped build self-IDs Vanilla+0.0.7-R67.