Tuesday, July 29, 2008

Tech stuff

Now I will explain exactly what happens inside the SNES emulator when you load the ROM (only the ROM specific stuff needs to be mentioned):

1) To disable any menu selections at the title screen other than the first option, patch 03:8176 INC to NOP:
Memory.ROM[(0x8000*3)+0x0176] = 0xEA;

2) To disable the 'demo race' at title screen (as this will trigger game states we don't need), patch 03:8143 BNE $8148 to BRA $8148:
Memory.ROM[(0x8000*3)+0x0143] = 0x80;

3) Then we wait for the player to select a car. To catch car selection, check memory status at $7E:0055. When it equals 0x03 we know a car has been selected. You can determine which car was selected by reading $7E:005A at this point. The order (from 0-3) is bf (blue falcon), gf (golden fox), wg (wild goose), fs (fire stingray). This is a different representation to what is used internally in the F-Zero ROM, where it is bf, wg, gf, fs. So I change the returned value to suit that.

4) We should not proceed to the game until all players have entered their car selections. So, we wait for the game to start transitioning to the next screen, breaking when $7E:0055 == 0x05. When this condition is met, we halt the screen progression until we receive notification from the server that everyone is ready. In order to pause the game while keeping the music playing etc, we patch the ROM 03/851F: A5 55 F0 74 LDA $55 to become 5C 1F 85 03 JMP 03851F.
Memory.ROM[(0x8000*3)+0x051F] = 0x5C;
Memory.ROM[(0x8000*3)+0x0520] = 0x1F;
Memory.ROM[(0x8000*3)+0x0521] = 0x85;
Memory.ROM[(0x8000*3)+0x0522] = 0x03;

5) We only continue when we have received notification from the server that all the players have selected their cars. Furthermore the server will have sent other data such as the track to race on, player id, number of players in the race etc.

6) Then we set the car types based on the data received from the server. The player previously received from the server a player id, this is a unique value from 0 to 3. It determines the players position in the starting line from left to right.

So, let's assume we are the first player to log on to the server, making our player id == 0. This means, our car will be in the fs position on the starting line, on the far left. However, let's assume we chose bf as our car.

We need to change the viewpoint of the car to match the fs location. To do this, we patch 00:D2EF A5 52 LDA $52 to A9 0x LDA #$x where x is a value from the internal representation of the car selection (being bf, wg, gf, fs):
Memory.ROM[0xD2EF-0x8000] = 0xA9;
Memory.ROM[0xD2F0-0x8000] = x;

To set the player car palette to be based on the player id, patch 00:D72B LDA $52 to LDA #$pid where pid is the player id:
Memory.ROM[0xD72B-0x8000] = 0xA9;
Memory.ROM[0xD72C-0x8000] = pid;

Then we can set the opponent car types, based on the data we received earlier. To do this, we write to RAM:
Memory.RAM[0x1133] = x;
Memory.RAM[0x1135] = y;
Memory.RAM[0x1137] = z;

where x, y and z are the player car types from right to left on the starting line, excluding the player id's location. So in our example of having player id == 0, with the starting line order being (fs, bf, wg, gf) the code would be:
Memory.RAM[0x1133] = player who is in the gf position;
Memory.RAM[0x1135] = player who is in the wg position;
Memory.RAM[0x1137] = player who is in the bf position;

Then we can set the colours of the opponent cars. Our aim is to make sure that the first car is always pink, second is always blue, third is green and fourth is yellow - no matter what car type is selected. We write to RAM at 0x0C41, 0x0C43, 0x0C45, 0x0C47 where 0x0C41 is the player car's colour, and the 0x0C43/5/7 are the colours of the opponents based on the player id. So, if we are in player id position 0:
Memory.RAM[0x0C41] = 0x0E;
Memory.RAM[0x0C43] = 0x08;
Memory.RAM[0x0C45] = 0x0A;
Memory.RAM[0x0C47] = 0x0C;

Finally we then make a few small patches that are needed to make things run smoothly. We patch the code at 00:D486 LDX #$00 to JMP $D4A7, to skip the car loop iteration:
Memory.ROM[0xD486-0x8000] = 0x4C;
Memory.ROM[0xD487-0x8000] = 0xA7;
Memory.ROM[0xD488-0x8000] = 0xD4;

we patch the code at 00:D32B STA $1131,X to NOPs, to make sure the car types are never overwritten:
Memory.ROM[0xD32B-0x8000] = 0xEA;
Memory.ROM[0xD32C-0x8000] = 0xEA;
Memory.ROM[0xD32D-0x8000] = 0xEA;

and to stop the player car palette from defaulting back to the original when it crosses the finish line, patch 00:8DB6 JSR $C782 to NOPs:
Memory.ROM[0x8DB6 - 0x8000] = 0xEA;
Memory.ROM[0x8DB7 - 0x8000] = 0xEA;
Memory.ROM[0x8DB8 - 0x8000] = 0xEA;

8) Now I need to explain how the F-Zero game actually works.

F-Zero tracks the progress of five displayed cars. The locations of these cars are stored at: 7E:0B70-0B79 (x values) and 7E:0B90-0B99 (y values). Since each race has something like 20 cars or more, there are many cars on the track whose locations are unaccounted for at any one time. When these cars need to be displayed, the game 'places' them on the track where it thinks they should be, based on a checkpoint system, rather than actually racing them around the track properly. This was probably done due to SNES system constraints, but discovering this answered a lot of questions for me. Ever wonder why there was always a car right behind you, no matter how well you were driving? Now you know :)

What this means, is we need to make a few more modifications.

We need to get rid of the annoying opponent 'catch up' code. As I stated above, the game often decides when a car is about to over take you. It's not like the cars are always racing around the track in a linear fashion. The game may decide to make a car jump from really far away, to just behind you, simply because you are playing poorly. We can't have random 'check' warning messages in our multiplayer races either.

We also need to get rid of all the generic enemy cars, these are the ones that have the boring racing stripe and never win the races. If we don't do this, the code will just keep introducing them in to the game everytime we crash in to the wall a few times.

The good news is we can solve both problems simply. We patch 00:DDFC JSR $DED0 to EA NOP * 3:
Memory.ROM[0xDDFC - 0x8000] = 0xEA;
Memory.ROM[0xDDFD - 0x8000] = 0xEA;
Memory.ROM[0xDDFE - 0x8000] = 0xEA;

9) Next we have to stop the opponent AI from working, while still allowing the cars movement. If we don't do this, then the car will have jagged motion around the track as it is receiving contradicting movement commands from the server and the AI. So, we patch 00:DDDA JSR $DE57 to EA NOP * 3:
Memory.ROM[0xDDDA - 0x8000] = 0xEA;
Memory.ROM[0xDDDB - 0x8000] = 0xEA;
Memory.ROM[0xDDDC - 0x8000] = 0xEA;

10) Now another thing you will notice is that when the race first starts, even though you have disabled the opponents AI, they still boost off the finish line (before slowing to a halt). If we don't stop this boost, the car will have the same jagged motion described earlier at the start of the race. So we patch 00:8D3E LDA #$02 to LDA #$00:
Memory.ROM[0x8D3F - 0x8000] = 0x00;

11) The race relies on synchronization, and if a player accidentally pauses the game they will break the synchronization. So we disable pausing by patching 00:C8FF F0 06 BEQ $C907 to be 80 06 BRA $C907:
Memory.ROM[0xC8FF - 0x8000] = 0x80;

12) Now we must make sure that when the race is complete, the game does not proceed beyond the race time summary screen until it receives notification from the server that all players have finished. We patch 00:CD84 90 03 BCC $CD89 to be 80 03 BRA $CD89:
Memory.ROM[0xCD84 - 0x8000] = 0x80;

13) And finally, we can resume execution of the ROM (from point 4 above) meaning the game will progress beyond the car select screen to the league selection:
Memory.ROM[(0x8000*3)+0x051F] = 0xA5;
Memory.ROM[(0x8000*3)+0x0520] = 0x55;
Memory.ROM[(0x8000*3)+0x0521] = 0xF0;
Memory.ROM[(0x8000*3)+0x0522] = 0x74;

14) We skip the league and difficulty selection because it is already chosen for us by the server. I do this crudely by patching 03/8795: B0 35 BCS $87CC to BRA $87CC 80 35:
Memory.ROM[(0x8000*3)+0x0795] = 0x80;

and by patching 03/87E6: 6B RTL to be another INC $55 (this makes it 7, to start the race). Note this pushes the RTL into the 'class' select code but that code is not used anyway:
Memory.ROM[(0x8000*3)+0x07E6] = 0xE6;
Memory.ROM[(0x8000*3)+0x07E7] = 0x55;
Memory.ROM[(0x8000*3)+0x07E8] = 0x6B;

15) We instead set the league and the track number by patching the RAM at 7E:0053 (track number) and 7E:005A (league number).
Memory.RAM[0x53] = track_num;
Memory.RAM[0x5A] = league_num;

16) By this stage the game is preparing to start the race. All players need to send their location and orientation to the server before the race starts, otherwise the opponent cars will temporarily disappear as the server has not been told where they are. We wait for the locations to be loaded by breaking when 7E:0054 == 2, 7E:0055 == 0 and 7E:0056 == 2:
if((Memory.RAM[0x54] == 0x02) && (Memory.RAM[0x55] == 0x00) && (Memory.RAM[0x56] == 0x02))

then we read the 16 bit value at 7E:0B70 (this means also 7E:0B71) to get the x value, and similarly 7E:0B90/1 for the y value. The car's orientation is at 7E:0BD1.

17) Once the server has received all the data required, it can instruct the clients to start the race. Each emulator must now send the player's location to the server, while receiving and updating the opponents locations from the server as fast as possible. It should be obvious that the opponent x and y location values are stored in RAM at 7E:0B72/3,7E:0B74/5, 7E:0B76/7 and 7E:0B92/3,7E:0B94/5, 7E:0B96/7. Similarly the opponent orientation data is at 7E:0BD3, 7E:0BD5 and 7E:0BD7 where the order is based on the player id.

18) We want to be able to tell the server when our player has finished the race, either by crossing the finish line or destroying the car. So we watch the RAM at 7E:0054 until it equals 3:
if(Memory.RAM[0x54] == 0x03) game_state = GAMESTATE_RACEFINISHED;

19) And there is just one tiny other detail. There is a fifth car. Remember above, I said the AI controls 5 cars at any one time? Well this fifth car is a generic car and we need to remove it. I do it the laziest way possible, I just constantly reset it's location to zero every vsync.
Memory.RAM[0x0B78] = 0;
Memory.RAM[0x0B79] = 0;
Memory.RAM[0x0B98] = 0;
Memory.RAM[0x0B99] = 0;


Anonymous said...

I totally wanted to play this today. Just wanting to let you know that we're excited for the first playable release! Thanks for the hard work!

foda said...

Finding all the memory addresses to patch was a very brave thing to do, what if you couldn't find one of them, the project would be impracticable, since they were all needed!