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 @@ </Compile> <Compile Include="movie\conversions\MovieConversionExtensions.cs" /> <Compile Include="movie\HeaderKeys.cs" /> + <Compile Include="movie\import\Fm2Import.cs" /> + <Compile Include="movie\import\IMovieImport.cs" /> <Compile Include="movie\import\MovieImport.cs" /> <Compile Include="movie\import\PJMImport.cs" /> <Compile Include="movie\interfaces\ILogEntryGenerator.cs" /> 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<string>(); + Errors = new List<string>(); + } + + public IList<string> Warnings { get; private set; } + public IList<string> 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 } } }