[#_lesson_30_shipping_it] == Leçon 30 : Le publier image::learn/30-shipping.png[La leçon terminée, 480] Vous avez un dossier. Il contient un script, quelques images, quelques sons, et le `games.dat` que vous avez écrit à la leçon treize. Il tourne, parce que tout ce dont il a besoin est là où vous l'avez laissé et que vous savez où c'est. Cette leçon transforme ce dossier en un seul fichier que quelqu'un qui ne vous a jamais rencontré peut copier sur sa machine et jouer. L'essentiel tient en deux lignes de commande. Le reste, c'est la partie que personne n'enseigne : ce qui a sa place dans le fichier, le travail de qui s'y trouve à part le vôtre, et comment savoir s'il marche ailleurs qu'ici. [#_one_file] === Un seul fichier Un jeu Singe fini se publie sous la forme d'un seul fichier `.game`. En empaqueter un, c'est : ---- Singe --pack MyGame MyGame.game ---- Le premier nom est le dossier de votre jeu, le second est le fichier à écrire. Singe lit chaque fichier du dossier dans une base de données et dit ce qu'il a fait : ---- >>> Packed 42 files (3819204 bytes) from MyGame into MyGame.game ---- Lancez-le de la même façon que vous lancez n'importe quoi : ---- Singe MyGame.game ---- Ça lance la première entrée du `games.dat` qu'il contient ; `--entry=2` lance la deuxième, pour un fichier qui contient plus d'un jeu. Pour l'installer, le joueur copie `MyGame.game` à côté du programme Singe. Le menu fourni le trouve et l'affiche dans sa liste. Il n'y a rien d'autre : pas d'installateur, pas d'archive à décompresser, pas de dossier à mettre au bon endroit. [#_what_packing_does_to_every_file_name] === Ce que l'empaquetage fait à chaque nom de fichier Rien dans votre jeu n'a besoin de changer pour être empaqueté, et ça vaut la peine de savoir pourquoi, parce que c'est ce qui risque le plus de vous surprendre. Chaque nom qu'utilise votre jeu -- `dofile`, `require`, `io.open`, `spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, un framefile, un `controls.cfg` -- passe par une seule et même recherche. Pour un dossier non empaqueté, cette recherche est le simple système de fichiers. Pour un jeu empaqueté, le nom est essayé à trois endroits et le premier trouvé l'emporte : . un dossier non empaqueté qui porte le nom du fichier sans son extension -- `MyGame/` à côté de `MyGame.game` ; . le dossier de données du jeu, sous `files/`, qui est l'endroit où atterrit tout ce que le jeu écrit ; . la base de données elle-même. Dans la base de données, les noms sont comparés sans tenir compte des majuscules, avec l'une ou l'autre sorte de barre oblique, et relativement à la racine du jeu. Un nom de dossier en tête égal à celui du jeu lui-même est ignoré, si bien que `MyGame/art/spaceship.png`, `art/spaceship.png` et `DIR .. "art/spaceship.png"` trouvent tous le même fichier. Deux règles en découlent, et si vous avez suivi le livre, vous obéissez déjà aux deux. Utilisez `DIR` pour tout ce que vous chargez, pour qu'un nom ne dépende jamais de l'endroit d'où Singe a été lancé. Et n'écrivez que sous `singeGetDataPath()`, là où `saveSet` range déjà les choses -- un jeu empaqueté est ouvert en lecture seule et n'est jamais écrit, si bien qu'un jeu qui sauvegarde à côté de lui-même marche non empaqueté et échoue empaqueté. Deux sortes de noms vont ailleurs exprès. Tout ce qui commence par `Singe/` reste sur le vrai système de fichiers, si bien que `dofile("Singe/Framework.singe")` continue de marcher, et le dossier de données aussi. Et un nom contenant `..` qui essaie de sortir du jeu ne mène nulle part : `io.open` renvoie `nil` et un message disant que le nom sort du jeu, et `spriteLoad` et ses semblables signalent le fichier comme manquant. [#_the_loose_folder_still_wins] === Le dossier non empaqueté l'emporte toujours Cette première place dans la liste est celle dont vous vous servirez tous les jours. Un dossier qui porte le nom du fichier empaqueté, posé à côté de lui, l'emporte sur les copies empaquetées, fichier par fichier. ---- MyGame.game le jeu empaqueté MyGame/ un dossier avec un seul fichier modifié dedans ---- Déposez-y un seul `art/spaceship.png` corrigé et le jeu empaqueté tourne avec votre nouvelle image et tout le reste tiré de la base de données. Rien n'a besoin d'être réempaqueté pour essayer une modification, et vous pouvez garder là tout votre dossier de travail pendant que vous finissez le jeu. C'est aussi comme ça qu'un joueur en fait un mod, ce que vous considérerez ou non comme une fonctionnalité. Deux choses en découlent. Vérifiez votre jeu une fois avec ce dossier renommé, sinon vous publierez un fichier qui ne marche que sur la machine qui a le dossier. Et sachez que l'empaquetage ne cache rien : `--unpack` est une option documentée et n'importe qui peut s'en servir. Empaquetez votre jeu parce qu'il tient en un seul fichier, pas parce qu'il serait verrouillé. [#_patching_a_released_game] === Corriger un jeu publié Quelqu'un a votre jeu, il y a un bogue dans un script, et vous n'avez pas envie de lui renvoyer quarante mégaoctets. ---- Singe --patch MyGame.game fixes ---- `fixes` est un dossier organisé comme le jeu -- les mêmes noms aux mêmes endroits -- qui ne contient que les fichiers qui ont changé. Les fichiers que le correctif ne nomme pas sont laissés tels quels. Tout le changement est une seule transaction, si bien qu'un correctif interrompu laisse le jeu exactement comme il était, plutôt qu'à moitié remplacé. Un correctif peut voyager dans un fichier à lui, créé avec l'empaqueteur : ---- Singe --pack fixes MyGame-1.0.2.patch Singe --patch MyGame.game MyGame-1.0.2.patch ---- Un `.patch` n'a pas besoin de `games.dat`, ne peut pas être lancé, et le menu l'ignore. Il peut aussi porter une table `removed(path)` qui nomme des fichiers à supprimer, pour les images que vous avez remplacées par quelque chose qui porte un autre nom. Dans tous les cas, Singe vérifie ensuite le jeu corrigé de la même manière qu'il vérifie un nouveau jeu, si bien qu'un correctif ne peut pas laisser votre `games.dat` pointer vers un fichier qui n'est plus là. Et pour tout ressortir : ---- Singe --unpack MyGame.game MyGameUnpacked ---- [#_what_the_packer_refuses] === Ce que l'empaqueteur refuse L'empaqueteur ne construit pas un jeu à partir de n'importe quoi, et chaque refus concerne quelque chose qui casserait plus tard. Il refuse un dossier sans `games.dat` à sa racine. Il refuse un `Framework.singe`, un `controls.dat`, tout ce qui se termine par `exe`, `sh`, `bat` ou `cmd`, et un fichier sans extension dont le nom commence par `singe` -- autrement dit, une copie du moteur ou de ses fichiers de support qui s'est égarée dans le dossier de votre jeu. Il refuse deux fichiers dont les noms ne diffèrent que par les majuscules, puisque la recherche ne peut pas les distinguer. Il refuse une entrée de premier niveau qui porte le nom du dossier lui-même. Et il vérifie votre `games.dat` avant de valider : chaque entrée a besoin d'un `SCRIPT`, et chaque `SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE` et `ATTRACT` qu'elle nomme doit être un fichier qui s'y trouve vraiment. Un mauvais chemin est signalé avec le titre de l'entrée et rien n'est écrit. Chaque plainte nomme le fichier, et vous en avez la liste complète plutôt que la première seulement, si bien qu'un seul passage vous dit tout ce qu'il faut corriger. [#_what_to_include_and_what_to_leave_out] === Ce qu'il faut inclure et ce qu'il faut laisser de côté Regardez dans le dossier de votre jeu. Une partie de ce qu'il contient est le jeu, et une autre partie, c'est la façon dont le jeu a été fait. À publier : le script et tout ce qu'il charge, le `games.dat`, les images que montre le menu (`CABINET`, `MARQUEE` et le clip `ATTRACT`), vos fichiers de crédits et de licence, et un lisez-moi si vous en avez un. À laisser de côté : les dessins dont vous avez exporté les images, les prises de son que vous n'avez pas utilisées, vos notes, les scripts de test, les captures d'écran, tout ce qui s'appelle `old` ou `backup`, le dossier `data` avec vos propres sauvegardes et vos propres meilleurs scores dedans, et tout ce que vous ne voudriez pas qu'un inconnu lise. Tout cela s'empaquette parfaitement bien, et c'est justement le problème : ça part avec le jeu, et n'importe qui y a accès en un `--unpack`. La façon fiable de savoir ce que vous avez vraiment publié, c'est de le désempaqueter dans un dossier vide et de regarder. Faites-le une fois pour chaque version publiée. Ça prend une minute, et c'est la seule vérification qui voit ce que vous avez oublié plutôt que ce dont vous vous souvenez. [#_credits_and_licenses] === Crédits et licences Si des images, des sons, de la musique ou une police de votre jeu ont été faits par quelqu'un d'autre, vous publiez son travail, et les conditions auxquelles il l'a donné sont une obligation plutôt qu'une politesse. Ce n'est pas une formalité et ce n'est pas une affaire d'avocats : une personne a fait cette chose et a demandé une seule chose précise en retour, en général son nom. Tenez un registre pendant que vous travaillez, pas à la fin. Un simple fichier texte dans le dossier du jeu, avec une ligne par élément : ce qu'est le fichier, d'où il vient, qui l'a fait, quelle licence, et ce que cette licence exige. L'écrire au fur et à mesure prend quelques secondes. Le reconstituer la veille d'une sortie, à partir d'un historique de navigateur, c'est comme ça que des crédits se perdent. Quelques choses à savoir avant d'en avoir besoin. « Téléchargement gratuit » n'est pas une licence ; cherchez les vraies conditions. Creative Commons `BY` veut dire que vous devez donner le crédit indiqué, et il n'y a pas d'exception pour les jeux. `CC0` et le domaine public ne demandent rien, et créditer quand même vous coûte une ligne. Les polices ont leurs propres licences, et certaines permettent de les embarquer alors que d'autres non. Vous n'avez presque jamais le droit d'utiliser la musique d'une vidéo. Tout ce qu'un outil a généré n'est pas plus propre que les conditions de cet outil. Ensuite, mettez les crédits quelque part où un joueur peut les atteindre, dans le jeu. Pas seulement dans un fichier texte à côté -- un jeu empaqueté est un seul fichier, et le fichier texte est dedans, là où personne ne regardera. Le script à la fin de cette leçon est cet écran-là. Le `Singe/LICENSES` de Singe lui-même est la forme à copier : une ligne par bibliothèque, avec sa version, sa licence et sa provenance. La liste de votre jeu sera plus courte, et le travail est le même. [#_a_version_number_that_means_something] === Un numéro de version qui veut dire quelque chose Votre jeu aura une deuxième version. La première circulera aussi longtemps qu'internet existera, et quelqu'un vous enverra un rapport de bogue à son sujet sans avoir la moindre idée de la version qu'il a. Alors choisissez un numéro, mettez-le là où un joueur peut le lire, et n'en réutilisez jamais un. Trois parties, c'est la forme courante -- `1.0.2` -- et ce que veulent dire les parties compte moins que le fait qu'elles ne font que monter. Augmentez la dernière pour une correction, celle du milieu pour quelque chose de nouveau, la première quand vous avez fait un autre jeu. Quoi que vous choisissiez, notez-le dans votre lisez-moi, pour que le vous du futur s'y tienne aussi. `games.dat` n'a pas de clé de version, alors c'est à vous de porter le numéro. Mettez-le dans le script sous forme de constante, affichez-le sur l'écran des crédits, et mettez-le dans le nom de ce que vous envoyez (`MyGame-1.0.2.game`, `MyGame-1.0.2.patch`). Le rapport de bogue donne alors la version affichée à l'écran, et vous savez de quels fichiers il parle. [#_testing_on_a_machine_that_is_not_yours] === Tester sur une machine qui n'est pas la vôtre Votre machine est le pire endroit possible pour tester une version. Elle a les polices que vous avez installées, le dossier que vous étiez en train de modifier, le fichier de sauvegarde avec vos réglages dedans, et le moteur dans l'état où un mois de travail l'a laissé. Le test minimum, dans l'ordre : . Désempaquetez la version dans un dossier vide et lisez la liste des fichiers. . Copiez *uniquement* le fichier `.game` dans un autre dossier, avec une copie neuve de Singe et rien d'autre, et lancez-le depuis le menu comme le fera un joueur. . Supprimez le dossier `data` de ce dossier et relancez. C'est un premier lancement sur une machine neuve : pas de réglages sauvegardés, pas de meilleurs scores, pas de compte connecté. Un jeu qui ne marche qu'à son deuxième lancement est un bogue courant, et c'est comme ça qu'on le trouve. . Donnez-le à une personne qui ne l'a jamais vu, et regardez sans l'aider. Puis testez la machine autant que le jeu. Elle aura un écran d'une autre forme, alors regardez votre jeu en fenêtre et en plein écran. Elle n'aura peut-être aucun périphérique 3D, et c'est pour ça que le menu propose un second moteur de rendu. La page *System Information* des outils de service affiche sur un seul écran la version du moteur, le système d'exploitation, le processeur, le moteur de rendu, les décodeurs et le dossier de données ; une photo de cette page répond à presque tout un rapport de bogue, et le dire à vos joueurs dans votre lisez-moi vous épargnera une semaine de devinettes. [#_the_script_2] === Le script Un écran de crédits et de version. C'est peu de chose à écrire, et c'est la partie de la publication que la plupart des jeux ratent. [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 ---- Remplacez chaque ligne de `CREDITS` par la vérité sur votre propre jeu. Les textes provisoires sont là pour montrer la forme : ce que c'est, qui l'a fait, quelle licence, et d'où ça vient. [#_what_just_happened_25] === Ce qui vient de se passer [source,lua] ---- local GAME_VERSION = "1.0.2" ---- Un seul endroit, en haut, en majuscules parce que la valeur ne change jamais pendant que le jeu tourne. Tout ce qui affiche la version lit ceci, si bien que l'augmenter demande une seule modification plutôt qu'une recherche. [source,lua] ---- "Music \"Night Drive\" by A. Composer", ---- Ces barres obliques inverses sont la séquence d'échappement que vous avez rencontrée à la leçon un : un guillemet double qui fait partie du texte au lieu de terminer la chaîne. Les titres contiennent des guillemets, et c'est pour ça qu'une liste de crédits tombe dessus dès sa première ligne. [source,lua] ---- for row = 0, ROWS - 1 do line = CREDITS[scroll + row + 1] if line then ---- Dix lignes d'écran qui montrent dix lignes d'une liste plus longue que dix. `scroll` indique jusqu'où la fenêtre est descendue dans la liste. Le `+ 1` est là parce que Lua compte une liste à partir de un et que les lignes de l'écran comptent à partir de zéro, et le `if line then` est là parce que la fin de la liste vaut `nil` -- lire au-delà de la fin d'une table n'est pas une erreur en Lua, ça ne vous donne simplement rien, mais afficher ce rien en serait une. [source,lua] ---- overlayPrint(2, 1, "Singe " .. SINGE_VERSION_STRING .. " id " .. (singeGetGameId() or "none")) ---- Trois faits dont un rapport de bogue a besoin et qu'un joueur ne peut pas trouver autrement : votre version sur la ligne du dessus, la version du moteur, et l'identifiant du jeu. `SINGE_VERSION_STRING` est une globale que le moteur définit avant que votre script tourne. `or "none"` couvre le cas où le jeu n'a pas d'entrée dans `games.dat`, pour que la ligne s'affiche au lieu de planter sur un `nil`. [source,lua] ---- overlayPrint(2, FIRST_ROW + ROWS + 2, singeGetDataPath()) ---- L'endroit où ce jeu écrit. Pour un jeu empaqueté, c'est le seul endroit où quoi que ce soit est écrit, et c'est le dossier qu'un joueur supprime pour repartir de zéro. L'afficher transforme « où sont mes sauvegardes ? » en une question à laquelle vous n'aurez jamais à répondre. Si ce chemin dépasse à droite de l'écran, ce n'est pas cassé : `overlayPrint` laisse tomber les caractères qui dépasseraient du bord. L'overlay par défaut fait soixante cellules de caractère de large. [#_try_it_30] === Essayez . *Empaquetez-le.* Mettez ce script dans le dossier de votre jeu, lancez `--pack`, et lancez le fichier `.game`. Puis désempaquetez-le dans un dossier vide et lisez la liste de ce qui en est sorti. . *Remplacez un fichier.* À côté du fichier empaqueté, créez un dossier qui porte son nom sans l'extension, et mettez-y un fichier modifié. Lancez le jeu empaqueté et regardez votre fichier non empaqueté l'emporter. . *Faites un correctif.* Passez la version à `1.0.3`, mettez seulement le script dans un dossier `fixes`, empaquetez-le en `.patch`, et appliquez-le au jeu que vous avez déjà fait. Vérifiez la version à l'écran. . *Cassez l'empaqueteur.* Renommez votre `games.dat` un instant et essayez d'empaqueter. Puis remettez-le, faites pointer l'un de ses noms `SCRIPT` vers un fichier qui n'existe pas, et réessayez. Deux refus différents, tous les deux avant que quoi que ce soit soit écrit. . *Écrivez les vrais crédits.* Parcourez le dossier de votre jeu fichier par fichier et écrivez une ligne pour chacun de ceux que vous n'avez pas faits. Si vous ne vous rappelez plus d'où vient quelque chose, vous savez maintenant si vous devriez le publier. [#_break_it_on_purpose_29] === Cassez-le exprès Lancez Singe une fois depuis l'*intérieur* du dossier de votre jeu, pour que le moteur y extraie ses fichiers de support, puis empaquetez-le : ---- Singe --pack MyGame MyGame.game ---- ---- !!! MyGame has Framework.singe: Singe/Framework.singe !!! MyGame has sh file: Menu.sh ---- Rien n'est écrit. Chaque ligne donne un fichier qu'il refuse et la raison, et vous pouvez les recevoir dans n'importe quel ordre, ou en avoir d'autres en plus. Il y a une copie du dossier de support du moteur lui-même dans votre jeu, et le script de lancement que le moteur a écrit à côté, parce que vous avez lancé Singe depuis là à un moment donné. Un jeu ne doit jamais emporter ni l'un ni l'autre : les fichiers de support appartiennent au Singe que lance le joueur, quel qu'il soit, pas à votre jeu, et un `Framework.singe` empaqueté serait chargé à la place du sien. Sous Windows, la seconde ligne nomme `Menu.bat` à la place. Supprimez le dossier `Singe` et `Menu.sh` (ou `Menu.bat`) de l'intérieur de votre jeu, gardez le dossier de votre jeu comme un dossier à côté du programme Singe plutôt qu'autour de lui, et empaquetez de nouveau. C'est la même organisation que celle que décrivait la leçon treize, et cette erreur est ce qui arrive quand elle se dérègle. [#_what_you_learned_30] === Ce que vous avez appris * `Singe --pack folder name.game` transforme un dossier de jeu en un seul fichier, et copier ce fichier à côté du programme Singe l'installe. * Chaque nom de fichier d'un jeu empaqueté est cherché d'abord dans un dossier non empaqueté, puis dans le dossier de données, puis dans la base de données. * Un dossier non empaqueté qui porte le nom du fichier empaqueté l'emporte sur lui fichier par fichier, et c'est comme ça que vous continuez à travailler -- et comme ça que vous publiez une version cassée si vous oubliez qu'il est là. * Chargez avec `DIR` et n'écrivez que sous `singeGetDataPath()`, sinon votre jeu marchera non empaqueté et échouera empaqueté. * `--patch` remplace des fichiers d'un jeu publié en une seule transaction ; `--unpack` ressort tout. * L'empaqueteur refuse les fichiers du moteur, les conflits de noms, et un `games.dat` qui pointe vers des fichiers qu'il n'a pas. * Désempaquetez votre propre version et lisez ce qu'elle contient avant de l'envoyer où que ce soit. * Les images et les sons de quelqu'un d'autre viennent avec des conditions, et les respecter veut dire un écran de crédits dans le jeu. * Un numéro de version ne fait que monter, et sa place est à l'écran. * Testez sur une machine qui n'est pas la vôtre, en commençant sans aucun dossier `data`. [#_next_time_30] === La prochaine fois Un seul fichier, installé, crédité et numéroté. La dernière leçon le met dans une caisse en bois avec une fente à pièces et sans clavier, ce qui change plus de choses dans votre jeu que vous ne le pensez.