Files
tts-workshop/docs/decisions.md
T
hypercross 08c767f399 docs: update architecture and plan for tree inspector UI
Record the viewer-registry decision and refresh the extract/web
descriptions and repo layout.
2026-08-08 12:19:47 +08:00

5.2 KiB

Decisions

Scope: The rationale behind key design decisions. For the system's architecture, see architecture.md. For the concrete build plan, see implementation-plan.md.

Each entry records the decision, the context, and the alternatives considered. New entries are appended; existing entries are updated only to correct facts, not to rewrite history.

D1 — Client-only, no caching

Decision: The system runs on the client only and has no caching layer.

Context: The tool is a personal, single-user utility. There is no shared state to protect and no multi-instance deployment.

Alternatives considered: In-memory TTL caching; Redis-backed caching. Both rejected as unnecessary complexity for a single client.

D2 — Separate packages/extract from packages/tts

Decision: Analysis lives in packages/extract; fetching lives in packages/tts. They are separate packages with a clean fetch-vs-analyze boundary.

Context: The user wants to analyze saves and extract objects for a future frontend. Keeping analysis separate lets it be reused independently of the fetcher.

Alternatives considered: Folding analysis into packages/tts. Rejected — couples fetch and analysis and would force the frontend to depend on the fetcher.

D3 — Traversal lives in packages/extract

Decision: traverseMod / markParent move from the fetcher into packages/extract.

Context: Traversal is analysis, not fetching. packages/tts should be fetch-only.

Alternatives considered: Keeping traversal in packages/tts. Rejected — blurs the fetch/analyze boundary.

D4 — packages/extract is isomorphic with zero runtime deps

Decision: packages/extract runs in browser and Node, using only fetch, Blob, and typed arrays. It has no external runtime dependencies.

Context: The frontend (deferred) will consume it directly, so it must be portable. Node 18+ provides global fetch and Blob.

Alternatives considered: Using Buffer and Node-only packages. Rejected — breaks browser use.

D5 — No backend traversal endpoints

Decision: The proxy exposes search and fetch only. Traversal is not exposed as HTTP endpoints.

Context: The frontend will use packages/extract directly. Exposing traversal on the backend would duplicate that logic and add endpoints with no current consumer.

Alternatives considered: Adding /items/:id/objects and similar endpoints. Deferred until a consumer exists.

D6 — Search scrapes the Workshop browse page

Decision: Search is implemented by scraping steamcommunity.com/workshop/browse/?appid=286160 with cheerio.

Context: Steam has no official Web API for searching the Workshop; the API only fetches details for known IDs.

Alternatives considered: None viable — there is no official search API. Accepted risk: scraping is fragile and may break if Steam changes its HTML.

D7 — bson npm package replaces the browser BSON global

Decision: The existing scraper's BSON.deserialize global is replaced with the bson npm package.

Context: The scraper was written for the browser; the proxy runs on Node.

Alternatives considered: Keeping a browser-only global. Rejected — not available in Node.

D8 — Download output type is Blob

Decision: packages/extract's downloadAsset / downloadAll return Blob.

Context: The primary consumer is a frontend that renders assets in <img> / <object> elements.

Alternatives considered: Raw ArrayBuffer. Rejected — less convenient for frontend rendering.

D9 — Frontend stack: React + React Router + Tailwind v4 + Zustand

Decision: The frontend (apps/web) uses React with React Router for routing, Tailwind CSS v4 for styling, and Zustand for state, built with Vite.

Context: The frontend consumes the proxy API for search/fetch and packages/extract directly for analysis. React is the de-facto standard for this kind of tool; React Router provides declarative routes (/ and /mod/:id); Tailwind v4 is the current major version and integrates via @tailwindcss/vite; Zustand is a minimal, unopinionated store that fits the small amount of client state (search + mod).

Alternatives considered: Next.js (heavier than needed for a client-only tool); Redux Toolkit (more boilerplate than warranted); CSS Modules (no utility styling).

D10 — Object inspection via a viewer registry

Decision: The mod page shows a containment-tree sidebar (mirroring how TTS nests objects) and an inspector pane. The inspector resolves a per-class viewer from a registry (registerViewer / resolveViewer), falling back to a DefaultViewer that renders an object's raw fields.

Context: The user wants to visualize objects in a save. A flat list of all objects and assets is hard to navigate, so the UI was reworked around a tree and a selected-object inspector. Custom viewers per object class (Card, Bag, ...) are expected later; a registry keeps that extension point explicit without coupling the page to any one viewer.

Alternatives considered: A single monolithic inspector component. Rejected — would grow unboundedly as per-class viewers are added; the registry keeps each viewer isolated and swappable.