Skip to content

Trasteando con POKEs #1: Abu Simbel Profanation (SPE) parte I

septiembre 18, 2016

En este inicio de mi colaboración en Program: Bytes: 48K voy a hablar del primer juego que tuve oportunidad de probar en un microordenador (concretamente el ZX Spectrum+ de un amigo): Abu Simbel Profanation, de Dinamic. Lejos de centrarme en otro análisis más del juego (que los hay a patadas), lo utilizaré para introducir esta sección, en la que trataré de presentar las formas clásicas de encontrar POKEs para un juego y que en futuras entregas abarcará otras plataformas y juegos.

 

Herramientas necesarias

 

Para llevar a cabo todo el proceso, utilizaremos el emulador FUSE (http://fuse-emulator.sourceforge.net/) y, en particular, las siguientes herramientas del mismo:

  • Poke Finder, con el que podremos observar cambios en los valores almacenados en direcciones de memoria determinadas.
  • Debugger, que nos permitirá inspeccionar el estado del programa y la CPU, así como ver y modificar el código y los datos, y utilizar puntos de ruptura (breakpoints y data breakpoints, que explicaré enseguida).
  • Memory Browser, un inspector de memoria con el que ver el contenido de la misma en cualquier momento. Dado que este inspector es muy primitivo, no permitiendo por ejemplo hacer búsquedas, en algunas ocasiones utilizaremos un editor hexadecimal externo (yo utilizo el editor online http://hexed.it/).

 

Decimal y hexadecimal

 

A lo largo de la serie de artículos trabajaremos con las notaciones decimal y hexadecimal. La notación decimal (también llamada en base 10) es la que utilizamos en nuestro día a día para valorar y medir cosas, y representa los números utilizando los dígitos del 0 al 9. La notación hexadecimal (base 16) añade 6 letras, de la A a la F, de forma que después del 9 viene la A (equivalente a 10 en decimal), luego la B (11 en decimal) y así sucesivamente; y tras la F viene el 10 (16 en decimal).

Los valores hexadecimales se denotan con el prefijo 0x, por ejemplo 0x1976 (que en decimal es 6518), aunque FUSE muchas veces no utiliza ese prefijo al mostrar información.

Para movernos entre decimal y hexadecimal se puede utilizar la Calculadora de Windows (la clásica, no la de Windows 8) en modo programador. No ahondaré más en su uso al tratarse de una herramienta conocida.

 

Las memorias RAM y ROM

 

Aunque prácticamente todo el mundo sabe qué es la memoria de un ordenador, no está de más refrescar, valga la redundancia, la memoria (la nuestra). De forma genérica, la memoria es un dispositivo que almacena información durante un tiempo dado. En el caso que nos interesa para este artículo, el dispositivo es uno o varios circuitos integrados y el tiempo es el que dure encendido el ordenador (o mejor dicho, la CPU). Habitualmente, distinguimos entre la ROM (memoria de solo lectura, que ya viene escrita con información de fábrica) y la RAM (memoria de lectura/escritura, en la que se cargan los programas y los datos asociados a éstos, como gráficos o músicas).

La memoria se organiza en celdas en las que guardamos información. Cada una de estas celdas (para el ZX Spectrum serían un total de 65536, resultado de sumar 16KB * 1024B/KB de ROM y 48KB * 1024B/KB de RAM) pueden albergar un valor entre 0 y 255. Estas celdas están numeradas consecutivamente de la 0 a la 65535. Normalmente, nos referimos a la posición de estas celdas en el total de la memoria con el término dirección de memoria.

El ZX Spectrum, igual que una gran parte de sus contemporáneos, se basa en el procesador Zilog Z80, que puede manejar hasta 64KB de memoria simultáneamente, en páginas de 16KB. De este modo, encontramos la página 0 (la ROM, cuya dirección de memoria más baja es 0), y tres páginas más (la RAM, cuyas direcciones más bajas son, respectivamente, 16384, 32768 y 49152). En ocasiones nos encontraremos con referencias a direcciones de memoria absolutas (es decir, un número de celda), y en otras como parejas página/celda (en este caso, la celda representa un desplazamiento desde la dirección de memoria más baja de la página). Para convertir de pareja página/celda a dirección de memoria absoluta, utilizaremos la fórmula (16384 * página) + celda (ya que cada página tiene 16384 celdas). Las direcciones de memoria se escriben habitualmente en formato hexadecimal. La misma fórmula en hexadecimal sería (0x8000 * página) + celda.

 

Los registros de CPU

 

Como el acceso a memoria, tanto para lectura como para escritura, es muy lento, la CPU utiliza variables internas mucho más rápidas (llamadas registros) para realizar la mayoría de las operaciones.

El número de estos registros es muy limitado, lo que obliga a los programadores a utilizarlos de forma inteligente. El Z80 ofrece al programador los registros A, B, C, D, E, H y L, capaces de albergar valores de 1 byte (en el rango 0 a 255). Algunos de ellos pueden usarse en pareja (BC, DE, HL) pudiendo entonces almacenar 1 word (2 bytes) cada uno (en el rango 0 a 65535). A estos registros se les llama registros dobles. Más allá de esto, algunas versiones del Z80 entre las que se encuentra la que utiliza el ZX Spectrum, ofrecen los registros dobles IX, IY, y un conjunto de registros adicionales llamados registros extendidos, marcados con el símbolo apóstrofe (prima, como A'). Existen otros registros en la CPU, pero no tienen relación directa con el contenido del artículo así que prescindiré de presentarlos por el momento.

 

Puntos de ruptura

 

En programación, un breakpoint (en castellano punto de ruptura) es una marca que asignamos a una dirección de memoria determinada para que el programa se detenga al pasar por allí y nos permita examinar tanto el estado de la memoria como de la CPU. Un data breakpoint es una variante de este concepto, en la que el programa se detiene en la siguiente instrucción que intente escribir en la dirección de memoria marcada (por ejemplo, la instrucción que decrementa la dirección de memoria en que el juego almacena el número de vidas restantes).

 

Nuestro primer POKE

 

La forma más habitual para encontrar un POKE (utilizada en soluciones hardware como el Multiface o el Amiga Action Replay) consiste en acceder al juego, interrumpir su ejecución para buscar y anotar todas las direcciones de memoria que contienen un valor concreto (p.e., el número inicial de vidas), volver al juego para realizar alguna acción que modifique este valor (p.e., perder una vida), interrumpir el juego de nuevo y, de las direcciones anteriormente anotadas, elegir aquellas que han modificado su valor al nuevo valor de búsqueda (p.e., vidas iniciales menos una), y repetir el proceso hasta que solamente quede una dirección de memoria candidata.

Por suerte, este proceso puede llevarse a cabo de forma sencilla gracias a la herramienta Poke Finder de FUSE, que se encarga de anotar por nosotros las direcciones de memoria candidatas, trazando sus cambios automáticamente. Si utilizamos otro emulador que no dispone de una herramienta de este tipo, se puede hacer el proceso a mano (con un inspector de memoria y una libreta).

Para comenzar, vamos a buscar el POKE que nos permita elegir el número de vidas que queremos que tenga el jugador al iniciar una partida:

Arrancamos FUSE y cargamos Abu Simbel Profanation, iniciando una partida. En este momento, tendremos el estado del juego cargado (esto es, el conjunto de datos en memoria que determina el número de vidas, la pantalla del mapa en que estamos, etc.).

En el menú Machine seleccionaremos Poke Finder y ponemos 10 (número inicial de vidas) en el campo Search For. Al hacer click en Search, FUSE nos informará de que hay más de 200 posiciones de memoria que contienen dicho valor. Por suerte, no tenemos que apuntarlas, ya que FUSE lo hará por nosotros.

Haciendo click en Close cerramos dicha ventana, volviendo a la partida, momento en el cual procederemos a perder una vida. Repetiremos la operación, abriendo Machine/Poke Finder, poniendo 9 en el campo Search For y haciendo click en Search. Quedará entonces un único resultado (page 2, address 0x17a2, cuya dirección de memoria absoluta es 0x97a2 según resulta de aplicar la fórmula indicada anteriormente), que será mostrado en la misma ventana (FUSE sólo nos detallará las direcciones de memoria cuando haya como máximo 20 entradas). Esta dirección es, por tanto, la que contiene el número de vidas de la partida. Modificando el valor en esta dirección, cambiará el número de vidas del jugador.

Para comprobarlo, abrimos Machine/Debugger y en el campo de evaluación, utilizamos el comando se que se utiliza para modificar el contenido de una celda de memoria. Escribimos se 0x97a2 5 y hacemos click en Evaluate. Cerramos la ventana. Un bug de FUSE hará que se acelere el tiempo de juego durante unos segundos (dependerá del tiempo que haya estado abierta la ventana Debugger). Esperando un poco todo volverá a la normalidad.

4_vidas

Dado que el juego no redibuja el marcador hasta que se pasa de pantalla o se pierde una vida, necesitaremos hacer alguna de las dos cosas para observar si el cambio ha tenido éxito.
Lo que hemos hecho, en realidad, es modificar el número de vidas en la partida, pero si las perdemos todas y comenzamos una nueva, volveremos a tener las 10 vidas iniciales. Esto es porque la celda de memoria que hemos modificado es un dato de partida (lo que se conoce como estado del juego o contexto del juego), pero no un valor de configuración. Por lo tanto podemos deducir que el programa, al iniciar una nueva partida, copia al contexto de juego este valor desde algún otro lugar. Así que ¡vamos a descubrirlo!

 

POKE #1: número de vidas inicial

 

Reiniciaremos el juego para volver al menú principal. Como ya sabemos que el contexto de juego guarda el número de vidas en 0x97a2, vamos a utilizar un data breakpoint para ver qué instrucción escribe el número de vidas inicial ahí, y así podremos deducir en qué dirección de memoria se guarda la configuración para el número de vidas por partida. Abrimos Machine/Debugger y observamos los cambios en la dirección escribiendo br w 0x97a2 y haciendo click en Evaluate. Cerramos la ventana Debugger.

Iniciamos una partida y ¡voilà! La ventana Debugger se abre y nos muestra las instrucciones de la dirección de memoria 0xc08F en adelante. Un data breakpoint siempre salta tras ejecutar la instrucción que modifica la dirección de memoria que queremos trazar, así que la instrucción que estamos buscando es la anterior, en la dirección de memoria 0xc08C. Esta instrucción es LD (97A2), HL, un comando en lenguaje máquina que significa guarda el contenido del registro HL en la posición de memoria 0x97a2 (LD es un acrónimo de LOAD, el paréntesis que rodea a 97A2 indica que es una dirección de memoria y, al ser el primer parámetro, es además el destino de la operación; el segundo parámetro –HL en este caso- representa el origen). Esto significa que no mucho antes en el programa ha de haber una instrucción que está poniendo el número de vidas inicial en el registro HL. Dado que el valor inicial es 10, y que 10 en hexadecimal es A, buscaremos una instrucción similar que cargue el valor A en el registro HL. Esa instrucción está en la dirección de memoria 0xC089, y es LD HL, 000A (donde como antes el primer parámetro, HL, representa el destino de la operación y 000A el origen, en este caso al no estar rodeado de paréntesis representa una cantidad).

Por tanto, debería bastar con modificar este valor para determinar el número de vidas que el juego otorga en cada inicio de partida.

Dado que no sabemos cómo estará codificada la instrucción LD HL, 000A en memoria, la mejor opción es acceder a Machine/Memory Browser, buscar la línea para la dirección de memoria C080 y ver en qué posición queda codificado el valor 000A:

browser

Sabiendo que las columnas se numeran de la 0 a la 15 (en hexadecimal, de la 0x00 a la 0x0f), buscamos C089 y nos encontramos con la secuencia 21 0A 00, donde 21 (en hexadecimal) es la instrucción LD HL, <valor>, y 0A 00 (también en hexadecimal) es el valor que estamos buscando (la CPU los codifica intercambiando los bytes de orden, esto se llamada codificación little endian). Vemos por tanto que el valor 0x0A está en la dirección de memoria 0xC08A (en hexadecimal, la siguiente a la que buscábamos, 0xC089).

Cerramos el Memory Browser y nos quedamos de nuevo en la ventana del Debugger. Eliminamos el breakpoint evaluando el comando cl 0x97a2, y modificamos el número inicial de vidas con partida (para la prueba, a 7 vidas) con el comando se 0xc08A 7. Iniciamos una partida para comprobar que… ¡seguimos comenzando con 10 vidas!

La realidad es que si perdemos una vida veremos como nos quedamos con 6. El marcador de Abu Simbel Profanation dibuja un 10 por defecto en esa posición, aunque el número de vidas inicial no sea ese.

Por suerte, este pequeño bug no importa demasiado, ya que en la próxima entrega explicaré cómo descubrir, a partir de lo que hemos aprendido hoy, la forma para obtener vidas infinitas e inmunidad en Abu Simbel Profanation. Así que… ¡hasta la próxima!

 

dcm_avatar

Sobre el autor

David Cañadas es programador de videojuegos, retroscener, demoscener y músico, ha colaborado en juegos retro publicados por Computer Emuzone y Retroworks, entre otros, y puesto música a remakes de PC como Abadía del Crimen Extensum, Capitán Sevilla o Go Bear Go.

No comments yet

Publica aquí tu comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: