[NES] We are writing a level editor for Prince of Persia. Chapter Four He is running! Or a skeleton in the closet
Chapter One , Chapter Two , Chapter Three , Chapter Four , Chapter Five , Epilogue
If we are editing a game, then we must bring all its details into line. It would be foolish to show your version of the game to the world if fragments of the original version come out in it, conflicting with the new meaning that we put into it. Editing game levels does not allow changing some plot twists that developers initially invested.
With a slight delay, we will correct the plot.
Before moving on, I would like to limit myself to redrawing the game. Indeed, if we want to change everything and everything, it will be easier to just write our game from the very beginning. But as originally planned, I still want to do something new, preserving, however, the old base. Decide on what we will edit.
I decided to leave the text, music, types and configuration of the blocks in their original form in order to preserve the spirit of the game. This can be changed as simply as everything else, since all objects are tied to arrays of pointers to fairly simple data.
By the way, I left the mouse in its place.
The dynamic elements of the game are not so easy to change by simple busting, as we did before. Accordingly, it is time to actively study the code. We met some of its elements. Now, let's see what happens during the game.
By pressing the “Step over” button in the debugger, we will inevitably go to the main loop, where its main component looks like this:
Above this loop is also the call of the many procedures that are called when moving between rooms or levels. The procedures listed in the above code change the state of the game, and also perform various kinds of checks. Of these, the following are not of interest:
- $ F2F7 - waiting for a change in a certain state in the VBlank interrupt;
- $ D010, $ CB00 - switching banks before calling procedures that are located in the corresponding bank.
I will not give the code of all the remaining procedures, since their consideration with all the branches will result in another ten posts. I will give only the one that is of interest now. I will not translate into pseudo-code, but I will give comments.
Before calling this procedure, the bank activation procedure # 02 - $ D010 is called. We turn on this bank by writing the number # 02 to the cell $ FFF2 and proceed to this procedure:
So, here we have a couple of cells with some states, another cell with an index and an array in the ROM file. The code shows that it is executed only if the flag is set in cell $ 04E4. Let's try to put it.
We start the game again, enter the first level and put a unit in $ 04E4.
A second passes and ... he runs himself! Even the buttons do not need to be pressed.
If we wait for Demo play, we will find out that there is already one in cell $ 04E4, and if we reset this cell during demo, demo play will disappear and control will be transferred to our hands. Very convenient: the demo passes for us the main part, then we can reset $ 04E4 and continue the game on our own.
Now we can, based on the game, parse the code. Obviously, the $ DC14 procedure sends us to the game menu. The $ A70F array stores some structures of two bytes each, and ends with the #FF token. Moreover, the first byte is a certain time interval, at the end of which the index increases, and the second byte from the following structure is transferred to the cell $ 040A. What does he mean?
Take a look at the array:
First we expect # 64 cu (conventional time units), then # A0 U.V.E., then # 28, and so on, to the #FF marker. Change the time in the first pair of bytes to #FE and in demo play we find that the character is standing idle after the start for a long time. If we put # 02 there, then he will turn left and for some time will be hollowed into the wall. Therefore, the second byte describes the action that it will perform during the specified time interval. Since the game is deterministic, it’s enough for us to rigidly set the sequence of certain actions, and it will “play” itself.
In fact, the second byte is an imitation of the buttons on the controller, where the first bit encodes the button to the right, the second bit - the button to the left, and so on. The combination of these bits determines the pseudo-state of the gamepad during demo-play. By editing this array, we can adjust demo play to our new level.
If we keep track of what happens next with the value in the cell $ 040A, then we will come to the data structure, which is located at the address $ 060E. It is described as follows:
Having reached this structure, we learn that there is an array of "actions" consisting of pointers to certain structures, which are then expanded into a sequence of sprite displays on the screen. By "action" is meant what the character performs on the screen: runs, jumps, crouches and other actions. Similarly, everything with the “pose” index: whether he is standing or sitting is determined by this member of the structure. We will need this structure in the future, but for now we need to find out how we get from level to level.
Here, as before, we will have a starting point - cell $ 70. When moving from level to level, we will see the code (as before, we catch by the breakpoint by the condition of writing to cell $ 70):
Here we see that a unit is added to cell $ 70 and then we move somewhere to the last bank. Moreover, there is a check: if the number is greater than 13, then zero this value. This means that if we put the output at level 14, we will move back to the first. This is the initiating procedure for moving to the next level. Next, they will show us the password and we will be in the next level. Let's see where it is called from.
As in the x86 architecture, here, the procedure is called by pushing the argument of the JSR instruction into the Instruction Pointer register and pushing the return address onto the stack. We have the following on the stack:
Therefore, the instruction that was before the instruction at $ CC22 called us:
It is bulky and it is not interesting to trace all of it. Switch the breakpoint from writing to $ 70 to reading and start debugging. First, we will stop in the familiar $ C0D5 procedure - it is not interesting to us, but the next stop will bring us here:
The value in our cell is compared with the number # 0B == 11 (I recall that the levels are numbered from scratch), and then reads from cell $ 51, where we have # 16 = 22. Based on the fact that we are in room 22 ( if you look at the level map), then at $ 51 we have a room number that we did not look for the last time. If these checks are performed, then we go to the address $ 86F3 - that is, to the procedure initiating the transition. The developers trivially entered the code to go to another level from this room, if the character leaves the room to the left.
We can find a similar check if we look for a transition from the sixth level to the seventh, when the character falls into the abyss. That is, it is enough for us to edit the CMP instructions, and another room will have these properties. Just condition must be met: the transition to the next level will take place if we go to the left (for this check) or fall down (for the second).
Having studied the launch code of a new level, we will also see that for the first level, the character’s coordinates are also overwritten by the values that are hardcoded in the code, so we could not move it when we changed the level header at the beginning of our research. Similar checks are carried out after the defeat of the guard at level 13, opening us a way out. All of them are quite simply found in a similar way.
The editor is almost done. Studying the transitional moments from level to level or from room to room, we will find the remaining parameters that can be edited. Since approaches to the search for such data have already been considered, I will not dwell here in detail. I will give only a general description, and at the end of the last chapter I will give a list of displacements in ROM on the structure of the game and their description.
The palette is built from two arrays of pointers and the actual array of the palettes themselves. The palette itself is stored exactly in the form in which it is sent to the PPU as a sequence of 32 bytes. The second array contains only 7 pointers, which lead us to the palettes acceptable for the level, and the first pointer is equal to the sixth. And the first array consists of 14 pointers (according to the number of levels), each of which leads us to one or another element of the second array.
At this stage, I already sketched a sample shell for the editor, there was only one minor detail. Later I will tell you what the magic numbers in the status bar mean.
In the fifth, final chapter, which is called “Reflection,” we will try to learn how to control the prince’s reflection. From a secondary element, which had practically no effect on the course of the game, we can turn it into one of the main ones, without which it will be impossible to pass the game, and the prince will no longer be so alone in those cold walls in which he was enclosed.
Disclaimer
If we are editing a game, then we must bring all its details into line. It would be foolish to show your version of the game to the world if fragments of the original version come out in it, conflicting with the new meaning that we put into it. Editing game levels does not allow changing some plot twists that developers initially invested.
With a slight delay, we will correct the plot.
What will we edit?
Before moving on, I would like to limit myself to redrawing the game. Indeed, if we want to change everything and everything, it will be easier to just write our game from the very beginning. But as originally planned, I still want to do something new, preserving, however, the old base. Decide on what we will edit.
- Colors. The NES version differs from the rest in that all levels in it have boring colors of the same type. Only the colors of law enforcement officers change;
- Demo play. As soon as we change at least a brick in the first room, our hero in demo play will like a blind rabbit hammering into walls or trying to climb on missing ledges;
- "Mirror reflection". It appears only when certain conditions are met and in strictly defined rooms of strictly defined levels. If we rebuild something, it will either appear out of place or it will not appear at all, distorting the plot of the game;
- ... The mouse is in the eighth level :-). She runs at the request of the princess to save us when we find ourselves in prison, revealing to us the bars.
- Types of adversaries. In the third and twelfth (in the original, in the twelfth level, “reflection”, but NES developers saved a little) levels we are met by a skeleton;
- Transitions.
- First level. According to the plot, the hero is thrown into the dungeon and the bars are closed behind him. Thus, he appears in the first level and immediately thereafter the escape route closes;
- Sixth level. The hero jumps over the abyss, but the insidious "reflection" closes the grate and he breaks into the abyss - in the dungeon. There is a non-standard transition to the next level;
- Seventh level. Once it falls down in the sixth level, then it should appear from above in the seventh level. Non-standard level entry;
- Twelfth and thirteenth levels. Here, the developers decided to divide one long into two levels shorter. We move from room to room, and we get to the next level;
- The fourteenth level simply has no transition to the next level
I decided to leave the text, music, types and configuration of the blocks in their original form in order to preserve the spirit of the game. This can be changed as simply as everything else, since all objects are tied to arrays of pointers to fairly simple data.
By the way, I left the mouse in its place.
The code
The dynamic elements of the game are not so easy to change by simple busting, as we did before. Accordingly, it is time to actively study the code. We met some of its elements. Now, let's see what happens during the game.
By pressing the “Step over” button in the debugger, we will inevitably go to the main loop, where its main component looks like this:
label_CC1A:
$CC1A:20 F7 F2 JSR $F2F7
$CC1D:20 10 D0 JSR $D010
$CC20:20 1E 86 JSR $861E
$CC23:20 00 CB JSR $CB00
$CC26:20 E8 A4 JSR $A4E8
$CC29:20 10 D0 JSR $D010
$CC2C:20 04 8B JSR $8B04
$CC2F:20 10 D0 JSR $D010
$CC32:20 F9 80 JSR $80F9
$CC35:20 FC D8 JSR $D8FC
$CC38:20 DF BA JSR $BADF
$CC3B:20 00 CB JSR $CB00
$CC3E:20 12 9F JSR $9F12
$CC41:20 DD A3 JSR $A3DD
$CC44:4C 1A CC JMP $CC1A
Above this loop is also the call of the many procedures that are called when moving between rooms or levels. The procedures listed in the above code change the state of the game, and also perform various kinds of checks. Of these, the following are not of interest:
- $ F2F7 - waiting for a change in a certain state in the VBlank interrupt;
- $ D010, $ CB00 - switching banks before calling procedures that are located in the corresponding bank.
I will not give the code of all the remaining procedures, since their consideration with all the branches will result in another ten posts. I will give only the one that is of interest now. I will not translate into pseudo-code, but I will give comments.
A4E8. He is running!
Before calling this procedure, the bank activation procedure # 02 - $ D010 is called. We turn on this bank by writing the number # 02 to the cell $ FFF2 and proceed to this procedure:
$A4E8:AD E4 04 LDA $04E4 = #$01
$A4EB:D0 13 BNE $A500 ;; проверяем состояние флага $04E4.
;; Если не 0, то выполняем основное тело процедуры
$A4ED:60 RTS ;; ...или выходим
$A4EE:4C FF A4 JMP $A4FF
$A4F1:A9 00 LDA #$00
$A4F3:8D E3 04 STA $04E3 = #$44
$A4F6:8D E0 04 STA $04E0 = #$76
$A4F9:8D E4 06 STA $06E4 = #$0A
$A4FC:4C 14 DC JMP $DC14 ;; обнуляем вообще все и уходим в главный цикл
;; видимо, игра здесь будет начинаться заново с какой-то отправной точки.
$A4FF:60 RTS ;; не буду приводить здесь код $DC14.
;; в нем очистка стека и переход на адрес чуть ранее основного цикла
label_A500:
$A500:AD 0A 04 LDA $040A = #$01
$A503:29 30 AND #$30
$A505:D0 EA BNE $A4F1 ;; если в некой переменной установлены определенные флаги,
;; то возвращаем игру в некую начальную точку
$A507:AE E3 04 LDX $04E3 = #$44 ;; в индексный регистр помещаем значение из ячейки $04E3
$A50A:BD 0F A7 LDA $A70F,X @ $A763 = #$AA ;; и читаем из ROM-файла значение, соответствующее индексу в ячейке $04E3
$A50D:C9 FF CMP #$FF
$A50F:F0 E0 BEQ $A4F1 ;; если прочитанное значение соответствует маркеру #FF,
;; то снова отправляем игру в начальную точку
$A511:CD E0 04 CMP $04E0 = #$76 ;; то же прочитанное значением сравниваем с ячейкой $04E0
$A514:D0 0E BNE $A524 ;; если не равно, то... (см. ниже)
$A516:A9 00 LDA #$00 ;; иначе обнуляем ряд ячеек...
$A518:8D F4 04 STA $04F4 = #$00
$A51B:8D E0 04 STA $04E0 = #$76
$A51E:EE E3 04 INC $04E3 = #$44 ;; а к нашему индексу прибавляем 2 (двойным инкрементом)
$A521:EE E3 04 INC $04E3 = #$44
label_A524:
$A524:BD 10 A7 LDA $A710,X @ $A764 = #$01
$A527:8D 0A 04 STA $040A = #$01 ;; переносим значение соседней ячейки в ROM-файле в $040A
$A52A:EE E0 04 INC $04E0 = #$76 ;; увеличиваем некий счетчик
$A52D:60 RTS ;; выходим
So, here we have a couple of cells with some states, another cell with an index and an array in the ROM file. The code shows that it is executed only if the flag is set in cell $ 04E4. Let's try to put it.
We start the game again, enter the first level and put a unit in $ 04E4.
A second passes and ... he runs himself! Even the buttons do not need to be pressed.
If we wait for Demo play, we will find out that there is already one in cell $ 04E4, and if we reset this cell during demo, demo play will disappear and control will be transferred to our hands. Very convenient: the demo passes for us the main part, then we can reset $ 04E4 and continue the game on our own.
Now we can, based on the game, parse the code. Obviously, the $ DC14 procedure sends us to the game menu. The $ A70F array stores some structures of two bytes each, and ends with the #FF token. Moreover, the first byte is a certain time interval, at the end of which the index increases, and the second byte from the following structure is transferred to the cell $ 040A. What does he mean?
Take a look at the array:
64 00 . A0 01 . 28 00 . E6 84 . E6 02 ... FF
First we expect # 64 cu (conventional time units), then # A0 U.V.E., then # 28, and so on, to the #FF marker. Change the time in the first pair of bytes to #FE and in demo play we find that the character is standing idle after the start for a long time. If we put # 02 there, then he will turn left and for some time will be hollowed into the wall. Therefore, the second byte describes the action that it will perform during the specified time interval. Since the game is deterministic, it’s enough for us to rigidly set the sequence of certain actions, and it will “play” itself.
In fact, the second byte is an imitation of the buttons on the controller, where the first bit encodes the button to the right, the second bit - the button to the left, and so on. The combination of these bits determines the pseudo-state of the gamepad during demo-play. By editing this array, we can adjust demo play to our new level.
If we keep track of what happens next with the value in the cell $ 040A, then we will come to the data structure, which is located at the address $ 060E. It is described as follows:
struct CHARACTER
{
char bCharType;
unsigned short X;
unsigned short Y;
unsigned short ptrAction;
char bDirection;
char bActionIndex;
char bPoseIndex;
char bReserved[4];
};
Having reached this structure, we learn that there is an array of "actions" consisting of pointers to certain structures, which are then expanded into a sequence of sprite displays on the screen. By "action" is meant what the character performs on the screen: runs, jumps, crouches and other actions. Similarly, everything with the “pose” index: whether he is standing or sitting is determined by this member of the structure. We will need this structure in the future, but for now we need to find out how we get from level to level.
We run through the mazes
Here, as before, we will have a starting point - cell $ 70. When moving from level to level, we will see the code (as before, we catch by the breakpoint by the condition of writing to cell $ 70):
$86F3:AD 35 07 LDA $0735 = #$00
$86F6:D0 0D BNE $8705
$86F8:A5 70 LDA $0070 = #$00
$86FA:18 CLC
$86FB:69 01 ADC #$01
$86FD:C9 0E CMP #$0E
$86FF:90 02 BCC $8703
$8701:A9 00 LDA #$00
$8703:85 70 STA $0070 = #$00 ;; <<< останов
$8705:A9 00 LDA #$00
$8707:8D 01 20 STA $2001 = #$18
$870A:85 15 STA $0015 = #$18
$870C:4C 1D CB JMP $CB1D
Here we see that a unit is added to cell $ 70 and then we move somewhere to the last bank. Moreover, there is a check: if the number is greater than 13, then zero this value. This means that if we put the output at level 14, we will move back to the first. This is the initiating procedure for moving to the next level. Next, they will show us the password and we will be in the next level. Let's see where it is called from.
Hardcode
As in the x86 architecture, here, the procedure is called by pushing the argument of the JSR instruction into the Instruction Pointer register and pushing the return address onto the stack. We have the following on the stack:
22, CC, ...
Therefore, the instruction that was before the instruction at $ CC22 called us:
$CC20:20 1E 86 JSR $861E
It is bulky and it is not interesting to trace all of it. Switch the breakpoint from writing to $ 70 to reading and start debugging. First, we will stop in the familiar $ C0D5 procedure - it is not interesting to us, but the next stop will bring us here:
$D0B8:A5 70 LDA $0070 = #$0B
$D0BA:C9 0B CMP #$0B
$D0BC:D0 0C BNE $D0CA
$D0BE:A5 51 LDA $0051 = #$16
$D0C0:C9 16 CMP #$16
$D0C2:D0 06 BNE $D0CA
$D0C4:20 10 D0 JSR $D010
$D0C7:4C F3 86 JMP $86F3
The value in our cell is compared with the number # 0B == 11 (I recall that the levels are numbered from scratch), and then reads from cell $ 51, where we have # 16 = 22. Based on the fact that we are in room 22 ( if you look at the level map), then at $ 51 we have a room number that we did not look for the last time. If these checks are performed, then we go to the address $ 86F3 - that is, to the procedure initiating the transition. The developers trivially entered the code to go to another level from this room, if the character leaves the room to the left.
We can find a similar check if we look for a transition from the sixth level to the seventh, when the character falls into the abyss. That is, it is enough for us to edit the CMP instructions, and another room will have these properties. Just condition must be met: the transition to the next level will take place if we go to the left (for this check) or fall down (for the second).
Having studied the launch code of a new level, we will also see that for the first level, the character’s coordinates are also overwritten by the values that are hardcoded in the code, so we could not move it when we changed the level header at the beginning of our research. Similar checks are carried out after the defeat of the guard at level 13, opening us a way out. All of them are quite simply found in a similar way.
We take out the skeleton from the cabinet and paint the walls
The editor is almost done. Studying the transitional moments from level to level or from room to room, we will find the remaining parameters that can be edited. Since approaches to the search for such data have already been considered, I will not dwell here in detail. I will give only a general description, and at the end of the last chapter I will give a list of displacements in ROM on the structure of the game and their description.
0x13BEB: 00 00 24 00 00 00 00 00 00 00 00 24 00 00
- an array containing the code for the set of tiles with which the guard will be drawn (the guard itself or the skeleton). This structure is similar to that which describes the type of level, and it is similar to that which determines the amount of health in a level.The palette is built from two arrays of pointers and the actual array of the palettes themselves. The palette itself is stored exactly in the form in which it is sent to the PPU as a sequence of 32 bytes. The second array contains only 7 pointers, which lead us to the palettes acceptable for the level, and the first pointer is equal to the sixth. And the first array consists of 14 pointers (according to the number of levels), each of which leads us to one or another element of the second array.
Almost ready
At this stage, I already sketched a sample shell for the editor, there was only one minor detail. Later I will tell you what the magic numbers in the status bar mean.
In the fifth, final chapter, which is called “Reflection,” we will try to learn how to control the prince’s reflection. From a secondary element, which had practically no effect on the course of the game, we can turn it into one of the main ones, without which it will be impossible to pass the game, and the prince will no longer be so alone in those cold walls in which he was enclosed.