Imported from 620nm/tales-from-space (
AGENTS.md). Install upstream withnpx skills add 620nm/tales-from-space. Copyright stays with the author.
Tales from Space — content rules
The engine API you may call is ../lunatic/idl/generated/lunatic-v1.d.luau (content) and ../lunatic/idl/generated/lunatic-spec.d.luau (spec); read the generated signatures first.
This repository is the standalone game content pack for Tales from Space.
Read README.md before editing: it owns the repository layout, authoring
references, run commands, and spec-runner surface. Keep this file focused on
agent decisions rather than duplicating those details.
Read docs/LUAU-DIALECT.md before writing Luau.
Where to work
- The two fixed-name entrypoints are both optional and load in order:
content/audiences.luaudeclares named delivery rosters, thencontent/main.luaumakes every other top-level declaration. Files such ascapabilities.luau,part_tree.luau,compositions.luau, andtuning.luauestablish pack-wide policy; roster directories undercontent/declare the game's prototypes and handlers. content/lib/contains shared Luau tables; see the rules below before adding or moving code there. Assign shared-module file ownership withdocs/LIB-CONTRACT.md.reference/manifest.roncatalogs preset files by ID. Its raw bodies stay outside trusted content; guest programs are standalone.luausources. Seedocs/scripting/reference-files.mdfor authoring and disk seeding.maps/contains shipped RON maps.tests/contains player-facing Luau specs, including any inline RON fixtures they need.assets/*.ronare source manifests.assets/tg-revisionpins the read-onlytgstationsource used by atlas baking; generated atlas output does not belong in this repository. Treat that checkout as reference only: never reconstruct its behavior from memory, and cite findings asfile:line.mod.tomlis the pack-owned identity, API version, native-edge, path, and requested-permission manifest.modlist.tomlis the host-owned default mod list and approval grant. Do not confuse a request with an approval or broaden either merely to make content convenient.
Engine boundary and validation
Mechanics (layout, running, spec commands) live in README.md. The
engine is the sibling checkout ../lunatic. A bare docs/… names a file
HERE; an engine contract is always written "the engine's docs/…". The
engine's docs/SCRIPTING.md is the v1 design, the engine's
docs/LUAU-API.md the surface these files CALL, the engine's
docs/CONTENT-SCHEMA.md the fields they DECLARE. Four names sit on both
sides (ATMOS, BIOLOGY, CHEMISTRY, GAMEMODES): the engine owns the
mechanism, this pack the numbers it chose. Run the engine with
LUNATIC_PACK pointing here.
Run engine commands from the engine checkout and set LUNATIC_PACK to this
pack's absolute path. A worktree is not necessarily a sibling of the engine;
resolve the engine checkout explicitly instead of deriving it with .. there.
Iterate with cargo run -q -p lunatic-server -- test "$LUNATIC_PACK" <name> (name substring), then --load-only, then the full suite once before completion.
Code Organization
Files per directory
- Target: 5–15
- Split at: 20–30 (split by feature or layer)
- Never exceed: 50
Subdirectories per directory
- Target: 3–10
- Rationale: a reader opening a directory should grasp its partitioning without re-reading names.
Directory depth
- Measured from the crate root (e.g.
crates/lunatic-module/). - Target: 3–5 levels for hand-written source
- Never exceed: 7
Rust module layout
- Use
xyz.rs+xyz/for modules with children. - Do not create
mod.rsfiles. - Enforce with
#![warn(clippy::mod_module_files)].
Rust unit tests
- Default: inline
#[cfg(test)] mod tests { ... }at the bottom of the file. - When the file exceeds the length threshold, move the test body to
file/tests.rsand leave#[cfg(test)] mod tests;infile.rs. - Never use
#[path]for test modules. - Never place unit tests in the crate-root
tests/directory; that directory is for integration tests only.
This pack has no crates: measure depth from the repository root, and apply
the directory rules to content/, ui/, tests/ and tools/. Generated
trees (ui/fixtures/out/, the ui/*.json build products) are outside the
counts. The Rust rules bind the engine checkout, where the same section is
its docs/CODE-ORGANIZATION.md. A roster file is named for the id it
declares (items/<id>.luau); a numeric ordering prefix NN_ is the one
allowance (access/10_engineering.luau).
Content design rules
- Content is the default home for game ideas: nouns (prototypes, rosters,
constants, ids, maps, manifests) are data here; verbs at discrete event
boundaries are Luau anchor handlers. The engine's
docs/LUAU-API.md§4 is the as-built list of record andidl/v1.jsonfreezes their names. An idea that seems to need per-tick native execution becomes a native system with data-driven knobs — never a faster handler. - Game fiction never says "lunatic"; engine words stay out of content.
- Wrap reusable domain concepts in typed records instead of loose primitives; reuse existing types and keep their operations together.
- Use distinct, descriptive names for types and shared helpers across the
repository. Avoid generic names (
Data,State) so agents can grep definitions and uses. - Shared code lives in
content/lib/; import its returned table withlocal vessel = require("@lib/vessel"). Dependencies are explicit in libraries too. Prototype sandboxes have imports but nosim; library initialization cannot callsim.define. Shared code exports tables, rosters declare things. A gesture spelled out in two roster files belongs there instead; that duplication is what it exists to prevent. Because alib/file may notsim.define, a shared answer is a FUNCTION there and theDefinition:handlenaming it lives in the roster file —lib/radio_relay.luauagainstfixtures/transceiver.luau,fixtures/access_point.luauandfixtures/network_router.luau. - Every trusted Luau source passes strict checking. From the engine run
node tools/luau.mjs check "$LUNATIC_PACK"after edits; appendcontentorspecand filenames to narrow it. One editor window offers both native APIs; the command enforces their separate runtime contexts. Authoring setup is the engine'sdocs/luau-api/authoring.md. - Radio policy is
lib/radio.luau(every number, and who finally hears a;line) andlib/radio_relay.luau(what a tower, a wall box and a router each do to one crossing them). Nothing else decides who hears: a second path would be a second answer. The four relay stages are one published native edge, somod.tomlmust keepspeech.relayinnative_edgesor every registration fails at load (the engine'sdocs/luau-api/radio-relay.md). - Specs (
tests/*_test.luau) assert what a PLAYER could cause, throught— the same SimCommand seam the Rust harness uses. No raw entity handles, no component access, no direct spawn, and none should ever be added. Seed 0,Tuning::default()pinned, ticks-not-time, drain after step. Budget output (load ms, spec ms) is advisory wall clock; the hard budget is the host's fuel, counted per invocation and per mod per tick (lunatic'scrates/lunatic-server/src/fuel.rs). - Map RON inside Luau specs goes in
[==[ ... ]==]long strings, not[[ ... ]]— rows like"####"can end a plain long string early (the Luau twin of Rust's raw-string trap). content/tuning.luauoverrides engine feel constants; the engine's compiled defaults are what its tests pin.- No player-visible sentence lives in code.
ui/*.tsand everycontent/**/*.luauname a catalog key;locale/<tag>.jsonsays it. One key is one finished sentence with its facts as{placeholders}, never a fragment joined with..— a per-outcome variant is its own key, and a sentence two files both say islib.<area>.<name>, hoisted once.node tools/keyed-messages.mjs --checkfails on a literal that came back, anddocs/WORDS.mdis the grammar and how to add a language. ui/owns every gameplay surface and binding in restricted TypeScript/TSX.ui/AGENTS.mdroutes surfaces to owners, narrow commands and acceptance evidence.docs/UI.mdowns pack composition and authoring rules; the engine'sdocs/pack-ui/authoring.mdowns the SDK. Native providers expose disclosed readouts and validated actions. Layout findings fail validation.editor/manifest.jsondeclares pack/mode palettes, property schemas, previews and bounded compositions of native edit operations. The trusted editor owns documents and undo; UI guests never receive editor drafts.