ITKarma pictureMany years ago I happened to Play Dendy's Snake Rattle'n'Roll game. I didn’t succeed then, because of the well-known in narrow circles bug with a fountain at level 7. Yes, and at the moment the game has not been completed. Progress has so far stopped at the last level because of its complexity. The game itself was rather non-standard for NES. It was an isometric world in which you had to climb the top, collecting bonuses along the way, eating nibbles (local animals) and fighting with legs, checkers and other mushrooms. It seems to be nothing unusual, but moving further through the levels I noticed that the world, although it was divided into levels, was a single whole, it was just that each of the levels occurred in another limited part of this world. And then one day I wanted to get a 3D model of this world, in order to print myself a souvenir on a 3D printer. Considering the characteristics of NES iron, I imagined that it would not be very simple, as it turned out to be really up to you to judge. So, if you are interested in exploring this world - welcome to Cat.

0. Landmark


As a guide, take this picture, its resolution is 2000x4000, so I’ll hide the pad spoiler.

Snake Rattle'n'Roll World
 image

Unfortunately, I don’t know the author, but done super !.

1. Search for others' experiences


Snake Rattle'n'Roll Level 1Given that I have no experience in parsing The assembler of the processor MOS6502 , which was used in NES, I decided to look for whether anyone had already posted the addresses at which the level and its format are stored. All that I could find (two years ago, it should be noted, maybe something has changed now) was the site http ://datacrystal.romhacking.net/wiki/Snake_Rattle_N_Roll: ROM_map ,
whence we can assume that the level at us has the sizes 64х64 and each block is encoded by one byte. A total of four kilobytes per level. One byte is a bit small, but if you only encode the block height there, it might be possible to allocate a couple more bits to some flags. So I thought...

So, open the ROM file, go to offset 0x63D0, see what is stored there:

CDMY0CDMY

If we assume that the level is built from the lower left corner, everything seems to converge. To make it clearer, I’ll cite the beginning of each line in the reverse order vertically:

CDMY1CDMY

If you compare with the screenshot at the beginning of this part, you can see the pattern. Great, I thought, and decided to visualize this business.

2. First visualization attempt


The question arose than to visualize. I didn’t really want to contact any file format, I wanted to quickly check that everything is correct. After looking at the list of installed programs, I found only two that could help. Oddly enough, this is Excel , in which you can build a chart of columns for each row, and 3D Studio Max . It contains a macro language, and you can write a program that generates a geometry construction macro from data from a file. So I did. I did not work with macros in 3D Studio, but I looked with the help of a tool for recording them, how and what is arranged. I put in a simple program. I started it, got a script for max and... And it turned out not at all what I expected.

For those who are interested in the code
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint8_t map[4096]; void read_world(); void genBox(uint8_t x, uint8_t y, uint16_t high); uint8_t getHigh(uint8_t x, uint8_t y); FILE * max_out; int main(){ read_world(); max_out=fopen("level.ms", "w"); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ genBox(x, y, getHigh(x,y)); } } fclose(max_out); return 0; } uint8_t getHigh(uint8_t x, uint8_t y){ return map[y*64 + x]; } void genBox(uint8_t x, uint8_t y, uint16_t high){ uint8_t color; uint8_t color_map[2][2]={{1,0}, {0,1}}; fprintf(max_out, "Box lengthsegs:1 widthsegs:1 heightsegs:1 length:4 width:4 height:%d mapCoords:off pos:[%d,%d,0] name:\"Box[%02d:%02d]\" ", high*4, x*4, y*4, x,y); color=color_map[(x % 2)][(y % 2)]; if(high > 0){ if(color == 1){ fprintf(max_out, "wirecolor:(color 00 200 00)"); } else { fprintf(max_out, "wirecolor:(color 00 150 00)"); } } else { fprintf(max_out, "wirecolor:(color 00 00 255)"); } fprintf(max_out, "\r\n"); } void read_world(){ uint32_t readed; FILE * file; file=fopen("Snake_Rattle'n_Roll_(U).nes", "rb"); fseek(file, 0x63D0, SEEK_SET); readed=fread(map, 4096, 1, file); printf("Map Readed: %d\r\n", readed); fclose(file); } 


image

From other angles
ITKarma picture


ITKarma picture

In general, in some places the geometry is guessed, but not that much. I tried to look for patterns of the appearance of some blocks, but never found them. I had to take on the debugger.

3. Search for block designation principles.


The realization has come that life is pain. We'll have to look at the assembler 6502 and try to understand how it works inside. So, we take FCEUX-2.2.3 , well, simply because I already have it, and I don’t really know other tools.

What we have at the moment: there is a block in ROM 4 kilobytes, the game somehow builds a scene on it. The block can be located in both PPU and CPU space, but there is a hope that it is active in the main time.

Explanation
Dandy cartridges often had more memory than the set-top box could address, and so it could get to this memory, they came up with mappers, in this case MMC1, the game makes “magic” entries at specific addresses, after which maper changes a piece of ROM memory that is readable by the console. Learn more here .

I downloaded ROM, launched the game and entered the first level. After that, I opened Debug- > Hex Editor, made Edit → Find as a search template, knocked out a sequence from the offset 0x63D0, namely “13 04 04 04 04 00 00 00”, and this time I got lucky. What was needed was found at 0xE3C0 (I guess that there is a correct way to search for this address, but I was too lazy to look for it).

We set the breakpoint to read this byte, and go ahead a bit so that the game needs to redraw the level, and see this code:

CDMY2CDMY

What we see here: the first command we read into the register is loaded A with the value located at 0xE3C0, and it is there that the lower left corner of the level is located. Then there is a check that we didn’t read zero, then what we read, copy to register X and use it as an offset relative to the address, 0xD069.

That is, at address 0xD069 is stored, something that converts the block ID from the address 0x63D0 to something else.

Remember the first line from the map

CDMY3CDMY

Let's see what is stored in memory at such offsets

CDMY4CDMY

Total at offset 0x13 we see 5, at offset 0x04 we see 2, and at offset 0x01 we see 1.

If you look at the picture of the beginning of the first level, it looks quite similar. Well, it's time to check. Right-click on the address 0xD069 and select Go here in ROM File, after which we get to the address 0x5079. Modify the code generating the macro.

The same code with improvements
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint8_t map[4096]; uint8_t high_map[256]; void read_world(); void genBox(uint8_t x, uint8_t y, uint16_t high); uint8_t getHigh(uint8_t x, uint8_t y); FILE * max_out; int main(){ read_world(); max_out=fopen("level.ms", "w"); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ genBox(x, y, getHigh(x,y)); } } fclose(max_out); return 0; } uint8_t getHigh(uint8_t x, uint8_t y){ return high_map[map[y*64 + x]]; } void genBox(uint8_t x, uint8_t y, uint16_t high){ uint8_t color; uint8_t color_map[2][2]={{1,0}, {0,1}}; fprintf(max_out, "Box lengthsegs:1 widthsegs:1 heightsegs:1 length:4 width:4 height:%d mapCoords:off pos:[%d,%d,0] name:\"Box[%02d:%02d]\" ", high*4, x*4, y*4, x,y); color=color_map[(x % 2)][(y % 2)]; if(high > 0){ if(color == 1){ fprintf(max_out, "wirecolor:(color 00 200 00)"); } else { fprintf(max_out, "wirecolor:(color 00 150 00)"); } } else { fprintf(max_out, "wirecolor:(color 00 00 255)"); } fprintf(max_out, "\r\n"); } void read_world(){ uint32_t readed; FILE * file; file=fopen("Snake_Rattle'n_Roll_(U).nes", "rb"); fseek(file, 0x63D0, SEEK_SET); readed=fread(map, 4096, 1, file); printf("Map Readed: %d\r\n", readed); fseek(file, 0x5079, SEEK_SET); readed=fread(high_map, 256, 1, file); printf("HighMap Readed: %d\r\n", readed); fclose(file); } 


Now the geometry of the level is perfectly guessed, even the last levels are there, only they start from zero in height, apparently the level height of 255 was not enough. Fortunately, it is as if squared, and not spread out across the map, so you can simply raise it above the rest of the levels.

ITKarma picture

Other views
Beginning of the second level:

ITKarma picture

Hidden Levels 9-10-11:

ITKarma picture

A brief summary of the addresses 0x63D0 stores the block IDs, which, using the array at address 0x5079, are converted to the height of the block.

But the block, in addition to the height, is also a type of block: earth, water, a hatch, judging by the picture according to the obtained geometry, the scales and spittoon nibbles also have a height from this array. Well, then you need to somehow search for the type of block. So we leave the breakpoint to read the ID of the first block and wait for it to work somewhere else. But it does not work. Well, it remains to be hoped that it is somewhere alongside the code that calculated the height. Let's see what there is like a sample by index.

CDMY5CDMY

At 0xA631, something similar, if you hope that X does not change above. What happens in this piece, we are interested in code starting with 0xA62C

So:

  1. X move to A
  2. For A , we shift to the right (the least significant bit falls into the processor flag C )
  3. Do an OR operation with the contents of memory cell 0x00FA
  4. Now we are A moving to X
  5. Read A from cell 0xCF6A with offset X
  6. Check if the flag C is cocked, if not, then immediately go to the address 0xA63A, if it is set, then do four shifts to the right
  7. We execute AND c over the register A with the number 0x0F (cut off the upper nibble)

All sorts of checks went on, I don’t really want to delve into them. I looked at what was in the 0xFA cell at the time of execution, it was 0. It was not clear while it was changing or not, but to look for laziness. We look in memory of what is located at 0xCF6A

CDMY6CDMY

Recall the first line of level

CDMY7CDMY

So we need to convert these numbers according to the above algorithm, as a result we get for all 3 values ​​0. It seems to be true. You can try to write its ID over each block.
We add the program, and for one we raise the levels 9-10-11.

Appending Code
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint8_t map[4096]; uint8_t high_map[256]; uint8_t block_type[256]; void read_world(); void genBox(uint8_t x, uint8_t y, uint16_t high, uint8_t type); void genText(uint8_t x, uint8_t y, uint16_t high, uint8_t type); uint8_t getHigh(uint8_t x, uint8_t y); uint8_t getBlockType(uint8_t x, uint8_t y); #define LEVEL9_UP (114) FILE * max_out; int main(){ uint32_t i; read_world(); max_out=fopen("level.ms", "w+"); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ genBox(x, y, getHigh(x,y), getBlockType(x, y)); genText(x,y,getHigh(x,y), getBlockType(x, y)); } } fclose(max_out); return 0; } uint8_t getHigh(uint8_t x, uint8_t y){ return high_map[map[y*64 + x]]; } uint8_t getBlockType(uint8_t x, uint8_t y){ uint8_t block_id; uint8_t ret; block_id=map[y*64 + x]; ret=block_type[block_id >> 1]; if((block_id & 0x01) == 1) { ret=ret >> 4; } ret &= 0x0F; return ret; } void genText(uint8_t x, uint8_t y, uint16_t high, uint8_t type){ float fy; fy=y*4 - 1.5; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "text size:5 font:\"Courier New\" text:\"%X\" pos:[%d,%03.01f,%d.1] wirecolor:(color 108 8 136) name:\"TX[%02d:%02d]\" \r\n", type, x*4, fy, high*4, x,y); } void genBox(uint8_t x, uint8_t y, uint16_t high, uint8_t type){ uint8_t color; uint8_t color_map[2][2]={{1,0}, {0,1}}; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "Box lengthsegs:1 widthsegs:1 heightsegs:1 length:4 width:4 height:%d mapCoords:off pos:[%d,%d,0] name:\"Box[%02d:%02d][BT%02X]\" ", high*4, x*4, y*4, x,y, type); color=color_map[(x % 2)][(y % 2)]; if((high > 0) && (high < 114)){ if(color == 1){ fprintf(max_out, "wirecolor:(color 00 200 00)"); } else { fprintf(max_out, "wirecolor:(color 00 150 00)"); } } else if (high >= 114){ fprintf(max_out, "wirecolor:(color 200 200 250)"); } else { fprintf(max_out, "wirecolor:(color 00 00 255)"); } fprintf(max_out, "\r\n"); } void read_world(){ uint32_t readed; FILE * file; file=fopen("Snake_Rattle'n_Roll_(U).nes", "rb"); fseek(file, 0x63D0, SEEK_SET); readed=fread(map, 4096, 1, file); printf("Map Readed: %d\r\n", readed); fseek(file, 0x5079, SEEK_SET); readed=fread(high_map, 256, 1, file); printf("HighMap Readed: %d\r\n", readed); fseek(file, 0x4F7A, SEEK_SET); readed=fread(block_type, 256, 1, file); printf("block_type Readed: %d\r\n", readed); fclose(file); } 


ITKarma picture
A few creepy quality pictures
ITKarma picture
ITKarma picture
ITKarma picture

The assumption turned out to be true, all the blocks are signed accordingly with their graphic representation, but at the last levels, something is wrong, if you look at the big picture, or right in the game, you can see that some blocks with the same functionality have different IDs. We recall the 0xFA memory cell ignored above. We set a breakpoint on it, and look when it changes.

I had to go through the level, and only when moving to the next one we have a trigger and here is a piece of code:

CDMY8CDMY

Here we just read what was in the 0xAA cell, compare with 0x08, do a right shift, but not normal, and when it is taken from the C flag to the high order, and it will be set by the CMP command if the value in the 0xAA cell is greater than or equal to 0x08. After using AND, we clear all bits except the high order bit. And we write it already in 0xFA.It remains to find out what is stored in the 0xAA cell, but here a site comes to our aid on which, we found the location of the level
http://datacrystal.romhacking.net/wiki/Snake_Rattle_N_Roll:RAM_map

And the number of the current level is stored there, and the levels are counted from zero. From what we get for levels one through eight there is written 0x00, for levels greater than the eighth 0x80. We correct the code considering this feature, and we get the correct values ​​for all levels.

Fix the code
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint8_t map[4096]; uint8_t high_map[256]; uint8_t block_type[256]; void read_world(); void genBox(uint8_t x, uint8_t y, uint16_t high, uint8_t type); void genText(uint8_t x, uint8_t y, uint16_t high, uint8_t type); uint8_t getHigh(uint8_t x, uint8_t y); uint8_t getBlockType(uint8_t x, uint8_t y); #define LEVEL9_UP (114) FILE * max_out; int main(){ uint32_t i; read_world(); max_out=fopen("level.ms", "w+"); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ genBox(x, y, getHigh(x,y), getBlockType(x, y)); genText(x,y,getHigh(x,y), getBlockType(x, y)); } } fclose(max_out); return 0; } uint8_t getHigh(uint8_t x, uint8_t y){ return high_map[map[y*64 + x]]; } uint8_t getBlockType(uint8_t x, uint8_t y){ uint8_t block_id; uint8_t ret; uint8_t level_id; level_id=0; if((x<29) && (y>35)){ level_id=0x80; } block_id=map[y*64 + x]; ret=block_type[(block_id >> 1) | level_id]; if((block_id & 0x01) == 1) { ret=ret >> 4; } ret &= 0x0F; return ret; } void genText(uint8_t x, uint8_t y, uint16_t high, uint8_t type){ float fy; fy=y*4 - 1.5; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "text size:5 font:\"Courier New\" text:\"%X\" pos:[%d,%03.01f,%d.1] wirecolor:(color 108 8 136) name:\"TX[%02d:%02d]\" \r\n", type, x*4, fy, high*4, x,y); } void genBox(uint8_t x, uint8_t y, uint16_t high, uint8_t type){ uint8_t color; uint8_t color_map[2][2]={{1,0}, {0,1}}; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "Box lengthsegs:1 widthsegs:1 heightsegs:1 length:4 width:4 height:%d mapCoords:off pos:[%d,%d,0] name:\"Box[%02d:%02d][BT%02X]\" ", high*4, x*4, y*4, x,y, type); color=color_map[(x % 2)][(y % 2)]; if((high > 0) && (high < 114)){ if(color == 1){ fprintf(max_out, "wirecolor:(color 00 200 00)"); } else { fprintf(max_out, "wirecolor:(color 00 150 00)"); } } else if (high >= 114){ fprintf(max_out, "wirecolor:(color 200 200 250)"); } else { fprintf(max_out, "wirecolor:(color 00 00 255)"); } fprintf(max_out, "\r\n"); } void read_world(){ uint32_t readed; FILE * file; file=fopen("Snake_Rattle'n_Roll_(U).nes", "rb"); fseek(file, 0x63D0, SEEK_SET); readed=fread(map, 4096, 1, file); printf("Map Readed: %d\r\n", readed); fseek(file, 0x5079, SEEK_SET); readed=fread(high_map, 256, 1, file); printf("HighMap Readed: %d\r\n", readed); fseek(file, 0x4F7A, SEEK_SET); readed=fread(block_type, 256, 1, file); printf("block_type Readed: %d\r\n", readed); fclose(file); } 


4. Appearance of blocks


ID Appearance Level 1-8
0 ITKarma picture The usual block on which you can walk, if located at zero height (1.2 levels), it looks like water, but allows you to jump from it, unlike water at other heights, painted in the form of a chessboard, the lower left cell light, then alternate. Vertically has a pattern of interwoven vegetation.
2 ITKarma picture A hatch with a lid, contains bonuses or traps inside, after the fifth level it does not occur
4 ITKarma picture Nibloid, from bottom to top has an intricate mechanical pattern, the top is crowned with two sockets.
5 ITKarma picture The usual block that you can walk on top of it looks like a flat surface, on the side of the left plane there is a brickwork texture, with either a crack or a plant on the right
6 ITKarma picture The pyramidal block, when you try to land on it, the snake falls into the abyss
7 ITKarma picture Same as block six of just a different color
8 ITKarma picture Scales, look like a scale with divisions, and a big bell on top
A ITKarma picture Block of water when viewed from above, and a waterfall if viewed from the side
ID Appearance Level 9-11
0 The block denotes a hole where you can fail; it is not drawn in any way
4 ITKarma picture The place of the battle with the final boss, looks like a moon surface (or cheese :)
5 ITKarma picture Smooth block of ice, ice texture on top, side cracked ice surface
6 ITKarma picture The pyramidal block, when you try to land on it, the snake falls into the abyss
8 ITKarma picture Scales, look like a scale with divisions, and a big bell on top
9 ITKarma picture Ice block with a left-down inclination (on the observer)
B ITKarma picture Ice block with a right-down inclination (to the observer)

This information is enough to build a 3D model and print it. But now there is a problem, namely the printer at work, and I am on self-isolation. And this is only the first problem. If you load the full model into the slicer, then with the size of one cell in four millimeters, and filling in ten percent, the resulting model will be printed for five days. And considering that a model of this size does not fit in our modest working printer. You will have to print in parts, which will increase the printing time. Therefore, put it off for the future. But while the excitement of researching this game has not faded, we will continue to study what else we can get out of the game.

5. Bonuses in hatches


ITKarma pictureIn the first five levels, hatches in which can be: traps, bonus items, transitions to the bonus level or warp to another level are placed on the map. You can of course go through the levels and rewrite everything manually, but it's fast and boring. Or you can try to find where and how it is stored in the game. This time I chose the option of loading/saving the state of the game, and using the Tool- > Ram search... tool and using the Equal/Not Equal options, I started to see what changes. After several attempts, it became noticeable that the cell 0x1B1 was changing, before opening the hatch there 0x41 after opening 0x4E. I tried to open the hatch next to the cell changed 0x1B3, was 0x29 became 0x2E. You can already notice that the lower four bits change in both cases.But we still set the breakpoint, and see if it’s easy to figure out how the game operates with these bytes.

We try to open the hatch and find ourselves in such a piece of code:

CDMY9CDMY

I won’t paint everything, I’ll just say that here http://datacrystal.romhacking.net/wiki/Snake_Rattle_N_Roll : RAM_map addresses are described
0x4D7 and 0x4C3 , namely, this is the player’s coordinates on X on the world map in “pixels”, and the cell width is sixteen pixels, this is set manually metering. It turns out that the high part of the byte 0x4D7 and the youngest 0x4C3 form an X coordinate in blocks. Only here these parts of the byte are interchanged, and compared with the cell 0x1B0, and there at the moment 0x80 is just stored (and the first hatch that I checked just is located at coordinates 8: 4 if you count from zero). Cell 0x4FF stores the player’s coordinate on Y and only the oldest part is used for comparison. And finally, after all this, the lower four bits are taken and then there are a lot of comparisons. It turns out this piece of code is looking for the coordinates of the open hatch starting at address 0x1B0 and then moving each step two bytes further, and so on until it finds the desired hatch. There is no way out of impossibility to find. Therefore, if you change the coordinates to nonexistent, then when you try to open the hatch, the game will hang.

Tearing the Cover
In fact, the author is now misleading the readers, since the analysis of the data has been going on for quite some time, and as a result, the author first decrypted the data analytically, without looking into the code. It was during the search and all kinds of experiments that it turned out that the game was hanging. But now he realized that it’s easier, more correct and faster.

As a result, the type of bonus should be stored in the last four bits, 0x1B1, which can be easily checked with the HEX editor. Having experimented, we get the following list of values ​​of the last byte:
ID Description
0x0 Red Bomb
0x1 Red ball
0x2 Inverse of motion
0x3 Acceleration
0x4 Critters
0x5 Alarm Clock
0x6 Transition to Bonus
0x7 Life
0x8 Foot
0x9 Blue/purple ball
0xA Yellow Ball
0xB Invulnerability
0xC Transition to Warp
0xD Bomb life (a bonus that at the beginning looks like life but then turns into a bobma)
0xE Empty (maybe the first letter from Empty or maybe just a coincidence)
0xF Transition to Warp

So now we need to figure out where it is stored in ROMe. And how it is loaded into memory. We set a breakpoint for writing to cell 1B0, and for some reason unknown to me, FCEUX starts responding to the CDMY10CDMY code, although it seems to be reading, not writing from the wrong cell. If suddenly someone knows why this is happening, write in the comments.

Okay, we will make the assumption that the entry in the cell is performed by the CDMY11CDMY instruction and that means that at A at the moment of entering the first level it should be 0x80 we will help FCEUX adding the condition to the breakpoint CDMY12CDMY

And we get the right place in the code:

CDMY13CDMY

First, save the level number in Y , then in A read the number at offset 0x700 + Level_number, transfer A to Y . Then we read the byte at offset 0x700 + Y and copy it to 0x1B0 + X, increment X and Y , check that X is not equal to 0x30 and if it so repeat the copy cycle.

Let's see what lies at offset 0x700 in memory at the time of copying:

CDMY14CDMY

Following the algorithm for the first level, we take the value at the address 0x700 in this case it is 0x06 and then copy 48 bytes from the address 0x700 + 06 to the area 0x1B0.After checking, we were able to verify that the data was exactly what we needed. Then an interesting thing is obtained, bonuses are always copied by 48 bytes. But if you look at the first six offsets (I recall after the sixth level, hatches no longer occur), it becomes obvious that the data in the memory between the levels intersect, although knowing how the bonuses are checked, we can say that this is not a problem. Now it remains to find where this data is stored in ROM. Since this data is stored at 0x700, and this is RAM, it means they were loaded there from the outside.

You can look for a place where they are loaded, or you can try your luck and look for the above sequence in ROM. And the only occurrence of such a record, at address 0xF4D0, we now calculate the length of the block, the offset for the sixth level 0x7E is the length of the block 0x30 total 0xAE.

Downloading and parsing all the bonuses at once, we got three intersections. For one thing, I know that it is true (cell 14:11), a tricky place that can be reached from two levels at once, and in the fifth there will be an alarm, but in the sixth it will be a warp to the eighth level. Two more apparently coincided due to the fact that they are on the same line [54:51] and [54:03], 51 in the hexadecimal system is 0x33, and only the first 4 bits are checked for Y, so they coincide in the end. If necessary, can be cut off with a hardcode. To draw in the graph it was too lazy for me, I just brought it to the console. I made sure that the data is as expected. And he left it, it is still not clear what to do next. And we still have a few places that would be interesting to clarify.

6. Bonus Levels


ITKarma pictureIn the first four levels there are transitions to bonus levels, where you can safely fear nothing to eat nibble. They are stored somewhere separately. The first bonus level is very simple in terms of geometry, elevation, and about 12 cells of unit height. But the attempt to search for this by pattern in ROM failed. This is worse, so there may be a lot of fun later on, and maybe not. If we assume that bonuses are built in the same way as the rest of the level, then they should use the table at 0xD069. We approach the transition to the bonus, set the reading point 0xD069-0xD168 and try to switch to the bonus, the game constantly reads to these addresses. Therefore, it was difficult to go to the bonus when the breakpoint was working. But if you turn on the break point, at the moment of transition to the bonus level, then the break point will work at the right time.

In the same place as in ordinary levels. CDMY15CDMY

But the address at which the level is read is changed, now the reading goes from the memory of the console, and not ROM. Let's see what lies there:

ITKarma picture

it is quite clearly visible that somewhere between 0x200 and 0x300 lies a bonus level. Only the upper right corner, here it became the lower left. We must now understand how he ends up there. Well, we take whatever cell from the block (I took 0x221 there was a well-recognized 0x3F), and put a breakpoint on it by record. During the game, something is constantly written there, so we add the condition CDMY16CDMY

And here it is:

CDMY17CDMY

Here I already had to rummage around, look for how and what works, although I had a lot of knowledge about the work of NES. A small digression, the cartridge contains two types of memory, program memory and sprite memory for the video processor (this is simplified). The processor has direct access to the program memory, but access to the video processor memory is only through the video processor registers. Well, both of them can be divided into switchable pages using the mapper. So, in this piece the data is pulled just from the memory of the video processor.

You can read more about the work of PPU here . Yes, a small addition to FCEUX can replace register addresses with friendly names, in this listing PPU_ADDRESS is 0x2006, PPU_DATA=0x2007.

Well, now we will analyze what and how. At the beginning, the level number is read, then it is shifted to the left, which is similar to multiplying by two, and is transferred to Y. Then it is read at the address 0x75B + Y and the PPU address register is sent, then the same thing is repeated for the address 0X75C + Y.By this we indicated the address from which we want to read data from the PPU. After an empty read is made from the PPU data register, this is a feature of the PPU, after writing the first read will contain obsolete data. And now the fun begins. The register X is reset, and the first read from the PPU occurs, which is written at the address 0x200 + X, the next value is read from the PPU and stored at the address 0x04, then we subtract what we saved in 0x200 + X to register A, increment X and if it has become equal to zero, there is a jump to exit this subprogram, if not, then save the obtained value again, and to the cell 0x200 + X, reduce the value in the cell 0x04, and if it is not zero, jump to the increment X, if it is equal, then jump again to read data from PPU register date.

If it’s easier to describe, this is a variation on the theme RLE encoding, the first byte describes what we write to memory, the second how many times we do it. The bonus size is 256 bytes, which gives a room size of 16x16.

And at 0x75B, eight bytes are stored that describe the level offset in the PPU, two bytes per offset, for a total of four bonus levels. Level offsets are:

CDMY18CDMY

We switch the HEX FCEUX editor to the PPU display (View- > PPU Memory) and go to the specified offset there we see CDMY19CDMY, (it is important to do this at the time of loading the level, otherwise the game may switch the memory bank and it will not be clear at the specified offset). If decrypted according to the specified algorithm, it is quite similar to the first bonus. Let's look for this sequence in the ROM file, and it is located at 0xE1EA, try to draw. And all the geometry is obtained both in the game and what I wanted:

Bonus Levels
ITKarma picture
ITKarma picture
ITKarma picture
ITKarma picture

Modified program
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint8_t map[4096]; uint8_t high_map[256]; uint8_t block_type[256]; uint8_t bonus_offset[6]; uint8_t bonuses[84][2]; uint8_t bonus_levels[1024]; uint16_t bl_offsets[4]={ 0x0000, 0x022A - 0x01DA, 0x029E - 0x01DA, 0x030E - 0x01DA, }; void read_world(); void genBox(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type); void genText(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type); uint8_t getHigh(uint8_t x, uint8_t y); uint8_t getBlockType(uint8_t x, uint8_t y); void bonuses_dec(); #define LEVEL9_UP (114) FILE * max_out; int main(){ uint32_t i; read_world(); max_out=fopen("level.ms", "w+"); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ genBox(max_out, x, y, getHigh(x,y), getBlockType(x, y)); genText(max_out,x, y, getHigh(x,y), getBlockType(x, y)); } } fclose(max_out); bonuses_dec(); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ if(getBlockType(x, y) == 2){ printf("BN[%02u:%02u]=[", x, y); for(uint8_t n=0; n<84; n++){ uint8_t bx, by, bt; bx=((bonuses[n][0] >> 4)& 0x0F) | ((bonuses[n][0] << 4) & 0xF0); by=((bonuses[n][1] >> 4)& 0x0F); bt=bonuses[n][1] & 0x0F; if((bx == x) && ((y & 0x0F) == by)){ printf("[%02u]%X ", n, bt); } } printf("]\r\n"); } } } return 0; } uint8_t getHigh(uint8_t x, uint8_t y){ return high_map[map[y*64 + x]]; } uint8_t getBlockType(uint8_t x, uint8_t y){ uint8_t block_id; uint8_t ret; uint8_t level_id; level_id=0; if((x<29) && (y>35)){ level_id=0x80; } block_id=map[y*64 + x]; ret=block_type[(block_id >> 1) | level_id]; if((block_id & 0x01) == 1) { ret=ret >> 4; } ret &= 0x0F; return ret; } void genText(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type){ float fy; fy=y*4 - 1.5; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "text size:5 font:\"Courier New\" text:\"%X\" pos:[%d,%03.01f,%d.1] wirecolor:(color 108 8 136) name:\"TX[%02d:%02d]\" \r\n", type, x*4, fy, high*4, x,y); } void genBox(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type){ uint8_t color; uint8_t color_map[2][2]={{1,0}, {0,1}}; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "Box lengthsegs:1 widthsegs:1 heightsegs:1 length:4 width:4 height:%d mapCoords:off pos:[%d,%d,0] name:\"Box[%02d:%02d][BT%02X]\" ", high*4, x*4, y*4, x,y, type); color=color_map[(x % 2)][(y % 2)]; if((high > 0) && (high < 114)){ if(type != 0xA){ if(color == 1){ fprintf(max_out, "wirecolor:(color 00 200 00)"); } else { fprintf(max_out, "wirecolor:(color 00 150 00)"); } } else { fprintf(max_out, "wirecolor:(color 00 00 230)"); } } else if (high >= 114){ fprintf(max_out, "wirecolor:(color 200 200 250)"); } else { fprintf(max_out, "wirecolor:(color 00 00 255)"); } fprintf(max_out, "\r\n"); } void bonuses_dec(){ uint32_t cnt, offset, i, j; uint8_t item, count, bnn; uint8_t x, y, block_id; FILE * file; char fname[32]; for(i=0; i<4; i++){ printf("Decode bonus level %d\r\n", i+1); sprintf(fname, "bonus_level_%d.ms", i+1); file=fopen(fname, "w+"); offset=bl_offsets[i]; cnt=0; x=0; y=0; do { item=bonus_levels[offset++]; count=bonus_levels[offset++]; for(j=0; j < count; j++){ cnt++; block_id=block_type[(item >> 1)]; if((item & 0x01) == 1) { block_id=block_id >> 4; } block_id &= 0x0F; genBox(file, x, y, high_map[item], block_id); genText(file,x, y, high_map[item], block_id); x++; if(x > 15){ y++; x=0;} } } while (cnt<256); fclose(file); } } void read_world(){ uint32_t readed; FILE * file; file=fopen("Snake_Rattle'n_Roll_(U).nes", "rb"); fseek(file, 0x63D0, SEEK_SET); readed=fread(map, 4096, 1, file); printf("Map Readed: %d\r\n", readed); fseek(file, 0x5079, SEEK_SET); readed=fread(high_map, 256, 1, file); printf("HighMap Readed: %d\r\n", readed); fseek(file, 0x4F7A, SEEK_SET); readed=fread(block_type, 256, 1, file); printf("Block_type Readed: %d\r\n", readed); fseek(file, 0xF4D0, SEEK_SET); readed=fread(bonus_offset, 6, 1, file); printf("Bonuses offsets: %d\r\n", readed); fseek(file, 0xF4D0+6, SEEK_SET); readed=fread(bonuses, 168, 1, file); printf("Bonuses Readed: %d\r\n", readed); fseek(file, 0xE1EA, SEEK_SET); readed=fread(bonus_levels, 1024, 1, file); printf("Bonus levels Readed: %d\r\n", readed); fclose(file); } 

7. Underwater Levels


ITKarma pictureAnd without which any good game cannot do without, correctly without an underwater level. I immediately remember the underwater level in the first Ninja Turtles, the bathyscaphe from Jim's Worm, Crash Bandicoot 3, and even part of the levels in Unreal. In general, even if they had normal difficulty, I never managed to enjoy them. There are such levels in this wonderful game, and oh my god they are just wonderful here, after a rather difficult seventh level, and before the very difficult ninth and tenth, we are allowed to relax and take a breath, thanks to the developers of Rare for this. But enough lyrics. The eighth level for the most part consists of five (you can skip the fifth) underwater rooms, in appearance they look like bonus level rooms, and the geometry is simply not looked for in rum. After searching, in the same place where they were, nothing was found for bonus levels either. We repeat everything that we did for bonus levels and find that now the level lies at 0x700, try to track who puts it there. We get this piece:

CDMY20CDMY

That is, who the level is loaded initially at 0x200, and then copied to 0x700, we rearrange the breakpoint to 0x200, and we end up in the same piece as for bonus levels. But the bonus was selected depending on the level number, and then there are five different rooms, and the level number does not change. So there is a chance that we get here from the already installed Y .

It's time to try the code tracer, run Debug → Trace Logger... set 100 lines should be enough, and at the moment the breakpoint is triggered, we see the following:

CDMY21CDMY
And so it is, the room number is taken from cell 0xС5 and counted from one, then three are added to it, and then, as well as with bonus levels. Getting Level Offsets:

CDMY22CDMY

And we see that the last two levels coincide, and so it is in the game. We look where these levels are located in ROM, these values ​​and find the address 0xE688. We correct the code and cases decryption. And here they are underwater levels that are entirely in the game and are not visible, the only way you can consider them in their entirety.

Underwater Levels
ITKarma picture

ITKarma picture

ITKarma picture

ITKarma picture

The code generator that I was already fed up with writing at this point
#include <stdio.h> #include <stdint.h> #include <stdlib.h> uint8_t map[4096]; uint8_t high_map[256]; uint8_t block_type[256]; uint8_t bonus_offset[6]; uint8_t bonuses[84][2]; uint8_t bonus_levels[1024]; uint16_t bl_offsets[4]={ 0x0000, 0x022A - 0x01DA, 0x029E - 0x01DA, 0x030E - 0x01DA, }; uint8_t uw_levels[1024]; uint16_t uw_offsets[5]={ 0x0000, 0x06FE - 0x0678, 0x0774 - 0x0678, 0x07DC - 0x0678, 0x07DC - 0x0678, }; void read_world(); void genBox(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type); void genText(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type); uint8_t getHigh(uint8_t x, uint8_t y); uint8_t getBlockType(uint8_t x, uint8_t y); void bonuses_dec(); void underwater_dec(); #define LEVEL9_UP (114) FILE * max_out; int main(){ uint32_t i; read_world(); max_out=fopen("level.ms", "w+"); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ genBox(max_out, x, y, getHigh(x,y), getBlockType(x, y)); genText(max_out,x, y, getHigh(x,y), getBlockType(x, y)); } } fclose(max_out); bonuses_dec(); underwater_dec(); for(uint8_t y=0; y<64; y++){ for(uint8_t x=0; x<64; x++){ if(getBlockType(x, y) == 2){ printf("BN[%02u:%02u]=[", x, y); for(uint8_t n=0; n<84; n++){ uint8_t bx, by, bt; bx=((bonuses[n][0] >> 4)& 0x0F) | ((bonuses[n][0] << 4) & 0xF0); by=((bonuses[n][1] >> 4)& 0x0F); bt=bonuses[n][1] & 0x0F; if((bx == x) && ((y & 0x0F) == by)){ printf("[%02u]%X ", n, bt); } } printf("]\r\n"); } } } return 0; } uint8_t getHigh(uint8_t x, uint8_t y){ return high_map[map[y*64 + x]]; } uint8_t getBlockType(uint8_t x, uint8_t y){ uint8_t block_id; uint8_t ret; uint8_t level_id; level_id=0; if((x<29) && (y>35)){ level_id=0x80; } block_id=map[y*64 + x]; ret=block_type[(block_id >> 1) | level_id]; if((block_id & 0x01) == 1) { ret=ret >> 4; } ret &= 0x0F; return ret; } void genText(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type){ float fy; fy=y*4 - 1.5; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "text size:5 font:\"Courier New\" text:\"%X\" pos:[%d,%03.01f,%d.1] wirecolor:(color 108 8 136) name:\"TX[%02d:%02d]\" \r\n", type, x*4, fy, high*4, x,y); } void genBox(FILE * max_out, uint8_t x, uint8_t y, uint16_t high, uint8_t type){ uint8_t color; uint8_t color_map[2][2]={{1,0}, {0,1}}; if((x<29) && (y>35)){ high += LEVEL9_UP; } fprintf(max_out, "Box lengthsegs:1 widthsegs:1 heightsegs:1 length:4 width:4 height:%d mapCoords:off pos:[%d,%d,0] name:\"Box[%02d:%02d][BT%02X]\" ", high*4, x*4, y*4, x,y, type); color=color_map[(x % 2)][(y % 2)]; if((high > 0) && (high < 114)){ if(type != 0xA){ if(color == 1){ fprintf(max_out, "wirecolor:(color 00 200 00)"); } else { fprintf(max_out, "wirecolor:(color 00 150 00)"); } } else { fprintf(max_out, "wirecolor:(color 00 00 230)"); } } else if (high >= 114){ fprintf(max_out, "wirecolor:(color 200 200 250)"); } else { fprintf(max_out, "wirecolor:(color 00 00 255)"); } fprintf(max_out, "\r\n"); } void bonuses_dec(){ uint32_t cnt, offset, i, j; uint8_t item, count, bnn; uint8_t x, y, block_id; FILE * file; char fname[32]; for(i=0; i<4; i++){ printf("Decode bonus level %d\r\n", i+1); sprintf(fname, "bonus_level_%d.ms", i+1); file=fopen(fname, "w+"); offset=bl_offsets[i]; cnt=0; x=0; y=0; do { item=bonus_levels[offset++]; count=bonus_levels[offset++]; for(j=0; j < count; j++){ cnt++; block_id=block_type[(item >> 1)]; if((item & 0x01) == 1) { block_id=block_id >> 4; } block_id &= 0x0F; genBox(file, x, y, high_map[item], block_id); genText(file,x, y, high_map[item], block_id); x++; if(x > 15){ y++; x=0;} } } while (cnt<256); fclose(file); } } void underwater_dec(){ uint32_t cnt, offset, i, j; uint8_t item, count, bnn; uint8_t x, y, block_id; FILE * file; char fname[32]; for(i=0; i<5; i++){ printf("Decode underwater level %d\r\n", i+1); sprintf(fname, "underwater_level_%d.ms", i+1); file=fopen(fname, "w+"); offset=uw_offsets[i]; cnt=0; x=0; y=0; do { item=uw_levels[offset++]; count=uw_levels[offset++]; for(j=0; j < count; j++){ cnt++; block_id=block_type[(item >> 1)]; if((item & 0x01) == 1) { block_id=block_id >> 4; } block_id &= 0x0F; genBox(file, x, y, high_map[item], block_id); genText(file,x, y, high_map[item], block_id); x++; if(x > 15){ y++; x=0;} } } while (cnt<256); fclose(file); } } void read_world(){ uint32_t readed; FILE * file; file=fopen("Snake_Rattle'n_Roll_(U).nes", "rb"); fseek(file, 0x63D0, SEEK_SET); readed=fread(map, 4096, 1, file); printf("Map Readed: %d\r\n", readed); fseek(file, 0x5079, SEEK_SET); readed=fread(high_map, 256, 1, file); printf("HighMap Readed: %d\r\n", readed); fseek(file, 0x4F7A, SEEK_SET); readed=fread(block_type, 256, 1, file); printf("Block_type Readed: %d\r\n", readed); fseek(file, 0xF4D0, SEEK_SET); readed=fread(bonus_offset, 6, 1, file); printf("Bonuses offsets: %d\r\n", readed); fseek(file, 0xF4D0+6, SEEK_SET); readed=fread(bonuses, 168, 1, file); printf("Bonuses Readed: %d\r\n", readed); fseek(file, 0xE1EA, SEEK_SET); readed=fread(bonus_levels, 1024, 1, file); printf("Bonus levels Readed: %d\r\n", readed); fseek(file, 0xE688, SEEK_SET); readed=fread(uw_levels, 1024, 1, file); printf("Underwater levels Readed: %d\r\n", readed); fclose(file); } 

8. Bonuses on the map


And as it turned out in the process, not only bonuses

ITKarma pictureBesides bonuses to hatches, there are just bonuses scattered on the map. It would be nice to find how they are stored. As with hatches, looking for changing cells, I found that when you take the first bonus, the value in cell 0x692 changes from 0x34 to 0x00. The same thing happens for cell 0x6A0 if you take life near the waterfall, and for cell 0x699 if you take another bonus. If you return 0x34 to these cells, then the bonuses reappear. By not cunningly looking for patterns, you can see that there are seven bytes between these elements. Further experiments showed that if you take 0x692 for the first byte, in the sequence the seventh is responsible for the type of bonus. What the sixth is responsible for is incomprehensible, but from the second to the fifth they are somehow responsible for the coordinates. We'll figure it out later, now we need to find where it all gets transferred to memory from, and how long this block is. We set the breakpoint on the record 0x692 and for convenience we add the condition A == # 34 We try to enter the first level. We find ourselves in such a place:

CDMY23CDMY

The article has grown, so a summary of this. The value is taken from cell 0x00C5 (the room number of the underwater level) if it is not zero, three are added to it, then the current level number is added to the resulting number, and multiplied by two. By the received index go to the table lying at the time of loading the level at 0x254. They take the offset from there, after which it calculates the block size, and the block from the PPU is copied to the address 0x653. We look at what is in the PPU at this moment, and then look for the same match in the ROM file, and get the address 0xE906.

We figured it out, now let's see how it is encoded, for example, take the first-level block:

CDMY24CDMY
ITKarma pictureIn the first column we see a number, 0x34 which, as we saw above, changes to 0x00 if you take the bonus. And it repeats four times. If you go over the level, there are four bonuses, two language extensions, life and an alarm clock. We also see that in the seventh column there are two 0x70 matches and two different numbers. It can be assumed that the seventh column is responsible for the type of bonus. After the experiments, this was partially confirmed.

But what happens if 0x34 is changed for anything else, for example, 0x35 is also repeated here. We change and "life" turns into a "checker". So, the first number is responsible for the type of element. What the remaining columns are responsible for, through experiments we were able to partially figure it out.The coordinates are the bytes from the second to the sixth, sixth and seventh bytes are responsible for the parameters, and for each type of element are decrypted in their own way. The coordinates are also rather strange encoded, graphically it can be represented as follows:

ITKarma picture

For those who understand the code, the entry is:

uint16_t x=data[2] + (256 * ((data[1] & 0xF0) >> 5)) ; uint16_t y=data[3] + (256 * ((data[1] & 0x0F) >> 2)); uint16_t z=data[4] + (256 * (((data[1] << 1) | ((data[5]) >> 7)) & 0x7)); 

The elements of the array are counted from zero, as expected. It should be borne in mind that we raised the last levels when rendering, the same thing must be done with these “things”. As it turned out, in this array not only bonuses are encoded, but also different elements of the map, enemies, the places where the nibble generators are located (as it turned out there is only a graphical representation on the map, and the generator itself is taken from this list), as well as doors and much more. Many elements in the last two bytes have their parameters encoded, I did not make out everything, but I will describe what I learned. Although much needs to be clarified. The list is pretty impressive:
ID Description
0x0A The door through which the snake enters or leaves the level, the parameters encoded is the input or the exit, and the orientation of the door
0x0B Scales, or rather the tongue of the scales, in the parameters, it seems, the initial height of the tongue of the scales is indicated
0x0D There is a “sneezer” of snakes in the fifth and sixth levels, in the parameters most likely the direction in which it works
0x0E Blades attacking snakes, in the parameters of the direction of movement of the blade
0x10 Most likely the flag, located on top of the world, during the battle with the final leg
0x13 Critters in levels up to the ninth, on the ninth to tenth, a block of ice.The parameters indicate the direction of movement, and the number of cells on which the movement and the color of the tooth brush occurs
0x14 Incomprehensible substance, shooting with needles in all directions
0x21 places? where the nibbles are fired from, the parameters, if any, are not clear
0x25 Carpet plane, in the parameters most likely the direction of movement, the number of cells that moves, and the inclusion of invisibility is possible
0x26 I didn’t understand what it is, but it’s clearly connected with bonus levels, at the first four levels of the game
0x27 It looks like the initial coordinates of the leg are on the level, but if the leg is in the hatch, then placing this element above the hatch causes the hatch to open automatically when you approach it, even if there is no leg in the hatch
0x2C Another toothbrush, moves in a circle, unlike others who move in a straight line
0x2D The door from which bells fly out at the sixth level, and the scuba gear for climbing the waterfall, in the door orientation parameters
0x2F Caviar from which fish hatch in underwater levels, in the parameters, it seems, the number of eggs
0x34 Bonuses arranged by levels: in the seventh byte the type of bonus is indicated, 0x6D - alarm clock, 0x6F - “continuum”, 0x70 - tongue extension, 0x71 - invulnerability, 0x72 - life, 0x73 - movement inversion, 0x74 - “key” motion accelerator
0x35 Enemies, in the first level checkers, in the second mushrooms, in the ninth and tenth, ice transparent balls, in the parameters the direction of movement, and the number of cells on which it occurs
0x36 Places where the anvils fall from, in the type anvil type (only color is possible)
0x37 Fountainlet, the very fountains that should throw the snakes up, and on which everything hung on most of the players in Russia, there seem to be no parameters
0x38 Drills crawling out of the ground at the seventh level, the parameters are not yans, but should be interesting enough
0x39 Also drill, but of a different color and possibly have different parameters
0x3A Falling balls, in the parameters most likely the height at which the ball falls, and the direction of movement
0x3B Bells from the sixth level, the parameters are most likely like the type 0x35
0x3D Perhaps the UFO hull from the last level, or its cabin
0x3E Perhaps the UFO hull from the last level, or its cabin
0x3F It is completely incomprehensible that this is possibly somehow connected with a rocket to the eighth level, or maybe not

However, having examined the seventh level, I found that the elements are clearly not enough. And there are no fountains, but in the game they appear after taking the alarm, and the article about the bug says that the game downloads new data at the time of taking this alarm (It should be noted that it seems only at the seventh level). To find where the rest is loaded from, it was enough to set a breakpoint to change the first element of the list, and we get into this piece:

CDMY25CDMY
That is, we copy seventy-seven bytes from the PPU at 0xF78, finding matches in ROM gives the address 0xEF88. That just goes right behind the main unit, with the data elements. Well, we must put all the points about the eighth level. Firstly, this is the only level without the main protagonist of the Leg, and secondly, this is the level of the elements in which they are loaded up to six times. The first time we enter the level, and then every time we get into the next underwater room. But here, too, there is a little trick, loading is not only for underwater rooms, but also for the main level, that's why when we go into the third room we hear the sound of a fountain, although it seems to be nowhere to be found.

9. Foot


- This leg is for the one who needs the leg. ITKarma picture
Throughout the game, at all levels of the game (except the eighth), we are pursued by one enemy, the Leg. She is the final boss. It will not be good to get around it, so let's figure out what algorithm it is moving on.I guess that hacking on assembler code is already fed up with everyone. Therefore, I will describe what I managed to find out.

In ROM there are two arrays of indices in which the level number is, I recall the levels inside the game are considered from zero.

CDMY26CDMY

The addresses 0x3E88 are delays. Delays are counted in frames. After we count the required number of frames, the Foot makes one move, at its end the counter is reset to zero. And the process starts off. Offsets are located at addresses 0x3FC0, from which the “script” of the Foot operation begins. Offsets are relative to the address 0x3FCB. The logic of the work is quite simple. We take a byte at the specified offset, if it is not equal to 0xFF then we execute the command, and increment the offset values. If it is 0xFF, then read the next byte, and set it as the current offset. The team consists of three parts:

ITKarma picture

The two least significant bits are responsible for the height of the jump that the foot makes each “move”. The height is calculated simply, a two is added to these two lower bits. That is, the minimum jump height we get two, the maximum five. But the height is not quite linear (probably it can even be called the jump impulse). Here are the heights in pixels that the foot jumps in the Z coordinate, depending on this coefficient: 0 - 15, 1 - 35, 2 - 64, 3 - 100.

But then everything is rather confused. Bits six through two are responsible for the direction of the jump. And here I’ll tell you honestly, I have not mastered the calculation algorithm, it is rather confused and uses some tricks, and pre-calculation tables.

The motion calculation function is divided into several parts, one is located at 0xCD4C and the second part is possibly here 0xC480.

In general, I just took and measured how it works, and in the end I got this tablet:

ITKarma picture

The code can be written like this:

int8_t X[32]={30,33,35,37,39,40,41,42,42,42,41,40,39,37,35,33,30,27,23,20,16,12,8,4,0,-4,-8,-12,-16,-20,-23,-27}; int8_t Y[32]={30,27,23,20,16,12,8,4,0,-4,-8,-12,-16,-20,-23,-27,-30,-33,-35,-37,-39,-40,-41,-42,-42,-42,-41,-40,-39,-37,-35,-33}; 

How to decipher it? Very simple, take the number encoded from the sixth to the second bit. And from the table we find out how much to add to the X and Y coordinates at the end of the move with a jump height of zero (two if you take it inside the game) to find out how much the leg will jump. If the length of the jump is one, we divide both numbers by two, and then multiply by three (let me remind you that a two is added to the length inside the game).

And the last seventh bit remains, this is the inversion bit, if it is set, then you need to change the sign of the numbers in the table to the opposite.

And it is important to consider that, unlike for example checkers, the foot processes a conflict from the worlds, and can hit walls.

We figured it out, not perfect, but I think it's understandable. And there was one more question, how many “lives” have the Legs, at each level. There were legends that at the last level the leg is indestructible. Although perhaps this is true. But let's get started. The “life” of the legs is arranged in such a way that each frame of the foot by one unit restores its health. The damage that a snake does to a foot is calculated as follows:

CDMY27CDMY

That is, the damage depends only on the level:

ITKarma picture

We get such a picture, in order to kill a leg, it is necessary to inflict damage faster than she manages to restore it. For the NTSC version, in the first level, this requires two or more blows per second. While at the eleventh level, it is already more than four times per second. Therefore, in general, it may seem that the leg is not killed. However, yes, it may well be that there were cartridges with an indestructible leg.

Conclusion


ITKarma pictureI hope you were interested. Or maybe even something useful. Maybe even someone wants to make a level editor for this or a game, or God forbid a remake (only good, please). According to the tradition that has developed here, I’m laying out all the achievements, although I don’t see any sense in this, but tradition is tradition. Writing this article took a lot longer than I expected.However, because of this, it was possible to study in more detail many things to which hands would not have reached. Well, at the same time slightly raise the level of knowledge of the MOS6502 assembler.

Thank you all for your attention !.

Source