Trasteando con POKEs #2: Abu Simbel Profanation (SPE) parte II
Primera parte: Trasteando con POKEs #1: Abu Simbel Profanation (SPE) parte I.
···
Continuamos con esta sección descubriendo algunos trucos más para Abu Simbel Profanation; concretamente, y tal como comenté al final del anterior artículo, los POKE
s de vidas infinitas e inmunidad para ZX Spectrum. Quiero hacer notar que, aún con estos trucos, terminarse el juego requiere de una cierta habilidad, ya que algunas pantallas dependen de saltos muy precisos para los que no hay segundas oportunidades.
Para llevar a cabo esta tarea volveremos a utilizar las herramientas presentadas en el artículo anterior de esta serie, esto es, el emulador FUSE, sus herramientas internas y un editor hexadecimal externo. Recomiendo encarecidamente la lectura de dicho artículo antes de acometer este.
POKE #2: Vidas infinitas
Al final del primer artículo, buscando el POKE #1: Número de vidas inicial, descubrimos que la dirección de memoria 0x97a2
es la que contiene el número de vidas del jugador durante la partida. Por tanto, para conseguir vidas infinitas en el juego deberíamos empezar buscando la parte del programa que decrementa el valor almacenado en dicha dirección de memoria cuando nos matan.
Arrancamos FUSE, cargamos Abu Simbel Profanation e iniciamos una partida. Abrimos Machine/Debugger y ponemos un data breakpoint para la dirección de memoria 0x97a2
con el comando br w 0x97a2
para inspeccionar las escrituras en dicha posición de memoria. Perdemos una vida y el Debugger salta en la dirección 0xba4e
, que como ya dije en el artículo que precede a este, es la instrucción posterior a la que disparó el data breakpoint, así que vamos a la anterior, y vemos que es LD (97A2), HL
en 0xba4b
. Esta instrucción significa escribe el contenido de HL
en la posición de memoria 0x97a2
.
Dado que la instrucción LD (97A2), HL
que hemos encontrado en 0xba4b
es la responsable de actualizar el número de vidas cuando perdemos una, podemos optar por eliminarla para obtener vidas infinitas.
Para ello, necesitamos saber cómo se codifica esta instrucción en memoria. Abrimos Machine/Memory Browser, buscamos la línea para la dirección de memoria 0xba4b
y podremos ver que la instrucción está codificada como 22 A2 97
.
La forma de evitar que se ejecute esta instrucción es reemplazándola por otra que no haga nada. El Z80 tiene una instrucción que hace precisamente eso, es decir, nada, y es la instrucción NOP
, que codifica como 00
.
Abrimos Machine/Debugger y evaluamos:
se 0xba4b 0
se 0xba4c 0
se 0xba4d 0
para sustituir los tres bytes ocupados por la instrucción LD (97A2), HL
por tres instrucciones NOP
consecutivas. Evaluamos también cl 0x97a2
para eliminar el data breakpoint y reanudamos la ejecución. Nos dejamos matar y… ¡no perdemos ninguna vida!
Nota: incluso disponiendo de vidas infinitas debemos jugar con cuidado, ya que al acceder a una pantalla en la que perdemos una vida antes de poder interactuar con el juego (por ejemplo, al caer a un foso con pinchos como el de la pantalla número 3, o al sumergirnos en el agua) entraremos en un bucle infinito del que no podremos salir, y en el que nuestro personaje morirá una y otra vez de la misma forma, sin perder ninguna vida y sin darnos la oportunidad de terminar la partida para volver al menú principal para intentarlo de nuevo. Para solventar este problema podemos escribir un cero en la dirección de memoria 0x97a2
con Machine/Debugger mediante el comando se 0x97a2 0
para que el siguiente chequeo de vidas falle y el juego crea que la partida ha terminado.
De rutinas y subrutinas
Es el momento de hacer un inciso acerca de cómo se organiza el código de un programa en memoria para comprender el siguiente POKE
en su totalidad.
Las secuencias de instrucciones en un programa se organizan en bloques que llamamos rutinas. Cada uno de estos bloques tiene una finalidad concreta, por ejemplo dibujar la pantalla, actualizar la puntuación o la lógica de un enemigo. Las rutinas se llaman desde otras rutinas, que a su vez pueden ser llamadas desde otras, y así múltiples veces.
Una rutina puede llamar a otra de dos formas, la primera es mediante un salto; en este caso, la ejecución continua a partir de la dirección de memoria indicada. Esto se hace utilizando la instrucción JP
(acrónimo de JumP
). La segunda es mediante una llamada, lo que significa que desde la rutina actual llamamos a otra para que se ejecute, nos avise cuando termine y podamos así continuar ejecutando la rutina actual. Las llamadas se hacen utilizando la instrucción CALL
(que significa precisamente llamar en inglés). La rutina que ha sido llamada retorna el control a la rutina que la llamó mediante la instrucción RET
. Una rutina llamada con la instrucción CALL
se llama subrutina. Las subrutinas tienen la ventaja de que no necesitan conocer de antemano quién las ha llamado, de forma que una misma subrutina puede llamarse desde distintas partes del programa, y al terminar su ejecución mediante la instrucción RET retornará el control a la rutina que la llamó de forma transparente para el programador.
Cuando la instrucción RET
se ejecuta, el procesador necesita saber a dónde debe volver. ¿Y de qué forma lo hace? Pues consultando una lista de valores en memoria llamada pila. En la pila, los valores se añaden utilizando la instrucción PUSH
, y se recuperan utilizando la instrucción POP
(ambas instrucciones han aparecido justamente en la rutina analizada en el apartado anterior). Una pila es una forma de almacenamiento LIFO (acrónimo de Last In, First Out, o lo que es lo mismo, el último valor en entrar es el primero en salir), lo que significa que solo podemos recuperar los valores que hemos almacenado en el orden inverso en el que fueron apilados.
La instrucción CALL
añade automáticamente a la pila la dirección de memoria inmediatamente posterior a ella, y la instrucción RET
recupera dicha dirección de la pila para volver a la instrucción siguiente al CALL
. Como la pila es LIFO, podemos anidar llamadas a CALL
unas dentro de otras garantizando que el orden de retorno se respetará, ya que cada RET
volverá inmediatamente a la rutina que contenía el CALL
que realizó la llamada.
Para examinar la pila, FUSE nos ofrece la vista de pila, en Machine/Debugger, justo a la derecha de la lista de instrucciones en ensamblador. La pila crece hacia abajo en memoria (es decir, cuanto más alta es la dirección de memoria en que aparece un valor en pila, más tiempo hace que añadimos dicho valor a la pila). Por tanto, si queremos conocer el primer valor que se añadió a la pila miraremos la primera entrada en la vista de pila, y si queremos conocer el más reciente, miraremos la entrada al final de dicha vista.
POKE #3: Inmunidad
Como ya hemos descubierto la rutina que se encarga de restar una vida, debería bastar con tirar del hilo para descubrir qué provoca que se ejecute esta rutina y poder obtener así la ansiada inmunidad.
Iniciamos una partida, abrimos Machine/Debugger y nos movemos hasta la instrucción en la dirección 0xba4b
(que es la dirección que modificamos en nuestro POKE
de vidas infinitas). Vamos navegando por las líneas anteriores hasta encontrar la instrucción RET
, que se corresponderá con la última instrucción de la subrutina inmediatamente anterior en memoria. La siguiente instrucción a RET
marcaría, por tanto, el inicio de la rutina que se encarga de restar una vida al jugador. Esta dirección es 0xba38
.
Ponemos ahora un breakpoint en 0xba38
evaluando el comando br 0xba38
, salimos de Machine/Debugger y perdemos una vida. El programa se parará y podremos examinar el estado del programa. Eliminamos el breakpoint con cl 0xba38
.
Lo que ha ocurrido es que algún punto del programa ha llamado a esta subrutina haciendo
CALL BA38
, y a tenor de lo que he comentado más arriba, podemos saber qué punto ha sido leyendo en la vista de pila el valor más recientemente apilado, es decir, el que aparece más abajo en la lista, que es concretamente 0xb0f5
, y que representaría la instrucción inmediatamente posterior al CALL
. Navegamos hasta esta dirección en la vista de instrucciones y encontramos que, efectivamente, la instrucción anterior a la dirección de memoria 0xb0f5
es CALL BA38
en la dirección de memoria 0xb0f2
.
Dado que conocemos la posición de memoria del CALL
(0xb0f2
) y la de su instrucción siguiente (0xbaf5
), podemos dedudir que el tamaño de la instrucción es de 3 bytes (0xb0f5 - 0xb0f2 = 3
); además, hemos aprendido que la instrucción NOP
se puede utilizar para sobreescribir instrucciones y evitar así que ocurran cosas que no queremos. Con toda esta información, parece factible conseguir la inmunidad que perseguimos sustituyendo CALL BA38
por tres NOP
, cosa que podemos hacer evaluando los siguientes comandos en Machine/Debugger:
se 0xb0f2 0
se 0xb0f3 0
se 0xb0f4 0
Cerramos la ventana Machine/Debugger, jugamos un poco y ¡los enemigos nos tocan sin hacernos ningún daño! ¿¡Podremos por fin terminarnos el juego!? Como decía al iniciar el artículo, incluso con estos trucos resulta algo complicado terminarlo ya que algunos saltos requieren de mucha precisión, y cometer un error puede conducirnos a un callejón sin salida.
Nota: la inmunidad propuesta en este artículo plantea un problema con los elementos de escenario destinados a matarnos debido a cómo el juego procesa las colisiones: aunque el agua nos matará, los pinchos que hay en los fosos no lo harán, lo que puede terminar dejándonos en una situación en que estemos encerrados en una pantalla sin salida posible (como por ejemplo en el foso de la pantalla número 3).
Es el momento de dejaros jugando a Abu Simbel Profanation con los POKE
s que hemos descubierto. En el próximo artículo cerraré la parte dedicada a este juego exponiendo POKE
s alternativos para conseguir vidas infinitas e inmunidad, un truco para acceder directamente al final del juego sin necesidad de jugar, y también os contaré algunas curiosidades e interioridades del mismo en sus distintas versiones. Así que… ¡hasta la próxima entrega!
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.
Muy entretenido… tan solo espero que la tercera parte no tarde en llegar tanto como ha tardado esta segunda 🙂
Gracias por tu trabajo.
Gracias a tí por dedicar tu tiempo a leerlo 🙂 Intentaré que la tercera parte no se demore tanto 😛
Un saludo.
No, no nos equivoquemos. Cada cosa en su sitio. Leer es una cosa y pegarse un lote de trabajar, no ya escribiendo el texto, si no maquetándolo, capturando las pantallas, probando los ejemplos, etc, etc, es un trabajo que sólo él que lo hace -o lo ha hecho en otras ocasiones- sabe valorar… y créeme, yo si sé valorar tu artículo (aunque no por eso deje de disfrutar leyéndolo :-)).
GRACIAS de nuevo.
Un saludo.