== 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 ", "", "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.