-.Close()'d everything.

-Added a FourScore header and applied it to ImportFile and ImportFCM.
--Not sure if there's any equivalent header for FMV/FCM.
-Seemingly have the update bytes being read properly.

TODO: Make ImportFCM actually utilize the update bytes to write frames.
This commit is contained in:
brandman211 2012-03-02 04:15:15 +00:00
parent ff12c2c5ef
commit bedbf1d5e1
2 changed files with 320 additions and 180 deletions

View File

@ -26,6 +26,7 @@ namespace BizHawk.MultiClient
public const string RERECORDS = "rerecordCount";
public const string GUID = "GUID";
public const string STARTSFROMSAVESTATE = "StartsFromSavestate";
public const string FOURSCORE = "FourScore";
public static string MovieVersion = "BizHawk v0.0.1";

View File

@ -100,7 +100,8 @@ namespace BizHawk.MultiClient
errorMsg = "";
warningMsg = "";
Movie m = new Movie(Path.ChangeExtension(path, ".tas"), MOVIEMODE.PLAY);
var file = new FileInfo(path);
FileInfo file = new FileInfo(path);
StreamReader sr = file.OpenText();
string[] buttons = new string[] { };
string controller = "";
string emulator = "";
@ -121,146 +122,152 @@ namespace BizHawk.MultiClient
break;
}
m.Header.SetHeaderLine(MovieHeader.PLATFORM, platform);
using (StreamReader sr = file.OpenText())
int lineNum = 0;
string line = "";
while ((line = sr.ReadLine()) != null)
{
int lineNum = 0;
string line = "";
while ((line = sr.ReadLine()) != null)
lineNum++;
if (line == "")
continue;
if (line.StartsWith("emuVersion"))
m.Header.Comments.Add("emuOrigin " + emulator + " version " + ParseHeader(line, "emuVersion"));
else if (line.StartsWith("version"))
m.Header.Comments.Add(
"MovieOrigin " + Path.GetExtension(path) + " version " + ParseHeader(line, "version")
);
else if (line.StartsWith("romFilename"))
m.Header.SetHeaderLine(MovieHeader.GAMENAME, ParseHeader(line, "romFilename"));
else if (line.StartsWith("comment author"))
m.Header.SetHeaderLine(MovieHeader.AUTHOR, ParseHeader(line, "comment author"));
else if (line.StartsWith("rerecordCount"))
{
lineNum++;
if (line == "")
continue;
if (line.StartsWith("emuVersion"))
m.Header.Comments.Add("emuOrigin " + emulator + " version " + ParseHeader(line, "emuVersion"));
else if (line.StartsWith("version"))
m.Header.Comments.Add(
"MovieOrigin " + Path.GetExtension(path) + " version " + ParseHeader(line, "version")
);
else if (line.StartsWith("romFilename"))
m.Header.SetHeaderLine(MovieHeader.GAMENAME, ParseHeader(line, "romFilename"));
else if (line.StartsWith("comment author"))
m.Header.SetHeaderLine(MovieHeader.AUTHOR, ParseHeader(line, "comment author"));
else if (line.StartsWith("rerecordCount"))
int rerecordCount;
// Try to parse the re-record count as an integer, defaulting to 0 if it fails.
try
{
int rerecordCount;
// Try to parse the re-record count as an integer, defaulting to 0 if it fails.
try
{
rerecordCount = int.Parse(ParseHeader(line, "rerecordCount"));
}
catch
{
rerecordCount = 0;
}
m.SetRerecords(rerecordCount);
rerecordCount = int.Parse(ParseHeader(line, "rerecordCount"));
}
else if (line.StartsWith("guid"))
m.Header.SetHeaderLine(MovieHeader.GUID, ParseHeader(line, "GUID"));
else if (line.StartsWith("StartsFromSavestate"))
catch
{
// If this movie starts from a savestate, we can't support it.
if (ParseHeader(line, "StartsFromSavestate") == "1")
{
errorMsg = "Movies that begin with a savestate are not supported.";
return null;
}
rerecordCount = 0;
}
else if (line.StartsWith("palFlag"))
{
bool pal;
// Try to parse the PAL setting as a bool, defaulting to false if it fails.
try
{
pal = Convert.ToBoolean(int.Parse(ParseHeader(line, "palFlag")));
}
catch
{
pal = false;
}
m.Header.SetHeaderLine("PAL", pal.ToString());
}
else if (line.StartsWith("sub"))
{
Subtitle s = new Subtitle();
// The header name, frame, and message are separated by a space.
int first = line.IndexOf(' ');
int second = line.IndexOf(' ', first + 1);
if (first != -1 && second != -1)
{
// Concatenate the frame and message with default values for the additional fields.
string frame = line.Substring(first + 1, second - first - 1);
string message = line.Substring(second + 1, line.Length - second - 1);
m.Subtitles.AddSubtitle("subtitle " + frame + " 0 0 200 FFFFFFFF " + message);
}
}
else if (line[0] == '|')
{
// Handle a frame of input.
ArrayList frame = new ArrayList();
// Split up the sections of the frame.
string[] sections = line.Split('|');
// Start building the data for the controllers.
SimpleController controllers = new SimpleController();
controllers.Type = new ControllerDefinition();
controllers.Type.Name = controller;
// Get the first invalid command warning message that arises.
if (Path.GetExtension(path).ToUpper() == ".FM2" && warningMsg == "" && sections[1].Length != 0)
{
switch (sections[1][0])
{
case '0':
break;
case '1':
controllers["Reset"] = true;
break;
case '2':
if (m.Length() != 0)
{
warningMsg = "hard reset";
}
break;
case '4':
warningMsg = "FDS Insert";
break;
case '8':
warningMsg = "FDS Select";
break;
default:
warningMsg = "unknown";
break;
}
if (warningMsg != "")
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
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++)
{
// The player number is one less than the section number for the reasons explained above.
int player = section - 1;
// Only count lines with that have the right number of buttons and are for valid players.
if (
sections[section].Length == buttons.Length &&
player <= Global.PLAYERS[controllers.Type.Name]
)
for (int button = 0; button < buttons.Length; button++)
// Consider the button pressed so long as its spot is not occupied by a ".".
controllers["P" + (player).ToString() + " " + buttons[button]] = (
sections[section][button] != '.'
);
}
// Convert the data for the controllers to a mnemonic and add it as a frame.
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
}
else
// Everything not explicitly defined is treated as a comment.
m.Header.Comments.Add(line);
m.SetRerecords(rerecordCount);
}
else if (line.StartsWith("guid"))
m.Header.SetHeaderLine(MovieHeader.GUID, ParseHeader(line, "GUID"));
else if (line.StartsWith("StartsFromSavestate"))
{
// If this movie starts from a savestate, we can't support it.
if (ParseHeader(line, "StartsFromSavestate") == "1")
{
errorMsg = "Movies that begin with a savestate are not supported.";
sr.Close();
return null;
}
}
else if (line.StartsWith("palFlag"))
{
bool pal;
// Try to parse the PAL setting as a bool, defaulting to false if it fails.
try
{
pal = Convert.ToBoolean(int.Parse(ParseHeader(line, "palFlag")));
}
catch
{
pal = false;
}
m.Header.SetHeaderLine("PAL", pal.ToString());
}
else if (line.StartsWith("fourscore"))
m.Header.SetHeaderLine(
MovieHeader.FOURSCORE,
Convert.ToBoolean(
int.Parse(ParseHeader(line, "fourscore"))
).ToString()
);
else if (line.StartsWith("sub"))
{
Subtitle s = new Subtitle();
// The header name, frame, and message are separated by a space.
int first = line.IndexOf(' ');
int second = line.IndexOf(' ', first + 1);
if (first != -1 && second != -1)
{
// Concatenate the frame and message with default values for the additional fields.
string frame = line.Substring(first + 1, second - first - 1);
string message = line.Substring(second + 1, line.Length - second - 1);
m.Subtitles.AddSubtitle("subtitle " + frame + " 0 0 200 FFFFFFFF " + message);
}
}
else if (line[0] == '|')
{
// Handle a frame of input.
ArrayList frame = new ArrayList();
// Split up the sections of the frame.
string[] sections = line.Split('|');
// Start building the data for the controllers.
SimpleController controllers = new SimpleController();
controllers.Type = new ControllerDefinition();
controllers.Type.Name = controller;
// Get the first invalid command warning message that arises.
if (Path.GetExtension(path).ToUpper() == ".FM2" && warningMsg == "" && sections[1].Length != 0)
{
switch (sections[1][0])
{
case '0':
break;
case '1':
controllers["Reset"] = true;
break;
case '2':
if (m.Length() != 0)
{
warningMsg = "hard reset";
}
break;
case '4':
warningMsg = "FDS Insert";
break;
case '8':
warningMsg = "FDS Select Side";
break;
default:
warningMsg = "unknown";
break;
}
if (warningMsg != "")
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
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++)
{
// The player number is one less than the section number for the reasons explained above.
int player = section - 1;
// Only count lines with that have the right number of buttons and are for valid players.
if (
sections[section].Length == buttons.Length &&
player <= Global.PLAYERS[controllers.Type.Name]
)
for (int button = 0; button < buttons.Length; button++)
// Consider the button pressed so long as its spot is not occupied by a ".".
controllers["P" + (player).ToString() + " " + buttons[button]] = (
sections[section][button] != '.'
);
}
// Convert the data for the controllers to a mnemonic and add it as a frame.
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
}
else
// Everything not explicitly defined is treated as a comment.
m.Header.Comments.Add(line);
}
sr.Close();
return m;
}
@ -296,6 +303,8 @@ namespace BizHawk.MultiClient
if (signature != "FCM\x1A")
{
errorMsg = "This is not a valid .FCM file.";
r.Close();
fs.Close();
return null;
}
// 004 4-byte little-endian unsigned int: version number, must be 2
@ -317,6 +326,8 @@ namespace BizHawk.MultiClient
if ((int)(flags & 2) == 0)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
bool pal = false;
@ -340,8 +351,13 @@ namespace BizHawk.MultiClient
m.SetRerecords((int)rerecordCount);
// 014 4-byte little-endian unsigned int: length of controller data in bytes
uint movieDataSize = r.ReadUInt32();
// 018 4-byte little-endian unsigned int: offset to the savestate inside file
uint savestateOffset = r.ReadUInt32();
/*
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
4-byte aligned. At the savestate offset there is a savestate file. The savestate exists even if the movie is
reset-based.
*/
r.ReadUInt32();
// 01C 4-byte little-endian unsigned int: offset to the controller data inside file
uint firstFrameOffset = r.ReadUInt32();
// 020 16-byte md5sum of the ROM used
@ -374,28 +390,115 @@ namespace BizHawk.MultiClient
r.ReadByte();
string author = System.Text.Encoding.UTF8.GetString(authorBytes.ToArray());
m.Header.SetHeaderLine(MovieHeader.AUTHOR, author);
// Advance to first byte of input data
// byte[] throwaway = new byte[firstFrameOffset];
// r.Read(throwaway, 0, (int)firstFrameOffset);
// Advance to first byte of input data.
r.BaseStream.Position = firstFrameOffset;
// moviedatasize stuff
// read frame data
// TODO: special commands like fds disk switch, etc, and power/reset
// TODO: use stringbuilder class for speed
// string ButtonLookup = "RLDUSsBARLDUSsBARLDUSsBARLDUSsBA"; //TODO: This assumes input data is the same in fcm as bizhawk, which it isn't
// string frame = "|0|"; //TODO: read reset command rather than hard code it off
for (int frame = 0; frame < frameCount; frame++)
string[] buttons = new string[8] { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" };
bool fourscore = false;
int frame = 1;
while (frame <= frameCount)
{
/*
byte joy = r.ReadByte();
// Read each byte of controller one data
frame += "|";
r.ReadBytes(3); //Lose remaining controllers for now
m.AppendFrame("Test");
*/
byte update = r.ReadByte();
SimpleController controllers = new SimpleController();
controllers.Type = new ControllerDefinition();
controllers.Type.Name = "NES Controller";
if ((int)(update & 0x128) != 0)
{
// Control update: 1aabbbbb
bool reset = false;
int command = update & 0x1F;
/*
bbbbb:
** 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:
break;
case 1:
reset = true;
controllers["Reset"] = true;
break;
case 2:
reset = true;
if (frame != 1)
{
warningMsg = "hard reset";
}
break;
case 7:
warningMsg = "VS System Insert Coin";
break;
case 8:
warningMsg = "VS System Dipswitch 0 Toggle";
break;
case 24:
warningMsg = "FDS Insert";
break;
case 25:
warningMsg = "FDS Eject";
break;
case 26:
warningMsg = "FDS Select Side";
break;
default:
warningMsg = "unknown";
break;
}
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
quicksave is actually loaded. This flag can't therefore be trusted. To check if the movie actually begins
from reset, one must analyze the controller data and see if the first non-idle command in the file is a
Reset or Power Cycle type control command.
*/
if (!reset && frame == 1)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
}
else
{
/*
Controller update: 0aabbccc
* bb: Gamepad number minus one (?)
*/
int player = ((update >> 3) & 3) + 1;
/*
ccc:
* 0 A
* 1 B
* 2 Select
* 3 Start
* 4 Up
* 5 Down
* 6 Left
* 7 Right
*/
int button = update & 7;
}
// aa: Number of delta bytes to follow
int delta = (update >> 5) & 3;
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(controllers);
string mnemonic = mg.GetControllersAsMnemonic();
while (false)
m.AppendFrame(mnemonic);
frame++;
}
// set 4 score flag if necessary
m.Header.SetHeaderLine(MovieHeader.FOURSCORE, fourscore.ToString());
r.Close();
fs.Close();
return m;
}
@ -421,6 +524,8 @@ namespace BizHawk.MultiClient
if (signature != "FMV\x1A")
{
errorMsg = "This is not a valid .FMV file.";
r.Close();
fs.Close();
return null;
}
// 004 1-byte flags:
@ -432,6 +537,8 @@ namespace BizHawk.MultiClient
if ((int)(flags & 0x80) != 0)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
// 005 1-byte flags:
@ -483,6 +590,8 @@ namespace BizHawk.MultiClient
if (!controller1 && !controller2 && !FDS)
{
warningMsg = "No input recorded.";
r.Close();
fs.Close();
return m;
}
/*
@ -503,23 +612,23 @@ namespace BizHawk.MultiClient
if (FDS)
bytesPerFrame++;
long frames = (fs.Length - 144) / bytesPerFrame;
for (long frame = 0; frame < frames; frame++)
/*
* 01 Right
* 02 Left
* 04 Up
* 08 Down
* 10 B
* 20 A
* 40 Select
* 80 Start
*/
string[] buttons = new string[8] { "Right", "Left", "Up", "Down", "B", "A", "Select", "Start" };
for (long frame = 1; frame <= frames; frame++)
{
byte controllerstate;
SimpleController controllers = new SimpleController();
controllers.Type = new ControllerDefinition();
controllers.Type.Name = "NES Controller";
/*
* 01 Right
* 02 Left
* 04 Up
* 08 Down
* 10 B
* 20 A
* 40 Select
* 80 Start
*/
string[] buttons = new string[8] { "Right", "Left", "Up", "Down", "B", "A", "Select", "Start" };
/*
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
@ -552,6 +661,8 @@ namespace BizHawk.MultiClient
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
}
r.Close();
fs.Close();
return m;
}
@ -577,8 +688,12 @@ namespace BizHawk.MultiClient
if (signature != "NSS\x1A")
{
errorMsg = "This is not a valid .NMV file.";
r.Close();
fs.Close();
return null;
}
r.Close();
fs.Close();
return m;
}
@ -614,6 +729,8 @@ namespace BizHawk.MultiClient
if (signature != "MMV\0")
{
errorMsg = "This is not a valid .MMV file.";
r.Close();
fs.Close();
return null;
}
// 0004: 4-byte little endian unsigned int: dega version
@ -629,6 +746,8 @@ namespace BizHawk.MultiClient
if (reset == 0)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
// 0014: 4-byte little endian unsigned int: offset of state information
@ -675,24 +794,24 @@ namespace BizHawk.MultiClient
// 00e4-00f3: binary: rom MD5 digest
string MD5 = BytesToString(r, 16, true);
m.Header.SetHeaderLine("MD5", MD5);
for (int frame = 0; frame < frameCount; frame++)
/*
76543210
* bit 0 (0x01): up
* bit 1 (0x02): down
* bit 2 (0x04): left
* bit 3 (0x08): right
* bit 4 (0x10): 1
* bit 5 (0x20): 2
* bit 6 (0x40): start (Master System)
* bit 7 (0x80): start (Game Gear)
*/
string[] buttons = new string[6] { "Up", "Down", "Left", "Right", "B1", "B2" };
for (int frame = 1; frame <= frameCount; frame++)
{
byte controllerstate;
SimpleController controllers = new SimpleController();
controllers.Type = new ControllerDefinition();
controllers.Type.Name = "SMS Controller";
/*
76543210
* bit 0 (0x01): up
* bit 1 (0x02): down
* bit 2 (0x04): left
* bit 3 (0x08): right
* bit 4 (0x10): 1
* bit 5 (0x20): 2
* bit 6 (0x40): start (Master System)
* bit 7 (0x80): start (Game Gear)
*/
string[] buttons = new string[6] { "Up", "Down", "Left", "Right", "B1", "B2" };
/*
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
@ -717,6 +836,8 @@ namespace BizHawk.MultiClient
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
}
r.Close();
fs.Close();
return m;
}
@ -734,6 +855,8 @@ namespace BizHawk.MultiClient
if (signature.Substring(0, 3) != "SMV")
{
errorMsg = "This is not a valid .SMV file.";
r.Close();
fs.Close();
return null;
}
@ -750,6 +873,8 @@ namespace BizHawk.MultiClient
default:
{
errorMsg = "SMV version not recognized, 143, 151, and 152 are currently supported.";
r.Close();
fs.Close();
return null;
}
}
@ -799,11 +924,11 @@ namespace BizHawk.MultiClient
//TODO: get extra rom info
r.BaseStream.Position = FrameDataOffset;
for (int x = 0; x < frameCount; x++)
for (int frame = 1; frame <= frameCount; frame++)
{
//TODO: FF FF for all controllers = Reset
//string frame = "|0|";
for (int y = 0; y < numControllers; y++)
for (int controller = 1; controller <= numControllers; controller++)
{
ushort fd = r.ReadUInt16();
}
@ -842,6 +967,8 @@ namespace BizHawk.MultiClient
if (signature != 0x56424D1A)
{
errorMsg = "This is not a valid .VBM file.";
r.Close();
fs.Close();
return null;
}
@ -865,6 +992,8 @@ namespace BizHawk.MultiClient
if (startfromquicksave & startfromsram)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
@ -894,6 +1023,8 @@ namespace BizHawk.MultiClient
if (is_gb & is_gbc & is_gba & is_sgb)
{
errorMsg = "Not a valid .VBM platform type.";
r.Close();
fs.Close();
return null;
}
//TODO: set platform in header
@ -916,6 +1047,8 @@ namespace BizHawk.MultiClient
if ((flags & 0x08) != 0)
{
errorMsg = "Invalid .VBM file.";
r.Close();
fs.Close();
return null;
}
if ((flags & 0x04) != 0) rtcenable = true;
@ -983,6 +1116,8 @@ namespace BizHawk.MultiClient
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(controllers);
m.AppendFrame(mg.GetControllersAsMnemonic());
r.Close();
fs.Close();
return m;
}
@ -999,8 +1134,12 @@ namespace BizHawk.MultiClient
if (signature != "VirtuaNES MV")
{
errorMsg = "This is not a valid .VMV file.";
r.Close();
fs.Close();
return null;
}
r.Close();
fs.Close();
return m;
}
}