hypercross d9d38c2bee fix(tabletop): key placements by surface, path, and piece
A piece can appear on more than one path of the same surface (e.g. the
poker deck expands to every card while the flop also places the ace), so
the render key must include the path to stay unique.
2026-08-09 22:46:06 +08:00

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 /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 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.
S
Description
No description provided
Readme
2.5 MiB
Languages
TypeScript 98.8%
JavaScript 1%
HTML 0.2%