Manic Miner on BBC Micro

Background

On February 19th, 2018, I figured out two small ORIC Manic Miner hacks. They enable infinite lives and free transporting to play any room. I documented the hacks on page Manic Miner on ORIC. Today (February 21st, 2018) I wanted to try out the BBC Micro version of Manic Miner. More specifically, it was my intention to find similar hacks for the BBC Micro version, too, but I soon discovered that the B-em BBC Micro emulator does not have a machine language monitor! How would I be able to modify RAM at run-time? And how would I get RAM dumps to examine in the first place? The situation looked pretty bad.

I was working on Fedora Linux 27, so I checked out file linux-gui.c and learned how the Allegro 4 GUI menu system worked. But I could not find a way to create text boxes that would accept keyboard input from the user. I tried googling and everything, but I guess this Allegro menu/dialog system is pretty primitive and not comparable to the "real" GUI frameworks that allow all kinds of windows to be created. I know next to nothing about GUIs, I am a command-line guy, basically. Why did I study the Allegro menu system? Because I wanted to add very primitive machine language monitor features to B-em, that was the reason. I was not sure whether I could do it, but I only needed four operations to work:

  1. Suspending BBC Micro emulation and entering the monitor
  2. Writing to arbitrary BBC Micro RAM locations
  3. Writing BBC Micro RAM dumps to files for analysis
  4. Quitting the monitor and resuming BBC Micro emulation

Because I could not use the Allegro menu GUI, I went for a pretty ugly hack: I made B-em a TCP-server listening at localhost (IP address 127.0.0.1) port 9999. That is the interface for the four operations described above. When B-em 2.2.3kk is running, you can activate its TCP-server hack by pressing F1. It will suspend the BBC Micro emulation just like F11 does when entering the Allegro GUI menus. You can use any TCP-client to connect to B-em as long as you know the very simple text-based command protocol B-em 2.2.3kk supports on top of TCP/IP.

If you are not interested in a pretty detailed explanation of my Manic Miner hacking, please go to the bottom of this page's Conclusion to find out how to make use of B-em 2.2.3kk and the BBC Micro Manic Miner hacks.

Hacking infinite lives

I first created a small C program to find out where the Miner Willy lives variable is located. Then I took three RAM dumps of a running Manic Miner: first with three lives, second with two lives and finally third with one life left. The C program to analyze the dumps is as follows:

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

#define MEM_DUMP_SIZE 65536

int main(void)
{
	FILE *f1, *f2, *f3;
	unsigned char membuf1[MEM_DUMP_SIZE];
	unsigned char membuf2[MEM_DUMP_SIZE];
	unsigned char membuf3[MEM_DUMP_SIZE];
	int i;

	f1 = fopen("first", "r");
	fread(membuf1, MEM_DUMP_SIZE, 1, f1);
	fclose(f1);

	f2 = fopen("second", "r");
	fread(membuf2, MEM_DUMP_SIZE, 1, f2);
	fclose(f2);

	f3 = fopen("third", "r");
	fread(membuf3, MEM_DUMP_SIZE, 1, f3);
	fclose(f3);

	for (i = 0; i < MEM_DUMP_SIZE; i++) {
		if (membuf1[i] == 0x02 && membuf2[i] == 0x01 && membuf3[i] == 0x00) {
				printf("address %d could control lives number\n", i);
		}
	}

	return 0;
}

I got the following output:

[kalevi@localhost memdumps]$ ./a.out 
address 26558 could control lives number

I wrote number 4 to the suggested RAM location 26558, killed Miner Willy and yes, the location was right. Then I created a second C program to examine first dump:

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

#define MEM_DUMP_SIZE 65536

int main(void)
{
	FILE *f;
	unsigned char membuf[MEM_DUMP_SIZE];
	int i;

	f = fopen("first", "r");
	fread(membuf, MEM_DUMP_SIZE, 1, f);
	fclose(f);

	/* Look for LDA $67be */
	for (i = 0; i < MEM_DUMP_SIZE - 3; i++) {
		if (membuf[i] == 0xad && membuf[i+1] == 0xbe && membuf[i+2] == 0x67) {
				printf("address %d %x matches\n", i, i);
		}
	}

	return 0;
}

That program gave me two clues:

[kalevi@localhost memdumps]$ ./a.out
address 26505 6789 matched
address 26597 67e5 matched

Then I had a problem: I needed to disassemble the machine code, but my primitive machine language monitor only supported RAM writes and raw RAM dumps to a file. I did not want to code a disassembler, because it would have been very time-consuming. I wanted to use an existing 6502 disassembler. I found DCC6502 from github.com and surely enough it was simple and neat, containing only one C source file. But the damn program would always disassemble files from the beginning and I needed to disassemble from a specific offset!

So I had to digress and add a command line option -s to specify the starting offset. The code was extremely simple to understand, so it took only 10-15 minutes. I will probably do a git pull request to the author, because my patch is nice and useful.

I could then disassemble the machine code, but the first clue did not look promising, so I moved on to the second:

[kalevi@localhost memdumps]$ dcc6502 -o 26597 -s 26597 -m 50 first 
; Source generated by DCC6502 version v2.0
; For more info about DCC6502, see https://github.com/tcarmelveilleux/dcc6502
; FILENAME: first, File Size: 50, ORG: $67E5
;---------------------------------------------------------------------------
$67E5   LDA $67BE       ;
$67E8   CMP #$00        ;
$67EA   BEQ $67FA       ;
$67EC   DEC $67BE       ;
$67EF   LDA $80         ;
$67F1   JSR $62C3       ;
$67F4   JSR $62F4       ;
$67F7   JMP $6208       ;
$67FA   LDA #$32        ;
$67FC   STA $71         ;
$67FE   LDA #$80        ;
$6800   STA $70         ;
$6802   LDX #$29        ;
$6804   LDY #$00        ;
$6806   TYA             ;
$6807   STA ($70),Y     ;
$6809   INY             ;
$680A   BNE $6807       ;
$680C   INC $71         ;
$680E   DEX             ;
$680F   BNE $6807       ;
$6811   JSR $2EF0       ;
$6814   LDX #$00        ;
$6816   LDA $0000,X     ;

With 6502 CPU, DEC stands for "DECrease memory" and I was pretty sure that this was indeed the right code to get rid of! Manic Miner was making lives variable smaller, so I prevented it by inserting three NOP opcodes (0xEA in hex, 234 in decimal) to replace the DEC and its 16-bit operand (split in two 8-bit values):

[kalevi@localhost memdumps]$ telnet 127.0.0.1 9999
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
s 26604 234
OKAY
s 26605 234
OKAY
s 26606 234
OKAY
q
QUITTING
Connection closed by foreign host.

Hacking room selection

Now that the infinite lives case was done, it was time to investigate room selection. I played Manic Miner and took two RAM dumps: second after I reached the second room and third after reaching the third room. I already had the first RAM dump from the lives investigation, so I just copied it to the new memdumps2 directory. Then I created my third small C program:

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

#define MEM_DUMP_SIZE 65536

int main(void)
{
	FILE *f1, *f2, *f3;
	unsigned char membuf1[MEM_DUMP_SIZE];
	unsigned char membuf2[MEM_DUMP_SIZE];
	unsigned char membuf3[MEM_DUMP_SIZE];
	int i;

	f1 = fopen("first", "r");
	fread(membuf1, MEM_DUMP_SIZE, 1, f1);
	fclose(f1);

	f2 = fopen("second", "r");
	fread(membuf2, MEM_DUMP_SIZE, 1, f2);
	fclose(f2);

	f3 = fopen("third", "r");
	fread(membuf3, MEM_DUMP_SIZE, 1, f3);
	fclose(f3);

	for (i = 0; i < MEM_DUMP_SIZE; i++) {
		if (membuf1[i] == 0x00 && membuf2[i] == 0x01 && membuf3[i] == 0x02) {
				printf("address %d could control room number\n", i);
		}
	}

	return 0;
}

So this time I was looking for RAM locations that were increasing by one. I got the following result:

[kalevi@localhost memdumps2]$ ./a.out 
address 25437 could control room number

I tried to modify the RAM location:

kalevi@localhost memdumps2]$ telnet 127.0.0.1 9999
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
s 25437 5
OKAY
q
QUITTING
Connection closed by foreign host.

But it was a total failure - no observable effect. I killed Miner Willy, but he was not transported to a new room. So I created a fourth C program:

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

#define MEM_DUMP_SIZE 65536

int main(void)
{
	FILE *f;
	unsigned char membuf[MEM_DUMP_SIZE];
	int i;

	f = fopen("first", "r");
	fread(membuf, MEM_DUMP_SIZE, 1, f);
	fclose(f);

	/* Look for STA $635d */
	for (i = 0; i < MEM_DUMP_SIZE - 3; i++) {
		if (membuf[i] == 0x8d && membuf[i+1] == 0x5d && membuf[i+2] == 0x63) {
				printf("address %d %x matches\n", i, i);
		}
	}

	return 0;
}

In fact I created several program variants looking for LDA, LDX and LDY commands, but none of them found anything. STA absolute finally matched:

[kalevi@localhost memdumps2]$ ./a.out 
address 25368 6318 matches
address 25397 6335 matches

I disassembled the first location, but it did not look good, how about the second? I backtracked from the suggested RAM location a little bit, trying earlier addresses before STA $635D:

[kalevi@localhost memdumps2]$ dcc6502 -o 25387 -s 25387 -m 50 first 
; Source generated by DCC6502 version v2.0
; For more info about DCC6502, see https://github.com/tcarmelveilleux/dcc6502
; FILENAME: first, File Size: 50, ORG: $632B
;---------------------------------------------------------------------------
$632B   ADC #$0A        ;
$632D   STA $635C       ;
$6330   JMP $6310       ;
$6333   STA $80         ;
$6335   STA $635D       ;
$6338   DEC $635D       ;
$633B   LDA $6B7F       ;
$633E   CMP #$00        ;
$6340   BEQ $6351       ;
$6342   LDA $80         ;
$6344   CMP #$13        ;
$6346   BCC $6351       ;
$6348   LDA #$01        ;
$634A   STA $80         ;
$634C   LDA #$00        ;
$634E   NOP             ;
$634F   NOP             ;
$6350   NOP             ;
$6351   RTS             ;
$6352   .byte $DF       ; INVALID OPCODE !!!

$6353   STX $8C8D       ;
$6356   .byte $EB       ; INVALID OPCODE !!!

$6357   .byte $8B       ; INVALID OPCODE !!!

$6358   TXA             ;
$6359   SBC #$89        ;
$635B   DEY             ;
$635C   NOP             ;

I looked at STA $80 right before STA $635D and remembered that ORIC Manic Miner had its room variable stored in the zero page so $80 (128 decimal) could be what I was looking for! So I tried to write to that RAM location:

[kalevi@localhost memdumps2]$ telnet 127.0.0.1 9999
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
s 128 4
OKAY
q
QUITTING
Connection closed by foreign host.

And to my great delight, it finally did the trick! After Miner Willy died once, he was transported to a new room! Note that this BBC Micro version room variable starts from number one, not from zero like on Commodore 16, Commodore 64 and ORIC versions, for example.

BBC Micro Manic Miner rooms

Here are the room numbers and their names:

room numbername
1Central Cavern
2The Cold Room
3The Menagerie
4Abandoned Uranium Workings
5Eugene's Lair
6Processing Plant
7The Vat
8Miner Willy Meets The Kong Beast
9Wacky Amoebatrons
10The Endorian Forest
11Attack Of The Mutant Telephones
12Return Of The Alien Kong Beast
13Ore Refinery
14Skylab Landing Bay
15The Bank
16The Sixteenth Cavern
17The Warehouse
18Amoebatrons' Revenge
19The Meteor Storm
20The Final Barrier

Conclusion

I hope this article provided you with a little bit of interesting information about some simple 6502 based machine language/assembly hacking. I am by no means an expert, so most things are new to me. To summarize, if you want to try these Manic Miner hacks, you need my special B-em 2.2.3kk release. It contains support for joystick-to-keyboard mapping (i.e. you can use USB controller with games that support only keyboard on BBC Micro) and it has a pretty ugly TCP-server hack to enable two basic machine language monitor features (i.e. ability to dump BBC Micro RAM to files and ability to write to arbitrary BBC Micro RAM locations).


The required steps are:


Have fun! I know I will, very soon. Now I am too tired to play Manic Miner.

manic miner black t-shirt