Manic Minerin hakkerointia

Eilisen Manic Miner pelini sujuivat yleisesti ottaen todella hyvin ja taso pysyi tasaisesti korkeana, mutta valitettavasti yksikään peli ei edennyt pitemmälle kuin SIXTEENTH CAVERN huoneeseen.

Olen aiemmin hakkeroinut Commodore 16 Manic Minerin harjoituspelejä varten siten, että elämät eivät koskaan vähenny. Se ei kuitenkaan auta siinä, että peli on harjoituksissakin aloitettava aina alusta. Paljon mukavampaa olisi jos voisimme aloittaa harjoittelun valitsemastamme huoneesta. Siispä päivän kysymys kuuluukin: Olisiko meidän mahdollista muokata Manic Mineria vähällä vaivalla siten, että aloitushuone olisi valittavissa?

Mieleeni tuli ensin yksi idea. Voisimme etsiä INC, INX ja INY opkoodeja ja korvata ne yksitellen NOP komennoilla. Jos tuloksena olisi Manic Minerin versio, joka jumittaa ykköshuoneessa, tietäisimme, että osuimme oikeaan kasvatusoperaatioon, ja voisimme melko pienellä vaivalla selvittää mitä muistiosoitetta operaatio koskee. Kyseinen muistiosoite olisi se muuttuja, joka meidän pitäisi alustaa haluamaamme arvoon.

Toinen ideani tuntui kuitenkin paremmalta. Todennäköisesti pelissä määritellään aloitushuone olemaan nolla, ja tallennetaan arvo jonnekin muistiin. Voisin siis kirjoittaa ohjelman, joka etsii seuraavanlaisia opkoodisekvenssejä:

LDA #$00
STA $absoluuttinen_muistiosoite

LDX #$00
STX $absoluuttinen_muistiosoite

LDY #$00
STY $absoluuttinen_muistiosoite

Toisin sanoen, haluaisin löytää ohjelmakoodinpätkiä, joissa rekisteriin asetaan välitön arvo nolla ja heti perään arvo tallennetaan absoluuttiseen muistiosoitteeseen. Sellaiset opkoodisekvenssit olisivat hyviä kandidaatteja olemaan aloitushuoneen asettaminen.

C-ohjelmani näyttää tältä:

#include <stdio.h>
#include <stdlib.h>

#define MANIC_SIZE 12288

#define LDA 0xa9	/* immediate */
#define LDX 0xa2	/* immediate */
#define LDY 0xa0	/* immediate */
#define STA 0x8d	/* absolute */
#define STX 0x8e	/* absolute */
#define STY 0x8c	/* absolute */


enum reg_name { REGISTER_A, REGISTER_X, REGISTER_Y };


struct reg_to_str {
	enum reg_name name;
	const char *str;
} reg_to_str[] = {
	{ REGISTER_A, "A" },
	{ REGISTER_X, "X" },
	{ REGISTER_Y, "Y" },
	{ -1, NULL }
};


const char *reg_get_str(enum reg_name reg_name)
{
	struct reg_to_str *p = reg_to_str;

	while (p->str) {
		if (p->name == reg_name)
			return p->str;
		++p;
	}
	return NULL;
}


unsigned char buf[MANIC_SIZE];


int main(void)
{
	FILE *fp;
	FILE *mm;
	int n;
	int i;
	int total = 0;
	char namebuf[12];
	enum reg_name reg_name;
	int found;

	fp = fopen("manic_miner.prg", "r");

	if ((n = fread(buf, MANIC_SIZE, 1, fp)) != 1) {
		fprintf(stderr, "fread error\n");
		exit(EXIT_FAILURE);
	}

	fclose(fp);

	fp = fopen("info.txt", "w");

	for (i = 0; i < MANIC_SIZE - 5; i++) {
		found = 0;
		if (buf[i] == LDA && buf[i+1] == 0x00 && buf[i+2] == STA) {
			found = 1;
			reg_name = REGISTER_A;
		}
		else if (buf[i] == LDX && buf[i+1] == 0x00 && buf[i+2] == STX) {
			found = 1;
			reg_name = REGISTER_X;
		}
		else if (buf[i] == LDY && buf[i+1] == 0x00 && buf[i+2] == STY) {
			found = 1;
			reg_name = REGISTER_Y;
		}

		if (found) {
			++total;	
			snprintf(namebuf, sizeof namebuf, "mm%d.prg", total);
			fprintf(fp, "%s offset=%d (%x) register %s set to zero followed by absolute store\n",
					namebuf, i, i, reg_get_str(reg_name));

			buf[i+1] = 0x01;

			mm = fopen(namebuf, "w");
			if ((n = fwrite(buf, MANIC_SIZE, 1, mm)) != 1) {
				fprintf(stderr, "fwrite mm%d.prg error\n", total);
				exit(EXIT_FAILURE);
			}
			fclose(mm);
			buf[i+1] = 0x00;
		}
	}

	fclose(fp);
	return 0;
}

Tuloksena oli 25 erilaista Manic Minerin versiota. Toinen niistä eli mm2.prg tuntui olevan haluamani. Peli alkoi siinä suoraan THE MENAGERIE huoneesta. Tosin ihmettelen sitä miksi hyppy tapahtui kolmanteen huoneeseen eikä toiseen. Koska korvasin nollan ykkösellä, olisin olettanut, että peli olisi alkanut huoneesta THE COLD ROOM. Jostain syystä niin ei käy. En kuitenkaan jaksa alkaa tutkimaan koodia sen syvällisemmin, vaan tyydyn siihen, että tilanne on tämä.

Nyt meidän olisi helposti mahdollista tuottaa monta erillistä Manic Mineria, joista kukin alkaisi eri huoneesta. Voisimme nimittäin asettaa aloitushuoneen arvon erilliseksi joka versioon. Se olisi kuitenkin kohtuullisen kömpelöä. Mielestäni parempi vaihtoehto olisi jos voisimme muokata aloitushuonetta dynaamisesti pelin aikana. Tutkitaan siis seuraavaksi sitä.

VICE emulaattorissa on sisäänrakennettu konekielimonitori, joka on hyvin hyödyllinen. Etsin konekielimonitorin manuaalin ja löysin sieltä haluamani komennon:

hunt <address_range> <data_list>
Hunt memory in the specified address range for the data in <data_list>. If the data is found, the starting address of the match is displayed. The entire range is searched for all possible matches. The data list may have `xx' as a wildcard.

Seuraavaksi katsoin C-ohjelmani tuottamaa info.txt tiedostoa. Se kertoo tarkemmin minkä operaation ohjelma oli Manic Minerista löytänyt. Olin tietysti kiinnostunut nimenomaan kakkosohjelman koodista, koska se oli haluamani ohjelmaversio. Tiedostossa luki:

mm2.prg offset=6659 (1a03) register A set to zero followed by absolute store

Nyt tiedämme, että operaatio koski rekisteriä A ("Accumulator"). Tiedostossa mainittu offset ei kuitenkaan suoraan auta meitä, sillä se viittaa Manic Minerin binääritiedostoon, mutta kun ohjelma on ladattu Commodore 16:n RAM-muistiin, muistiosoite on eri, koska ohjelmaa ei voida ladata niin, että se alkaisi nollasivulta ("zero page"). En jaksanut alkaa selvittämään mihin osoitteeseen ohjelma ladataan ja laskeskelemaan oikeaa offsettia, vaan päädyin toiseen vaihtoehtoon.

Koska hunt komennolla voi etsiä haluamiaan datasekvenssejä ajossa olevan ohjelman muistista, kokeilin sitä menetelmää. Ensin katsoin C-ohjelmastani heksadesimaalikoodit LDA ja STA opkoodeille. Sitten käynnistin emulaattorin xterm-terminaaliemulaattoriohjelmasta komennolla:

xplus4 manic_miner.prg

En käynnistänyt sitä tausta-ajona, jotta terminaali olisi xplus4 ohjelman käytettävissä. Sitten valitsin graafisesta käyttöliittymästä activate monitor, jolloin terminaaliini käynnistyi konekielimonitori. Annoin etsintäkomennon:

hunt 0,ffff a9 00 8d

Konekielimonitori vastasi löytäneensä seuraavat osumat:

2970
2a02
2a12
2a60
2ab0
2b22
2b27
2b5e
2b63
2b79
2cde
2f3a
2fb9
2ffc
3069
310a
3131
3411
3416
3625
3948
8ae5
90df
b655
c34c
c959
db11
dd8b
e301
e3dd
e457
e819
eab0

Koska ohjelma mm2.prg eli toinen ohjelma oli ollut haluamani, arvelin, että listan toinen muistiosoite saattaisi olla etsimäni. Osoite 2a02 sisältää opkoodin LDA, osoite 2a03 arvon nolla, ja 2a04 opkoodin STA. Haluaisin siis muuttaa muistiosoitteen 2a03 arvoksi haluamani Manic Miner huoneen. Miten se tapahtuisi?

Konekielimonitorin manuaalista selviää, että fill komennolla on mahdollista muuttaa muistissa olevia arvoja. Tarkkaan ottaen:

fill <address_range> <data_list>
Fill memory in the specified address range with the data in <data_list>. If the size of the address range is greater than the size of the data_list, the data_list is repeated.

Halusin muuttaa vain yhtä tavua muistista, joten kokeilin ensin komentoa:

fill 2a03 1

Konekielimonitori ei kuitenkaan hyväksynyt sitä, vaan antoi virheilmoituksen:

ERR:syntax error
ERROR -- Wrong syntax:
fill 2a03 1

Antamalla sama alku- ja loppuosoite komento onnistui. Sitten vain komento exit, joka lopettaa konekielimonitorin ja jatkaa ohjelman, tässä tapauksessa Manic Minerin, suoritusta:

fill 2a03,2a03 1
exit

Aloitushuoneen muutos ei kuitenkaan tulisi voimaan heti, joten peli on ensin lopetettava hassaamalla kolme Miner Willyä.

Onnekseni muistiosoite 2a03 oli ollut oikea. Nyt voin treenata kuudettatoista huonetta niin paljon kuin huvittaa menemällä konekielimonitoriin ja antamalla komennot:

fill 2a03,2a03 e
exit

Kuten alla olevasta kuvasta näkyy, se outous tästä muutoksesta tosin seurasi, että SCORE ei alakaan nollasta, vaan yhdestä:

strange_score

Joka tapauksessa olen erittäin tyytyväinen ja toivon pelitasoni paranevan tehostettujen harjoittelumahdollisuuksien myötä.