Kabosu - Creando cosas

Logo de la página. Gato esférico con colores verdoso.

Noiz2sa: portando un juego a Linux

Publicado: 2024-07-31 (actualizado 2024-10-06)

Etiquetas: Juegos, Linux, Proyectos


Actualizo el 2024-10-06 para corregir algunos errores.

En este artículo hablo un pequeño reto que me propuse hace unas semanas: migrar un juego opensource para Windows a Linux.

Los juegos de cdlibre.org

Gracias a una publicación de Mastodon me reencontré con cdlibre.org. Administrada por Bartolomé Sintes Marco, esta web que ya tiene más de 20 años recopila y categoriza centenares de aplicaciones libres (y juegos). Principalmente para sistemas Windows pero también se indica si están disponibles para Linux.

Logo de cdlibre.org

Estuve un rato curioseando las diferentes categorías de juegos y me di cuenta de qué había algunos que según el listado solo tenían versión para Windows. Es raro que por ejemplo Tuxbomber, protagonizado por la mascota de Linux, solo esté disponible para Windows.

Captura de cdlibre.org mostrando la descripción de Tuxbomber

Se me ocurrió, a modo de reto y para practicar, portar alguno de estos juegos a Linux. Descarté el Tuxbomber porque estaba programado en Pascal y es un lenguaje que no he tocado en décadas.

Noiz2sa

En la sección de arcade todos los juegos menos uno tenían versión de Linux. El que faltaba era Noiz2sa, publicado en 2002 por Kenta Cho de ABA Games. Es un shmup bullet hell dónde todos los elementos son formas geométricas. Estuve jugándolo un poco con Wine y no estaba mal. Además según la documentación estaba programando con SDL así que debería ser relativamente sencillo de portar.

Estuve investigando un poco a ver si ya existía versión de Linux. ABA games tiene el código de muchos de sus juegos en Github pero el de Noiz2sa no estaba así que pensé que sería un buen candidato.

Captura del juego Noiz2sa

En ese momento descubrí una cosa: que cdlibre.org tenga solo el icono de Windows en la ficha de un juego no quiere necesariamente decir que no exista port a Linux. Simplemente indica que no hay port oficial. Buscando en Google encontré varios ports de otra gente. En concreto, creo que el port de Evil Mr Henry es el más antiguo de todos.

A pesar de que ya existían ports del juego, como mi objetivo era practicar un poco decidí seguir adelante con el reto asegurándome de no mirar el código de otras personas.

Portando Noiz2sa

Este tipo de pruebas siempre las hago en una máquina virtual para no ensuciar mi sistema. En este caso usé Ubuntu dentro de Virtualbox.

Me bajé el fichero noiz2sa0_52.zip de la web del juego. Incluye tanto el binario para Windows como el código fuente en C.

Lo primero que hice fue intentar compilarlo tal cual en Ubuntu.

$ make
gcc `sdl-config --cflags` -O3 `sdl-config --cflags` -O3 -I./bulletml/  -c -o 
noiz2sa.o noiz2sa.c
/bin/sh: 1: sdl-config: not found
/bin/sh: 1: sdl-config: not found
noiz2sa.c:12:10: fatal error: SDL.h: No such file or directory
   12 | #include "SDL.h"
      |          ^~~~~~~
compilation terminated.
make: *** [<builtin>: noiz2sa.o] Error 1

Como no podía ser de otra manera, faltaba la librería de SDL. Ni en la documentación ni la web indicaba qué versión exactamente. Yo he usado SDL2 en un par de proyectos personales pero al ser Noiz2sa de 2002 sospeché que necesitaría SDL 1. Por suerte Ubuntu tenía en los repositorios la versión 1.2.

$ sudo apt install libsdl1.2-dev

Al compilar ya no se quejaba de SDL pero buscaba una librería llamada libBulletML que yo no tenía.

$ make
gcc `sdl-config --cflags` -O3 `sdl-config --cflags` -O3 -I./bulletml/  -c -o 
noiz2sa.o noiz2sa.c
gcc `sdl-config --cflags` -O3 `sdl-config --cflags` -O3 -I./bulletml/  -c -o 
ship.o ship.c
gcc `sdl-config --cflags` -O3 `sdl-config --cflags` -O3 -I./bulletml/  -c -o 
shot.o shot.c
gcc `sdl-config --cflags` -O3 `sdl-config --cflags` -O3 -I./bulletml/  -c -o 
frag.o frag.c
gcc `sdl-config --cflags` -O3 `sdl-config --cflags` -O3 -I./bulletml/  -c -o 
bonus.o bonus.c
g++  `sdl-config --cflags` -O3 -I./bulletml/  -c -o foe.o foe.cc
In file included from foe.cc:12:
barragemanager.h:15:10: fatal error: bulletml/bulletmlparser.h: No such file 
or directory
   15 | #include "bulletml/bulletmlparser.h"
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [<builtin>: foe.o] Error 1

En la documentación había un enlace a la web de libBulletML pero la página ya no parece existir. Por suerte encontré una versión antigua en archive.org y pude descargar libBulletML 0.0.4 aunque no tenía ni idea de si era la versión que necesitaba para compilar Noiz2sa.

Compilando libBulletML

Un pequeño paréntesis para compilar la librería libBulletML. En este caso escrita en C++. Parece que se usa para cargar ficheros de definición de patrones de balas en formato XML para juegos shmup.

Para generar el parser necesita bison así que tuve que instalarlo. Creo que hace 20 años que no tocaba ese programa. Por suerte make se encargaba de todo y solo tuve que preocuparme de que estuviera instalado.

Arreglé algunos fallos de compilación seguramente debidos a que los compiladores hoy en día son menos permisivos:

Por suerte con esos pequeños cambios ya compilaba aunque soltando montones de warnings. No me preocupé demasiado por ellos.

Compilando Noiz2sa

Ahora que ya tenía la librería libbulletml.a y las cabeceras ya podía seguir con la compilación del juego en sí.

El siguiente intento de compilar con make me di cuenta de la ausencia de SDL Mixer.

soundmanager.c:18:10: fatal error: SDL_mixer.h: No such file or directory
   18 | #include "SDL_mixer.h"
      |          ^~~~~~~~~~~~~
compilation terminated.
make: *** [<builtin>: soundmanager.o] Error 1

Procedí a instalarlo:

$ sudo apt install libsdl-mixer1.2-dev 

Con esto el código compilaba correctamente pero en la fase de enlazado no encontraba la función sqrt.

/usr/bin/ld: vector.o: undefined reference to symbol 'sqrt@@GLIBC_2.2.5'

En el Makefile había alguna referencias a MinGW que es una colección de utilidades para compilar programas en Windows.

LDFLAGS        = `sdl-config --libs` -L. -lmingw32 -lmingwex -lSDL_mixer -lbulletml -mwindows -lstdc++

También se usaba un programa llamado windres para generar uno de los ficheros del juego. No conocía ese programa pero con una búsqueda rápida aprendí que es parte de MinGW.

Ubuntu también tiene los paquetes de MinGW y, como pensaba que los necesitaba para compilar, instalé unos cuantos. El paso de windres funcionaba pero luego daba errores de falta de ficheros de cabecera. Modifiqué bastante el Makefile porque pensaba que el problema era por mezclar librerías de MinGW con las de típicas de Ubuntu.

Finalmente conseguí compilar el programa en Ubuntu con las librerías MinGW y... no funcionaba. Entonces descubrí que había estado haciendo el tonto. MinGW se usa para crear programas para Windows. Había pasado la tarde compilando un binario para Windows de Noiz2sa.

Eliminé todas las referencias a MinGW del Makefile. Miré más detenidamente qué hacía windres y parece que su función era únicamente crear el icono de la aplicación así que directamente borré esas referencias.

Y funcionó. Había compilado Noiz2sa para Linux.

Arreglando los controles de Noiz2sa

Ejecuté el binario y el juego se inició. Podía ver el menú principal del juego y unas naves disparando de fondo pero había un problema: no funcionaban las teclas. No podía moverme por el menú ni iniciar la partida. Qué raro. Extrañamente la tecla escape sí que funcionaba para salir el juego.

Captura del juego Noiz2sa

Me puse a investigar qué ocurría. Lo primero fue localizar dónde se está leyendo el estado del teclado. El bucle principal del juego está en src/noiz2sa.c y tiene las siguientes líneas:

keys = SDL_GetKeyState(NULL);
if ( keys[SDLK_ESCAPE] == SDL_PRESSED || event.type == SDL_QUIT ) done = 1;

En el bucle principal también se llama a la función getPadState que está en scr/screen.c. En esta función es dónde se comprueban las teclas de dirección y de acción.

Claramente el código para comprobar la tecla escape en el bucle principal funcionaba pero las de getPadState no así que me puse a investigar más a fondo esa función. Este es el código resumido:

int getPadState() {
   int x = 0, y = 0;
   int pad = 0;
   if ( stick != NULL ) {
     x = SDL_JoystickGetAxis(stick, 0);
     y = SDL_JoystickGetAxis(stick, 1);
   }
   if ( keys[SDLK_RIGHT] == SDL_PRESSED || keys[SDLK_KP6] == SDL_PRESSED || x > JOYSTICK_AXIS ) {
     pad |= PAD_RIGHT;
   }
   ...
   return pad
}

Con "investigar más a fondo" me refiero a que puse un montón de printfs para ver qué valores tenía cada variable. La comprobación de la pulsación de teclas parecía funcionar pero por alguna razón el resultado final en la variable pad era inválido.

El problema parecía venir de la comprobación del joystick. Las variables x e y se inicializaban a 0 pero luego recibían el valor de la posición de los joysticks. Yo no tenía ningún mando conectado porque estaba ejecutando el juego en una máquina virtual pero por alguna razón las variables recibían valores extraños como -32767. Quizá había alguna variable sin inicializar o el juego asumía que siempre había un mando conectado.

Comenté las líneas de código referidas al joystick y al compilar el juego función. Estaba claro que el problema se encontraba ahí.

Añadí algunas líneas para obtener información sobre los mandos conectados. SDL_NumJoysticks que devuelve cuántos hay conectados y SDL_JoystickIndex que devuelve el índice del mando usado en estos momentos y finalmente SDL_JoystickName para saber su nombre.

printf("num joysticks: %d\n", SDL_NumJoysticks());
printf("joystick name: %s\n", SDL_JoystickName(SDL_JoystickIndex(stick)));

Al volver a ejecutar el juego vi que, efectivamente, había un mando conectado pero lo más extraño de todo es que su nombre era "VirtualBox USB Tablet". Ahí estaba el culpable. Era un dispositivo que añade automáticamente el software de virtualización que uso (VirtualBox). No encontré mucha información online sobre ese dispositivo. En el menú de configuración de la máquina virtual tampoco aparecía opción de activar o desactivarlo. Como finalmente el problema no estaba en el juego si no en la integración de Virtualbox no le di mucha importancia.

Podría haber dejado el código del joystick comentado pero como quería hacer el port bien añadí un argumento -stick al juego para activar el soporte para mando. Si no se pasa ese argumento el juego no intentará leer el estado de los joysticks.

Lo único tuve que hacer fue:

Y ya estaba todo. Compilé el juego, lo inicié sin soporte para mandos y estuve un rato jugando. Objetivo cumplido.

Captura del juego Noiz2sa

Comentarios finales

Me ha gustado el reto de intentar hacer funcionar el software "antiguo". Quizá en un futuro intente algo parecido con algún juego de MS-DOS para añadir algo más de dificultad. Pensaba que me iba a costar varias horas de trabajo y que iba a tener que modificar mucho el código pero todo fue relativamente simple. Lo único que lamento es que, por desconocimiento, me puse a instalar MinGW y perdí bastante tiempo con eso.

No voy a publicar el código de mi port porque ya hay otros anteriores y seguramente son mucho mejores. Incluso hay un binario disponible en Flathub.

Durante esta aventura estuve buscando información sobre la persona que desarrolló el juego. Se trata de Kenta Choo de ABA Games (web, Wikipedia). El autor creó varios shmups y, aunque yo no lo conocía, parece que es mediamente conocido dentro del género. Desde hace años parece que se dedica a crear microjuegos. Cientos de ellos por lo que se puede ver en su web y la mayoría parece que se controlan con un solo botón. También ha escrito un texto llamado Joys of Small Game Development que parece interesante. Tengo pendiente su lectura.


Artículo siguiente: He vuelto a Godot: Numbers Vision
Artículo anterior: Cuando el wifi me iba lento