Despite the fact that I wanted to make a simple editor that would only change the “appearance” of the levels of the game, the twin of the character did not give me rest. I really did not want to climb deep into the game engine, but this villain, emerging from the mirror, then drinking a bottle of "poison" in the middle of the game, and then completely dropping the prince into the abyss, did not give me rest. For a week, I struggled with the psychological effect of the “unfinished action”, but I could not overcome it.
On the night from Friday to Saturday, I again opened the debugger, RAM Filter and started looking for ... ')
Birth
It all started with the fact that I was sent a modification of the game, made in the editor, with the question: Why does it hang on entering one of the rooms? The room looked quite normal in the editor. Checking the correctness of the work of the editor showed that everything is in order: what they asked for was saved. I tried to move the objects that the author of the modification put in the room, and found that the hangup was gone, but a double appeared in the room, which, moreover, also had the audacity to attack: That is, the presence of objects in the room somehow affects the presence / absence of a twin. Everything is clear with the guards - their arrangement is spelled out directly in the level header, but there is not a single byte of information about the twin. Data arrays for 4, 5 and 6 levels were no different from all the others in their essence. The completely copied fifth level, say, the first one, did not “cause” a twin from the core of the engine code, which means it is somehow sewn into the engine itself. It was necessary to understand what changes if we enter a room with a double.
I began to study the fifth level, since the counterpart there performed the most actions: if you press the button that opens the exit, he would appear, drink the precious contents of the bottle, and run away. RAM Filter revealed anomalous activity when a double appeared in the memory in the $ 0400- $ 0410 address area: when entering the room, the flag in the $ 0401 cell was cocked, after pressing the button, the flag in the $ 0402 cell was additionally cocked, and then, after running away from the room, cell $ 0401 was reset and no longer changed. So, we will study what is happening here.
The presence of an additional character (guard, skeleton or twin) in a room in the NES version causes a slowdown in the engine, and this feature should be noted. We start the game, set it at $ 0401 units and really begin to observe the slowdown of the engine. Moreover, the double begins to “attack” us again. Once the cell $ 0401 is responsible for the presence / absence of this, then we go to the debugger, set a breakpoint: In the Condition field we set the condition: the battery should not contain 0 when writing to the cell. Cell changes can also occur through the index registers, but often this is done through the battery. Stop brings us here:
This sheet is quite difficult to understand, so I will translate it into pseudocode, calling the cells as follows: - $ 70 - LEVEL; - $ 51 - ROOM; - $ 401 - MIRROR_FLAG. We will deal with the rest later:
Now it's easier to learn. As we see, those levels and rooms in them where the twin appears appear, the flag of its presence is set and two more procedures are called. I will not give them, I will only describe what they do. - $ F2AD - searches for the doubled #FF marker, starting at address $ 060E, then returns the length of the byte sequence between $ 060E and the #FFFF marker (not including the last one) in the Y register; - $ DB85 — shifts a sequence of bytes, starting from the #FFFF marker found by the previous procedure, forward by the length of the sequence whose address is stored in $ 2F cells: $ 30, and which also ends with the #FFFF marker. As we can see, hard-coded addresses are inserted somewhere in the ROM into $ 2F and $ 30 cells: $ A22D, $ A23D, $ A24D.
Just making a unit in the $ 0401 cell is not enough, you still have to do some magical actions.
Two of the same casket from the face
Since the double appears only when the flag is cocked in the $ 0402 cell, we will look first at where the entry is made to it, and then where the value is read from it. The flag is cocked, as expected, then when we press the button that opens the exit (there is nothing interesting there). But reading from the cell is done here:
A remarkable procedure, isn't it? The essence of its actions, if you study it entirely, is very similar to the procedure that plays for us in demo play. In cells $ 72: $ 73 we have the address $ A389: 15 01 06 0D 02 02 03 32 0C 4E 02 05 17 01 FF The same #FF marker, the same structures consisting of two bytes, where the first one is time, and the second one ... no, the second one is no longer an imitation of a gamepad, but something else. When the value counted in cell $ 0404 is compared with the first byte of the structure, the counter in cell $ 0403 is incremented by 2 and the process repeats. If you look at what happens during this process with the second byte, then we come to a certain array of pointers: 0x15602: 00 00 AE 96 DB 96 64 97 9D 97 ... The index in this array will be our second byte. Each pointer in this array points to a structure with which the engine works rather cunningly. If this structure contains a value that is greater than a certain number, then it is decoded into an index, which is used in the array of pointers to certain procedures in the code. If the number is less than a certain number, then it is used as an argument for the above procedures. These procedures, performing certain actions, bring the following pointer into the structure responsible for the character. Thus, after a certain starting pointer is set in the character structure, a self-regulating cycle begins to be executed, which sets the sprite in motion. For example, if we write the “action” pointer into the structure, then each step of the run will be initiated by the previous one, specifying a new pointer in the same structure in the ActionPtr field during each iteration. In addition, in these procedures, the sprite will be moved on the screen and its actions will be voiced.
Total pointers in the array 93 pieces. That is, the game supports 93 actions for the character. But since pointers are sometimes repeated, there are fewer different actions. I cited this structure (character) earlier, so I will not dwell on its analysis. If we study the actions of the twin, we can see that its structure repeats the structure of the prince himself. In other words, when a double appears in the room, then after the structure describing the prince, the same structure is inserted that describes the double. The flag is cocked in the $ 0401 cell, and then the engine, depending on the level number, the room number and our actions, makes changes to this structure, thus setting the double into motion.
The full code (in pseudocode) of the 'double drive'
char sub_A25D() { if ( !MIRROR_FLAG ) goto label_A277; $06E0 = MIRROR_FLAG; if ( level == #03 ) goto label_A28D; if ( level != #04 ) goto label_A272; goto label_A319; label_A272: // here CMP #05goto label_A398; label_A277: return; label_A278: $0072 = #86; $0073 = #A3; $04FE = #00; MIRROR.Y.LOW = #40; return sub_A326(); label_A28D: $04FB = $0402 = #00; MIRROR.DIRECTION = #FF; if ( room != #03 ) goto label_A2CF; if ( $04FD ) goto label_A278; if ( PRINCE.Y.LOW >= #48 ) goto label_A2CF; $04FB = $04FC; // if prince around mirror (by Y pos) if ( !$04FB ) goto label_A2C3; if ( PRINCE.X.HI ) goto label_A2C3; if ( PRINCE.X.LOW <= #AC ) goto label_A2D0; label_A2C3: A = #02; Y = #0E; sub_CAFD(); MIRROR.X.HI = #02; label_A2CF: return; label_A2D0: X = #98; if ( !PRINCE.DIRECTION ) goto label_A2D9; X = #94; label_A2D9: MIRROR.X.LOW = X; MIRROR.X.HI = #00; MIRROR.Y.LOW = PRINCE.Y.LOW; if ( PRINCE.ACTION_INDEX == #06 ) goto label_A2C3; if ( PRINCE.POSE_INDEX <= #06 ) goto label_A2F9; if ( PRINCE.POSE_INDEX <= #0E ) goto label_A301; label_A2F9: if ( PRINCE.POSE_INDEX <= #20 ) goto label_A302; if ( PRINCE.POSE_INDEX >= #28 ) goto label_A302; label_A301: return; label_A302: X = #00; Y = #05; label_A306: MIRROR.ACTION_PTR = PRINCE.ACTION_PTR; X++; Y--; if ( Y ) goto label_A306; MIRROR.DIRECTION = PRINCE.DIRECTION xor #FF; return; label_A319: $0072 = #89; $0073 = #A3; if ( !$0402 ) goto label_A379; label_A326: if ( !$0401 ) goto label_A379; $404++; if ( $72[Y] == #FF ) goto label_A37A; if ( $0404 ) goto label_A347; $0404 = #00; $0403 += 2; label_A347: Y++; A = $72[Y]; Y = #0E; sub_CAFD(); if ( MIRROR.POSE_INDEX != #6D ) goto label_A379; $0054 = $06F0 = #00; $04B1 = #03; sub_DB23(); A = #27; sub_F203(); #0610[X] = #02; A = #20; sub_F203(); #0610[X] = #02; label_A379: return; label_A37A: $060E = $061C = $0401 = #00; return; A386: .data[XX:YY],1C:01 FF // XX:time, YY:action, FF:EOF A389: .data[XX:YY],15:0106:0D 02:0203:320C:4E 02:0517:01 FF label_A398: if ( level == #05 && !$610 ) goto label_A3A7; if ( PRINCE.X.LOW <= #10 ) goto label_A3A7; goto label_A3D1; label_A3A7: $0054 = #00; $04B1 = #0C; if ( !sub_DB18() ) goto label_A3D1; $0072 = #D2; $0073 = #A3; sub_A326(); if ( MIRROR.POSE_INDEX != #7C ) goto label_A3D1; $06FC = #00; $06FB = #0B; label_A3D1: return; A3D2: .data[XX:YY]: 04:0219:2A F0:02 F0:02 F0:02 FF } char sub_F203() { $0017 = A; switch_bank(#02); sub_B298(); $04BF = Y; switch_bank($06D1); Y = $04BF; return; } char sub_B298() { X=#00; label_B29A: // aka sub_B29A Y=#00; if ( #060E[X] != #FF ) goto label_B2A4; Y++; label_B2A4: if ( #060E[X] & #7F == $0017 ) goto label_B2BC; if ( #060F[X] != #FF ) goto label_B2B2; Y++; label_B2B2: if ( Y == #02 ) goto label_B2BF; sub_F215(); goto label_B29A; label_B2BC: Y = #01; return; label_B2BF: Y = #00; return; } char sub_8730() { if ( A == #0616[Y] ) goto label_874B; // $0616+Y - address of MIRROR.ACTION_INDEX $0616[Y] = A; X = A << 1; #0613[Y] = #95F2[X]; // set MIRROR.ACTION_PTR to new value ($0613 + Y - address of ACTION_PTR in MIRROR struct) #0614[Y] = #95F3[X]; #0618[Y] = #FF; // set EOF marker label_874B: return; }
It is worth noting that the “reflection” procedure in the mirror is quite interesting. If the prince is located next to the mirror, then the double is placed in the next position from him, the pointer of the prince’s action is copied into the double structure, and the direction byte is inverted. If the position of the prince is "run jump", then the twin "runs out" from the mirror and runs away.
Make patch
This code is quite difficult to link with the editor. You can, of course, edit those short data arrays used by the above code, but you would have to do some work, and the effect would be insignificant. Editing this code with the editor is too difficult. I wanted something more. And the solution was found.
To manage the twin, it will be enough for us to copy its structure after the structure of the prince himself and set the flag $ 0401. In the future, we will set the actions that it will perform by writing a pointer to its structure. But how to do that? Need to write code. But where to insert it? There are practically no free places in the game, and those small stubs of several bytes left between the code and data cannot be used. So, it is necessary to seek additional space in other ways.
Wider circle
As we remember, Mapper # 02 contains two different types of mapping. One of them - UNROM, - contains 8 banks of PRG-ROM, and the second - UOROM, - 16. If you insert another 8x16 kB in the ROM file, the mapper will change to UOROM without harm to the game. However, it is necessary to insert it so that the last bank remains the last, and the first 7 should remain at the beginning.
We climb to the hex editor, change the number of banks in the header (fifth byte, offset 0x04) from # 08 to # 10, then paste 8x16 kB zeros into the file, starting at offset 0x1C010. The size of the ROM file changes and becomes equal to 262,160 bytes. Run the ROM in the emulator ... It works! If we perform this procedure in hardware, we will need to change the mapper controller, and put the increased ROM memory - 32x8 KB, and we will also get a working game.
ROM increased, there is a place, but how to use it? In order to call the code or read the data from there, we need to turn on this bank and transfer control there. This can be done safely only from the last bank, but there is no space in it. Where to write the code? Let us set the requirements:
The code should be called from the main game loop, since we will manage the twin right during the main game;
The code should be called and return control to the last bank;
Called code should not break the original code.
Recall the main loop. There, we call various procedures, before which the procedures for including the relevant bank are called. It is quite difficult to insert two new procedures and add new calls to the main loop, but we can change one of the bank inclusion procedures. Take the end of the cycle:
Procedures $ 9F12, $ A3DD cannot be touched, as they are in another bank, in the place of which ours should be. It is also impossible to transfer them there, as they will pull the rest of the contents of the bank with them. You can, however, change the address of $ CB00 to the address of the new procedure, which will include our # 07 bank, call our code, then call the original $ CB00 procedure and return control back to the loop. Code like this:
12 bytes (3 bytes for each instruction). Not much, but they need to be inserted somewhere, and there is no place anyway. Few places can be won by modifying the existing code in the last bank. It is enough to take some long procedure that does not use data from banks # 00 to # 06, which also does not call procedures using these banks, and which is called from procedures that do not use these banks. After we find it, we will be able to transfer it to a new bank, place our code in its place, and put a call from the new bank before our code.
What she does is an open question, but, incidentally, it doesn't matter. The main thing is that it meets all the necessary requirements: it is not called from “dynamic” banks, it does not call the code of them and does not apply to them. And the transition to $ C139 we will redo a little. We modify our bank inclusion code a bit and place it at $ C111:
Not only did we successfully cram the call to our code, we still have a lot of space left from $ C125 to $ C138, which we can use somehow else: after all, we still have 7 (!) Free banks, and they will also need to work from the last bank (if we use them in the future). I placed the address of the new code at $ B010 (approximately the middle of the bank), since we will have to place a copy of the levels and rooms in our bank, plus some other data. But more about that below.
We also modify the procedure itself, since it contains jumps to the address $ C139, which is outside of it:
Everything! What once passed to the address $ C139 now goes to the address $ BFB9, according to which the JMP instruction makes you jump to $ C139, as if we hadn’t moved the code anywhere. The body of the main loop, we can now change to the following:
We can place some dummy like “RTS” at $ B010 and run the game in the emulator. Everything was as it was - it remained.
We write our "drive" for "reflection"
It remains only to develop its own algorithm for the appearance of reflections in the room. I developed the following data structure:
When entering the room, we extract the pointer by the level number in the first pointer array (Levels ptrs), if it is not zero, then go to the second array (Rooms ptrs). If the pointer extracted by the room number is also not zero, then proceed to reading the structure of the reflection. The structure of the reflection is as follows:
Structure describing the initial state of the character (struct CHARACTER);
Pairs “time”: “action”, which describe what the character will do and in what time interval;
Graduation marker # FF.
In order not to bore the reader with an excess of code, I will not give it here. It will be at the end of the article.
We respond to events
The unconditional appearance of reflection is uninteresting. It seems it is, but no sense. I would like it to appear in accordance with any game events and know how to do something.
Starting with the address $ 0500 in memory, we have data that determine certain changes in the rooms. For example, if a character drinks a potion from a bottle, the bottle will not appear there anymore. Or, if the plate falls, then this array will contain both the fact that now there is a hole in its place, as well as the fact that in the place where it fell - its fragments. The same with open and closed grids. Each such event is encoded with two bits. In the room we have 30 blocks, for each line of 10 blocks there are 3 bytes (4 blocks fit in 1 byte, and in the third byte the remaining 4 bits remain unused), totaling 9 bytes per room. Thus, the level goes 9 * 24 = 216 bytes.
As soon as an action has taken place, the corresponding pair of bits in this array is set to a specific value. Total possible combinations - 3 (00 - means that nothing happened), and a lot of events: the grate opened, the grate closed, drank from the bottle, the plate is missing (dropped), fragments of a fallen plate; accordingly, events overlap. For example, if we place a bottle and hang a falling plate above it, after its fall we will either miss the bottle or not see fragments of the fallen plate.
Apply this knowledge to our patch. In the structure that we came up with, we add an address that should be constantly read, and a value with which to compare. As long as the specified value is not the desired value, we will not set in motion our twin.
We force to push the button
And finally, it remains to teach him to do something. Double out of the box can not do anything. Able only to appear on the screen and make any movements, calluses of the eye. He does not know how to make contact with the walls, he does not know how to press buttons, and in general he is not able to do anything.
While our prince is doing something in the game, his coordinates are constantly compared with the block in which he is located, and if this block is “active”, certain actions are taken: for example, if we hit the “Button” block, this button will be to push. The double runs on its own and no one follows its movements. So, we in our patch must do this for the main engine. To do this, it is enough to transfer to the main engine (to the verification procedure) the coordinate of the twin instead of the coordinate of the prince, and then everything will happen by itself. But the problem is that the engine compares the block with the data array of the room, which is stored in another bank. Nevertheless, there is still enough space in our bank, and it will be included during the check, so we ... just copy the game data from the original bank to ours at the same place, and the engine will read this data as if nothing had happened ( Remember, earlier we placed the code in the middle of the bank?). During editing, however, now it will be necessary to consider that changes should be made to the main copy and to the backup.
The result is obvious:
FIN
In the end, we managed to force the twin to be not just furniture, but to perform the simplest action — to press a button and open a door for us. Already with such a simple thing, it was possible to inject a puzzle element into the game: until you press a button, break a plate or drink from a bottle at one end of a level, a reflection appears in the other that does not press a button that opens, for example, an exit.
Well, now you can add the game together, or new characters, or new blocks, but ... it would be a different game. By this time I was completely satisfied with my curiosity, so I closed the debugger and went to sleep. It was a warm July Monday.
PS
But this is not the end. After the end, an epilogue will follow: I have finished with the editor, but there is still one unclear question that I would like to make out. So ahead “Epilogue. Dungeon.
I attach a link where you can download the archive with the editor, a small documentation on the game and the source code of the patch.