Project format
An Inkforge project is a flat set of text files plus an assets/ folder, bundled into a .inkforge file for sharing. Inside the studio the same files are held in a virtual file system, so paths always use forward slashes regardless of the host operating system.
Required entry files
Every project must contain scenario.yaml and the Lua entry file named by its composed scripts.main field:
| Path | Purpose |
|---|---|
scenario.yaml | The entry point. The composer starts here and follows !imports. |
scripts/main.lua by default | The Lua entry file. Set scripts.main in scenario.yaml to use another project-relative path. |
Only scenario.yaml has a fixed name. Everything else — the entry script, locations.yml, ui.yml, modals.yml, anything — can be rearranged if the scenario points to it.
A pack without scenario.yaml is rejected with Package must include scenario.yaml. A pack whose manifest omits the configured Lua entry file is rejected with Package must include configured Lua entry script: <path>. The Author explorer protects scenario.yaml, the configured entry script, and any folder containing that script.
The entry check needs a composed scenario to know which script is the entry one, so it only runs when the YAML composes. A pack whose YAML is unfinished or broken is still accepted and imports normally; its problem surfaces in the diagnostics line at boot instead, which is where you fix it. Exporting such a project is also allowed, so an in-progress project can still be packed as a backup.
manifest.json
The manifest is generated by export, not authored by hand, and it is not part of the project's source file list. It records what the pack contains so the importer can validate it:
{
"format": "inkforge-pack",
"packVersion": 2,
"project": {
"id": "lantern-below",
"title": "Lantern Below",
"author": "Theros",
"version": "1.0.0"
},
"files": [
"scenario.yaml",
"locations.yml",
"scripts/main.lua",
"assets/map.svg"
]
}| Key | Meaning |
|---|---|
format | Always "inkforge-pack" |
packVersion | Always 2. Any other value is refused: Unsupported Inkforge package; expected pack version 2 |
project.id | Stable identity. Importing a pack whose id matches an existing project updates that project rather than adding a second copy |
project.title | Shown in the header and the scenario library |
project.author | Free text |
project.version | Semantic version, x.y.z with an optional -prerelease |
files | Every packaged path, in the archive, excluding manifest.json itself |
project.version is what decides whether an import is accepted. Importing a pack with a version equal to or older than the stored one is declined, and the diagnostics line says so: ● <Title> is already v<version>. Versions compare by SemVer precedence — numeric core first, then prerelease rules, so 1.0.0 outranks 1.0.0-rc.1. Bump the version when you want an import to land.
The package version is not the scenario version
The two numbers mean different things and move independently:
| Version | Lives in | Means |
|---|---|---|
| Package version | manifest.json → project.version | The version of the files. The importer compares it. |
| Scenario version | scenario.yaml → meta.version | The version of the work — "Lantern Below v1.0". |
project.version is a record of the package's contents, so the importer has something to compare. Change any project file and it needs to move, whether or not the work itself changed; change only meta.version and nothing about an import changes, because the importer never reads it.
meta.version is the author's own number for the work, and it is what the scenario library puts on the card — alongside meta.description and the time the record was last written. project.version is not shown anywhere: it moves every time a file does, which is a fact about the package rather than about the work. The two are free to drift, so a template can ship a dozen package bumps with the scenario still at 1.0.
Packing enforces the first of those. deno task pack compares the pack it is about to write against the one already on disk, and if the project files differ while project.version has not moved, it refuses:
Content changed, but project.version is still 0.1.1:
- changed locations.yml
Importing this pack would be declined as "already v0.1.1", leaving existing installs on the old content.Two ways forward, both explicit:
deno task pack -- templates/lantern-below --bump patch # 0.1.1 -> 0.1.2, written to manifest.json
deno task pack -- templates/lantern-below # after editing project.version yourself--bump takes major, minor or patch, raises that field, drops any prerelease, and rewrites only the version value in manifest.json — the rest of the file, including your formatting, is left alone. --force writes the pack anyway, for repacking in place before a version is settled. A version older than the packed one is refused too, since an importer declines it.
Assets
Anything under assets/ is treated as binary rather than source. The supported types are fixed:
| Extension | MIME |
|---|---|
.svg | image/svg+xml |
.png | image/png |
.jpg, .jpeg | image/jpeg |
.webp | image/webp |
.mp3 | audio/mpeg |
.ogg | audio/ogg |
.wav | audio/wav |
Any other extension under assets/ is refused — on import, Unsupported asset type: <path>, and on upload through the ↑ Asset button, ● Unsupported asset type: <name>. The upload button also refuses to overwrite: ● Asset already exists: <path>.
Assets are referenced by their project path. An image element uses src: assets/map.svg; audio uses GameAudio.play("assets/theme.mp3"). See Audio.
Because assets are referenced by path rather than embedded, a missing one is reported rather than silently blank: Asset not found: <path>. A path that is already a data:, http(s): or blob: URL is passed through untouched.
Path rules
Project paths are deliberately restricted, because a .inkforge file is something you may have received from someone else:
- Forward slashes only; a backslash is refused.
- No leading
/— paths are relative to the project root. - No
.or..segments, and no empty segments. manifest.jsonis reserved and cannot appear infiles..inkforge-diris reserved as a filename at any depth for the Author view's empty-folder markers. Packs containing it are refused.- Duplicate paths in
filesare refused.
On import, an archive is also bounded: 16 MB compressed, at most 256 entries, 8 MB per entry and 32 MB uncompressed in total. Exceeding any of these reports the specific limit that was hit rather than failing vaguely.
Export and import
Export Scenario writes the current project — every VFS file plus every asset — as a ZIP with a generated manifest.json. The download is named after the project title, sanitised for a filesystem. Empty folders created in the Author view are recorded with a hidden marker that survives saves but is filtered from exports, so a pack contains only real files and assets.
Import Scenario in the Scenarios dialog validates the manifest, checks the version against any stored project with the same id, and then either declines it or installs it and starts it. Source files must be valid UTF-8; a file that is not is reported as Scenario source is not valid UTF-8: <path>.
Exporting does not change the stored project's version. To ship an update, edit project.version — in the project library, or in manifest.json of the source folder if you are packing from disk with deno task pack.
The scenario library
Scenarios opens the library, which lists every scenario stored in the browser. Scenarios persist locally in IndexedDB; nothing is sent anywhere, and there is no server. The bundled starter is marked pinned: pinning survives an import, so a newer pack updates the starter in place rather than adding a second copy — which is how the template that ships with the app is refreshed in an existing install.
Each card is titled with the project's title and described by the work's own front matter: <meta.description> · v<meta.version> · Updated <when>. The version there is meta.version, never project.version: the package number moves whenever any file does, which is not something a reader of the library chose. Description and version are read from the stored project's files each time the library opens, so a card cannot describe an older draft of the work.
Because the library is local, it holds whatever was stored the last time that project was written. A template edit in templates/ reaches a running install through an import of the re-packed .inkforge, not by being copied into the library at boot: the starter is seeded only when no project with its id exists yet. That is the whole reason the package version has to move — see the package version is not the scenario version.
deno task check:pack guards the other end of the same problem: it unzips every committed templates/*.inkforge and compares it, file by file, against the folder it was built from, so a pack left behind by an edit fails the matrix instead of shipping stale content.
New scenario in the Scenarios dialog restores a fresh copy of the starter, leaving the existing scenarios untouched.