132 lines
18 KiB
Markdown
132 lines
18 KiB
Markdown
# Project: diablo2-web Module Split & Single Entry Consolidation
|
||
|
||
## Architecture
|
||
|
||
`diablo2-web` is refactored in-repo on `/usr/local/google/home/taodao/diablo2-web/.worktrees/module-split` into five strictly isolated, compiler-enforced TypeScript modules under `src/` plus developer/verification CLI scripts under `tools/` and a single Web HTML entry (`play.html`):
|
||
|
||
```text
|
||
diablo2-web/
|
||
├── play.html # Sole HTML entry point (loads /src/client/main.ts)
|
||
├── src/
|
||
│ ├── common/ # D2Common.dll + Storm.dll + D2Lang.dll (ES2022, types: [], zero DOM/Node)
|
||
│ │ ├── mpq/ # Pure MPQ archive & decompression (no file-source.ts)
|
||
│ │ ├── formats/ # DC6, DCC, DT1, DS1, COF, PL2, PAL, CEL, PCX, TBL, AnimData, Bitstream
|
||
│ │ ├── data/ # Excel .txt data-registry (injected ArchiveSource, no hardcoded d2-data) & canonical tables
|
||
│ │ ├── drlg/ # 1:1 1.13c DRLG map generation, collision masks, preset units, warps, light-grid
|
||
│ │ ├── world/ # acts, act-variants, levels-meta, level-links, level-names-zh, world-graph, map, d2map, warp-tiles, automap-table, animated-tiles timing
|
||
│ │ ├── items/ # Static item/affix/set/unique/runeword/gem tables, drop-tables, inventory-model, belt-model, item-bridge, item-cost, item-upgrade, item-tooltip, ground-item-physics
|
||
│ │ ├── skills/ # Static Skills.txt/Missiles.txt/Overlay.txt tables, skill-tree, skill-calc-engine, formulas, calc-ast, skills-meta
|
||
│ │ ├── units/ # classes, character, monster-mapping, monsters-data, npc-table, object-lookup, objects, plr-mode, pure monster-art compositor
|
||
│ │ ├── i18n/ # Pure .tbl lookup (lookupTbl, TblLang, GameLang) & tooltip-i18n decoupled from UI lang state
|
||
│ │ ├── rng/ # d2-rng, SplitMix64 pickVariant (variants.ts)
|
||
│ │ └── pack-contract/ # Shared tile/scene/entity pack manifest schemas & fail-fast universal DT1 / missingFiles===0 validators
|
||
│ ├── netproto/ # D2Net.dll + Bnclient.dll + D2MCPClient.dll (strictly isolated; external code imports only src/netproto/index.ts)
|
||
│ ├── server/ # D2Game.dll (ES2022 + WebWorker, types: [], zero DOM, zero client/baker imports, zero Math.random/Date.now/performance.now)
|
||
│ │ ├── engine/ # GameEngine, world-arena, combat-pipeline, missile-engine, state-bus, aura-scanner, summon-manager, anim-dispatcher, environment, stat-list, state-hash
|
||
│ │ ├── ai/ # Monster AI, spawning, monster-rooms, monster-streaming
|
||
│ │ ├── skills/ # Skill registry (static imports, zero node:*), skills simulation, impl/{ama,ass,bar,dru,nec,pal,sor,uni}
|
||
│ │ ├── items/ # drop-pipeline, treasure-class, treasure-engine, auto-tc, affix-generator, automagic, ground-items, runeword-socket, gems, vendor-generator, vendor-service
|
||
│ │ ├── world/ # objects interaction, quests, npc, portal, class-starter-profiles
|
||
│ │ ├── save/ # .d2s character save read/write
|
||
│ │ ├── scene/ # chat-commands, scene-links-nav, scene-population, skill-caster, weapon-combat
|
||
│ │ └── adapter/ # LocalLoopbackAdapter implementing GameServerAdapter from src/netproto/index.ts
|
||
│ ├── client/ # D2Client.dll + D2Gfx.dll + D2Win.dll + D2Launch.dll + D2Sound.dll (ES2022 + DOM + DOM.Iterable, types: [], zero Node/server/baker imports)
|
||
│ │ ├── main.ts # Sole browser entry point for play.html
|
||
│ │ ├── render/ # WebGL2/Canvas renderer, atlas, cull, roofs, unit-sprites, missile-renderer, missiles-meta, overlays-meta, monster-art atlas loader, char-appearance
|
||
│ │ ├── ui/ # HUD, inventory UI, belt UI, character-sheet, control-bar, cursor, font, globes, ground-labels, hotkeys, minimap, skill-tree-panel, world-panels, baked-ui-meta
|
||
│ │ ├── frontend/ # Title, login, character-select, lobby state & renderer (flow-state.ts, frontend-renderer.ts)
|
||
│ │ ├── scene/ # actor-animator, animation, entity-art-loader, ground-item-renderer, lighting-presets, missile-overlay-renderer, mouse-controller, object-animation, overhead-labels, pack-loader, scene-state, toolbar-chat-ui, transition, world-renderer
|
||
│ │ ├── audio/ # AudioManager & sound-service
|
||
│ │ ├── sim/ # Camera, Input, Loop
|
||
│ │ ├── i18n/ # Browser URL/DOM/localStorage language state & TOOLBAR_I18N
|
||
│ │ └── {assets,automap,input,inspector,map,motion,session,settings,toolbar,ui-model,view,world}/
|
||
│ └── baker/ # Node-only offline asset & table baker (ES2022, types: ["node"], zero DOM, imports only src/baker/** and src/common/**)
|
||
│ ├── cli.ts # Unified CLI (npm run bake -- <target> | --verify)
|
||
│ ├── io/ # file-source.ts, png.ts, drlg-mpq-source.ts, tbl-writer.ts, publish-packs.ts
|
||
│ ├── packers/ # 16 pack-*.ts scripts + rebuild-pack-index-and-monsters.ts (incl. SuperUnique minion DCC/DC6 closure)
|
||
│ ├── extract/ # 11 table/font/code extractors (extract-d2net-tables, extract-d2moo-tables, gen-drlg-ids, port-object-lookup, build-d2-ttf, etc.)
|
||
│ └── audit/ # verify-packs.ts, verify-entity-packs.ts, verify-tiles.ts, verify-tile-alignment.ts
|
||
├── tools/ # Developer, Bot, Replay & Verification CLI Tools (d2-bot.ts, netproto-replay.ts, d2s-unlock.ts, verify-play.ts, audit-*.ts, etc.)
|
||
└── tests/ # Unit, architecture boundary, integration, and E2E test suites
|
||
```
|
||
|
||
### Strict Dependency DAG
|
||
|
||
| From ↓ Imports → | `src/common/**` | `src/netproto/index.ts` | `src/server/**` | `src/client/**` | `src/baker/**` | `node:*` | DOM Globals |
|
||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||
| **`src/common/**`** | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
||
| **`src/netproto/**`** | ✗ | ✓ (internal) | ✗ | ✗ | ✗ | Only `transport/node-tcp-stream.ts` (excluded from browser) | ✗ |
|
||
| **`src/server/**`** | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
|
||
| **`src/client/**`** | ✓ | ✓ (`index.ts` only) | ✗ | ✓ | ✗ | ✗ | ✓ |
|
||
| **`src/baker/**`** | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ |
|
||
| **`tools/**`** | ✓ | ✓ | ✓ | ✓ (audit/bot) | ✓ | ✓ | ✓ (Playwright) |
|
||
|
||
---
|
||
|
||
## Feature Inventory
|
||
|
||
Every feature discovered in the Phase 0 Survey (from `ORIGINAL_REQUEST.md`, `module_split_plan.md`, `AGENTS.md`, and Survey Reports 1, 2, 3) is inventoried below with its assigned milestone. Zero features are unassigned.
|
||
|
||
| # | Feature | Description | Milestone | Source |
|
||
|---|---------|-------------|-----------|--------|
|
||
| F1 | `src/common/` Pure Module & Data Registry Decoupling | Extract pure MPQ (`mpq/` without `file-source.ts`), binary decoders (`formats/`), Excel `.txt` `data-registry` (with injected `ArchiveSource` and zero hardcoded `/usr/local/google/home/taodao/d2-data`), 1:1 DRLG (`drlg/`), world topology (`world/`), static items/skills/units (`items/`, `skills/`, `units/`), pure `.tbl` lookup (`i18n/`), deterministic RNG & SplitMix64 `pickVariant` (`rng/`), compiling cleanly under `tsconfig.common.json` (`lib: ["ES2022"]`, `types: []`). | M1 | R1.1, Survey 1 & 2 |
|
||
| F2 | Shared Pack Contract (`src/common/pack-contract/`) | Extract `PackedDrlgBundleManifest`, `PackedTileCatalogIndex`, `PackIndex`, `PackSceneJson`, `EntityMeta`, `ClipMeta`, and fail-fast universal DT1 (`Blank.dt1`, `InvisWal.dt1` style=49 seq=7, `Warp.dt1`) + `missingFiles === 0` validators into `src/common/pack-contract/` to break `client <-> baker` coupling. | M1 | R1.1, R1.5, Survey 2 & 3 |
|
||
| F3 | Elimination of All 23 Server-to-Client Reverse Dependencies | Extract pure inventory/belt/socket container models (`inventory-model.ts`, `belt-model.ts`, `item-bridge.ts`), `CharacterAttributes`, `resolvePlayerCharToken`, `HotkeySkillEntry`, `getSkillName`, `subTileToScene`, and explicit `lang: TblLang` parameters into `src/common/`, and split mixed files (`monsters.ts`, `skills.ts`, `engine.ts`, `world-arena.ts`, `skill-caster.ts`, `monster-art.ts`, `animated-tiles.ts`, `i18n/lang.ts`) so server simulation has 0 imports from UI/render/client. | M1 | R1.3, Survey 1 & 2 |
|
||
| F4 | `src/baker/` Offline Baker Module & Unified CLI (`src/baker/cli.ts`) | Migrate `src/mpq/file-source.ts`, `png.ts`, `drlg-mpq-source.ts`, `tbl-writer.ts`, `publish-packs.ts`, 16 `pack-*.ts` packers + `rebuild-pack-index-and-monsters.ts`, 11 extractors, and 4 offline verifiers into `src/baker/`; implement `src/baker/cli.ts` (`npm run bake -- <target>` and `npm run bake -- --verify`); enforce `tsconfig.baker.json` (`lib: ["ES2022"]`, `types: ["node"]`, zero imports from `src/client/**` or `src/server/**`). | M2 | R1.5, R3, Survey 3 |
|
||
| F5 | `tools/` CLI Consolidation & Legacy Relay Deletion | Delete legacy P2P lockstep server scripts (`scripts/net-relay.ts`, `scripts/net-server.ts`, `npm run net`); migrate all 93 developer, bot, replay, unlock, benchmark, and verification tools (`d2-bot.ts`, `netproto-replay.ts`, `d2s-unlock.ts`, `verify-play.ts`, `audit-levels-browser.ts`, `lib/drlg-oracle.ts`, etc.) from `scripts/` to `tools/`; remove `scripts/` directory and update `package.json` scripts & test imports. | M2 | R1.6, R2, Survey 3 |
|
||
| F6 | `src/server/` Authoritative Simulation Module & `LocalLoopbackAdapter` | Consolidate all server simulation files (`engine/`, `ai/`, `skills/`, `items/`, `world/`, `save/`, `scene/`) into `src/server/`; remove dynamic `node:*` in `skills/registry.ts`; verify 0 occurrences of `Math.random`, `Date.now`, `performance.now`, or DOM globals under `tsconfig.server.json` (`lib: ["ES2022", "WebWorker"]`, `types: []`); implement `src/server/adapter/LocalLoopbackAdapter.ts` implementing `GameServerAdapter` from `src/netproto/index.ts`. | M3 | R1.3, R3, Survey 1 & 2 |
|
||
| F7 | `src/client/` Pure Browser Module & `MapService` Node Decoupling | Consolidate `render/`, `ui/`, `frontend/`, `audio/`, `sim/`, `scene/frontend/`, `transition.ts`, client `i18n/lang.ts`, and client art/animation modules into `src/client/`; remove `node:fs`/`node:path` from `src/client/map/map-service.ts` via injected `PackedTileBundleLoader`; decouple `HudManager` and `WorldRenderer` from `GameEngine`; enforce `tsconfig.client.json` (`lib: ["ES2022", "DOM", "DOM.Iterable"]`, `types: []`, zero imports from `src/server/**`, `src/baker/**`, or `node:*`). | M3 | R1.4, R3, Survey 2 |
|
||
| F8 | Single HTML Entry (`play.html`) Consolidation & Legacy Entry Removal | Delete `index.html`, `acts.html`, `acts-zh.html`, `acts-en.html`, `bnet.html`, `src/main.ts`, `src/walk.ts`, `src/frontend/bnet-main.ts`, `src/scene/act-scene.ts`, `src/scene/net-scene.ts`, `src/scene/backend/net-simulation.ts`, and `src/net/*`; preserve `computeLockstepHash` in `src/server/engine/`; add `D2Exocet`/`D2Formal436` `@font-face` CSS to `play.html`; configure `vite.config.ts` with `play.html` as sole input; update all `act-scene.ts` and HTML-reading tests. | M3 | R2, Survey 2 & 3 |
|
||
| F9 | Compiler-Level Isolation (`tsconfig.*.json`) & Zero-Exception Boundary Gate (`tests/arch/boundaries.test.ts`) | Wire `tsconfig.common.json`, `tsconfig.netproto.json`, `tsconfig.server.json`, `tsconfig.client.json`, `tsconfig.baker.json`, and root `tsconfig.json` into `npm run typecheck`; implement `tests/arch/boundaries.test.ts` enforcing all 7 R3 boundary rules with **0 whitelist exceptions**; ensure `src/` contains only `{common,netproto,server,client,baker}` and `npm run build` succeeds with 0 Node-builtin externalization warnings. | M3 | R3, Survey 1, 2 & 3 |
|
||
| F10 | Full 1.13c Parity, Replay, Baker, Walk, 136-Level DRLG & Headless `play.html` E2E Verification | Pass 100% of `npx vitest run`, `npm run bake -- --verify`, `tools/netproto-replay.ts --strict` (`unknownPackets === 0`, `opaquePackets === 0`, `protocolErrors === 0`), `tools/d2-bot.ts` walk test (`hardSnaps === 0`) & 136-level DRLG / 5-Act 20-WP `audit-levels`, `tools/verify-play.ts` in `800×600` & `1024×768`, and Tier 1–5 E2E test suites. | M4 | R4, Acceptance Criteria, Survey 3 |
|
||
|
||
---
|
||
|
||
## Milestones
|
||
|
||
| # | Name | Scope | Dependencies | Status |
|
||
|---|------|-------|-------------|--------|
|
||
| M1 | `src/common/`, `pack-contract` & Shared Model Extraction | F1, F2, F3: Build AST move/rewrite tool (`tools/refactor/move.ts`); populate `src/common/` (`mpq`, `formats`, `data`, `drlg`, `world`, `items`, `skills`, `units`, `i18n`, `rng`, `pack-contract`); split mixed files and extract shared models to eliminate all 23 `server -> client` reverse dependencies; pass `tsc -p tsconfig.common.json --noEmit` and `npx vitest run`. | none | DONE (`src/common/*`, `pack-contract`, 0 errors in `tsc -p tsconfig.common.json --noEmit`) |
|
||
| M2 | `src/baker/` & `tools/` Migration + Unified Baker CLI | F4, F5: Migrate `file-source.ts` + 31 baking/extraction/audit scripts to `src/baker/`; implement `src/baker/cli.ts`; delete `net-relay.ts` & `net-server.ts`; migrate 93 CLI/bot/replay/verify scripts to `tools/` and remove `scripts/`; update `package.json` scripts and test imports; pass `tsc -p tsconfig.baker.json --noEmit` and `npm run bake -- --verify`. | M1 | DONE (`src/baker/*`, `src/baker/cli.ts`, `tools/*`, `scripts/` removed, `tsc -p tsconfig.baker.json` & `bake -- --verify` exit 0) |
|
||
| M3 | `src/server/`, `src/client/`, `LocalLoopbackAdapter`, `play.html` Consolidation & Boundary Gate | F6, F7, F8, F9: Consolidate `src/server/` + `LocalLoopbackAdapter`; consolidate `src/client/` + decouple `MapService` from `node:fs`/`node:path`; codemod `act-scene.ts` test imports and delete `index.html`, `acts*.html`, `bnet.html`, `act-scene.ts`, `net-scene.ts`, `src/net/*`, and legacy top-level `src/` dirs; wire all 5 `tsconfig.<module>.json` configs; implement `tests/arch/boundaries.test.ts` with 0 exceptions; pass all `tsc` checks, `npm run build`, and `npx vitest run`. | M2 | DONE (`src/` contains only `{baker,client,common,netproto,server}`, 6 tsconfigs 0 errors, `boundaries.test.ts` 0 exceptions, `dist/play.html` built cleanly) |
|
||
| M4 | Final Milestone: E2E Test Suite Pass (Tiers 1–4), Live Parity Gates & Tier 5 Adversarial Hardening | F10: Phase 1 — Pass 100% of E2E test suite (`TEST_READY.md` Tiers 1–4) and all live/offline parity gates (`vitest`, `bake -- --verify`, `netproto-replay --strict`, `d2-bot` walk & 136-level `audit-levels`, `verify-play` at `800×600` & `1024×768`). Phase 2 — Tier 5 white-box adversarial coverage hardening & 2B Gate (2 Reviewers, 2 Challengers, 1 Forensic Auditor). | M3, E2E Track | DONE (Phase 1 & Phase 2 passed: 556 Vitest files / 6,640 tests, 138/138 Tiers 1–5 E2E tests, 9/9 offline & live gates, Reviewers `APPROVE`, Challengers `APPROVE`, Forensic Auditors `CLEAN`) |
|
||
| E2E | E2E Testing Track (`TEST_INFRA.md`, Tiers 1–5 Test Suite, `TEST_READY.md`) | Opaque-box, requirement-driven 4-tier test suite + Tier 5 adversarial regression suite covering F1–F10 in `tests/e2e-module-split/**` + `TEST_INFRA.md` + `TEST_READY.md`. | none | DONE (`TEST_INFRA.md`, `TEST_READY.md`, 138/138 tests passing in `tests/e2e-module-split/tier{1..5}-*.test.ts`) |
|
||
|
||
---
|
||
|
||
## Interface Contracts
|
||
|
||
### 1. `src/common/data/data-registry.ts` ↔ Callers (`src/server/`, `src/client/`, `src/baker/`)
|
||
- `ArchiveSource` interface in `src/common/data/data-registry.ts`:
|
||
- `readText(mpqRelativePath: string): Promise<string | null> | string | null`
|
||
- `readBytes(mpqRelativePath: string): Promise<Uint8Array | null> | Uint8Array | null`
|
||
- `D2DataRegistry` and `getSharedDataRegistry(source: ArchiveSource)` MUST require an explicit `ArchiveSource` (no default `/usr/local/google/home/taodao/d2-data` path, no `node:fs`, no `window`/`fetch`). Missing `source` throws an explicit `Error` immediately (fail-fast per `AGENTS.md` §3.1).
|
||
|
||
### 2. `src/common/pack-contract/` ↔ `src/baker/` & `src/client/`
|
||
- Exports `PackedDrlgBundleManifest`, `PackedTileCatalogIndex`, `PackedTilePlacement`, `PackedDt1TileEntry`, `PackedDt1LibraryEntry`, `PackIndex`, `PackIndexEntry`, `PackObject`, `PackObjectFrame`, `PackSceneJson`, `EntityMeta`, `ClipMeta`.
|
||
- Exports fail-fast validators `validateUniversalDt1Libraries(libraries)` (enforcing `Blank.dt1`, `InvisWal.dt1` with style=49 sequence=7, `Warp.dt1`) and `validatePackManifest(manifest)` (enforcing `missingFiles === 0` and `missingTiles === 0`).
|
||
|
||
### 3. `src/client/map/map-service.ts` ↔ `tools/` & Browser
|
||
- `MapService.fromLoader(loader: PackedTileBundleLoader): Promise<MapService>` and `MapService.fromPackedUrls(baseUrl: string): Promise<MapService>` live in `src/client/map/map-service.ts` with zero `node:fs` or `node:path` imports.
|
||
- Node filesystem loading (`createNodePackedTileLoader(dirPath)` / `loadMapServiceFromPackedDir(dirPath)`) lives in `tools/lib/node-map-loader.ts` (or `src/baker/io/`).
|
||
|
||
### 4. `src/netproto/index.ts` ↔ `src/server/adapter/LocalLoopbackAdapter.ts` & `src/client/`
|
||
- `LocalLoopbackAdapter` in `src/server/adapter/LocalLoopbackAdapter.ts` implements `GameServerAdapter` from `src/netproto/index.ts`:
|
||
- `connect(): Promise<void>`
|
||
- `disconnect(): void`
|
||
- `readonly isConnected: boolean`
|
||
- `readonly tick: number`
|
||
- `send(cmd: ClientCommand): void`
|
||
- `onEvent(handler: (ev: ServerEvent) => void): () => void`
|
||
- `onRawPacket?(handler: (direction: 'S2C' | 'C2S', opcode: number, bytes: Uint8Array, decoded?: ServerEvent | ClientCommand) => void): () => void`
|
||
- `src/client/**` never imports `src/server/**`; `src/client/main.ts` consumes any `GameServerAdapter` via `OnlineSession`.
|
||
|
||
---
|
||
|
||
## Code Layout & Write Ownership
|
||
|
||
- **Canonical `src/` directories (post-M3)**: ONLY `src/common/`, `src/netproto/`, `src/server/`, `src/client/`, `src/baker/`. All legacy top-level `src/` directories (`src/game/`, `src/scene/`, `src/mpq/`, `src/formats/`, `src/data/`, `src/render/`, `src/ui/`, `src/frontend/`, `src/audio/`, `src/sim/`, `src/i18n/`, `src/net/`) must be completely removed by the end of M3.
|
||
- **Concurrent Write Ownership**:
|
||
- `M1`, `M2`, and `M3` execute sequentially on the codebase so `git mv` and import codemods never collide.
|
||
- The parallel **E2E Testing Orchestrator** owns EXCLUSIVELY `/usr/local/google/home/taodao/diablo2-web/.worktrees/module-split/TEST_INFRA.md`, `/usr/local/google/home/taodao/diablo2-web/.worktrees/module-split/TEST_READY.md`, and `/usr/local/google/home/taodao/diablo2-web/.worktrees/module-split/tests/e2e-module-split/**`. It MUST NOT edit any file outside those paths.
|