# TTS Workshop Search the Tabletop Simulator Steam Workshop, fetch full TTS save files, and analyze their contents. A lightweight, client-only pnpm monorepo. ## Packages | Package | Role | Runtime | | ------------------- | ------------------------------------------------------ | ---------- | | `apps/web` | React frontend: search + mod pages + 3D object viewers | Browser | | `apps/proxy` | Hono HTTP server: Workshop search, save fetch, tracing | Node | | `packages/tts` | Fetch save from Steam, BSON-parse to `TTSMod` | Node | | `packages/extract` | Analyze a `TTSMod`: objects, asset refs, downloads | Isomorphic | | `packages/mesh` | 2D shapes + extrusion into 3D mesh geometry | Isomorphic | | `packages/shared` | Shared types + zod schemas | Isomorphic | See [`docs/architecture.md`](docs/architecture.md) for the architecture and [`docs/implementation-plan.md`](docs/implementation-plan.md) for the plan. ## Setup ```sh pnpm install cp .env.example .env # then set STEAM_API_KEY pnpm dev # runs the proxy at http://localhost:3000 ``` Get a Steam Web API key at https://steamcommunity.com/dev/apikey (free). To run the frontend alongside the proxy, open a second terminal and run `pnpm --filter @tts/web dev` (serves at http://localhost:5173 and proxies `/search`, `/items`, `/health`, `/asset`, and `/trace` to the backend). ## API | Method | Path | Description | | ------ | ----------------- | -------------------------------------------- | | GET | `/health` | Liveness | | GET | `/search?q=&page=`| Search the Workshop (scrapes browse page) | | GET | `/items/:id` | Full parsed `TTSMod` (BSON save) | | GET | `/items/:id/file` | Raw save bytes, filename from header | | GET | `/asset?url=` | CORS-safe proxy for external assets (textures, models) | | GET | `/trace?url=&mode=&format=&offset=` | Trace an image into a vector shape (BSON); `offset` insets/outsets in pixels | ## Commands ```sh pnpm dev # run the proxy (tsx watch) pnpm dev:web # run the frontend (vite) pnpm build # compile all packages pnpm typecheck # typecheck all packages pnpm test # run the unit tests (vitest) pnpm lint # lint all packages ``` ## Object viewers The mod page renders each selected object in 3D. Tiles, tokens, cards, and custom models each have a viewer built on `@react-three/fiber`, `@react-three/drei`, and `@react-three/postprocessing`, registered per object class and lazy-loaded so the three.js stack is code-split out of the main bundle. - **Tiles / tokens** — extruded from a 2D shape; tokens trace the image's alpha channel via `/trace` to match the artwork's silhouette. - **Cards** — a thin rounded rect. Deck images are sheets divided into a `NumWidth` x `NumHeight` grid; the face/back sprite is selected by `CardID` from the containing deck's config. - **Custom models** — GLTF/OBJ/FBX loaded from `CustomMesh.MeshURL`. The camera fits the object's bounds on load, and back faces are flipped so they aren't mirrored. See [`docs/decisions.md`](docs/decisions.md) for the rationale behind these choices. ## How search works Steam has no official search API. The proxy fetches the Workshop browse page (`steamcommunity.com/workshop/browse/?appid=286160`) and parses the embedded `window.SSR.renderContext` JSON (a React Query cache containing a `workshop_browse` entry with the results). This is more robust than scraping the DOM, but Steam can still change the page structure — if search breaks, that parser is the first place to look. ## Notes - `STEAM_API_KEY` is optional. Search works without it. The mod page works without it too when you arrive from a search result: the frontend passes the item's `file_url` (already present in search results) to `/items/:id` via a `fileUrl` query param, so the proxy downloads the save directly instead of calling the Steam API. Without a key *and* without a `fileUrl` (e.g. a deep-linked mod page), `/items/*` returns 500. - Each request fetches fresh; there is no caching by design (client-only tool). - `packages/extract` has zero runtime dependencies and runs in browser or Node, ready for a future frontend.