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:
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ä:
Joka tapauksessa olen erittäin tyytyväinen ja toivon pelitasoni paranevan tehostettujen harjoittelumahdollisuuksien myötä.