singe/docs/lessons/29-shipping.adoc
2026-09-22 21:57:42 -05:00

464 lines
18 KiB
Text

== Lesson 29: Shipping It
image::learn/29-shipping.png[The finished lesson, 480]
You have a folder. It has a script in it, some pictures, a few sounds, and the
`games.dat` you wrote in lesson thirteen. It runs, because everything it needs
is where you left it and you know where that is.
This lesson turns that folder into one file that somebody who has never met
you can copy onto their machine and play. Most of it is two command lines. The
rest is the part nobody teaches: what belongs in the file, whose work is in
there besides yours, and how to find out whether it works anywhere but here.
=== One File
A finished Singe game ships as a single `.game` file. Packing one is:
----
Singe --pack MyGame MyGame.game
----
The first name is your game's folder, the second is the file to write. Singe
reads every file in the folder into a database and says what it did:
----
>>> Packed 42 files (3819204 bytes) from MyGame into MyGame.game
----
Run it the same way you run anything:
----
Singe MyGame.game
----
That runs the first entry in the `games.dat` inside it; `--entry=2` runs the
second, for a file holding more than one game. To install it, the player copies
`MyGame.game` next to the Singe program. The bundled menu finds it and lists
it. There is nothing else: no installer, no archive to unpack, no folder to put
in the right place.
=== What Packing Does to Every File Name
Nothing in your game has to change to be packed, and it is worth knowing why,
because it is the thing most likely to surprise you.
Every name your game uses -- `dofile`, `require`, `io.open`, `spriteLoad`,
`soundLoad`, `fontLoad`, `videoLoad`, a framefile, a `controls.cfg` -- goes
through one lookup. For a loose folder, that lookup is the ordinary
filesystem. For a packed game, the name is tried in three places and the first
hit wins:
. a loose folder named like the file without its extension -- `MyGame/` beside
`MyGame.game`;
. the game's data directory, under `files/`, which is where anything the game
writes has landed;
. the database itself.
Inside the database, names are matched without regard to capital letters, with
either kind of slash, and relative to the game's root. A leading folder name
equal to the game's own is ignored, so `MyGame/art/ship.png`, `art/ship.png`,
and `DIR .. "art/ship.png"` all find the same file.
Two rules follow from that, and if you have been following the book you already
obey both. Use `DIR` for everything you load, so a name never depends on where
Singe was started. And write only under `singeGetDataPath()`, which is where
`saveSet` already puts things -- a packed game is opened read only and is never
written to, so a game that saves beside itself works loose and fails packed.
Two names go somewhere else on purpose. Anything beginning with `Singe/` stays
on the real filesystem, so `dofile("Singe/Framework.singe")` keeps working, and
so does the data directory. And a name with `..` in it that tries to climb out
of the game resolves nowhere at all: `io.open` hands back `nil` and a message
saying the name reaches outside the game, and `spriteLoad` and its relatives
report the file as missing.
=== The Loose Folder Still Wins
That first place in the list is the one you will use every day. A folder named
like the packed file, sitting beside it, beats the packed copies file by file.
----
MyGame.game the packed game
MyGame/ a folder with one changed file in it
----
Drop a single corrected `art/ship.png` in there and the packed game runs with
your new picture and everything else out of the database. Nothing has to be
repacked to try a change, and you can keep your whole working folder there
while you finish the game. It is also how a player mods one, which you may or
may not think is a feature.
Two things follow. Check your game once with that folder renamed away, or you
will ship a file that only works on the machine that has the folder. And know
that packing hides nothing: `--unpack` is a documented option and anybody can
run it. Pack your game because it is one file, not because it is a locked one.
=== Patching a Released Game
Somebody has your game, there is a bug in one script, and you do not want to
send them forty megabytes again.
----
Singe --patch MyGame.game fixes
----
`fixes` is a folder laid out like the game -- the same names in the same
places -- holding only the files that changed. Files the patch does not name
are left alone. The whole change is one transaction, so an interrupted patch
leaves the game exactly as it was rather than half replaced.
A patch can travel as a file of its own, made with the packer:
----
Singe --pack fixes MyGame-1.0.2.patch
Singe --patch MyGame.game MyGame-1.0.2.patch
----
A `.patch` needs no `games.dat`, cannot be run, and the menu ignores it. It may
also carry a `removed(path)` table naming files to delete, for the artwork you
replaced with something under a different name. Either way, Singe checks the
patched game afterwards the same way it checks a new one, so a patch cannot
leave your `games.dat` pointing at a file that is no longer there.
And to get everything back out:
----
Singe --unpack MyGame.game MyGameUnpacked
----
=== What the Packer Refuses
The packer will not build a game out of just anything, and every refusal is
about something that would break later. It refuses a folder with no
`games.dat` at its root. It refuses a `Framework.singe`, a `controls.dat`,
anything ending in `exe`, `sh`, `bat`, or `cmd`, and an extensionless file
whose name starts with `singe` -- in other words, a copy of the engine or its
support files that wandered into your game folder. It refuses two files whose
names differ only in capital letters, since the lookup cannot tell them apart.
It refuses a top level entry named like the folder itself. And it checks your
`games.dat` before it commits: every entry needs a `SCRIPT`, and every
`SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE`, and `ATTRACT` it names has to be a
file that is actually in there. A bad path is reported with the entry's title
and nothing is written.
Each complaint names the file, and there is a list of them rather than the
first one, so one run tells you everything to fix.
=== What to Include and What to Leave Out
Look in your game folder. Some of what is in it is the game, and some of it is
how the game got made.
Ship: the script and anything it loads, the `games.dat`, the artwork the menu
shows (`CABINET`, `MARQUEE`, and the `ATTRACT` clip), your credits and licence
files, and a read-me if you have one.
Leave out: the drawings you exported the artwork from, the takes of the sound
you did not use, your notes, test scripts, screenshots, anything named
`old` or `backup`, the `data` folder with your own saves and your own high
scores in it, and anything you would not want a stranger reading. All of it
packs perfectly well, which is the problem: it goes out with the game and it is
one `--unpack` away from anybody.
The reliable way to find out what you actually shipped is to unpack it into an
empty folder and look. Do that once for every release. It takes a minute and it
is the only check that sees what you forgot rather than what you remember.
=== Credits and Licences
If any art, sound, music, or font in your game was made by somebody else, you
are shipping their work, and the terms they gave it under are an obligation
rather than a courtesy. This is not a formality and it is not about lawyers: a
person made that thing and asked for one specific thing in return, usually
their name.
Keep a record while you work, not at the end. A plain text file in the game
folder with one line per item: what the file is, where it came from, who made
it, what licence, and what that licence requires. Writing it as you go takes
seconds. Reconstructing it the night before a release, from a browser history,
is how credits get left out.
A few things worth knowing before you need them. "Free to download" is not a
licence; look for the actual terms. Creative Commons `BY` means you must give
the named credit, and it has no exception for games. `CC0` and public domain
ask nothing, and crediting anyway costs you a line. Fonts carry their own
licences, and some allow embedding while others do not. Music from a video is
almost never yours to use. Anything generated by a tool is only as clean as the
tool's terms.
Then put the credits somewhere a player can reach them, inside the game. Not
only in a text file next to it -- a packed game is one file and the text file
is inside it, where nobody will look. The script at the end of this lesson is
that screen.
Singe's own `Singe/LICENSES` is the shape to copy: one line per library, with
its version, its licence, and where it came from. Your game's list will be
shorter and the job is the same.
=== A Version Number That Means Something
Your game will have a second release. The first one will be out there for as
long as the internet lasts, and somebody will send you a bug report about it
with no idea which version they have.
So pick a number, put it where a player can read it, and never reuse one. Three
parts is the common shape -- `1.0.2` -- and what the parts mean matters less
than that they only ever go up. Raise the last one for a fix, the middle for
something new, the first when you have made a different game. Whatever you
choose, write it down in your read-me so that future you obeys it too.
`games.dat` has no version key, so the number is yours to carry. Put it in the
script as a constant, show it on the credits screen, and put it in the name of
what you send out (`MyGame-1.0.2.game`, `MyGame-1.0.2.patch`). Then the bug
report says the version on the screen, and you know which files it is about.
=== Testing on a Machine That Is Not Yours
Your machine is the worst possible place to test a release. It has the fonts
you installed, the folder you were editing, the save file with your settings in
it, and the engine in whatever state a month of work left it.
The minimum test, in order:
. Unpack the release into an empty folder and read the file list.
. Copy *only* the `.game` file to a different folder, with a fresh copy of
Singe and nothing else, and run it from the menu the way a player will.
. Delete that folder's `data` directory and run it again. That is a first run
on a new machine: no saved settings, no high scores, no signed-in account.
A game that only works on its second run is a common bug and this is how you
find it.
. Give it to one person who has never seen it, and watch without helping.
Then test the machine as well as the game. It will have a different screen
shape, so look at your game windowed and full screen. It may have no 3D device
at all, which is why the menu has a second renderer. The service tools' *System
Information* page lists the engine version, the operating system, the
processor, the renderer, the decoders, and the data directory on one screen; a
photograph of that page answers most of a bug report, and telling your players
that in your read-me will save you a week of guessing.
=== The Script
A credits and version screen. It is a small thing to write and it is the part
of shipping that most games get wrong.
[source,lua]
----
dofile("Singe/Framework.singe")
local GAME_NAME = "Rock Dodger"
local GAME_VERSION = "1.0.2"
local CREDITS = {
"Written and drawn by <your name here>",
"",
"Music \"Night Drive\" by A. Composer",
" Creative Commons BY 4.0",
" example.org/night-drive",
"",
"Explosion freesound.org user \"thud\"",
" Creative Commons CC0",
"",
"Font FreeSansBold",
" GPL with the font exception",
"",
"Engine Singe " .. SINGE_VERSION_STRING,
" GPL version 3",
" See Singe/LICENSES for the",
" libraries Singe is built on.",
"",
"Thank you for playing.",
}
local FIRST_ROW = 4
local ROWS = 10
local scroll = 0
local function scrollBy(lines)
local most = #CREDITS - ROWS
if most < 0 then
most = 0
end
scroll = scroll + lines
if scroll < 0 then
scroll = 0
end
if scroll > most then
scroll = most
end
end
function onInputPressed(what)
if what == SWITCH_UP then
scrollBy(-1)
elseif what == SWITCH_DOWN then
scrollBy(1)
elseif what == SWITCH_BUTTON1 then
scroll = 0
end
end
function onOverlayUpdate()
local line = nil
overlayClear()
overlayPrint(2, 0, GAME_NAME .. " version " .. GAME_VERSION)
overlayPrint(2, 1, "Singe " .. SINGE_VERSION_STRING .. " id " .. (singeGetGameId() or "none"))
for row = 0, ROWS - 1 do
line = CREDITS[scroll + row + 1]
if line then
overlayPrint(2, FIRST_ROW + row, line)
end
end
overlayPrint(2, FIRST_ROW + ROWS + 1, "Saves go to:")
overlayPrint(2, FIRST_ROW + ROWS + 2, singeGetDataPath())
overlayPrint(2, 17, "Up and down scroll. Button 1 goes back to the top.")
return OVERLAY_UPDATED
end
----
Replace every line of `CREDITS` with the truth about your own game. The
placeholders are there to show the shape: what it is, who made it, what licence,
and where it came from.
=== What Just Happened
[source,lua]
----
local GAME_VERSION = "1.0.2"
----
One place, at the top, in capitals because it never changes while the game
runs. Everything that shows the version reads this, so raising it is one edit
rather than a search.
[source,lua]
----
"Music \"Night Drive\" by A. Composer",
----
Those backslashes are the escape you met in lesson one: a double quote that
belongs to the text rather than ending the string. Titles have quotes in them,
which is why a credits list runs into this on its first line.
[source,lua]
----
for row = 0, ROWS - 1 do
line = CREDITS[scroll + row + 1]
if line then
----
Ten rows of screen showing ten lines out of a list that is longer than ten.
`scroll` is how far down the list the window has moved. The `+ 1` is because
Lua counts a list from one and the screen rows count from zero, and the `if
line then` is because the end of the list is `nil` -- reading past the end of
a table is not an error in Lua, it just gives you nothing, and printing
nothing would be.
[source,lua]
----
overlayPrint(2, 1, "Singe " .. SINGE_VERSION_STRING .. " id " .. (singeGetGameId() or "none"))
----
Three facts a bug report needs and a player cannot otherwise find: your version
on the line above, the engine's version, and the game's id. `SINGE_VERSION_STRING`
is a global the engine sets before your script runs. `or "none"` covers the
case where the game has no `games.dat` entry, so the line prints instead of
crashing on a `nil`.
[source,lua]
----
overlayPrint(2, FIRST_ROW + ROWS + 2, singeGetDataPath())
----
Where this game writes. On a packed game that is the only place anything is
written, and it is the folder a player deletes to start clean. Showing it turns
"where are my saves" into a sentence you never have to answer.
If that path runs off the right of the screen, it is not broken: `overlayPrint`
drops the characters that would go past the edge. The default overlay is sixty
character cells wide.
=== Try It
. *Pack it.* Put this script in your game folder, run `--pack`, and run the
`.game` file. Then unpack it into an empty folder and read the list of what
came out.
. *Override one file.* Beside the packed file, make a folder named like it
without the extension, and put one changed file inside. Run the packed game
and watch your loose file win.
. *Make a patch.* Change the version to `1.0.3`, put only the script into a
`fixes` folder, pack that as a `.patch`, and apply it to the game you
already made. Check the version on screen.
. *Break the packer.* Rename your `games.dat` for a moment and try to pack.
Then put it back, point one of its `SCRIPT` names at a file that is not
there, and try again. Two different refusals, both before anything is
written.
. *Write the real credits.* Go through your game's folder file by file and
write a line for each one you did not make. If you cannot remember where
something came from, that is the answer to whether you should be shipping
it.
=== Break It on Purpose
Run Singe once from *inside* your game folder, so the engine unpacks its
support files there, and then pack it:
----
Singe --pack MyGame MyGame.game
----
----
!!! MyGame has Framework.singe: Singe/Framework.singe
!!! MyGame has sh file: Menu.sh
----
Nothing is written. Each line is the file it will not take and why, and you
may get them in either order or get others besides. There is a copy of the
engine's own support folder inside your game, and the launcher script the
engine wrote beside it, because you ran Singe from in there at some point. A
game must never carry either one: the support files belong to whatever Singe
the player is running, not to your game, and a packed `Framework.singe` would
be loaded in place of theirs. On Windows the second line names `Menu.bat`
instead.
Delete the `Singe` folder from inside your game, keep your game folder as one
folder beside the Singe program rather than around it, and pack again. This is
the same layout lesson thirteen described, and this error is what happens when
it slips.
=== What You Learned
* `Singe --pack folder name.game` turns a game folder into one file, and
copying that file beside the Singe program installs it.
* Every file name inside a packed game is looked up in a loose folder first,
then the data directory, then the database.
* A loose folder named like the packed file overrides it file by file, which is
how you keep working -- and how you ship a broken release if you forget it is
there.
* Load with `DIR` and write only under `singeGetDataPath()`, or your game will
work loose and fail packed.
* `--patch` replaces files in a released game in one transaction; `--unpack`
writes everything back out.
* The packer refuses engine files, name clashes, and a `games.dat` that points
at files it does not have.
* Unpack your own release and read what is in it before you send it anywhere.
* Somebody else's art and sound come with terms, and meeting them means a
credits screen inside the game.
* A version number only ever goes up, and it belongs on the screen.
* Test on a machine that is not yours, starting with no `data` folder at all.
=== Next Time
One file, installed, credited, and numbered. The last lesson puts it in a
wooden box with a coin slot and no keyboard, which changes more about your
game than you would think.