= Forge Scott Duensing :revnumber: 3.00 :revdate: 2026 :doctype: book :toc: left :toclevels: 3 :sectnums: :sectnumlevels: 3 :source-highlighter: rouge :icons: font :experimental: [preface] == About Forge Forge is Singe's authoring tool: a way to make a game by describing it -- placing things, attaching behaviours, writing rules -- and having ordinary Singe Lua written for you. It is itself a Singe game and is distributed on its own, beside the engine rather than inside it. This document covers the description format, the vocabulary, the compiler and the editor. The engine calls the generated code makes (`playerNew`, `collidePointRect`, `onKeyPressed`, `scriptPush` and the rest) are documented in the Singe Manual. == Describing a Game Instead of Writing One A game can be written as a *description* -- a table of layers, entities and rules -- and compiled into an ordinary Singe game. `Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime the result calls. **No part of Forge ships with Singe** -- not the editor, not the compiler, not the runtime -- so a game built with it carries its own copy of that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules become real Lua `if` statements, so a description costs nothing per frame on a Raspberry Pi, and the game it produces can be opened, read and edited by hand like any other. There is no notion of genre anywhere in it. A game declares which of the engine's own layers it uses, and that is the only difference between a light gun game, a platformer and a quick-time event over video. === The three nouns *Layers* are what the game draws through: `world2d` (physics in the XY plane, drawn into the overlay), `overlay` (flat drawing over everything) and `disc` (the video the game is played over). A game lists the ones it wants. *Entities* are things on a layer. Each has a position, a `look` (`box`, `sprite` or `text`) and any number of behaviours. Every entity is a node, whether or not the 3D scene is drawing, which is what lets a 2D game built this way run on a machine with no GPU. *Behaviours* are bundles over engine calls that already exist: `platformer` is the character controller (`playerNew` and friends), `solid` is a static body, `drift` moves at a constant velocity. Attaching one is a line in the description rather than code. *Rules* are conditions and actions. Every rule is tested every frame, in the order written, and all of a rule's conditions must hold for its actions to run. .A description, in full [source,lua] ---- return { title = "One rule", layers = { { kind = "world2d", gravity = 1500 } }, entities = { { id = "ground", x = 360, y = 440, look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 }, behaviours = { { kind = "solid" } } }, { id = "hero", x = 120, y = 380, look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 }, behaviours = { { kind = "platformer", speed = 210, jump = 620 } } } }, rules = { { note = "Run right", when = { { "keyHeld", key = "RIGHT" } }, act = { { "run", entity = "hero", direction = 1 } } } } } ---- Compile it and run what comes out: [source,lua] ---- dofile("Forge/AuthorCompile.singe") dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe")) ---- `testScripts/author/platformer.game` and `testScripts/author/qte.game` are worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile and play them. === The vocabulary, and adding to it Conditions and actions are not built into the compiler. Each is an entry in the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind of game is a set of entries rather than a new release. The conditions today are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`, `touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`, `moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`. `once` deserves a word. Rules run every frame, so anything that should happen a single time -- a door opening, a score awarded -- needs it: [source,lua] ---- { when = { { "touching", entity = "hero", other = "prize" }, { "once", tag = "prize" } }, act = { { "addScore", amount = 100 }, { "show", entity = "prize", visible = false } } } ---- === The way out The `lua` action takes a line of Lua and emits it as it stands. It is there on purpose: when a rule needs something the vocabulary cannot say, that rule drops to Lua and the rest of the game is unaffected. A description is a convenience, not a cage, and the compiled output is a normal game you can stop describing and start editing whenever it suits you. === The editor `Forge/Forge.singe` edits a description, and it is itself a Singe game. It has its own directory beside the games, appears in the menu like one, and packs to `Forge.game` with `--pack`; the build does that itself (the `forge` target), with this manual inside. Nothing in it is a preview: the canvas is the same overlay at the same coordinates the game will be played in, so what is placed is what is seen. Started from the menu it opens on a chooser: the descriptions in its data directory, any dropped into the `Forge` directory itself (those are opened as a copy, since inside a `.game` they are read only), and *New game*, which writes a starter -- ground, a hero that runs and jumps, a score readout -- and opens it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into that data directory, and the chooser says where it is. `Esc` in the editor closes the description (twice, when it has unsaved changes) and `Esc` on the chooser leaves Forge. `P` plays: the description is saved, compiled beside a copy of the runtime, and handed to the engine with `scriptPush`; when the game ends Forge comes back on the same file. A script can drive the editor instead, which is how the test scenes do it: [source,lua] ---- FORGE_LIBRARY = true dofile("Forge/Forge.singe") forgeBegin("mygame.game") function onOverlayUpdate() local x, y = mouseGetPosition(0) forgeDraw(x, y) return OVERLAY_UPDATED end ---- The entity list and the details are an RmlUi document; the canvas beside them is drawn into the overlay and picked with `collidePointRect`. The two compose because the engine offers a button to the GUI first and passes on what it did not use, while pointer motion is never consumed at all -- so point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse callbacks and clicks on the panels will not reach the canvas. `forgeSave()` writes the description back, `forgeBuild(path)` compiles what is on screen (and puts the runtime beside it, so the result plays), and `forgeMove(index, x, y)` moves an entity without a pointer, which is how `testScripts/scene54.singe` drives it. === Editing the entities `A` adds a box in the middle of the canvas, `D` duplicates the selected entity a little to one side, and `Delete` removes it. `Enter` walks the entity's fields the same way it walks a rule's parameters: `id`, `x`, `y`, the look's `kind`, then whatever that look takes, then `behaviours` -- typed as a list of kinds, `platformer, solid` -- and then each behaviour's own parameters as `platformer.speed` and so on. The fields come from the `AUTHOR` manifest, so a new look or behaviour is editable the moment it is declared. Renaming an entity renames it in every rule that talks about it. A `sprite` look is drawn with its image once the file name is right, and as a box until then. From a script, `forgeEntityAdd(x, y)`, `forgeEntityDuplicate()`, `forgeEntityDelete()` and `forgeEntitySet(entity, field, value)` do the same. === Editing the rules The panel shows either the entities or the event sheet; `Tab` swaps them. In the rules, the selected rule opens in place and its conditions and actions are listed under it, because a rule only means anything whole -- a `when` without a `then` tells you nothing. Point `onKeyPressed` at `forgeKey` and the whole editor works without a pointer, which is how the bundled menu has always been driven and what a cabinet wants: [cols="1,4"] |=== | `Tab` | entities or rules | Up, Down | move through the list, and through the parts of the open rule | Left, Right | slide the panel | `Enter` | type a value for whatever is selected; again for its next value | `Esc` | put the value back | `A`, `D`, `Delete` | add, duplicate, delete the selected entity | `N` | a new rule | `C`, `T` | add a condition, an action -- picked from the vocabulary, with its help beside it | `[`, `]` | move the rule up or down the sheet | `Delete` | delete the selected rule, or the selected condition or action | `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history | `S`, `B`, `P` | save, build, play |=== With the rule itself selected rather than one of its parts, `Enter` edits its note. A typed number comes back a number rather than a string, because `100` and `"100"` compile to different source and a description that changed shape when a value was retyped would stop round-tripping. From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act", name)`, `forgePartSet(key, value)`, `forgePartDelete()`, `forgeRuleMove(by)`, `forgeUndo()` and `forgeRedo()` do the same work. Adding a condition selects it, so its parameters can be set at once. The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs changing when a condition or an action is added: it offers whatever is declared. A new parameter gets a sensible starting value for its type, so a rule compiles the moment it is made rather than only once every field is filled in. A description survives the round trip: loading one, saving it and loading it again compiles to the same game, byte for byte. The editor depends on that and the test asserts it. === Releasing a game `forgeExport(folder, name)` writes everything a finished game needs into a directory of its own: the compiled script, a `games.dat` so the menu lists it, the description it was built from so it can be opened again, and **a copy of the runtime**, taken out of Forge. `--pack` turns that directory into a `.game` like any other, and it runs on a machine that has never had Forge on it. Every built game finds its own directory to load that runtime from, using `debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the engine was *launched* with, so a game reached by `dofile` -- a test, a launcher, a preview -- would otherwise look beside the caller. The panel slides. Drag the tab on its outer edge and it moves across the window, so an entity that lives underneath it -- a score readout at `12, 12` does -- is never permanently out of reach. `forgePanelTo(x)` moves it from a script, and `forgeOverPanel(x)` says whether a point is currently covered. [#migrating]