Hey all, just wanted to post some information on F-Zero VS, the server-based multiplayer version of SNES F-Zero. Basically what FZVS is for those who don't know is a 4-player multiplayer version of SNES F-Zero that uses a modified version of snes9x to allow people to connect via a server. The author of it stated,
"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."
) at the FZVS blog (long abandoned).
The author got asked a lot if this could work on real hardware, either with an XBAND or via a link cable between systems. He said it was definitely possible, his code would just have to be tinkered with a little. Good news is that he uploaded the source code of the project (in two parts, the snes9x code and the server code) at his blog:
Bad news is the link to the source code download no longer works and I can't find a working link anywhere across the internet. None of the previous links work in archive.org either. Basically this source code is lost. The only other thing I've thought of trying is sending emails to the commenters on the source code page who did indeed download it and see if they still have it and can upload it somewhere like github or something (however, a lot of them, don't look active anymore). I also found a blog that linked to the source code, but that person hasn't wrote a article on the blog in years so who knows if they would respond to an email.
I wouldn't be able to work on this even if I had the source code, but I'd like the information out there for anyone in the future who would ever want to give it a try. This seems to me to be a good project for the SNESoIP team to work on (
) or to get working on real hardware via link cable. I found an old thread on the old version on romhacking.org via archive.org that discusses the feasibility of making a link cable.
"It wouldn't require a lot of code modification, although I'd need to do some things differently. I'd need to inject some code into the main loop to do what the emulator essentially does (updating the RAM etc). But I also patch the ROM in realtime, so in these cases I'd probably need to substitute it with different approach. All possible of course."
User neviksti wrote about the possibility of making a joyport link cable (sorry this is a lot of text, can't find spoiler tags?),
"Also, with the joyport link cable, you wouldn't have to worry about latency issues.
fzvs, how are you currently dealing with latency issues? (also, what about other synchronizing issues ... like if a car explodes?)
Anyway, here are the cable details:
SNES controller connector:
rounded tip
GND
IO
D1
separator
D0
latch
clk
Vcc
square end
The SNES can read the values on IO,D0,D1.
Clk is an output from the SNES that is pulsed when reading certain SNES hardware registers.
The SNES can output values on IO and latch.
Unfortunately, all the SNES "extender" cables I've found on Ebay actually only work for controllers (not the guns, etc.). That is because they are cheap and only pass through Gnd,Vcc, Clk,latch,D0.
So the very simplest design would be take two of these extenders and:
GND <-> GND
D0 <- latch
latch -> D0
The problem with this is there is no easy way to know when the next bit is ready. Carefully written code can make this work though.
If all lines pass through (buy two of those cheap TribalTaps and gut them for the cable connectors):
GND <--> GND
D0 <- latch
D1 <- IO
latch -> D0
IO -> D1
Or, you could even make it asymmetric so the snes can detect which side it is, and use this to automatically select one machine as "player 1". You can either use careful code and send twice as much now, or use one of the data lines to indicate when the next bit is ready (easier to code and debug).
If the first way works, that is clearly preferable, as it would be the easiest and cheapest for anyone to make. Also, the extenders are usually 6 foot, so after splicing two to make the cable it would be 12 foot ... which would be a decent distance for setting up the two systems. You could instead make the SNES connectors attach to phone line connectors (most phone lines have at least 4 wires internally)... then you could string a long phone line to set the distance however you want."
I know some people want to work on a split screen version of F-Zero, but I think a link cable version would be great too and wouldn't lessen the gameplay by having less view-able driving area on the screen. The author of FZVS only ever made it v1.1 before he disappeared. In v1.2, he was going to correct the cars crash behavior (in the original game your momentum is transferred to the car you hit). He was also working on a never released program that looked at debugging SNES games in an unusual, but interesting way:
. Below I will post a bunch of code that the original F-Zero author posted on his blog so that in case it ever disappears off his site it will still be on this thread; hopefully it can be useful to someone in the future who would ever want to work on this:
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;
............................................................
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.
Also, looks like he was maybe working on a disassembly of the game (or just research for FZVS):