464 lines
18 KiB
Text
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.
|