Replace the dark surface plane with a grey background and a spotlight, size the contact shadow to the table, and scale its blur and fade with the shadow plane. Disable depth test on the stacking curve overlay.
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 for the architecture and
docs/implementation-plan.md for the plan.
Setup
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
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
/traceto match the artwork's silhouette. - Cards — a thin rounded rect. Deck images are sheets divided into a
NumWidthxNumHeightgrid; the face/back sprite is selected byCardIDfrom 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 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_KEYis optional. Search works without it. The mod page works without it too when you arrive from a search result: the frontend passes the item'sfile_url(already present in search results) to/items/:idvia afileUrlquery param, so the proxy downloads the save directly instead of calling the Steam API. Without a key and without afileUrl(e.g. a deep-linked mod page),/items/*returns 500.- Each request fetches fresh; there is no caching by design (client-only tool).
packages/extracthas zero runtime dependencies and runs in browser or Node, ready for a future frontend.