-Added EMULATIONORIGIN and MOVIEORIGIN as constants ImportMovie instead of using magic strings.

-Fixed some error and warning messages:
--Made it so that the first .FCM warning is shown instead of the last.
--Added an error for .FCM files that aren't version 2.
--Did the same for ImportVBM, but with version 1.
-Cleaned up comments / code throughout.
-Fixed the file format for GBx; it had an extra bar which made reading Player 1 impossible.
-Seemingly finished ImportVBM.
--The results seem same. I have no way of testing whether they sync up or not.
--As I can't actually handle additional controllers, I just skip the bytes for them.
--As I can't actually handle the miscellaneous buttons and commands (Ex. L, R, Reset), I just give warnings whenever they come up in the file.

TODO: ImportGMV, ImportVMV, ImportNMV, and maybe ImportSMV...I'm going to need a new assignment soon! :)
This commit is contained in:
brandman211 2012-03-06 03:41:11 +00:00
parent e3ba08fefd
commit c9b5f9989a
2 changed files with 338 additions and 283 deletions

View File

@ -179,7 +179,7 @@ namespace BizHawk.MultiClient
if (ControlType == "Gameboy Controller") if (ControlType == "Gameboy Controller")
{ {
input.Append(".|"); //TODO: reset goes here input.Append("."); // TODO: reset goes here
} }
if (ControlType == "NES Controller") if (ControlType == "NES Controller")
{ {

View File

@ -11,6 +11,9 @@ namespace BizHawk.MultiClient
{ {
public static class MovieImport public static class MovieImport
{ {
public const string EMULATIONORIGIN = "emuVersion";
public const string MOVIEORIGIN = "MovieOrigin";
// Attempt to import another type of movie file into a movie object. // Attempt to import another type of movie file into a movie object.
public static Movie ImportFile(string path, out string errorMsg, out string warningMsg) public static Movie ImportFile(string path, out string errorMsg, out string warningMsg)
{ {
@ -81,8 +84,8 @@ namespace BizHawk.MultiClient
} }
/* /*
Read bytes from a BinaryReader and translate them into a string for either the hexidecimal representation of the Read bytes from a BinaryReader and translate them into a string for either the hexidecimal representation of the
binary numbers or the UTF-8 string they represent. binary numbers or the UTF-8 string they represent.
*/ */
private static string BytesToString(BinaryReader r, int size, bool hexadecimal = false) private static string BytesToString(BinaryReader r, int size, bool hexadecimal = false)
{ {
@ -130,10 +133,10 @@ namespace BizHawk.MultiClient
if (line == "") if (line == "")
continue; continue;
if (line.StartsWith("emuVersion")) if (line.StartsWith("emuVersion"))
m.Header.Comments.Add("emuOrigin " + emulator + " version " + ParseHeader(line, "emuVersion")); m.Header.Comments.Add(EMULATIONORIGIN + " " + emulator + " version " + ParseHeader(line, "emuVersion"));
else if (line.StartsWith("version")) else if (line.StartsWith("version"))
m.Header.Comments.Add( m.Header.Comments.Add(
"MovieOrigin " + Path.GetExtension(path) + " version " + ParseHeader(line, "version") MOVIEORIGIN + " " + Path.GetExtension(path) + " version " + ParseHeader(line, "version")
); );
else if (line.StartsWith("romFilename")) else if (line.StartsWith("romFilename"))
m.Header.SetHeaderLine(MovieHeader.GAMENAME, ParseHeader(line, "romFilename")); m.Header.SetHeaderLine(MovieHeader.GAMENAME, ParseHeader(line, "romFilename"));
@ -240,8 +243,8 @@ namespace BizHawk.MultiClient
warningMsg = "Unable to import " + warningMsg + " command on line " + lineNum + "."; warningMsg = "Unable to import " + warningMsg + " command on line " + lineNum + ".";
} }
/* /*
Skip the first two sections of the split, which consist of everything before the starting | and the Skip the first two sections of the split, which consist of everything before the starting | and the
command. Do not use the section after the last |. In other words, get the sections for the players. command. Do not use the section after the last |. In other words, get the sections for the players.
*/ */
for (int section = 2; section < sections.Length - 1; section++) for (int section = 2; section < sections.Length - 1; section++)
{ {
@ -309,44 +312,49 @@ namespace BizHawk.MultiClient
} }
// 004 4-byte little-endian unsigned int: version number, must be 2 // 004 4-byte little-endian unsigned int: version number, must be 2
uint version = r.ReadUInt32(); uint version = r.ReadUInt32();
m.Header.Comments.Add("MovieOrigin .FCM version " + version); if (version != 2)
{
errorMsg = ".FCM movie version must always be 2.";
r.Close();
fs.Close();
return null;
}
m.Header.Comments.Add(MOVIEORIGIN + " .FCM version " + version);
// 008 1-byte flags // 008 1-byte flags
byte flags = r.ReadByte(); byte flags = r.ReadByte();
// bit 0: reserved, set to 0
/* /*
* bit 0: reserved, set to 0 bit 1:
* bit 1: * if "0", movie begins from an embedded "quicksave" snapshot
** if "0", movie begins from an embedded "quicksave" snapshot * if "1", movie begins from reset or power-on[1]
** if "1", movie begins from reset or power-on[1]
* bit 2:
** if "0", NTSC timing
** if "1", PAL timing
** see notes below
* other: reserved, set to 0
*/ */
if ((int)(flags & 2) == 0) if ((flags & 2) == 0)
{ {
errorMsg = "Movies that begin with a savestate are not supported."; errorMsg = "Movies that begin with a savestate are not supported.";
r.Close(); r.Close();
fs.Close(); fs.Close();
return null; return null;
} }
bool pal = false;
if ((int)(flags & 4) != 0)
pal = true;
/* /*
Starting with version 0.98.12 released on September 19, 2004, a PAL flag was added to the header but bit 2:
unfortunately it is not reliable - the emulator does not take the PAL setting from the ROM, but from a user * if "0", NTSC timing
preference. This means that this site cannot calculate movie lengths reliably. * if "1", PAL timing
* see notes below
*/
bool pal = ((flags & 4) != 0);
// other: reserved, set to 0
/*
Starting with version 0.98.12 released on September 19, 2004, a PAL flag was added to the header but
unfortunately it is not reliable - the emulator does not take the PAL setting from the ROM, but from a user
preference. This means that this site cannot calculate movie lengths reliably.
*/ */
m.Header.SetHeaderLine("PAL", pal.ToString()); m.Header.SetHeaderLine("PAL", pal.ToString());
bool movieSyncHackOn = true; bool movieSyncHackOn = ((flags & 16) != 0);
if ((int)(flags & 16) != 0)
movieSyncHackOn = false;
m.Header.SetHeaderLine("SyncHack", movieSyncHackOn.ToString()); m.Header.SetHeaderLine("SyncHack", movieSyncHackOn.ToString());
/* /*
009 1-byte flags: reserved, set to 0 009 1-byte flags: reserved, set to 0
00A 1-byte flags: reserved, set to 0 00A 1-byte flags: reserved, set to 0
00B 1-byte flags: reserved, set to 0 00B 1-byte flags: reserved, set to 0
*/ */
r.ReadBytes(3); r.ReadBytes(3);
// 00C 4-byte little-endian unsigned int: number of frames // 00C 4-byte little-endian unsigned int: number of frames
@ -357,10 +365,10 @@ namespace BizHawk.MultiClient
// 014 4-byte little-endian unsigned int: length of controller data in bytes // 014 4-byte little-endian unsigned int: length of controller data in bytes
uint movieDataSize = r.ReadUInt32(); uint movieDataSize = r.ReadUInt32();
/* /*
018 4-byte little-endian unsigned int: offset to the savestate inside file 018 4-byte little-endian unsigned int: offset to the savestate inside file
The savestate offset is <header_size + length_of_metadata_in_bytes + padding>. The savestate offset should be The savestate offset is <header_size + length_of_metadata_in_bytes + padding>. The savestate offset should be
4-byte aligned. At the savestate offset there is a savestate file. The savestate exists even if the movie is 4-byte aligned. At the savestate offset there is a savestate file. The savestate exists even if the movie is
reset-based. reset-based.
*/ */
r.ReadUInt32(); r.ReadUInt32();
// 01C 4-byte little-endian unsigned int: offset to the controller data inside file // 01C 4-byte little-endian unsigned int: offset to the controller data inside file
@ -370,7 +378,7 @@ namespace BizHawk.MultiClient
m.Header.SetHeaderLine("MD5", MD5); m.Header.SetHeaderLine("MD5", MD5);
// 030 4-byte little-endian unsigned int: version of the emulator used // 030 4-byte little-endian unsigned int: version of the emulator used
uint emuVersion = r.ReadUInt32(); uint emuVersion = r.ReadUInt32();
m.Header.Comments.Add("emuOrigin FCEU " + emuVersion.ToString()); m.Header.Comments.Add(EMULATIONORIGIN + " FCEU " + emuVersion.ToString());
// 034 name of the ROM used - UTF8 encoded nul-terminated string. // 034 name of the ROM used - UTF8 encoded nul-terminated string.
List<byte> gameBytes = new List<byte>(); List<byte> gameBytes = new List<byte>();
while (r.PeekChar() != 0) while (r.PeekChar() != 0)
@ -380,9 +388,9 @@ namespace BizHawk.MultiClient
string gameName = System.Text.Encoding.UTF8.GetString(gameBytes.ToArray()); string gameName = System.Text.Encoding.UTF8.GetString(gameBytes.ToArray());
m.Header.SetHeaderLine(MovieHeader.GAMENAME, gameName); m.Header.SetHeaderLine(MovieHeader.GAMENAME, gameName);
/* /*
After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM name After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM
and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the name and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the
emulator. emulator.
*/ */
List<byte> authorBytes = new List<byte>(); List<byte> authorBytes = new List<byte>();
while (r.PeekChar() != 0) while (r.PeekChar() != 0)
@ -409,13 +417,13 @@ namespace BizHawk.MultiClient
int delta = (update >> 5) & 3; int delta = (update >> 5) & 3;
int frames = 0; int frames = 0;
/* /*
The delta byte(s) indicate the number of emulator frames between this update and the next update. It is The delta byte(s) indicate the number of emulator frames between this update and the next update. It is
encoded in little-endian format and its size depends on the magnitude of the delta: encoded in little-endian format and its size depends on the magnitude of the delta:
Delta of: Number of bytes: Delta of: Number of bytes:
0 0 0 0
1-255 1 1-255 1
256-65535 2 256-65535 2
65536-(2^24-1) 3 65536-(2^24-1) 3
*/ */
for (int b = 0; b < delta; b++) for (int b = 0; b < delta; b++)
frames += r.ReadByte() * (int)Math.Pow(2, b * 8); frames += r.ReadByte() * (int)Math.Pow(2, b * 8);
@ -435,58 +443,57 @@ namespace BizHawk.MultiClient
// Control update: 1aabbbbb // Control update: 1aabbbbb
bool reset = false; bool reset = false;
int command = update & 0x1F; int command = update & 0x1F;
/* // bbbbb:
bbbbb: if (warningMsg == "")
** 0 Do nothing
** 1 Reset
** 2 Power cycle
** 7 VS System Insert Coin
** 8 VS System Dipswitch 0 Toggle
** 24 FDS Insert
** 25 FDS Eject
** 26 FDS Select Side
*/
switch (command)
{ {
case 0: switch (command)
break; {
case 1: // Do nothing
reset = !controllers["Reset"]; case 0:
controllers["Reset"] = reset; break;
break; // Reset
case 2: case 1:
reset = true; reset = !controllers["Reset"];
if (frame != 1) controllers["Reset"] = reset;
{ break;
warningMsg = "hard reset"; // Power cycle
} case 2:
break; reset = true;
case 7: if (frame != 1)
warningMsg = "VS System Insert Coin"; warningMsg = "hard reset";
break; break;
case 8: // VS System Insert Coin
warningMsg = "VS System Dipswitch 0 Toggle"; case 7:
break; warningMsg = "VS System Insert Coin";
case 24: break;
warningMsg = "FDS Insert"; // VS System Dipswitch 0 Toggle
break; case 8:
case 25: warningMsg = "VS System Dipswitch 0 Toggle";
warningMsg = "FDS Eject"; break;
break; // FDS Insert
case 26: case 24:
warningMsg = "FDS Select Side"; warningMsg = "FDS Insert";
break; break;
default: // FDS Eject
warningMsg = "unknown"; case 25:
break; warningMsg = "FDS Eject";
break;
// FDS Select Side
case 26:
warningMsg = "FDS Select Side";
break;
default:
warningMsg = "unknown";
break;
}
if (warningMsg != "")
warningMsg = "Unable to import " + warningMsg + " command at frame " + frame + ".";
} }
if (warningMsg != "")
warningMsg = "Unable to import " + warningMsg + " command at frame " + frame + ".";
/* /*
1 Even if the header says "movie begins from reset", the file still contains a quicksave, and the 1 Even if the header says "movie begins from reset", the file still contains a quicksave, and the
quicksave is actually loaded. This flag can't therefore be trusted. To check if the movie actually begins quicksave is actually loaded. This flag can't therefore be trusted. To check if the movie actually
from reset, one must analyze the controller data and see if the first non-idle command in the file is a begins from reset, one must analyze the controller data and see if the first non-idle command in the
Reset or Power Cycle type control command. file is a Reset or Power Cycle type control command.
*/ */
if (!reset && frame == 1) if (!reset && frame == 1)
{ {
@ -516,8 +523,8 @@ namespace BizHawk.MultiClient
*/ */
int button = update & 7; int button = update & 7;
/* /*
The controller update toggles the affected input. Controller update data is emitted to the movie file The controller update toggles the affected input. Controller update data is emitted to the movie file
only when the state of the controller changes. only when the state of the controller changes.
*/ */
controllers["P" + player + " " + buttons[button]] = !controllers["P" + player + " " + buttons[button]]; controllers["P" + player + " " + buttons[button]] = !controllers["P" + player + " " + buttons[button]];
} }
@ -556,27 +563,20 @@ namespace BizHawk.MultiClient
} }
// 004 1-byte flags: // 004 1-byte flags:
byte flags = r.ReadByte(); byte flags = r.ReadByte();
/* // bit 7: 0=reset-based, 1=savestate-based
* bit 7: 0=reset-based, 1=savestate-based if ((flags & 0x80) != 0)
* other bits: unknown, set to 0
*/
if ((int)(flags & 0x80) != 0)
{ {
errorMsg = "Movies that begin with a savestate are not supported."; errorMsg = "Movies that begin with a savestate are not supported.";
r.Close(); r.Close();
fs.Close(); fs.Close();
return null; return null;
} }
// other bits: unknown, set to 0
// 005 1-byte flags: // 005 1-byte flags:
flags = r.ReadByte(); flags = r.ReadByte();
/* // bit 5: is a FDS recording
* bit 5: is a FDS recording
* bit 6: uses controller 2
* bit 7: uses controller 1
* other bits: unknown, set to 0
*/
bool FDS; bool FDS;
if ((int)(flags & 0x20) != 0) if ((flags & 0x20) != 0)
{ {
FDS = true; FDS = true;
m.Header.SetHeaderLine(MovieHeader.PLATFORM, "FDS"); m.Header.SetHeaderLine(MovieHeader.PLATFORM, "FDS");
@ -586,30 +586,26 @@ namespace BizHawk.MultiClient
FDS = false; FDS = false;
m.Header.SetHeaderLine(MovieHeader.PLATFORM, "NES"); m.Header.SetHeaderLine(MovieHeader.PLATFORM, "NES");
} }
bool controller2 = false; // bit 6: uses controller 2
if ((int)(flags & 0x40) != 0) bool controller2 = ((flags & 0x40) != 0);
{ // bit 7: uses controller 1
controller2 = true; bool controller1 = ((flags & 0x80) != 0);
} // other bits: unknown, set to 0
bool controller1 = false;
if ((int)(flags & 0x80) != 0)
{
controller1 = true;
}
// 006 4-byte little-endian unsigned int: unknown, set to 00000000 // 006 4-byte little-endian unsigned int: unknown, set to 00000000
r.ReadInt32(); r.ReadInt32();
// 00A 4-byte little-endian unsigned int: rerecord count minus 1 // 00A 4-byte little-endian unsigned int: rerecord count minus 1
uint rerecordCount = r.ReadUInt32(); uint rerecordCount = r.ReadUInt32();
/* /*
The rerecord count stored in the file is the number of times a savestate was loaded. If a savestate was never loaded, The rerecord count stored in the file is the number of times a savestate was loaded. If a savestate was never
the number is 0. Famtasia however displays "1" in such case. It always adds 1 to the number found in the file. loaded, the number is 0. Famtasia however displays "1" in such case. It always adds 1 to the number found in
the file.
*/ */
m.SetRerecords(((int)rerecordCount) + 1); m.SetRerecords(((int)rerecordCount) + 1);
// 00E 2-byte little-endian unsigned int: unknown, set to 0000 // 00E 2-byte little-endian unsigned int: unknown, set to 0000
r.ReadInt16(); r.ReadInt16();
// 010 64-byte zero-terminated emulator identifier string // 010 64-byte zero-terminated emulator identifier string
string emuVersion = RemoveNull(BytesToString(r, 64)); string emuVersion = RemoveNull(BytesToString(r, 64));
m.Header.Comments.Add("emuOrigin Famtasia version " + emuVersion); m.Header.Comments.Add(EMULATIONORIGIN + " Famtasia version " + emuVersion);
// 050 64-byte zero-terminated movie title string // 050 64-byte zero-terminated movie title string
string gameName = RemoveNull(BytesToString(r, 64)); string gameName = RemoveNull(BytesToString(r, 64));
m.Header.SetHeaderLine(MovieHeader.GAMENAME, gameName); m.Header.SetHeaderLine(MovieHeader.GAMENAME, gameName);
@ -621,14 +617,14 @@ namespace BizHawk.MultiClient
return m; return m;
} }
/* /*
The file format has no means of identifying NTSC/PAL. It is always assumed that the game is NTSC - that is, 60 The file format has no means of identifying NTSC/PAL. It is always assumed that the game is NTSC - that is, 60
fps. fps.
*/ */
m.Header.SetHeaderLine("PAL", "False"); m.Header.SetHeaderLine("PAL", "False");
/* /*
090 frame data begins here 090 frame data begins here
The file has no terminator byte or frame count. The number of frames is the <filesize minus 144> divided by The file has no terminator byte or frame count. The number of frames is the <filesize minus 144> divided by
<number of bytes per frame>. <number of bytes per frame>.
*/ */
int bytesPerFrame = 0; int bytesPerFrame = 0;
if (controller1) if (controller1)
@ -655,9 +651,9 @@ namespace BizHawk.MultiClient
controllers.Type = new ControllerDefinition(); controllers.Type = new ControllerDefinition();
controllers.Type.Name = "NES Controller"; controllers.Type.Name = "NES Controller";
/* /*
Each frame consists of 1 or more bytes. Controller 1 takes 1 byte, controller 2 takes 1 byte, and the FDS Each frame consists of 1 or more bytes. Controller 1 takes 1 byte, controller 2 takes 1 byte, and the FDS
data takes 1 byte. If all three exist, the frame is 3 bytes. For example, if the movie is a regular NES game data takes 1 byte. If all three exist, the frame is 3 bytes. For example, if the movie is a regular NES game
with only controller 1 data, a frame is 1 byte. with only controller 1 data, a frame is 1 byte.
*/ */
int player = 1; int player = 1;
while (player <= 3) while (player <= 3)
@ -668,11 +664,11 @@ namespace BizHawk.MultiClient
player++; player++;
if (player != 3) if (player != 3)
{ {
byte controllerstate = r.ReadByte(); byte controllerState = r.ReadByte();
byte and = 0x1; byte and = 0x1;
for (int button = 0; button < buttons.Length; button++) for (int button = 0; button < buttons.Length; button++)
{ {
controllers["P" + player + " " + buttons[button]] = ((int)(controllerstate & and) != 0); controllers["P" + player + " " + buttons[button]] = ((int)(controllerState & and) != 0);
and <<= 1; and <<= 1;
} }
} }
@ -889,7 +885,7 @@ namespace BizHawk.MultiClient
} }
// 0004: 4-byte little endian unsigned int: dega version // 0004: 4-byte little endian unsigned int: dega version
uint emuVersion = r.ReadUInt32(); uint emuVersion = r.ReadUInt32();
m.Header.Comments.Add("emuOrigin Dega version " + emuVersion.ToString()); m.Header.Comments.Add(EMULATIONORIGIN + " Dega version " + emuVersion.ToString());
// 0008: 4-byte little endian unsigned int: frame count // 0008: 4-byte little endian unsigned int: frame count
uint frameCount = r.ReadUInt32(); uint frameCount = r.ReadUInt32();
// 000c: 4-byte little endian unsigned int: rerecord count // 000c: 4-byte little endian unsigned int: rerecord count
@ -915,22 +911,16 @@ namespace BizHawk.MultiClient
m.Header.SetHeaderLine(MovieHeader.AUTHOR, author); m.Header.SetHeaderLine(MovieHeader.AUTHOR, author);
// 0060: 4-byte little endian flags // 0060: 4-byte little endian flags
byte flags = r.ReadByte(); byte flags = r.ReadByte();
/* // bit 0: unused
* bit 0: unused // bit 1: PAL
* bit 1: PAL bool pal = ((flags & 2) != 0);
* bit 2: Japan
* bit 3: Game Gear (version 1.16+)
*/
bool pal = false;
if ((int)(flags & 2) != 0)
pal = true;
m.Header.SetHeaderLine("PAL", pal.ToString()); m.Header.SetHeaderLine("PAL", pal.ToString());
bool japan = false; bool japan = ((flags & 4) != 0);
if ((int)(flags & 4) != 0) // bit 2: Japan
japan = true;
m.Header.SetHeaderLine("Japan", japan.ToString()); m.Header.SetHeaderLine("Japan", japan.ToString());
// bit 3: Game Gear (version 1.16+)
bool gamegear; bool gamegear;
if ((int)(flags & 8) != 0) if ((flags & 8) != 0)
{ {
gamegear = true; gamegear = true;
m.Header.SetHeaderLine(MovieHeader.PLATFORM, "GG"); m.Header.SetHeaderLine(MovieHeader.PLATFORM, "GG");
@ -966,23 +956,23 @@ namespace BizHawk.MultiClient
controllers.Type = new ControllerDefinition(); controllers.Type = new ControllerDefinition();
controllers.Type.Name = "SMS Controller"; controllers.Type.Name = "SMS Controller";
/* /*
Controller data is made up of one input packet per frame. Each packet currently consists of 2 bytes. The Controller data is made up of one input packet per frame. Each packet currently consists of 2 bytes. The
first byte is for controller 1 and the second controller 2. The Game Gear only uses the controller 1 input first byte is for controller 1 and the second controller 2. The Game Gear only uses the controller 1 input
however both bytes are still present. however both bytes are still present.
*/ */
for (int player = 1; player <= 2; player++) for (int player = 1; player <= 2; player++)
{ {
byte controllerstate = r.ReadByte(); byte controllerState = r.ReadByte();
byte and = 1; byte and = 1;
for (int button = 0; button < buttons.Length; button++) for (int button = 0; button < buttons.Length; button++)
{ {
controllers["P" + player + " " + buttons[button]] = ((int)(controllerstate & and) != 0); controllers["P" + player + " " + buttons[button]] = ((int)(controllerState & and) != 0);
and <<= 1; and <<= 1;
} }
if (player == 1) if (player == 1)
controllers["Pause"] = ( controllers["Pause"] = (
((int)(controllerstate & 0x40) != 0 && (!gamegear)) || ((int)(controllerState & 0x40) != 0 && (!gamegear)) ||
((int)(controllerstate & 0x80) != 0 && gamegear) ((int)(controllerState & 0x80) != 0 && gamegear)
); );
} }
MnemonicsGenerator mg = new MnemonicsGenerator(); MnemonicsGenerator mg = new MnemonicsGenerator();
@ -1046,23 +1036,23 @@ namespace BizHawk.MultiClient
byte ControllerFlags = r.ReadByte(); byte ControllerFlags = r.ReadByte();
int numControllers; int numControllers;
if ((int)(ControllerFlags & 16) != 0) if ((ControllerFlags & 16) != 0)
numControllers = 5; numControllers = 5;
else if ((int)(ControllerFlags & 8) != 0) else if ((ControllerFlags & 8) != 0)
numControllers = 4; numControllers = 4;
else if ((int)(ControllerFlags & 4) != 0) else if ((ControllerFlags & 4) != 0)
numControllers = 3; numControllers = 3;
else if ((int)(ControllerFlags & 2) != 0) else if ((ControllerFlags & 2) != 0)
numControllers = 2; numControllers = 2;
else else
numControllers = 1; numControllers = 1;
byte MovieFlags = r.ReadByte(); byte MovieFlags = r.ReadByte();
if ((int)(MovieFlags & 1) == 0) if ((MovieFlags & 1) == 0)
return null; //TODO: Savestate movies not supported error return null; //TODO: Savestate movies not supported error
if ((int)(MovieFlags & 2) != 0) if ((MovieFlags & 2) != 0)
{ {
m.Header.SetHeaderLine("PAL", "True"); m.Header.SetHeaderLine("PAL", "True");
} }
@ -1108,167 +1098,232 @@ namespace BizHawk.MultiClient
{ {
errorMsg = ""; errorMsg = "";
warningMsg = ""; warningMsg = "";
//Converts vbm to native text based format.
Movie m = new Movie(Path.ChangeExtension(path, ".tas"), MOVIEMODE.PLAY); Movie m = new Movie(Path.ChangeExtension(path, ".tas"), MOVIEMODE.PLAY);
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
BinaryReader r = new BinaryReader(fs); BinaryReader r = new BinaryReader(fs);
// 000 4-byte signature: 56 42 4D 1A "VBM\x1A"
//0xoffset string signature = BytesToString(r, 4);
//0x00 if (signature != "VBM\x1A")
uint signature = r.ReadUInt32(); //always 56 42 4D 1A (VBM\x1A)
if (signature != 0x56424D1A)
{ {
errorMsg = "This is not a valid .VBM file."; errorMsg = "This is not a valid .VBM file.";
r.Close(); r.Close();
fs.Close(); fs.Close();
return null; return null;
} }
// 004 4-byte little-endian unsigned int: major version number, must be "1"
uint versionno = r.ReadUInt32(); //always 1 uint majorVersion = r.ReadUInt32();
uint uid = r.ReadUInt32(); //time of recording if (majorVersion != 1)
{
errorMsg = ".VBM major movie version must be 1.";
r.Close();
fs.Close();
return null;
}
/*
008 4-byte little-endian integer: movie "uid" - identifies the movie-savestate relationship, also used as the
recording time in Unix epoch format
*/
uint uid = r.ReadUInt32();
m.Header.SetHeaderLine(MovieHeader.GUID, uid.ToString()); m.Header.SetHeaderLine(MovieHeader.GUID, uid.ToString());
// 00C 4-byte little-endian unsigned int: number of frames
uint frameCount = r.ReadUInt32(); uint frameCount = r.ReadUInt32();
// 010 4-byte little-endian unsigned int: rerecord count
//0x10
uint rerecordCount = r.ReadUInt32(); uint rerecordCount = r.ReadUInt32();
m.SetRerecords((int)rerecordCount); m.SetRerecords((int)rerecordCount);
m.Header.SetHeaderLine(MovieHeader.RERECORDS, m.Rerecords.ToString()); // 014 1-byte flags: (movie start flags)
Byte moviestartflags = r.ReadByte(); byte flags = r.ReadByte();
// bit 0: if "1", movie starts from an embedded "quicksave" snapshot
bool startfromquicksave = false; bool startfromquicksave = ((flags & 1) != 0);
bool startfromsram = false; // bit 1: if "1", movie starts from reset with an embedded SRAM
bool startfromsram = ((flags & 2) != 0);
if ((moviestartflags & 0x01) != 0) startfromquicksave = true; // other: reserved, set to 0
if ((moviestartflags & 0x02) != 0) startfromsram = true; // We can't start from either save option.
if (startfromquicksave || startfromsram)
if (startfromquicksave & startfromsram)
{ {
errorMsg = "Movies that begin with a savestate are not supported."; errorMsg = "Movies that begin with a save are not supported.";
// (If both bits 0 and 1 are "1", the movie file is invalid)
if (startfromquicksave && startfromsram)
errorMsg = "The movie file is invalid.";
r.Close(); r.Close();
fs.Close(); fs.Close();
return null; return null;
} }
//015 1-byte flags: controller flags
//0x15 flags = r.ReadByte();
Byte controllerflags = r.ReadByte(); // TODO: Handle the additional controllers.
int players = 0;
int numControllers; //number of controllers changes for SGB // bit 0: controller 1 in use
if ((flags & 1) != 0)
if ((controllerflags & 0x08) != 0) numControllers = 4; players++;
else if ((controllerflags & 0x04) != 0) numControllers = 3; else
else if ((controllerflags & 0x02) != 0) numControllers = 2;
else numControllers = 1;
//0x16
Byte systemflags = r.ReadByte(); //what system is it?
bool is_gba = false;
bool is_gbc = false;
bool is_sgb = false;
bool is_gb = false;
if ((systemflags & 0x04) != 0) is_sgb = true;
if ((systemflags & 0x02) != 0) is_gbc = true;
if ((systemflags & 0x01) != 0) is_gba = true;
else is_gb = true;
if (is_gb & is_gbc & is_gba & is_sgb)
{ {
errorMsg = "Not a valid .VBM platform type."; errorMsg = "Controller 1 must be in use.";
r.Close(); r.Close();
fs.Close(); fs.Close();
return null; return null;
} }
//TODO: set platform in header // bit 1: controller 2 in use (SGB games can be 2-player multiplayer)
if ((flags & 2) != 0)
//0x17 players++;
Byte flags = r.ReadByte(); //emulation flags // bit 2: controller 3 in use (SGB games can be 3- or 4-player multiplayer with multitap)
if ((flags & 4) != 0)
//placeholder for reserved bit (set to 0) players++;
bool echoramfix = false; // bit 3: controller 4 in use (SGB games can be 3- or 4-player multiplayer with multitap)
bool gbchdma5fix = false; if ((flags & 8) != 0)
bool lagreduction = false; players++;
//placeholder for unsupported bit // other: reserved
bool rtcenable = false; // 016 1-byte flags: system flags (game always runs at 60 frames/sec)
bool skipbiosfile = false; flags = r.ReadByte();
bool usebiosfile = false; // bit 0: if "1", movie is for the GBA system
bool is_gba = ((flags & 1) != 0);
if ((flags & 0x40) != 0) echoramfix = true; // bit 1: if "1", movie is for the GBC system
if ((flags & 0x20) != 0) gbchdma5fix = true; bool is_gbc = ((flags & 2) != 0);
if ((flags & 0x10) != 0) lagreduction = true; // bit 2: if "1", movie is for the SGB system
if ((flags & 0x08) != 0) bool is_sgb = ((flags & 4) != 0);
// other: reserved, set to 0
// (At most one of bits 0, 1, 2 can be "1")
if (!(is_gba ^ is_gbc ^ is_sgb) && (is_gba || is_gbc || is_sgb))
{ {
errorMsg = "Invalid .VBM file."; errorMsg = "This is not a valid .VBM file.";
r.Close(); r.Close();
fs.Close(); fs.Close();
return null; return null;
} }
if ((flags & 0x04) != 0) rtcenable = true; // (If all 3 of these bits are "0", it is for regular GB.)
if ((flags & 0x02) != 0) skipbiosfile = true; string platform = "GB";
if ((flags & 0x01) != 0) usebiosfile = true; if (is_gba)
platform = "GBA";
//0x18 if (is_gbc)
uint winsavetype = r.ReadUInt32(); platform = "GBC";
uint winflashsize = r.ReadUInt32(); if (is_sgb)
platform = "SGB";
//0x20 m.Header.SetHeaderLine(MovieHeader.PLATFORM, platform);
uint gbemulatortype = r.ReadUInt32(); // 017 1-byte flags: (values of some boolean emulator options)
flags = r.ReadByte();
char[] internalgamename = r.ReadChars(0x0C); /*
string gamename = new String(internalgamename); * bit 0: (useBiosFile) if "1" and the movie is of a GBA game, the movie was made using a GBA BIOS file.
m.Header.SetHeaderLine(MovieHeader.GAMENAME, gamename); * bit 1: (skipBiosFile) if "0" and the movie was made with a GBA BIOS file, the BIOS intro is included in the
* movie.
//0x30 * bit 2: (rtcEnable) if "1", the emulator "real time clock" feature was enabled.
Byte minorversion = r.ReadByte(); */
Byte internalcrc = r.ReadByte(); // bit 3: (unsupported) must be "0" or the movie file is considered invalid (legacy).
ushort internalchacksum = r.ReadUInt16(); if ((flags & 8) != 0)
uint unitcode = r.ReadUInt32(); {
uint saveoffset = r.ReadUInt32(); //set to 0 if unused errorMsg = "This is not a valid .VBM file.";
uint controllerdataoffset = r.ReadUInt32(); r.Close();
fs.Close();
//0x40 start info. return null;
char[] authorsname = r.ReadChars(0x40); //vbm specification states these strings }
string author = new String(authorsname); //are locale dependant. /*
* bit 4: (lagReduction) if "0" and the movie is of a GBA game, the movie was made using the old excessively
* laggy GBA timing.
* bit 5: (gbcHdma5Fix) if "0" and the movie is of a GBC game, the movie was made using the old buggy HDMA5
* timing.
* bit 6: (echoRAMFix) if "1" and the movie is of a GB, GBC, or SGB game, the movie was made with Echo RAM Fix
* on, otherwise it was made with Echo RAM Fix off.
* bit 7: reserved, set to 0.
*/
/*
018 4-byte little-endian unsigned int: theApp.winSaveType (value of that emulator option)
01C 4-byte little-endian unsigned int: theApp.winFlashSize (value of that emulator option)
020 4-byte little-endian unsigned int: gbEmulatorType (value of that emulator option)
*/
r.ReadBytes(12);
/*
024 12-byte character array: the internal game title of the ROM used while recording, not necessarily
null-terminated (ASCII?)
*/
string gameName = RemoveNull(BytesToString(r, 12));
m.Header.SetHeaderLine(MovieHeader.GAMENAME, gameName);
// 030 1-byte unsigned char: minor version/revision number of current VBM version, the latest is "1"
byte minorVersion = r.ReadByte();
m.Header.Comments.Add(MOVIEORIGIN + " .VBM version " + majorVersion + "." + minorVersion);
/*
031 1-byte unsigned char: the internal CRC of the ROM used while recording
032 2-byte little-endian unsigned short: the internal Checksum of the ROM used while recording, or a calculated
CRC16 of the BIOS if GBA
034 4-byte little-endian unsigned int: the Game Code of the ROM used while recording, or the Unit Code if not
GBA
038 4-byte little-endian unsigned int: offset to the savestate or SRAM inside file, set to 0 if unused
*/
r.ReadBytes(11);
// 03C 4-byte little-endian unsigned int: offset to the controller data inside file
uint firstFrameOffset = r.ReadUInt32();
// After the header is 192 bytes of text. The first 64 of these 192 bytes are for the author's name (or names).
string author = RemoveNull(BytesToString(r, 64));
m.Header.SetHeaderLine(MovieHeader.AUTHOR, author); m.Header.SetHeaderLine(MovieHeader.AUTHOR, author);
// The following 128 bytes are for a description of the movie. Both parts must be null-terminated.
//0x80 string movieDescription = RemoveNull(BytesToString(r, 128));
char[] moviedescription = r.ReadChars(0x80); m.Header.Comments.Add("comment " + movieDescription);
/*
//0x0100 TODO: implement start data. There are no specifics on the googlecode page as to how long the SRAM or savestate
//End of VBM header should be.
*/
//if there is no SRAM or savestate, the controller data should start at 0x0100 by default, // If there is no "Start Data", this will probably begin at byte 0x100 in the file, but this is not guaranteed.
//but this is not buaranteed r.BaseStream.Position = firstFrameOffset;
/*
//TODO: implement start data. There are no specifics on the googlecode page as to how long * 01 00 A
//the SRAM or savestate should be. * 02 00 B
* 04 00 Select
uint framesleft = frameCount; * 08 00 Start
* 10 00 Right
r.BaseStream.Position = controllerdataoffset; //advances to controller data. * 20 00 Left
* 40 00 Up
int currentoffset = (int)controllerdataoffset; * 80 00 Down
*/
SimpleController controllers = new SimpleController(); string[] buttons = new string[8] { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down" };
controllers.Type = new ControllerDefinition(); /*
controllers.Type.Name = "Gameboy Controller"; * 00 01 R
string[] buttons = new string[8] {"A", "B", "Select", "Start", "Right", "Left", "Up", "Down"}; * 00 02 L
* 00 04 Reset (old timing)
* 00 08 Reset (new timing since version 1.1)
* 00 10 Left motion sensor
* 00 20 Right motion sensor
* 00 40 Down motion sensor
* 00 80 Up motion sensor
*/
string[] other = new string[8] {
"R", "L", "Reset (old timing)" , "Reset (new timing since version 1.1)", "Left motion sensor",
"Right motion sensor", "Down motion sensor", "Up motion sensor"
};
for (int frame = 1; frame <= frameCount; frame++) for (int frame = 1; frame <= frameCount; frame++)
{ {
ushort controllerstate = r.ReadUInt16(); /*
// TODO: reset, GBA buttons go here A stream of 2-byte bitvectors which indicate which buttons are pressed at each point in time. They will come
byte and = 0x1; in groups of however many controllers are active, in increasing order.
*/
ushort controllerState = r.ReadUInt16();
SimpleController controllers = new SimpleController();
controllers.Type = new ControllerDefinition();
controllers.Type.Name = "Gameboy Controller";
ushort and = 0x1;
for (int button = 0; button < buttons.Length; button++) for (int button = 0; button < buttons.Length; button++)
{ {
controllers["P1 " + buttons[button]] = ((int)(controllerstate & and) != 0); controllers[buttons[button]] = ((int)(controllerState & and) != 0);
and <<= 1; and <<= 1;
} }
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
// TODO: Handle the other buttons.
and = 0x100;
if (warningMsg == "")
{
for (int button = 0; button < other.Length; button++)
{
if ((controllerState & and) != 0)
{
warningMsg = other[button];
break;
}
and <<= 1;
}
if (warningMsg != "")
warningMsg = "Unable to import " + warningMsg + " at frame " + frame + ".";
}
// TODO: Handle the additional controllers.
r.ReadBytes((players - 1) * 2);
} }
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
r.Close(); r.Close();
fs.Close(); fs.Close();
return m; return m;