Skip to content

Chat item links: share items in chat as [Item Name] with the native tooltip - #96

Open
errolgr wants to merge 1 commit into
Project-Diablo-2:mainfrom
errolgr:feature/chat-item-links
Open

errolgr wants to merge 1 commit into
Project-Diablo-2:mainfrom
errolgr:feature/chat-item-links

Conversation

@errolgr

@errolgr errolgr commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds chat item links: Ctrl+Shift+click an identified item to put [Item Name] in the chat box, send it, and anyone running BH sees the name in the item's quality colour. Clicking the link opens the game's own item tooltip, with the same stats, sockets and colours as hovering the item in your inventory.

It's client-only. The item travels inside the chat message, so nothing is needed on the server.

You send Others see
send link
demo-link.mp4

Problem

Trading and gear questions in chat are all "wts shako 3 ist" or a screenshot pasted somewhere else. There's no way to show the actual item, with its exact rolls, to the people you're talking to.

Changes

New module BH/Modules/ItemLinks/ (4 files + a small API header), registered like the other modules. No changes to existing modules.

Part What it does
Send (ItemLinks.cpp) Ctrl+Shift+left-click on an item appends [Name] #i<token> to the chat input. The token is base64url of the item's save record (without JM) plus its socketed items. At most 248 characters, so it fits the 255-character chat box.
Validate (ItemLinkCodec.cpp) Every #i token is parsed by a strict, bounded validator against the live data tables (item codes, stat ids, skill/class parameters, socket rules), then re-encoded in canonical form. Only those canonical bytes ever reach the engine. Invalid or realm-only data stays plain text.
Receive (ItemLinkGame.cpp) Builds a transient item unit with the engine's own decoder (D2Common #11145). It's never added to a room or inventory, and it's freed when the link scrolls away or you leave the game.
Draw + click The chat rows' DrawText calls show [Name] in the quality colour and record each link's box. Clicking a box opens the native hover popup for that item. Esc, right-click or another click closes it.
Safety 1.13c only. On startup every engine function it calls or hooks is byte-checked; if anything differs, the feature turns itself off and says why in chat. A function another module has detoured (e.g. PD2's D2GL renderer, which detours D2Win #10085/#10177 for HD text) is still accepted when everything after the detour matches.
Config item_links.enabled (default on).
API ChatLinkApi.h: FormatMessage / Release / OnClick, so a future chat UI can reuse the links.

Testing

Check Result
In game, 16 item kinds (normal, magic, rare, set, unique, crafted, runeword ×2, ethereal, corrupted ×2, low quality, keys, javelins, …): link tooltip vs the real inventory hover 16/16 identical
Codec unit tests (12 new: strict base64, truncated/oversized payloads, unknown ids, socket rules, realm data, canonical form, corpus round-trip) 64 passed, 8 xfail, 0 failed (base: 52 / 8 / 0)
Fuzz: 300k mutated tokens through the validator, ASan + UBSan clean
Fuzz in game: 2000 mutated tokens sent through real chat no crash, invalid ones stay plain text
Launch parity (game starts, menu, shift-click, PD2 and vanilla) PASS
Existing in-game scenarios (boot, nav, single-player enter, script smoke) unchanged vs base
Renderers default (DirectDraw) and PD2's D2GL (-3dfx)
MSVC Release|Win32 build success, 0 new warnings (164, same as base)

A couple of things the tests caught, now fixed in this PR:

  • PD2's inventory hover leaves out Quantity: N for stacks of equippable types (javelins, throwing weapons). The link tooltip does the same, so the two match.
  • The register-convention D2Client helpers are called through naked __fastcall stubs, so the compiler knows eax/ecx/edx are clobbered.

…ooltip

Ctrl+Shift+left-click on an identified item puts "[Name] #i<token>" in the chat input. The token
is base64url of the item's save record (without "JM") plus its socket fillers' records.

Receive: the PrintGameString entry is hooked; every "#i<token>" word is parsed by a bounded
validator against the live data tables, canonicalized and re-encoded, and only those bytes reach
the engine decoder (D2Common #11145) to build a transient item unit (never hashed, no room). Valid
links are drawn as "[Item Name]" in the item's quality colour; invalid tokens stay plain text.
Clicking a link opens the game's own item tooltip for the transient copy (D2Client 0x93450 +
D2Win #10110); Esc, right-click or another click closes it.

The tooltip matches PD2's inventory hover: PD2's hover leaves "Quantity: N" out for stacks of an
equippable item type (ItemTypes Body set: javelins, throwing weapons), so the link tooltip drops
that line too and re-stages the popup at the builder's anchor for the shorter text. Keys and
other non-equippable stacks keep it.

The register-convention D2Client helpers are naked __fastcall stubs, so the compiler treats
eax/ecx/edx as clobbered across them. Link hit boxes leave a trailing colour code out of the
measured span (D2Win's TextNWidth measures one that ends the span as glyphs).

API for other chat UIs: BH/Modules/ItemLinks/ChatLinkApi.h (FormatMessage, Release, OnClick).
Config: item_links.enabled (default true). 1.13c only; the feature stays off if any code site
differs. A site another module has detoured (jmp rel32 + int3 padding, as PD2's D2GL
renderer does to D2Win #10085/#10177) still matches when every byte after the padding does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant