Tabletop Simulator mods: shared Lua lib, per-game scripts, build and lint tooling.
  • Python 63.1%
  • Lua 33.9%
  • Makefile 3%
Find a file
joeyr 7841c59062
All checks were successful
lint / lua (push) Successful in 17s
Merge pull request #16: 2D board layout
2026-08-16 23:41:50 -05:00
.forgejo/workflows Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00
.vscode Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00
games hanabi: lay the 2D board out as a table, not a stack 2026-08-16 23:40:59 -05:00
lib Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00
tools Validate UI XML in lint, and fix the two files that failed it 2026-08-16 18:01:38 -05:00
.gitattributes Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00
.gitignore Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00
.luacheckrc Fix three toolchain bugs found on the first real local run 2026-08-16 16:28:46 -05:00
CHANGELOG.md Fix three toolchain bugs found on the first real local run 2026-08-16 16:28:46 -05:00
CLAUDE.md Point release flow at the reconstructed projects/CLAUDE.md 2026-08-16 15:40:00 -05:00
LICENSE Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00
Makefile Validate UI XML in lint, and fix the two files that failed it 2026-08-16 18:01:38 -05:00
README.md Initial skeleton: monorepo layout, shared lib, build and lint tooling 2026-08-16 15:34:05 -05:00

tts-games

Games I build in Tabletop Simulator — the Lua that runs them, the custom UI XML, and the save files they ship as. One repo holding several games, plus the library they share.

Why one repo

TTS Lua has no package manager. require is not a runtime feature — it only works because a bundler inlines the module before the script reaches the game, and bundlers resolve modules by filesystem search path. Split these games into separate repos and every shared helper becomes a git submodule or a copy-paste, both of which rot. Keep them together and sharing code is one -p flag in the build.

The rest follows: one .luacheckrc, one CI workflow, one clone to open in VSCode, and a refactor of a shared helper lands in a single commit across every game that uses it.

When to split a game out: it gets real players filing issues and wants its own tracker, or a collaborator should see exactly one game. Extract it with git filter-repo and pull lib/ back in as a submodule — history survives. Merging repos later is the messier direction, which is why this starts unified.

Layout

lib/                        shared modules; no globals, no side effects
games/
  _template/                copy this to start a game
    game.json               manifest -- version here is the source of truth
    src/Global.-1.ttslua    Global script (-1 is TTS's reserved GUID)
    src/<Name>.<GUID>.ttslua   one file per scripted object
    ui/Global.xml           custom UI, if the game uses it
    saves/TS_Save_N.json    the exported mod
tools/normalize_save.py     makes save diffs readable

Requirements

  • Node — only for npx luabundler; nothing here is an npm project.
  • Lua 5.2 + luacheck (luarocks install luacheck) for linting.
  • Python 3.11+ for the save normalizer. Standard library only.
  • VSCode with a Tabletop Simulator Lua extension, if you want to push scripts into a running game instead of pasting them. See .vscode/settings.json — the config key prefix differs between the two extensions in circulation, and the file says how to tell.

Working on a game

make new GAME=cosmic-frogs     # copy the template
make lint                      # luacheck lib/ and games/
make bundle GAME=cosmic-frogs  # -> build/cosmic-frogs/Global.lua

Paste the bundle, never the source. src/Global.-1.ttslua contains require("log") calls that TTS cannot resolve on its own; they are inlined by make bundle. A script that works in the editor and errors in game is almost always this.

Save files

The save JSON is the actual artifact — it holds every object, asset URL, and snap point — so it stays committed. It is also machine-generated, tens of thousands of lines, and re-saving after nudging one card rewrites float positions across the whole file.

Run make normalize before committing a save. It sorts keys, rounds floats to four decimals, and reindents — all semantically inert, and it turns a 30,000-line diff back into the handful of lines that actually changed. CI enforces it.

It does not reorder ObjectStates by default. That array is spawn order and decides what lands on top of what among coplanar objects; --sort-objects is there if you want it, but check the table in game afterward.

Before you commit a save, know that it can carry more than the game:

  • notebook tabs keep whatever was typed in them, including playtest notes;
  • object descriptions and nicknames keep whatever anyone typed on them;
  • asset URLs point at wherever you hosted the art.

Releases

Games version independently. The version in a game's game.json is the source of truth; tags are prefixed with the slug:

cosmic-frogs/v1.2.0

Each game keeps its own CHANGELOG.md. The root CHANGELOG.md covers lib/ and the tooling only.

License

MIT — see LICENSE. The license covers the code in this repo. Art and other assets referenced by a save file are governed by whatever terms they shipped under, which is not necessarily this one.