echo echo echo
Sunday, October 19, 2008
Sunday, August 31, 2008
Car collisions will not work properly until we make a few modifications. When a collision between two cars occurs, both emulator instances involved try to control both car rebound velocities. This conflict results in jagged movement.
The solution to this is to:
a) Make sure that each emulator instance only rebounds its own player during a player-opponent collision by patching occurrences of the code 99 20 0B STA $0B20,Y to NOPs at ROM addresses 00:BDB2, 00:BDD5, 00:BDEF and 00:BDFE.
b) Make the player's rebound speed equal to his current speed instead of the opponents speed (which is what happens in the game) by patching occurrences of the code 9D 20 0B STA $0B20,X to NOPs at ROM addresses 00:BDAE, 00:BDCB and 00:BDE5.
Tuesday, July 29, 2008
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;
2) The first car is always pink, the second is always blue, the third is always green and the fourth is always yellow regardless on the car type. I did it this way so that no one got confused who was who when the same car type was selected. Also it looks good! The images below show a three player game with a pink wild goose and two fire stingrays.
IMAGES: 1) player 1 and 2 starting views, and 2) car positioning and orientation working.
4) F-Zero uses a series of lap checkpoints for updating the race progression (things like opponent steering, when an opponent should overtake you, ranks etc). As you can see above, I have the rank shown incorrectly (green car is first when it should be third) but when the car moves to the next checkpoint (very close) the rank will be updated correctly.
F-Zero VS is the result of some work I did due to nostalgia. The original F-Zero SNES game was amazing for its time. The graphics used full screen Mode 7 (unlike Super Mario Kart which sacrifices full screen Mode 7 in singleplayer in order to use the same half screen code for multiplayer). And the music... some of the best ever - I CANNOT STRESS how much I love the music!! When I was a kid, the entire futuristic atmosphere amazed me.
I really wanted to do some work with this game, and my first idea was to build a track editor, but then I found some people that have already commenced work on that.
So instead I thought.. why not add two player functionality to F-Zero? After all, that was the one thing missing from the original game. I thought about split screening it but quickly realised it just wasn't feasible. And it wouldn't look good as the field of vision would need to be cropped. So my solution was to allow two player gameplay on two seperate screens. The best way to achieve this was by making it networkable. And then I thought, if it's going to be networkable, why limit it to two players?
And that is F-Zero VS. Four player, networked F-Zero. It's a mixture of ROM hack, Snes9x modification and server code. It works as follows:
1. The server is started on a networked computer.
2. Client computers each start the modified Snes9x emulator and specify the IP address and port of the server. The F-Zero ROM is then loaded by the emulator. It shouldn't matter whether it is headerless or an overdump or whatever as long as the ROM itself is the US version.
(eg. F-ZERO (U) [!] if you know what that means.)
3. The emulator then proceeds to run the game. During the emulation process, many patches to the ROM are made to turn various aspects of the the gameplay into multiplayer, while several memory hooks watch the RAM for certain signals that indicate different game states etc.
4. Each emulator sends the state of their controlled car (location, orientation etc) to the server. Then it requests the state of all the other cars.
5. The states of all the other cars obtained from the server are used to update the car states in the client game.
This project actually wasn't that challenging, fairly standard ROM disassembly. But it was a lot of fun. Most of the work was done on an Asus EEE 900 during long plane and ferry trips while I was on holiday (just got back last night).