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
2 changes: 1 addition & 1 deletion 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` / `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/stash_tabs.md` - stash tabs: the game-agnostic `game::StashTab` identity handle (`game/StashTab.h`; backend-implemented `Type` / `RemoveOnly` / `Season` / `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
22 changes: 16 additions & 6 deletions docs/stash_tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class StashTab {
StashTabKind Kind() const; uint32_t Index() const; // identity, no resolve
explicit operator bool() const; // the tab exists right now
StashTabType Type() const; // always Normal on LoD
bool RemoveOnly() const; // items can be taken off, not put on; false on LoD
uint32_t Season() const; // a shared tab's season; 0 on LoD
std::string Name() const; // user-given, empty when unnamed
uint32_t Gold() const; // see Gold
std::vector<Unit> GetItems() const; // shown or not
Expand All @@ -46,6 +48,12 @@ holds items with stackable-item support, `Chronicle` tracks found set / unique
stays in the C++ enum only (marked `@internal`, so the API docs and the d.ts
leave it out): no tab ever reports it.

`RemoveOnly()` marks a tab items can only be taken off, never put on - shared
tabs from a previous season. `Deposit` and drops onto such a tab fail;
withdrawing and picking items up work as usual. `Season()` is the season a
shared tab belongs to, as the game numbers it. No LoD tab is remove-only, and
every LoD tab reports season 0.

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:

Expand Down Expand Up @@ -108,7 +116,8 @@ one to one, the same shape as `Unit` or `Room`:

- `getStashTabs()` -> `StashTab[]`, personal first, then shared
- `unit.stashTab` -> `StashTab | undefined`
- `tab.kind`, `tab.index`, `tab.type`, `tab.name`, `tab.gold` (getters)
- `tab.kind`, `tab.index`, `tab.type`, `tab.removeOnly`, `tab.season`,
`tab.name`, `tab.gold` (getters)
- `tab.items` -> `Unit[]`, built on each access, so listing tabs stays cheap
- `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
Expand Down Expand Up @@ -145,11 +154,12 @@ open. A backend whose pool only supports moving

The character-state producer (`components/characterstate`) sends the stash as
`containers.stash.pages`, one entry per tab from `GetStashTabs()`, each carrying
the tab's `kind`, `index`, `type`, `name` and `gold` plus the shared grid
`width` / `height` and its `items` (from `StashTab::GetItems()`, so tabs that
are not shown are included). 1.14d produces a single personal page; `name` is
the tab's name, empty when unnamed. Every item on an advanced page (`type` 1)
is a slot, so its document carries its stack size as `stackCount` (its
the tab's `kind`, `index`, `type`, `removeOnly`, `season`, `name` and `gold`
plus the shared grid `width` / `height` and its `items` (from
`StashTab::GetItems()`, so tabs that are not shown are included). 1.14d
produces a single personal page, never `removeOnly` and with `season` 0; `name`
is the tab's name, empty when unnamed. Every item on an advanced page (`type`
1) is a slot, so its document carries its stack size as `stackCount` (its
`ItemCount()`, which for any other item counts its socket fillers); no other
item has the key, so it never appears on 1.14d. The stash hash folds every
page's identity and contents, so a rename, a gold move, a page appearing or a
Expand Down
4 changes: 4 additions & 0 deletions src/backends/lod114d/game/Control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ void Control::Click(std::optional<Position> pos) const {
constexpr std::chrono::milliseconds CLICK_STEP_DELAY{100};
std::this_thread::sleep_for(CLICK_STEP_DELAY);
PostMouseMessage(WM_LBUTTONUP, static_cast<int32_t>(cx), static_cast<int32_t>(cy));
// Let the game process the release before scripts inspect the next menu.
// Without this pause, TCP-host login sees the old TCP/IP screen after
// clicking Host and queues Cancel onto the new character-select screen.
std::this_thread::sleep_for(CLICK_STEP_DELAY);
}

std::vector<Control::TextLine> Control::TextLines() const {
Expand Down
8 changes: 8 additions & 0 deletions src/backends/lod114d/game/Stash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ StashTabType StashTab::Type() const {
return StashTabType::Normal;
}

bool StashTab::RemoveOnly() const {
return false;
}

uint32_t StashTab::Season() const {
return 0;
}

std::string StashTab::Name() const {
GameReadLock guard;
return plugy::IsActive() && plugy::HasStashTabs() ? plugy::PageName(kind_, index_) : std::string{};
Expand Down
7 changes: 7 additions & 0 deletions src/contract/game/StashTab.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class StashTab {
// The tab exists right now (there is a player unit and the tab is present).
explicit operator bool() const;
StashTabType Type() const;
// Items can be taken off the tab but not put on it (shared tabs from a
// previous season), so Deposit and drops onto it fail. false wherever the
// game has no such tabs, 1.14d included.
bool RemoveOnly() const;
// The season a shared tab belongs to, as the game numbers it. 0 wherever the
// game has no seasons, 1.14d included.
uint32_t Season() const;
// User-given name; empty when unnamed.
std::string Name() const;
// Gold stored on the tab. Where gold is not tracked per tab it is attributed to
Expand Down
15 changes: 15 additions & 0 deletions src/runtime/api/classes/game/JSStashTab.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ class JSStashTab : public ClassBase<JSStashTab, game::StashTab> {
info.GetReturnValue().Set(static_cast<uint32_t>(Unwrap(info.Holder())->Type()));
});

/// @description True for a tab items can only be taken off, never put on (shared tabs from a previous
/// season): `deposit` and dropping an item on it fail. Always false on 1.14d.
/// @type {boolean}
Property(
isolate, inst, "removeOnly", +[](v8::Local<v8::Name>, const v8::PropertyCallbackInfo<v8::Value>& info) {
info.GetReturnValue().Set(Unwrap(info.Holder())->RemoveOnly());
});

/// @description The season a shared tab belongs to, as the game numbers it. Always 0 on 1.14d.
/// @type {number}
Property(
isolate, inst, "season", +[](v8::Local<v8::Name>, const v8::PropertyCallbackInfo<v8::Value>& info) {
info.GetReturnValue().Set(Unwrap(info.Holder())->Season());
});

/// @description User-given tab name; empty when unnamed or when the tab is gone.
/// @type {string}
Property(
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/components/characterstate/CharacterState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ json PageMeta(const game::StashTab& tab) {
meta["kind"] = tab.Kind();
meta["index"] = tab.Index();
meta["type"] = tab.Type();
meta["removeOnly"] = tab.RemoveOnly();
meta["season"] = tab.Season();
meta["name"] = tab.Name();
meta["gold"] = tab.Gold();
return meta;
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/components/characterstate/UnitJson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,20 @@ class JsonVisitor final : public UnitVisitor {

// Curated stat ids the manager's StatsPanel labels (D2BotNG stats.ts). Order is
// irrelevant; the manager renders only ids it recognises.
constexpr std::array<game::Stat, 22> STAT_IDS = {
constexpr std::array<game::Stat, 27> STAT_IDS = {
game::Stat::Strength,
game::Stat::Energy,
game::Stat::Dexterity,
game::Stat::Vitality,
game::Stat::StatPoints,
game::Stat::NewSkills,
game::Stat::MaxHitPoints,
game::Stat::MaxMana,
game::Stat::MaxStamina,
game::Stat::Level,
game::Stat::Experience,
game::Stat::LastExperience,
game::Stat::NextExperience,
game::Stat::Gold,
game::Stat::GoldBank,
game::Stat::FireResist,
Expand Down
Loading