Domingo, 13 de septiembre de 2026

El año pasado presenté el editor de niveles del Krakout en la RUN de otoño. Este editor permite parchear una imagen de cinta del juego (para MSX o Spectrum) con un nuevo set de 100 niveles.

Con respecto a este editor hay dos cuestiones que me seguían molestando. La primera es que la comprobación de que el fichero de entrada es realmente una cinta que contiene el Krakout se realiza buscando el bloque con los niveles originales dentro. Esto funciona y, además, tengo que hacerlo para localizar la posición donde se debe parchear, así que no es muy preocupante.

Sin embargo, el otro punto está relacionado con el cargador de MSX. Si habéis leído el post sobre el editor, al final comenté que el cargador de MSX realiza de forma activa un checksum sobre los datos cargados, lo que obliga a recalcularlo y modificar el último byte del bloque donde están los niveles. La forma en la que resolví esto fue, simplemente, calculando la posición de dicho byte a partir de la dirección donde están los niveles y arreando. ¿Funciona? Sí, pero no me gusta cómo está. Además, una posible extensión (que ya probé en su día) consiste en permitir modificar uno de los dos sets gráficos que son idénticos para introducir un set personalizado. Eso supone un parcheo adicional y, como no, un recálculo adicional de checksum.

Así que, en estas condiciones, hace unos meses me dije: ¿y si programo un lector de ficheros de cintas en JavaScript? Y me puse a ello con la intención de incorporarlo al editor.

Comencé por el formato TZX (que también se utiliza en los ficheros con extensión TSX), ya que es el más moderno y mejor documentado. Empecé reconociendo la cabecera, identificando el ID del bloque, leyendo los metadatos... Antes de darme cuenta tenía programada la lectura de unos cuantos bloques TZX, que devolvían un objeto con el contenido del bloque leído, pero todas las comprobaciones las hacía a mano y me parecía muy pesado. ¿Y si creo un visualizador en HTML específico para cada bloque? El código empezaba a crecer mucho y estaba demasiado enmarañado, por lo que opté por separar: lectores por un lado, visualizadores por otro y el código principal se encarga de decidir a quién llamar para leer. Con eso, pude hacer una pequeña web sencilla que me permitía visualizar el contenido de la cinta del Krakout, incluyendo un volcado hexadecimal de los datos contenidos en cada bloque.

¿Y si pruebo con otro fichero de cinta?

¡Y FUNCIONÓ!

Pero el siguiente (un TSX) no funcionó porque tenía bloques que no había implementado, por lo que los añadí tanto a los lectores como a los visualizadores... y pude ver los bloques contenidos en esa cinta. ¡Cómo mola! ¿Y si mejoro un poco la visualización? Y empecé a crear un fichero CSS para que los bloques se vieran mejor. Además era mucha información, por lo que hice que los bloques pudieran desplegarse y colapsarse visualmente con un evento de JavaScript. Ya de paso, si hay demasiados datos en el bloque, incluí un botón para alternar entre la visualización completa y el preview.

En este punto, el pequeño lector de ficheros de cintas había evolucionado a un código que cargaba un fichero TZX y se comunicaba con una capa de visualización a través de un array de bloques. Era el momento de parar y reorganizar el código de forma adecuada para poder seguir trabajando sobre él. Así nació TZX.js

Pero ese nombre duró poco, porque lo siguiente que se me ocurrió fue... ¿Y si pudiera cargar otras cintas además de TZX? ¡Voy a añadir lectores para los ficheros TAP!

Los ficheros TAP tienen menos estructura que los TZX, pero también son muy fáciles de leer. Los dos primeros bytes de cada bloque marcan la longitud de los datos del bloque (excluyendo esos dos bytes), así que directo. Y en cuanto conseguí cargar y visualizar el primer fichero TAP, tuve que cambiar el nombre a todo este código. ¿Qué hace? Carga cintas y las analiza. ¿Tape Analyzer? ¿Tape Extractor? ¿Tap-Ex? ¡TapeX! ¡Genial! El proyecto ha alcanzado su nombre definitivo.

Pero ya que tenía TZX y TAP, ¿y si añado soporte para CAS? No debe ser difícil, simplemente un lector para cada tipo de bloque CAS y su visualizador correspondiente... espera, ¿qué?

El formato CAS define un marcador de inicio para cada bloque compuesto por los siguientes 8 bytes:

0x1F, 0xA6, 0xDE, 0xBA, 0xCC, 0x13, 0x7D, 0x74

que han de estar siempre situados en una dirección múltiplo de 8 dentro del fichero CAS.

Después de ese marcador, el formato CAS tiene dos tipos de bloques definidos: cabeceras y bloques de datos. Reconocer una cabecera es fácil: tiene 16 bytes de longitud (sin contar el marcador) repartidos entre los primeros diez (que deben tener todos el mismo valor) y los últimos seis que forman el nombre del fichero. Pero los bloques de datos son otro cantar, ya que su contenido depende de la cabecera inmediatamente anterior. Así tenemos los bloques BASIC (grabados con CSAVE), que tienen el contenido de un programa BASIC tokenizado. También están los bloques binarios, que almacenan las direcciones de carga inicial, final y ejecución de un bloque grabado con BSAVE. Y también tenemos los bloques ASCII (grabados con SAVE), que guardan el contenido de un programa BASIC en ASCII... ¡y encima el programa se reparte en varios bloques consecutivos de 256 bytes de longitud!

Y no olvidemos a los bloques custom, que no se corresponden con ningún tipo de cabecera. Ahí están los datos en crudo, sin más información adicional.

¡Pero qué carajal es esto! Tengo que identificar el tipo de cabecera para poder leer los bloques de datos que la siguen, porque claro, cada uno se lee de una forma diferente. Esa información no está en la descripción física del fichero (tipo de bloque, longitud, etc.) sino que debe ser deducida de una interpretación semántica de los datos contenidos. Concretamente esos primeros diez bytes de la cabecera que deben ser todos idénticos:
Para los bloques custom, la única forma de proceder es buscar el siguiente marcador de inicio de bloque CAS y leer hasta ese punto.

Ok, si quiero leer bien los bloques de datos CAS tengo que identificar el tipo de cabecera que los precede. ¡Vamos a ello! Pero espera... ya que tengo esa información, ¿y si la devuelvo dentro de los datos del bloque? Y, ya que lo hago para CAS... ¿y si lo hago también para los bloques de ficheros TAP y TZX? Y ya que identifico las cabeceras de los ficheros grabados en MSX y Spectrum, ¿y si también almaceno el nombre del fichero?

Total, que toda esa información, que es información semántica y no pertenece al formato físico de los contenedores, es necesaria para delimitar/recorrer físicamente el contenido de algunos bloques CAS. Y ya que tenía que extraerla para ese formato, al final también se reconoce en los ficheros TAP y TZX.

En este punto ya tenía una pequeña biblioteca JavaScript que me permitía cargar un fichero de cinta y analizarlo, obteniendo un array con los diferentes bloques que lo forman e incluso con información semántica sobre el contenido del bloque (tipo de cabecera/bloque, nombre del fichero) cuando esto es posible. Toda esta información leída se fue incorporando a la capa de visualización para poder ver el tipo de cabecera/bloque y el nombre del fichero.

¿Y qué fue de esa pequeña web que me permitía seleccionar un fichero de cinta y visualizar rápidamente su contenido?

Pues a medida que iba añadiendo funcionalidades a TapeX yo seguía haciendo pruebas con ficheros de diferentes formatos. Y como todo empezó con el Krakout, iba probando con diferentes ficheros que contienen el juego: CAS, TAP, TSX y TZX. En la siguiente imagen podemos ver algunos bloques de un TSX con el juego:


Podemos ver que los bloques #5 y #6 contienen un fichero binario MSX llamado KRAK! que es el que cargamos desde el BASIC con la instrucción BLOAD"CAS:",R. Este fichero contiene el cargador que permite que el MSX lea los bloques no MSX que vienen a continuación. A partir de aquí los bloques se agrupan en parejas cabecera-datos, tal y como expliqué en el post sobre el editor del Krakout. ¿Y ahora qué miro? ¿Qué contiene cada bloque? Después de tanto pelearme con el cargador del juego, sé que los niveles están en el par de bloques #11 - #12 y en el par anterior (el #9 - #10) está la pantalla de carga.

Y entonces empiezan a surgir preguntas... ¿Y si pudiera extraer el contenido de un bloque? ¿Y sus datos? ¿Y si pudiera eliminar un bloque concreto? Así podría probar si el juego sigue funcionando si elimino la pantalla de carga de la cinta y ofrecer esta opción en el editor...

Para eso tengo que ampliar la aplicación web y añadir controles en cada uno de los bloques... pero eso es tarea de la capa de visualización y puede que esta aplicación use unos controles y otra otros. Así que para evitar tener que tocar la capa de visualización, hice que esta añadiera un contenedor especial para meter controles adicionales en cada bloque. De esta forma, pude añadir fácilmente un par de botones para grabar el bloque entero o su contenido y un switch para poder seleccionar qué bloques se incluirán en una nueva versión de la cinta.


¡Genial! Ahora mi pequeña aplicación web había crecido un poco y permitía extraer el contenido de los bloques además de generar nuevas versiones de la cinta excluyendo determinados bloques. El crecimiento de esta aplicación se iba produciendo en paralelo al de TapeX... y en este punto es cuando añadí soporte para cargar CAS, con sus bloques ASCII que distribuyen el contenido del programa BASIC en varios bloques.

Y al visualizar el contenido de uno de los bloques de un CAS, me topo con lo siguiente:


¡Qué bonito! Si se ve el código BASIC del programa... un momento... si ya tengo aquí el programa BASIC en ASCII, ¿por qué narices lo estoy mirando como un volcado hexadecimal? Además, el programa completo está distribuido por varios bloques (este, concretamente, por 8 bloques)... ¿y si reunimos el contenido de todos esos bloques y extraemos de ahí el código ASCII del programa? ¡Vamos a ello!


Si se identifica un bloque como una cabecera MSX ASCII (que puede aparecer tanto en un CAS como en un TSX), entonces junto a la cabecera aparece un nuevo botón BASIC listing, que permite visualizar el listado en BASIC del fichero. Durante la visualización podemos copiar el contenido al portapapeles o bien grabarlo a un fichero de texto.

¡Cómo mola! ¿Y si hago lo mismo para Spectrum? Se puede, sí... pero con un matiz. Los bloques de código BASIC de Spectrum se graban tokenizados, pero con una pequeña función que decodifica este tokenizado logré esto:


¿Y si hago lo mismo con el BASIC tokenizado de los ficheros MSX grabados con CSAVE? ¡Por supuesto! Pero esa tokenización es algo más complicada que la del Spectrum y eso me indicó que este era el punto de hacer un alto en el camino y mirar qué había creado.

TapeX

TapeX es una biblioteca JavaScript que permite cargar y analizar los bloques contenidos en ficheros CAS, TAP y TZX. Convierte el contenido del fichero en un array de bloques con toda la información asociada a cada uno de ellos. Además, está escrita en vanilla JavaScript, por lo que no depende de ninguna biblioteca externa. Está disponible en GitHub.

TapeX tiene los siguientes componentes: TapeX Core y TapeX Viewers.

TapeX Core

El núcleo de TapeX está formado por los ficheros: El uso de esta biblioteca es bastante sencillo. En primer lugar, tenemos que cargar los scripts del core en nuestra página HTML:

<script src="tapex-readers.js"></script>
<script src="tapex.js"></script>

A continuación, si tenemos un buffer con el contenido del fichero a analizar (que puede ser un ArrayBuffer o un Uint8Array), nos basta con hacer:

const tape = TapeX(buffer);

para analizar el fichero y obtener la instancia TapeX de la cinta. Sobre ella, TapeX ofrece las siguientes operaciones:

TapeX Viewers

TapeX Viewers es un componente opcional que permite crear un visualizador de bloques. Está compuesto por los ficheros: El uso de TapeX Viewers es también muy sencillo, ya que basta con incluir los dos ficheros anteriores (además de los dos del core):

<link rel="stylesheet" href="tapex-viewers/tapex-viewers.css">
<script src="tapex-viewers/tapex-viewers.js"></script>

Luego, en nuestra web tenemos que crear un contenedor para crear el visor:

<div id="TapeX"></div>

e instanciamos el visor asociándolo a dicho contenedor:

const viewers = TapeXblockViewers(document.getElementById("TapeX"));

Y así, cuando ya tenemos una instancia TapeX construida, solo tenemos que renderizar los bloques en el visor creado:

viewers.render(tape.getBlocks());

Cada bloque se muestra inicialmente plegado, pero podemos desplegarlo sin más que hacer click en cualquier parte de la cabecera que no sea un control adicional (y si lo hacemos otra vez, se vuelve a plegar). Si un bloque contiene datos, se muestra inicialmente una preview que podemos alternar con una vista del contenido completo gracias a un botón. Todo esto lo crea y lo gestiona el propio TapeX Viewers sin necesidad de que una aplicación tenga que hacer nada más que crear el visor y renderizar el contenido de una cinta.

TapeX Viewers ofrece las siguientes operaciones: Además, como ya hemos visto al leer la historia, en la cabecera de cada bloque se crea un contenedor .tapex-block-controls vacío. De esta forma, cualquier aplicación puede añadir sus controles personalizados y acceder a los datos del bloque correspondiente, ya que ese contenedor contiene el atributo data-block con el índice del bloque dentro del array de bloques (es decir, el primero tiene índice 0, el segundo 1 y así sucesivamente).

TapeX Workbench

Y esa pequeña web que empezó su camino para poder visualizar el contenido de los bloques y comprobar que todo estaba funcionando bien, evolucionó hasta una aplicación que permite cargar cintas, examinar sus bloques, extraer un bloque entero o los datos contenidos en él, visualizar (y copiar o grabar) el listado en BASIC de los programas BASIC reconocidos (tanto los de Spectrum como los grabados en ASCII en MSX mediante SAVE).

Por último, pero no menos importante, también nos ofrece la posibilidad de grabar una imagen de cinta con los bloques seleccionados. Esto ya no es una pequeña web, es todo un banco de trabajo sobre cintas: TapeX Workbench.

Y aquí termina la historia de cómo un simple «¿y si...?» fue liando la cosa hasta que un pequeño proyecto se me fue de las manos. Pero ni mucho menos es el final, ya que tanto TapeX como TapeX Workbench continuarán su evolución.

¿Y sabéis qué es lo mejor de todo esto? El editor del Krakout aún no utiliza TapeX.