diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj
index 8eba8e2c9d..53ed41ea9c 100644
--- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj
+++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj
@@ -180,6 +180,8 @@
+
+
diff --git a/BizHawk.Client.Common/movie/import/Fm2Import.cs b/BizHawk.Client.Common/movie/import/Fm2Import.cs
new file mode 100644
index 0000000000..22b472762c
--- /dev/null
+++ b/BizHawk.Client.Common/movie/import/Fm2Import.cs
@@ -0,0 +1,197 @@
+using System;
+using System.IO;
+
+using BizHawk.Common;
+using BizHawk.Common.BufferExtensions;
+
+namespace BizHawk.Client.Common
+{
+ [ImportExtension(".fm2")]
+ public class Fm2Import : MovieImporter
+ {
+ protected override void RunImport()
+ {
+ var emulator = "FCEUX";
+ var platform = "NES"; // TODO: FDS?
+
+ Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = platform;
+
+ using (var sr = SourceFile.OpenText())
+ {
+ string line;
+ int lineNum = 0;
+
+ while ((line = sr.ReadLine()) != null)
+ {
+ lineNum++;
+
+ if (line == string.Empty)
+ {
+ continue;
+ }
+ else if (line[0] == '|')
+ {
+ // TODO: import a frame of input
+ // TODO: report any errors importing this frame and bail out if so
+ }
+ else if (line.ToLower().StartsWith("sub"))
+ {
+ var subtitle = ImportTextSubtitle(line);
+
+ if (!string.IsNullOrEmpty(subtitle))
+ {
+ Result.Movie.Subtitles.AddFromString(subtitle);
+ }
+ }
+ else if (line.ToLower().StartsWith("emuversion"))
+ {
+ Result.Movie.Comments.Add(
+ string.Format("{0} {1} version {2}", EMULATIONORIGIN, emulator, ParseHeader(line, "emuVersion"))
+ );
+ }
+ else if (line.ToLower().StartsWith("version"))
+ {
+ string version = ParseHeader(line, "version");
+
+ if (version != "3")
+ {
+ Result.Warnings.Add("Detected a .fm2 movie version other than 3, which is unsupported");
+ }
+ else
+ {
+ Result.Movie.Comments.Add(MOVIEORIGIN + " .fm2 version 3");
+ }
+ }
+ else if (line.ToLower().StartsWith("romfilename"))
+ {
+ Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = ParseHeader(line, "romFilename");
+ }
+ else if (line.ToLower().StartsWith("cdgamename"))
+ {
+ Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = ParseHeader(line, "cdGameName");
+ }
+ else if (line.ToLower().StartsWith("romchecksum"))
+ {
+ string blob = ParseHeader(line, "romChecksum");
+ byte[] md5 = DecodeBlob(blob);
+ if (md5 != null && md5.Length == 16)
+ {
+ Result.Movie.HeaderEntries[MD5] = md5.BytesToHexString().ToLower();
+ }
+ else
+ {
+ Result.Warnings.Add("Bad ROM checksum.");
+ }
+ }
+ else if (line.ToLower().StartsWith("comment author"))
+ {
+ Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = ParseHeader(line, "comment author");
+ }
+ else if (line.ToLower().StartsWith("rerecordcount"))
+ {
+ int rerecordCount = 0;
+ int.TryParse(ParseHeader(line, "rerecordCount"), out rerecordCount);
+
+ Result.Movie.Rerecords = (ulong)rerecordCount;
+ }
+ else if (line.ToLower().StartsWith("guid"))
+ {
+ continue; //We no longer care to keep this info
+ }
+ else if (line.ToLower().StartsWith("startsfromsavestate"))
+ {
+ // If this movie starts from a savestate, we can't support it.
+ if (ParseHeader(line, "StartsFromSavestate") == "1")
+ {
+ Result.Errors.Add("Movies that begin with a savestate are not supported.");
+ break;
+ }
+ }
+ else if (line.ToLower().StartsWith("palflag"))
+ {
+ Result.Movie.HeaderEntries[HeaderKeys.PAL] = ParseHeader(line, "palFlag");
+ }
+ else if (line.ToLower().StartsWith("fourscore"))
+ {
+ bool fourscore = (ParseHeader(line, "fourscore") == "1");
+ if (fourscore)
+ {
+ // TODO: set controller config sync settings
+ }
+ }
+ else
+ {
+ Result.Movie.Comments.Add(line); // Everything not explicitly defined is treated as a comment.
+ }
+ }
+ }
+ }
+
+ private static string ImportTextSubtitle(string line)
+ {
+ line = SingleSpaces(line);
+
+ // The header name, frame, and message are separated by whitespace.
+ 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(0, first);
+ string length = line.Substring(first + 1, second - first - 1);
+ string message = line.Substring(second + 1).Trim();
+
+ return "subtitle " + frame + " 0 0 " + length + " FFFFFFFF " + message;
+ }
+
+ return null;
+ }
+
+ // Reduce all whitespace to single spaces.
+ private static string SingleSpaces(string line)
+ {
+ line = line.Replace("\t", " ");
+ line = line.Replace("\n", " ");
+ line = line.Replace("\r", " ");
+ line = line.Replace("\r\n", " ");
+ string prev;
+ do
+ {
+ prev = line;
+ line = line.Replace(" ", " ");
+ }
+ while (prev != line);
+ return line;
+ }
+
+ // Decode a blob used in FM2 (base64:..., 0x123456...)
+ private static byte[] DecodeBlob(string blob)
+ {
+ if (blob.Length < 2)
+ {
+ return null;
+ }
+ if (blob[0] == '0' && (blob[1] == 'x' || blob[1] == 'X'))
+ {
+ // hex
+ return Util.HexStringToBytes(blob.Substring(2));
+ }
+ else
+ {
+ // base64
+ if (!blob.ToLower().StartsWith("base64:"))
+ {
+ return null;
+ }
+ try
+ {
+ return Convert.FromBase64String(blob.Substring(7));
+ }
+ catch (FormatException)
+ {
+ return null;
+ }
+ }
+ }
+ }
+}
diff --git a/BizHawk.Client.Common/movie/import/IMovieImport.cs b/BizHawk.Client.Common/movie/import/IMovieImport.cs
new file mode 100644
index 0000000000..bea7525307
--- /dev/null
+++ b/BizHawk.Client.Common/movie/import/IMovieImport.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace BizHawk.Client.Common
+{
+ public interface IMovieImport
+ {
+ ImportResult Import(string path);
+ }
+
+ public abstract class MovieImporter : IMovieImport
+ {
+ public const string COMMENT = "comment";
+ public const string COREORIGIN = "CoreOrigin";
+ public const string CRC16 = "CRC16";
+ public const string CRC32 = "CRC32";
+ public const string EMULATIONORIGIN = "emuOrigin";
+ public const string GAMECODE = "GameCode";
+ public const string INTERNALCHECKSUM = "InternalChecksum";
+ public const string JAPAN = "Japan";
+ public const string MD5 = "MD5";
+ public const string MOVIEORIGIN = "MovieOrigin";
+ public const string PORT1 = "port1";
+ public const string PORT2 = "port2";
+ public const string PROJECTID = "ProjectID";
+ public const string SHA256 = "SHA256";
+ public const string SUPERGAMEBOYMODE = "SuperGameBoyMode";
+ public const string STARTSECOND = "StartSecond";
+ public const string STARTSUBSECOND = "StartSubSecond";
+ public const string SYNCHACK = "SyncHack";
+ public const string UNITCODE = "UnitCode";
+
+ public ImportResult Import(string path)
+ {
+ SourceFile = new FileInfo(path);
+
+ if (!SourceFile.Exists)
+ {
+ Result.Errors.Add(string.Format("Could not find the file {0}", path));
+ return Result;
+ }
+
+ var newFileName = SourceFile.FullName + "." + Bk2Movie.Extension;
+ Result.Movie = new Bk2Movie(newFileName);
+
+ RunImport();
+
+ return Result;
+ }
+
+
+ protected ImportResult Result = new ImportResult();
+
+ protected FileInfo SourceFile;
+
+ protected abstract void RunImport();
+
+ // Get the content for a particular header.
+ protected static string ParseHeader(string line, string headerName)
+ {
+ // Case-insensitive search.
+ int x = line.ToLower().LastIndexOf(
+ headerName.ToLower()
+ ) + headerName.Length;
+ string str = line.Substring(x + 1, line.Length - x - 1);
+ return str.Trim();
+ }
+ }
+
+ public class ImportResult
+ {
+ public ImportResult()
+ {
+ Warnings = new List();
+ Errors = new List();
+ }
+
+ public IList Warnings { get; private set; }
+ public IList Errors { get; private set; }
+
+ public Bk2Movie Movie { get; set; }
+ }
+
+ [AttributeUsage(AttributeTargets.Class)]
+ public class ImportExtension : Attribute
+ {
+ public ImportExtension(string extension)
+ {
+ Extension = extension;
+ }
+
+ public string Extension { get; private set; }
+ }
+}
diff --git a/BizHawk.Client.Common/movie/import/MovieImport.cs b/BizHawk.Client.Common/movie/import/MovieImport.cs
index edac187faf..8d008efd7f 100644
--- a/BizHawk.Client.Common/movie/import/MovieImport.cs
+++ b/BizHawk.Client.Common/movie/import/MovieImport.cs
@@ -73,9 +73,21 @@ namespace BizHawk.Client.Common
warningMsg = string.Empty;
string ext = path != null ? Path.GetExtension(path).ToUpper() : string.Empty;
+ // TODO: reflect off the assembly and find an IMovieImporter with the appropriate ImportExtension metadata
+ if (ext == ".FM2")
+ {
+ var result = new Fm2Import().Import(path);
+ errorMsg = result.Errors.First();
+ warningMsg = result.Errors.First();
+ return result.Movie;
+ }
+
if (ext == ".PJM")
{
- return PJMImport.Import(path, out errorMsg, out warningMsg);
+ var result = new PJMImport().Import(path);
+ errorMsg = result.Errors.First();
+ warningMsg = result.Errors.First();
+ return result.Movie;
}
BkmMovie m = new BkmMovie();
diff --git a/BizHawk.Client.Common/movie/import/PJMImport.cs b/BizHawk.Client.Common/movie/import/PJMImport.cs
index 57b0a1effd..342a12d492 100644
--- a/BizHawk.Client.Common/movie/import/PJMImport.cs
+++ b/BizHawk.Client.Common/movie/import/PJMImport.cs
@@ -6,13 +6,12 @@ using System.Text;
namespace BizHawk.Client.Common
{
- public static class PJMImport
+ [ImportExtension(".pjm")]
+ public class PJMImport : MovieImporter
{
- public static Bk2Movie Import(string path, out string errorMsg, out string warningMsg)
+ protected override void RunImport()
{
- errorMsg = string.Empty;
- warningMsg = string.Empty;
- return new Bk2Movie();
+ // TODO
}
}
}