[#_lesson_30_shipping_it] == Lección 30: Publicarlo image::learn/30-shipping.png[La lección terminada, 480] Tienes una carpeta. Dentro hay un script, unas cuantas imágenes, algunos sonidos y el `games.dat` que escribiste en la lección trece. Funciona, porque todo lo que necesita está donde lo dejaste y tú sabes dónde es. Esta lección convierte esa carpeta en un solo archivo que alguien que no te conoce de nada puede copiar en su ordenador y jugar. Casi todo son dos líneas de comandos. El resto es la parte que no enseña nadie: qué va en el archivo, qué trabajo de otros hay ahí además del tuyo y cómo averiguar si funciona en algún sitio aparte de aquí. [#_one_file] === Un solo archivo Un juego de Singe terminado se publica como un único archivo `.game`. Empaquetar uno es: ---- Singe --pack MyGame MyGame.game ---- El primer nombre es la carpeta de tu juego, el segundo es el archivo que hay que escribir. Singe mete todos los archivos de la carpeta en una base de datos y dice lo que ha hecho: ---- >>> Packed 42 files (3819204 bytes) from MyGame into MyGame.game ---- Se ejecuta igual que cualquier otra cosa: ---- Singe MyGame.game ---- Eso ejecuta la primera entrada del `games.dat` que lleva dentro; `--entry=2` ejecuta la segunda, para un archivo que contiene más de un juego. Para instalarlo, el jugador copia `MyGame.game` junto al programa Singe. El menú incluido lo encuentra y lo pone en la lista. No hay nada más: ni instalador, ni archivo comprimido que descomprimir, ni carpeta que poner en el sitio correcto. [#_what_packing_does_to_every_file_name] === Lo que el empaquetado le hace a cada nombre de archivo No hay que cambiar nada de tu juego para empaquetarlo, y vale la pena saber por qué, porque es lo que más probablemente te sorprenda. Todos los nombres que usa tu juego -- `dofile`, `require`, `io.open`, `spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, un framefile, un `controls.cfg` -- pasan por una única búsqueda. Para una carpeta suelta, esa búsqueda es el sistema de archivos normal. Para un juego empaquetado, el nombre se prueba en tres sitios y gana el primero que lo encuentra: . una carpeta suelta con el nombre del archivo sin su extensión -- `MyGame/` junto a `MyGame.game`; . la carpeta de datos del juego, dentro de `files/`, que es donde ha ido a parar todo lo que el juego escribe; . la propia base de datos. Dentro de la base de datos, los nombres se comparan sin tener en cuenta las mayúsculas, con cualquiera de los dos tipos de barra y a partir de la raíz del juego. Un nombre de carpeta inicial igual al del propio juego se ignora, así que `MyGame/art/spaceship.png`, `art/spaceship.png` y `DIR .. "art/spaceship.png"` encuentran todos el mismo archivo. De ahí salen dos reglas, y si has ido siguiendo el libro ya cumples las dos. Usa `DIR` para todo lo que cargues, para que un nombre nunca dependa de dónde se arrancó Singe. Y escribe solo dentro de `singeGetDataPath()`, que es donde `saveSet` ya pone las cosas -- un juego empaquetado se abre de solo lectura y nunca se escribe en él, así que un juego que guarda junto a sí mismo funciona suelto y falla empaquetado. Dos nombres van a otro sitio a propósito. Todo lo que empieza por `Singe/` se queda en el sistema de archivos de verdad, así que `dofile("Singe/Framework.singe")` sigue funcionando, y lo mismo pasa con la carpeta de datos. Y un nombre con `..` que intenta salirse del juego no lleva a ninguna parte: `io.open` devuelve `nil` y un mensaje que dice que el nombre apunta fuera del juego, y `spriteLoad` y sus parientes informan de que el archivo no existe. [#_the_loose_folder_still_wins] === La carpeta suelta sigue ganando El primer sitio de la lista es el que usarás todos los días. Una carpeta con el nombre del archivo empaquetado, puesta a su lado, gana a las copias empaquetadas archivo por archivo. ---- MyGame.game el juego empaquetado MyGame/ una carpeta con un archivo cambiado dentro ---- Mete ahí un solo `art/spaceship.png` corregido y el juego empaquetado se ejecuta con tu imagen nueva y todo lo demás sacado de la base de datos. No hay que volver a empaquetar nada para probar un cambio, y puedes tener ahí toda tu carpeta de trabajo mientras terminas el juego. También es como un jugador modifica uno, cosa que puede que te parezca una ventaja o puede que no. De ahí salen dos cosas. Prueba tu juego una vez con esa carpeta renombrada para que no estorbe, o publicarás un archivo que solo funciona en el ordenador que tiene la carpeta. Y que sepas que empaquetar no esconde nada: `--unpack` es una opción documentada y cualquiera puede usarla. Empaqueta tu juego porque es un solo archivo, no porque sea un archivo cerrado con llave. [#_patching_a_released_game] === Parchear un juego publicado Alguien tiene tu juego, hay un fallo en un script y no quieres volver a mandarle cuarenta megabytes. ---- Singe --patch MyGame.game fixes ---- `fixes` es una carpeta organizada como el juego -- los mismos nombres en los mismos sitios -- que contiene solo los archivos que han cambiado. Los archivos que el parche no nombra se dejan como están. Todo el cambio es una sola transacción, así que un parche interrumpido deja el juego exactamente como estaba en lugar de a medio sustituir. Un parche puede viajar como un archivo propio, hecho con el empaquetador: ---- Singe --pack fixes MyGame-1.0.2.patch Singe --patch MyGame.game MyGame-1.0.2.patch ---- Un `.patch` no necesita `games.dat`, no se puede ejecutar y el menú lo ignora. También puede llevar una tabla `removed(path)` que nombra archivos que borrar, para el arte que hayas sustituido por otro con un nombre distinto. En cualquier caso, Singe comprueba después el juego parcheado igual que comprueba uno nuevo, así que un parche no puede dejar tu `games.dat` apuntando a un archivo que ya no está. Y para volver a sacarlo todo: ---- Singe --unpack MyGame.game MyGameUnpacked ---- [#_what_the_packer_refuses] === Lo que el empaquetador rechaza El empaquetador no construye un juego con cualquier cosa, y cada rechazo tiene que ver con algo que se rompería más tarde. Rechaza una carpeta sin `games.dat` en su raíz. Rechaza un `Framework.singe`, un `controls.dat`, cualquier cosa que termine en `exe`, `sh`, `bat` o `cmd`, y un archivo sin extensión cuyo nombre empiece por `singe` -- es decir, una copia del motor o de sus archivos de soporte que se ha colado en la carpeta de tu juego. Rechaza dos archivos cuyos nombres solo se diferencian en las mayúsculas, porque la búsqueda no puede distinguirlos. Rechaza una entrada de primer nivel con el mismo nombre que la propia carpeta. Y comprueba tu `games.dat` antes de confirmar nada: cada entrada necesita un `SCRIPT`, y cada `SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE` y `ATTRACT` que nombre tiene que ser un archivo que esté de verdad ahí dentro. Una ruta mala se informa con el título de la entrada y no se escribe nada. Cada queja nombra el archivo, y sale la lista entera en lugar de solo la primera, así que una sola pasada te dice todo lo que hay que arreglar. [#_what_to_include_and_what_to_leave_out] === Qué incluir y qué dejar fuera Mira en la carpeta de tu juego. Parte de lo que hay es el juego, y parte es cómo se hizo el juego. Publica: el script y todo lo que carga, el `games.dat`, el arte que muestra el menú (`CABINET`, `MARQUEE` y el clip de `ATTRACT`), tus archivos de créditos y de licencias, y un léame si tienes uno. Deja fuera: los dibujos de los que exportaste el arte, las tomas de sonido que no usaste, tus notas, los scripts de prueba, las capturas de pantalla, cualquier cosa que se llame `old` o `backup`, la carpeta `data` con tus propias partidas guardadas y tus propios récords, y cualquier cosa que no quieras que lea un desconocido. Todo eso se empaqueta perfectamente, y ese es el problema: sale con el juego y está a un `--unpack` de cualquiera. La forma fiable de saber lo que has publicado de verdad es desempaquetarlo en una carpeta vacía y mirar. Hazlo una vez con cada versión. Lleva un minuto y es la única comprobación que ve lo que olvidaste en lugar de lo que recuerdas. [#_credits_and_licenses] === Créditos y licencias Si algo del arte, el sonido, la música o las fuentes de tu juego lo ha hecho otra persona, estás publicando su trabajo, y las condiciones con las que lo cedió son una obligación, no una cortesía. No es una formalidad ni es cosa de abogados: una persona hizo eso y pidió una cosa concreta a cambio, normalmente su nombre. Lleva un registro mientras trabajas, no al final. Un archivo de texto plano en la carpeta del juego con una línea por cosa: qué archivo es, de dónde salió, quién lo hizo, qué licencia tiene y qué exige esa licencia. Escribirlo sobre la marcha lleva segundos. Reconstruirlo la noche antes de publicar, a partir del historial del navegador, es como se quedan créditos fuera. Unas cuantas cosas que conviene saber antes de necesitarlas. «Descarga gratuita» no es una licencia; busca las condiciones de verdad. Creative Commons `BY` significa que tienes que dar el crédito que se indica, y no tiene ninguna excepción para juegos. `CC0` y el dominio público no piden nada, y dar el crédito de todos modos te cuesta una línea. Las fuentes llevan sus propias licencias, y unas permiten incrustarlas y otras no. La música de un vídeo casi nunca es tuya para usarla. Cualquier cosa generada por una herramienta solo está tan limpia como las condiciones de la herramienta. Luego pon los créditos en algún sitio al que pueda llegar un jugador, dentro del juego. No solo en un archivo de texto al lado -- un juego empaquetado es un solo archivo y el archivo de texto está dentro, donde nadie va a mirar. El script del final de esta lección es esa pantalla. El propio `Singe/LICENSES` de Singe es el modelo que copiar: una línea por biblioteca, con su versión, su licencia y de dónde salió. La lista de tu juego será más corta y el trabajo es el mismo. [#_a_version_number_that_means_something] === Un número de versión que signifique algo Tu juego tendrá una segunda versión. La primera seguirá por ahí mientras dure internet, y alguien te mandará un informe de fallo sobre ella sin tener ni idea de qué versión tiene. Así que elige un número, ponlo donde un jugador pueda leerlo y no repitas nunca uno. Tres partes es la forma habitual -- `1.0.2` -- y lo que signifique cada parte importa menos que el hecho de que solo suban. Sube la última por un arreglo, la del medio por algo nuevo y la primera cuando hayas hecho un juego distinto. Elijas lo que elijas, apúntalo en tu léame para que tu yo del futuro también lo cumpla. `games.dat` no tiene clave de versión, así que el número lo llevas tú. Ponlo en el script como constante, muéstralo en la pantalla de créditos y ponlo en el nombre de lo que mandes (`MyGame-1.0.2.game`, `MyGame-1.0.2.patch`). Así el informe de fallo dice la versión de la pantalla y sabes de qué archivos se trata. [#_testing_on_a_machine_that_is_not_yours] === Probar en un ordenador que no es el tuyo Tu ordenador es el peor sitio posible para probar una versión. Tiene las fuentes que instalaste, la carpeta que estabas editando, el archivo de partida guardada con tus ajustes dentro y el motor en el estado en que lo haya dejado un mes de trabajo. La prueba mínima, en orden: . Desempaqueta la versión en una carpeta vacía y lee la lista de archivos. . Copia *solo* el archivo `.game` en otra carpeta, con una copia recién sacada de Singe y nada más, y ejecútalo desde el menú como lo hará un jugador. . Borra la carpeta `data` de esa carpeta y vuelve a ejecutarlo. Eso es una primera ejecución en un ordenador nuevo: sin ajustes guardados, sin récords, sin una cuenta con la sesión iniciada. Un juego que solo funciona la segunda vez que se ejecuta es un fallo habitual, y así es como lo encuentras. . Dáselo a una persona que no lo haya visto nunca y mira sin ayudar. Luego prueba el ordenador además del juego. Tendrá una pantalla de otra forma, así que mira tu juego en ventana y a pantalla completa. Puede que no tenga ningún dispositivo 3D, que es por lo que el menú tiene un segundo renderizador. La página *System Information* de las herramientas de servicio muestra en una sola pantalla la versión del motor, el sistema operativo, el procesador, el renderizador, los decodificadores y la carpeta de datos; una foto de esa página responde a casi todo un informe de fallo, y decírselo a tus jugadores en tu léame te ahorrará una semana de adivinanzas. [#_the_script_2] === El script Una pantalla de créditos y versión. Es poca cosa de escribir y es la parte de publicar que la mayoría de los juegos hace mal. [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 ---- Sustituye cada línea de `CREDITS` por la verdad sobre tu propio juego. Los textos de relleno están ahí para mostrar la forma: qué es, quién lo hizo, qué licencia tiene y de dónde salió. [#_what_just_happened_25] === Qué acaba de pasar [source,lua] ---- local GAME_VERSION = "1.0.2" ---- Un solo sitio, arriba, en mayúsculas porque nunca cambia mientras el juego se ejecuta. Todo lo que muestra la versión lee esto, así que subirla es un cambio y no una búsqueda. [source,lua] ---- "Music \"Night Drive\" by A. Composer", ---- Esas barras invertidas son la secuencia de escape que conociste en la lección uno: unas comillas dobles que forman parte del texto en lugar de terminar la cadena. Los títulos llevan comillas, y por eso una lista de créditos se topa con esto en su primera línea. [source,lua] ---- for row = 0, ROWS - 1 do line = CREDITS[scroll + row + 1] if line then ---- Diez filas de pantalla que muestran diez líneas de una lista de más de diez. `scroll` es cuánto ha bajado la ventana por la lista. El `+ 1` está porque Lua cuenta una lista desde uno y las filas de la pantalla cuentan desde cero, y el `if line then` está porque el final de la lista es `nil` -- leer más allá del final de una tabla no es un error en Lua, simplemente no te da nada, e imprimir nada sí lo sería. [source,lua] ---- overlayPrint(2, 1, "Singe " .. SINGE_VERSION_STRING .. " id " .. (singeGetGameId() or "none")) ---- Tres datos que necesita un informe de fallo y que un jugador no puede encontrar de otra manera: tu versión, en la línea de arriba, la versión del motor y el id del juego. `SINGE_VERSION_STRING` es una variable global que el motor fija antes de que se ejecute tu script. `or "none"` cubre el caso en que el juego no tiene entrada en `games.dat`, para que la línea se imprima en lugar de fallar con un `nil`. [source,lua] ---- overlayPrint(2, FIRST_ROW + ROWS + 2, singeGetDataPath()) ---- Dónde escribe este juego. En un juego empaquetado es el único sitio donde se escribe algo, y es la carpeta que borra un jugador para empezar de cero. Mostrarla convierte «¿dónde están mis partidas guardadas?» en una pregunta que nunca tendrás que responder. Si esa ruta se sale por la derecha de la pantalla, no es que esté rota: `overlayPrint` descarta los caracteres que pasarían del borde. El overlay por defecto tiene sesenta celdas de carácter de ancho. [#_try_it_30] === Pruébalo . *Empaquétalo.* Pon este script en la carpeta de tu juego, ejecuta `--pack` y ejecuta el archivo `.game`. Luego desempaquétalo en una carpeta vacía y lee la lista de lo que ha salido. . *Sustituye un archivo.* Junto al archivo empaquetado, crea una carpeta con su nombre sin la extensión y pon dentro un archivo cambiado. Ejecuta el juego empaquetado y mira cómo gana tu archivo suelto. . *Haz un parche.* Cambia la versión a `1.0.3`, pon solo el script en una carpeta `fixes`, empaqueta eso como un `.patch` y aplícalo al juego que ya hiciste. Comprueba la versión en pantalla. . *Rompe el empaquetador.* Cambia el nombre de tu `games.dat` un momento e intenta empaquetar. Luego déjalo como estaba, haz que uno de sus `SCRIPT` apunte a un archivo que no existe y vuelve a intentarlo. Dos rechazos distintos, los dos antes de que se escriba nada. . *Escribe los créditos de verdad.* Repasa la carpeta de tu juego archivo por archivo y escribe una línea por cada uno que no hayas hecho tú. Si no recuerdas de dónde salió algo, esa es la respuesta a si deberías publicarlo. [#_break_it_on_purpose_29] === Rómpelo a propósito Ejecuta Singe una vez desde *dentro* de la carpeta de tu juego, para que el motor desempaquete ahí sus archivos de soporte, y luego empaquétala: ---- Singe --pack MyGame MyGame.game ---- ---- !!! MyGame has Framework.singe: Singe/Framework.singe !!! MyGame has sh file: Menu.sh ---- No se escribe nada. Cada línea es un archivo que no acepta y el porqué, y puede que te salgan en otro orden o que salgan otras además. Hay una copia de la propia carpeta de soporte del motor dentro de tu juego, y el script lanzador que el motor escribió a su lado, porque en algún momento ejecutaste Singe desde ahí dentro. Un juego no debe llevar nunca ninguno de los dos: los archivos de soporte pertenecen al Singe que esté usando el jugador, no a tu juego, y un `Framework.singe` empaquetado se cargaría en lugar del suyo. En Windows, la segunda línea nombra `Menu.bat` en su lugar. Borra la carpeta `Singe` y `Menu.sh` (o `Menu.bat`) de dentro de tu juego, deja la carpeta de tu juego como una carpeta junto al programa Singe y no alrededor de él, y vuelve a empaquetar. Es la misma organización que describía la lección trece, y este error es lo que pasa cuando se descuida. [#_what_you_learned_30] === Lo que has aprendido * `Singe --pack folder name.game` convierte la carpeta de un juego en un solo archivo, y copiar ese archivo junto al programa Singe lo instala. * Todo nombre de archivo dentro de un juego empaquetado se busca primero en una carpeta suelta, luego en la carpeta de datos y luego en la base de datos. * Una carpeta suelta con el nombre del archivo empaquetado lo sustituye archivo por archivo, que es como sigues trabajando -- y como publicas una versión rota si te olvidas de que está ahí. * Carga con `DIR` y escribe solo dentro de `singeGetDataPath()`, o tu juego funcionará suelto y fallará empaquetado. * `--patch` sustituye archivos de un juego publicado en una sola transacción; `--unpack` vuelve a sacarlo todo. * El empaquetador rechaza los archivos del motor, los nombres que chocan y un `games.dat` que apunta a archivos que no tiene. * Desempaqueta tu propia versión y lee lo que lleva antes de mandarla a ningún sitio. * El arte y el sonido de otros vienen con condiciones, y cumplirlas significa una pantalla de créditos dentro del juego. * Un número de versión solo sube, y su sitio es la pantalla. * Prueba en un ordenador que no sea el tuyo, empezando sin ninguna carpeta `data`. [#_next_time_30] === La próxima vez Un solo archivo, instalado, con sus créditos y su número. La última lección lo mete en una caja de madera con una ranura para monedas y sin teclado, lo que cambia más cosas de tu juego de lo que crees.