Hoy os presento un nuevo capítulo sobre DIV2 Games Studio, un entorno de programación de videojuegos para MS-DOS lanzado en 1999 (y su precuela un año antes) y que fue bastante popular en España, debido, entre otras cosas, a que se comercializó en quioscos a un precio bastante asequible (2.995 pesetas, unos 18€ de la época). Venía con un CD de instalación, un manual de usuario del tamaño de un diccionario y muchos ejemplos de código fuente. Su comunidad llegó a ser tan grande que incluso llegó a comercializarse una revista bimestral especializada en este framework: DIVmanía.
El capítulo de hoy quería dedicarlo a una funcionalidad que pasa bastante desapercibida tanto en DIV como en DIV2 y es el uso de ficheros DLL: Unas librerías externas, generalmente programadas en lenguaje C/C++, que en el caso de DIV Games Studio permiten añadir toda clase de efectos y funcionalidades extra e, incluso, expandir el propio lenguaje.
El objetivo de esta entrada no es detallar al milímetro cómo funcionan las DLL en DIV y DIV2 o pasear por el código fuente de los ejemplos, si no más bien recordaros que existen, mostraros qué permiten hacer cuando tienes mañana con el lenguaje C y dar varias píldoras acerca de su funcionamiento.
Para empezar, varios matices:
- En mi GitHub encontraréis las librerías de ejemplo de esta entrada, así como su código fuente (https://github.com/LeHamsterRuso/DIV2Examples/tree/main/DLL, carpetas bin y src).
- En la mayoría de DLL que veremos a continuación, comparto también un fichero PRG de ejemplo para que pruebes cómo funcionan (https://github.com/LeHamsterRuso/DIV2Examples/tree/main/PRG/DLL).
- Las librerías de DIV1 no funcionan en DIV2 y viceversa. Estas librerías sólo funcionan en DIV2.
- Es bastante complicado compilar librerías para que funcionen en DIV o en DIV2.
- DIV y DIV2 no permiten hacer un import de varias DLL, pero como paliativo sí que podemos hacer un CPP que haga uso de varios CPP, por lo que puedes bypasear este punto si dispones del código fuente de las librerías que quieres usar.
- Sólo he conseguido que mis librerías funcionen complicándolas con Watcom 10.6, un compilador de C/C++ de 1996. Lo he intentado con Open Watcom (alternativa libre y multiplataforma), pero las DLL que me generaba no eran idénticas y crasheaban al arrancar los juegos. Esto se debe a que el arranque de la DLL depende de símbolos que el runtime de Watcom 10.6 inyecta (main_entry_, la raíz de constructores C++); con OpenWatcom esos símbolos no salen idénticos y el loader de DIV32RUN no los reconoce.
¿Cómo hacemos que un juego utilice una DLL?
No es complicado, pero tampoco es tan sencillo como copiar una DLL en la carpeta DLL y lanzar el juego. Además hace falta disponer del PRG de éste (fichero del código fuente) e indicarle que tiene que importarla:
Nota: Que sepas que da igual si especificas o no la carpeta "DLL" en el import, DIV buscará las librerías directamente ahí, pero no dará error si lo especificas.
Con todo ello, pasemos a los ejemplos y luego veamos más en detalle a la parte técnica.
Los ejemplos de mi GitHub:
HUD.DLL (+ HUDTEST.PRG)
CRT.DLL (+ CRTDEMO.PRG)
Como su nombre da a entender, añade un filtro de scanlines para simular un monitor CRT. En este tuit puedes verlo en movimiento: https://x.com/Hamster_ruso/status/2062268417197933052/video/1
WIPE.DLL (+ WIPEDEMO.PRG)
Crea efectos de transición, útil para el tránsito entre pantallas. En el PRG de ejemplo se activan pulsando "Espacio".
KEYDISP.DLL
Muestra un teclado en la esquina inferior izquierda y dibuja las teclas que estamos pulsando mientras jugamos. Para este ejemplo no he escrito un PRG, pero puedes probarlo con "MALVADO.PRG" (uno de los ejemplos míticos de DIV/DIV2).
PLASMA.DLL (+PLASMADE.PRG)
Una animación que simula el movimiento de una lámpara de lava.
SAVESTAT.DLL
Guarda los estados de un juego para después volver a cargarlos. Es similar a las funciones de save/load states de los emuladores: Vuelca el contenido de la memoria de los procesos a un fichero "SSX.SAV" y después los puedes cargar. Los botones F1 a F5 guardan, mientras que los botones F6 a F10 cargan. Para este ejemplo no he escrito un PRG, pero puedes probarlo con "MALVADO.PRG" (uno de los ejemplos míticos de DIV/DIV2). Nota: Es bastante inestable, lo comparto como prueba de concepto.
En este tuit puedes verlo en movimiento: https://x.com/Hamster_ruso/status/2062264865318719979/video/1
KD3D.DLL (+ 3D_01.PRG y 3D_02.PRG)
KD3DT.DLL (+ 3D_03.PRG)
Carga un objeto 3D en formato OBJ y le aplica una textura. En este tuit puedes verlo en movimiento: https://x.com/Hamster_ruso/status/2063003682799603803/video/1
¿Cómo funcionan las DLL en DIV Games Studio 2?
El concepto
DIV Games Studio se compone de dos partes: el lenguaje DIV (con el que escribes tus juegos) y DIV32RUN, el módulo interno que contiene todas las rutinas reales del motor (vídeo, procesos, paleta, sonido, etc.). Una DLL es un trozo de código en C/C++ que se "fusiona" con el juego en tiempo de ejecución y que puede llamar a las funciones internas de DIV32RUN y manipular sus variables directamente en memoria.
En la práctica, una DLL sirve para dos cosas: extender el lenguaje con funciones nuevas que no existen en DIV, o modificar/sustituir el funcionamiento interno del motor (rutinas de vídeo, efectos, comportamiento de los procesos…).
El puente: DIV.H, importar y exportar
Toda DLL de DIV incluye la cabecera DIV.H, que es el contrato entre tu código y el motor. El truco central es que DIV no expone una API por copia de datos, sino punteros a sus propias variables internas. La DLL los "pide" con DIV_import("nombre") y el macro GLOBAL_IMPORT() los resuelve todos de golpe en la inicialización:
- Memoria del intérprete: mem[] (toda la memoria de los programas DIV), stack[] y sp (la pila por la que viajan los parámetros).
- Vídeo: punteros directos a buffer, background y ghost, más anchura/altura y la paleta de colores.
- Estructuras del lenguaje mapeadas sobre mem[] mediante offsets calculados: MOUSE, SCROLL, M7, M8, JOY, SETUP, NET, FILEINFO… y variables globales como fps, timer(), scan_code, ascii, etc.
- La lista de procesos: id_offset, id_init_offset, process_size… que permiten recorrer y modificar cualquier proceso vivo (su x, y, graph, angle, flags…).
- La comunicación es bidireccional: tú importas cosas de DIV con DIV_import, y le entregas tus funciones con DIV_export (o COM_export para funciones del lenguaje). Además de importar variables, DIV exporta un par de funciones de ayuda: div_rand(min,max) (números aleatorios) y div_text_out(texto,x,y) (escribir en pantalla). Es un detalle simpático que muestra que la relación es bidireccional.
Ejemplo de DLL mínima funcional, lo único que hace es dibujar una línea en pantalla:
Los tres tipos de DLL
1. Salvapantallas (ej. DEMO0.CPP, SS1.CPP, ejemplos presentes en el disco de DIV2)
Defines ss_init(), ss_frame() y ss_end(). DIV las llama tras un tiempo de inactividad (ss_time). En ss_frame() operas sobre el buffer de vídeo antes de cada volcado. Se controla con ss_status y se sale poniendo ss_exit=1.
2. DLL de autocarga (ej. DEMO1.CPP, AGUA.CPP, HBOY.CPP, ejemplos presentes en el disco de DIV2)
Llaman a AutoLoad() dentro de divmain() y se activan solas con solo copiarlas al directorio del juego (o junto a D.EXE). Su poder está en los "Entry-Points" estándar: funciones con nombre fijo que DIV invoca en momentos concretos de su ciclo si las defines. Aquí es donde una DLL puede reemplazar partes del motor sin que el juego DIV se entere.
Las más jugosas: set_video_mode, process_palette, process_active_palette, process_fpg / process_map / process_fnt / process_sound (modificar recursos al cargarlos), background_to_buffer, buffer_to_video (sustituir el volcado de pantalla), post_process_scroll, post_process_m7, post_process_buffer (aplicar efectos a cada ventana), post_process (tocar las variables de los procesos cada frame), put_sprite (interceptar el dibujado de sprites)...
3. DLL de funciones (ej. DEMO2.CPP, PARTS.CPP)
Añaden comandos nuevos al lenguaje. Defines divlibrary() y dentro haces un COM_export("NOMBRE", puntero, nº_parámetros) por cada función. Dentro de cada función lees los parámetros con getparm() y obligatoriamente devuelves con retval() (siempre, aunque no retornes nada útil). Desde el juego se usan tras un IMPORT "ruta\nombre.dll";.
Conviene decir que también necesitan divmain() con GLOBAL_IMPORT() además de divlibrary() - se ve en DEMO2.CPP.
Reglas que hay que respetar (lo que NO se puede/debe hacer)
- Está absolutamente prohibido usar malloc/free/fopen/fclose estándar. Hay que usar div_malloc, div_free, div_fopen, div_fclose (se comportan igual pero pasan por el gestor de memoria de DIV; usar las de C corrompe el estado).
- El retval() es obligatorio en toda función del lenguaje, y hay que hacer un getparm() por cada parámetro declarado, o se descuadra la pila.
- En el módulo principal hay que poner #define GLOBALS antes de #include "div.h" (define realmente las variables; el resto de fuentes las ven como extern). Esto permite repartir una DLL en varios .cpp.
- Las funciones de arranque/cierre (divmain/divend/divlibrary) van marcadas con __export.
- No se pueden importar DLL de otros lenguajes: el formato y el ABI son específicos de DIV.
- Sí puedes usar el resto de la librería estándar de C (math.h, etc.) con normalidad.
La parte técnica peculiar
DIV usa el formato de DLL de Windows NT aunque corre en DOS. Las DLL se construían con el Watcom C++ 10.6 original, añadiendo la directiva format windows nt dll al enlazador. ¿Implica esto que necesitas un Windows o un MS-DOS especial para compilar estas librerías? No necesariamente, en mi caso para compilarlas desde macOS ejecuto el toolchain de Watcom vía Wine en un contenedor Docker que contiene una imagen liviana de Linux.
Si esta entrada te ha interesado, te dejo el enlace a los capítulos anteriores:
- Ejemplos sencillos
- Programando un JRPG (dificultad avanzada)
- Programando un bullet-hell (dificultad avanzada)
- Programando un tamagochi













