diablo2-web/PROJECT.md

132 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.