Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Design docs live in `docs/`. Read the one(s) covering whatever you are about to
- `docs/inspector.md` - the V8 inspector (Chrome DevTools) attachment: the ixwebsocket transport, the InspectorServer / InspectorTarget / ScriptInspector split, the inbound-queue threading model, and releasing game locks during a breakpoint pause.
- `docs/compatibility.md` - the scripting compatibility-flag system: the `CompatibilityFlags` registry, the framework flag catalog, the `game::GetCompatibilityFlags()` extension point, the `Compatibility` JS object, the per-flag gating sites, and why the BOM strip and `delay` wrapper stay un-flagged.
- `docs/realms.md` - custom Battle.net gateways: the `RealmRegistry` store, the `-realm` launch option, in-memory injection into D2's gateway list by detouring the Storm registry read/write helpers (no registry persistence), realm enumeration (`game::GetRealms`) exposed as the global `getRealms()`.
- `docs/stash_tabs.md` - stash tabs: the game-agnostic `game::StashTab` identity handle (`game/StashTab.h`; backend-implemented `Type` / `Name` / `Gold` / `GetItems` / `Click` / `MoveGold`, `game::GetStashTabs`, `Unit::StashTab`), exposed as the `StashTab` JS class via `getStashTabs()` and `Unit.stashTab` (with `depositGold` / `withdrawGold` wrapping `MoveGold`), the per-tab gold attribution rule, why there is no "select tab", the character-state `pages` feed, and the `GoldActionMode` dialog codes.
- `docs/stash_tabs.md` - stash tabs: the game-agnostic `game::StashTab` identity handle (`game/StashTab.h`; backend-implemented `Type` / `Name` / `Gold` / `GetItems` / `Click` / `MoveGold` / `Withdraw` / `Deposit`, `game::GetStashTabs`, `Unit::StashTab`), exposed as the `StashTab` JS class via `getStashTabs()` and `Unit.stashTab` (with `depositGold` / `withdrawGold` wrapping `MoveGold`, and `withdraw` / `deposit`, mainly for advanced tabs), the per-tab gold attribution rule, why there is no "select tab", the character-state `pages` feed, and the `GoldActionMode` dialog codes.
- `docs/plugy_stash.md` - PlugY's multi-page stash on 1.14d, the backend behind `docs/stash_tabs.md` when PlugY is present: how PlugY pages the stash (only the shown page lives in the inventory; the rest hang off a `PYPlayerData` tail appended to the player data, parked items are stored-mode with no parent inventory), the version-allowlisted detection in `backends/lod114d/game/PlugY.cpp`, running PlugY's `Init` from `Bridge::Init` when PlugY.dll was injected without PlugY.exe, the patch-site overlap check against d2bs's hooks, the blocking swap-in / click / ack-wait / swap-back sequence over PlugY's 0x3A channel, the shared gold pool, and how to remove the PlugY code again.
- `docs/code_cache.md` - the script code cache: why per-script isolates make V8's own compilation cache useless across scripts, the memory + opt-in disk tiers of `runtime::script::CodeCache`, how it hooks into `CompileSource` (eager on a miss / consume on a hit / drop on a rejected blob), the key (post-transform source + origin + V8 build tag), the header-less on-disk entries and cross-instance sharing, and the `CodeCachePath` / `CodeCacheMemoryLimit` / `CodeCacheDiskLimit` settings.
- `docs/analytics.md` - anonymous usage analytics (Aptabase): the `Analytics` component, the `session_start` / `profile_active` events (the latter carrying a per-install `profileHash`, never the profile name) and exactly what they do / do not collect, the compile-time app key (build-time only; the project key is the committed default in `Directory.Build.props`, overridable per-build via `-p:D2bsAnalyticsKey` / gitignored `d2bs.local.props` / `D2BS_ANALYTICS_KEY`, and an empty value compiles analytics out), the per-launch opt-out (`-noanalytics`/`D2BS_ANALYTICS_DISABLE`), the backend-agnostic `features` tag list via `game::GetActiveFeatures()`, the anonymous `installId` (derived per launch, never stored), and the `game::GetAnalyticsLaunchOptions()` / `game::GetBackendVersion()` contract accessors.
Expand Down Expand Up @@ -89,7 +89,7 @@ Currently tests only cover the **pathfinding engine** (`src/runtime/components/n

The script-visible JS API is documented by a generator pipeline under `scripts/`. The bindings in `src/runtime/api/` carry structured `///` doc comments (`@description`, `@signature`, `@param`, `@returns`, `@throws`, `@callback`, `@event`, `@type`, `@mode`); their exact vocabulary lives at the top of `extract_api.py`. The `{type}` fields may be object literals / unions / generics (`{x:number,y:number}`, `Unit|null`, `Array<{x:number}>`) - the parser is brace-balanced.

**Enumerations as option sets**: a `{type}` that names a C++ `enum class` (libclang-parsed from `game/Types.h` / `game/Constants.h`) or a constant namespace (e.g. `ProfileType`) is rendered as a value table, not prose. `extract_api.py` reads the enumerators with libclang - plus the compatibility flags from `CompatibilityFlags::RegisterDefaults()`'s documented `Register("name")` calls - into an `enums` map (only sets referenced by some `{type}` are emitted); `gen_api_docs.py` renders an Enums section and auto-links the type; `gen_dts.py` emits a literal-union alias (`type Difficulty = 0 | 1 | 2 | 3`; bitfields and constant namespaces map to `number`). A bitfield enum is typed `number` (a value is an OR-combination) by marking it `/// @flags`. So document an enum-valued member as `@type {Difficulty}` and keep the values in the C++ enum, not in the description.
**Enumerations as option sets**: a `{type}` that names a C++ `enum class` (libclang-parsed from `game/Types.h` / `game/Constants.h`) or a constant namespace (e.g. `ProfileType`) is rendered as a value table, not prose. `extract_api.py` reads the enumerators with libclang - plus the compatibility flags from `CompatibilityFlags::RegisterDefaults()`'s documented `Register("name")` calls - into an `enums` map (only sets referenced by some `{type}` are emitted); `gen_api_docs.py` renders an Enums section and auto-links the type; `gen_dts.py` emits a literal-union alias (`type Difficulty = 0 | 1 | 2 | 3`; bitfields and constant namespaces map to `number`). A bitfield enum is typed `number` (a value is an OR-combination) by marking it `/// @flags`. An enumerator documented `/// @internal` stays C++-only and is left out of the docs and the d.ts, for a value no script can ever receive. So document an enum-valued member as `@type {Difficulty}` and keep the values in the C++ enum, not in the description.

| Script | Flow | Notes |
|--------|------|-------|
Expand Down
39 changes: 34 additions & 5 deletions docs/stash_tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ documents its own mechanics separately (`docs/plugy_stash.md` for PlugY).

## Contract

`src/contract/game/Types.h` holds the two enums; `src/contract/game/StashTab.h`
`src/contract/game/Types.h` holds the enums; `src/contract/game/StashTab.h`
the handle, an identity (kind + position) whose reads the backend resolves live,
like `Unit` or `Room`:

```cpp
enum class StashTabKind : uint8_t { Personal = 0, Shared = 1 };
enum class StashTabType : uint8_t { Normal = 0, AdvancedStash = 1, Chronicle = 2 };
enum class StashWithdrawTarget : uint8_t { Cursor = 0, Inventory = 1, Cube = 2, Belt = 3 };

class StashTab {
StashTabKind Kind() const; uint32_t Index() const; // identity, no resolve
Expand All @@ -27,6 +28,8 @@ class StashTab {
std::vector<Unit> GetItems() const; // shown or not
ClickResult Click(Position cell) const; // left click: pick up / drop / swap
bool MoveGold(GoldActionMode mode, uint32_t amount) const; // Deposit/Withdraw; fire and forget
bool Withdraw(const Unit& item, uint32_t count, StashWithdrawTarget target) const; // see below
bool Deposit(const Unit& item) const; // advanced tabs
};
std::vector<StashTab> GetStashTabs(); // personal first, then shared; empty with no player unit
std::optional<StashTab> Unit::StashTab() const; // the tab holding an item
Expand All @@ -39,7 +42,23 @@ one.

`StashTabType` mirrors D2R's tab kinds: `Normal` holds items, `AdvancedStash`
holds items with stackable-item support, `Chronicle` tracks found set / unique
/ runeword items and holds no items. Every LoD tab is `Normal`.
/ runeword items and holds no items. Every LoD tab is `Normal`. `Chronicle`
stays in the C++ enum only (marked `@internal`, so the API docs and the d.ts
leave it out): no tab ever reports it.

An advanced tab holds one stack per item class, and items move on and off it
only through `Withdraw` and `Deposit`, never through grid placement:

- `Withdraw(item, count, target)` takes `count` off a stack and places it at
`target` - the cursor, the inventory, the cube or the belt. The cursor holds
one item, so a count above 1 needs another target.
- `Deposit(item)` moves one item the player holds - on the cursor or carried -
onto the stack of its class, creating the stack if the tab has none.
- `Click(cell)` on an advanced tab is what a mouse click on the panel does:
with an item on the cursor it deposits that item and ignores `cell`,
otherwise it withdraws one of the stack at `cell` to the cursor.

All three need the stash panel open and are fire and forget.

There is deliberately no "select tab" in the contract. On a backend whose
packets cannot address a tab that is not shown, a tab switch is a server round
Expand Down Expand Up @@ -72,7 +91,11 @@ through the tab API.

One personal tab, index 0, whose `GetItems()` is the inventory walk filtered to
`ItemLocation::Stash`, whose `Gold()` is the character's stash gold, and whose
`Click` is `ClickContainerSlot(Left, cell, Stash)`. Implemented in
`Click` is `ClickContainerSlot(Left, cell, Stash)`. A plain grid tab has one
withdraw, a count of 1 to the cursor, which is `ClickItem(Left, item)` on an
item of the tab with the cursor empty; any other `Withdraw` and every `Deposit`
return false, since dropping an item needs a free cell, which only `Click(cell)`
is told. Implemented in
`backends/lod114d/game/Stash.cpp`. When PlugY's multi-page stash is active,
every member delegates to `plugy::` (`backends/lod114d/game/PlugY.h`) instead;
`docs/plugy_stash.md` covers that path, and the vanilla one is the fallback
Expand All @@ -87,11 +110,17 @@ one to one, the same shape as `Unit` or `Room`:
- `unit.stashTab` -> `StashTab | undefined`
- `tab.kind`, `tab.index`, `tab.type`, `tab.name`, `tab.gold` (getters)
- `tab.items` -> `Unit[]`, built on each access, so listing tabs stays cheap
- `tab.click(x, y)` -> `boolean` (true = handed to the game)
- `tab.click(x, y)` / `tab.click(item)` -> `boolean` (true = handed to the game);
the item form clicks the cell an item on this tab sits on, and is false for
an item on another tab
- `tab.withdraw(item, count = 1, target = StashWithdrawTarget.cursor)` /
`tab.deposit(item)` -> `boolean`; advanced tabs, plus one item to the cursor
for `withdraw` on 1.14d
- `tab.depositGold(amount)` / `tab.withdrawGold(amount)` -> `boolean`
- `clickItem(0, item)` works for items on tabs that are not shown
- `StashTabKind.personal` / `StashTabKind.shared`, `StashTabType.normal` /
`advancedStash` / `chronicle` constants
`advancedStash`, `StashWithdrawTarget.cursor` / `inventory` / `cube` / `belt`
constants

## Gold

Expand Down
3 changes: 2 additions & 1 deletion scripts/extract_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,10 +1143,11 @@ def _parse_enum_defs(tu, path):
and cur.location.file
and Path(cur.location.file.name) == path
):
# An enumerator documented `/// @internal` is C++-only: scripts never see the value.
rows = [
{"value": _enumerator_value(c), "name": c.spelling, "description": _enumerator_desc(c)}
for c in cur.get_children()
if c.kind == CursorKind.ENUM_CONSTANT_DECL
if c.kind == CursorKind.ENUM_CONSTANT_DECL and "@internal" not in (c.raw_comment or "")
]
if rows:
kind = "flags" if _enum_is_flags(path, cur.location.line) else "enum"
Expand Down
18 changes: 18 additions & 0 deletions src/backends/lod114d/game/Stash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ ClickResult StashTab::Click(Position cell) const {
return *this ? click() : ClickResult::StashTabUnavailable;
}

// A 1.14d tab is a plain grid, so the one withdraw it has is a click that picks
// the item up.
bool StashTab::Withdraw(const Unit& item, uint32_t count, StashWithdrawTarget target) const {
if (count != 1 || target != StashWithdrawTarget::Cursor || !GetUIFlag(UiFlag::Stash) || Unit::CursorItem()) {
return false;
}
const auto onTab = item.StashTab();
if (!onTab || onTab->Kind() != kind_ || onTab->Index() != index_) {
return false;
}
return ClickItem(ClickButton::Left, item) == ClickResult::Dispatched;
}

// Dropping an item needs a free cell, which only Click(cell) can be told.
bool StashTab::Deposit(const Unit& /*item*/) const {
return false;
}

// PlugY's shared pool for the shared tab, the vanilla gold dialog for the personal
// tab. Fire and forget, like gold().
bool StashTab::MoveGold(GoldActionMode mode, uint32_t amount) const {
Expand Down
14 changes: 14 additions & 0 deletions src/contract/ContractEnumNames.cpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/contract/ContractEnumNames.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions src/contract/game/StashTab.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,28 @@ class StashTab {
// and clicked, then best-effort restores the previously shown tab (which tab
// ends up shown is not part of the contract). StashTabUnavailable for a tab
// that does not exist or could not be brought in.
// An advanced stash tab has no grid to drop on: with an item on the cursor
// the click deposits it (Deposit) wherever `cell` is, otherwise it withdraws
// one of the stack at `cell` to the cursor (Withdraw). InvalidTarget when
// that is not possible.
ClickResult Click(Position cell) const;
// Take `count` items off `item`, one of the tab's items, and put them at
// `target`. On an advanced stash tab (Type() == AdvancedStash) `item` is a
// stack, and the server stops early when it runs out or the target is full.
// On a plain grid tab only a count of 1 to the cursor is possible: the item
// is picked up, which needs an empty cursor; not every backend offers it. The
// cursor holds one item, so it takes a count of 1 only. The stash panel must
// be open. Fire and forget like MoveGold: the item events update when the
// server answers. false for an item that is not on this tab, a count of 0,
// more than one to the cursor, a combination the tab cannot do, or a closed
// panel; true once the request is issued.
bool Withdraw(const Unit& item, uint32_t count, StashWithdrawTarget target) const;
// Advanced stash tabs only. Move `item` - on the cursor or carried - onto the
// tab's stack of its class, the same as dropping it on the tab. The stash
// panel must be open. Fire and forget; the server refuses a full stack (99).
// false on any other tab, for an item of a class the tab does not stack, one
// the player does not hold, or a closed panel; true once the request is issued.
bool Deposit(const Unit& item) const;
// Move gold between the carried gold and this tab; `mode` is GoldActionMode
// Deposit or Withdraw. Fire and forget like GoldAction: Gold() and the gold
// stats update when the server's reply lands. A shared pool may only support
Expand Down
13 changes: 11 additions & 2 deletions src/contract/game/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -1401,14 +1401,23 @@ enum class StashTabKind : uint8_t {
};

// What a stash tab holds. LoD tabs are all Normal; D2R adds tabs with stackable
// item support and the Chronicle tab that tracks found set / unique / runeword
// items instead of holding items.
// item support.
enum class StashTabType : uint8_t {
Normal = 0,
AdvancedStash = 1,
/// @internal A Chronicle panel, which tracks found set / unique / runeword
/// items as a list on the player and holds no items; no tab ever reports it.
Chronicle = 2,
};

// Where StashTab::Withdraw puts what it takes off an advanced stash tab.
enum class StashWithdrawTarget : uint8_t {
Cursor = 0,
Inventory = 1,
Cube = 2,
Belt = 3,
};

enum class ItemQuality : uint32_t {
Inferior = 1,
Normal = 2,
Expand Down
Loading
Loading