7.8 KiB
Decisions
Scope: The rationale behind key design decisions. For the system's architecture, see
architecture.md. For the concrete build plan, seeimplementation-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.
D11 — 3D viewers on the r3f/drei/postprocessing stack
Decision: Per-class 3D viewers for tiles, tokens, cards, and custom models
are built on @react-three/fiber, @react-three/drei, and
@react-three/postprocessing, registered through the existing viewer registry
and lazy-loaded so the three.js stack is code-split out of the main bundle.
Context: The user wants to visualize objects in a save in 3D. The viewer
registry (D10) already provides the extension point. Custom models load from
CustomMesh.MeshURL (GLTF/OBJ/FBX); tiles/tokens/cards use their
CustomImage/CustomDeck textures. Unity asset bundles are out of scope.
Alternatives considered: A single monolithic 3D inspector; a WebGL library other than three.js. Rejected — the registry keeps viewers isolated, and r3f is the de-facto React/three.js integration.
D12 — CORS-safe asset proxy
Decision: The proxy exposes GET /asset?url=..., which fetches an external
asset and streams it back with a Content-Type header. Only http(s) URLs are
allowed.
Context: Workshop asset hosts (steamusercontent.com, etc.) often omit CORS
headers, which would block three.js TextureLoader / GLTFLoader in the
browser. Routing asset fetches through the proxy makes them loadable.
Alternatives considered: Loading assets directly in the browser. Rejected — CORS failures on common Workshop hosts would break the viewers.
D13 — Server-side image tracing to a mesh-ready shape
Decision: The proxy exposes GET /trace?url=..., which fetches an image,
traces it into a vector shape, and returns the result BSON-encoded. Tracing is
configurable: mode (alpha default, bw, color) selects how the region is
derived, and format (shape default, svg) selects the response. The
shape format returns a parsed { outline, holes } polygon matching
@tts/mesh's Shape interface.
Context: The user wants to build a mesh from an image (e.g. a token or tile
art) using @tts/mesh. vtracer only returns SVG, but the mesh package consumes
{ outline, holes }. Tracing on the server keeps the SVG-to-shape parsing out
of the browser and lets the BSON response carry a ready-to-extrude shape.
sharp decodes the image to RGBA so alpha/luminance masks can be built before
feeding vtracer's convertPixels.
Alternatives considered: Returning only the raw SVG and parsing in the web
app near @tts/mesh; tracing by color only (no alpha). Rejected — server-side
parsing yields a shape the mesh package can consume directly, and alpha-based
tracing (the default) is the common case for token/tile art.