diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj index 03718020c8..97d0baa0e4 100644 --- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj +++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj @@ -159,6 +159,7 @@ + diff --git a/BizHawk.Client.Common/movie/import/MmvImport.cs b/BizHawk.Client.Common/movie/import/MmvImport.cs new file mode 100644 index 0000000000..9ce5c21d01 --- /dev/null +++ b/BizHawk.Client.Common/movie/import/MmvImport.cs @@ -0,0 +1,149 @@ +using System.IO; +using BizHawk.Common.BufferExtensions; +using BizHawk.Emulation.Cores.Sega.MasterSystem; + +namespace BizHawk.Client.Common.movie.import +{ + // ReSharper disable once UnusedMember.Global + // MMV file format: http://tasvideos.org/MMV.html + [ImportExtension(".mmv")] + public class MmvImport : MovieImporter + { + protected override void RunImport() + { + using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read); + using var r = new BinaryReader(fs); + + // 0000: 4-byte signature: "MMV\0" + string signature = new string(r.ReadChars(4)); + if (signature != "MMV\0") + { + Result.Errors.Add("This is not a valid .MMV file."); + return; + } + + // 0004: 4-byte little endian unsigned int: dega version + uint emuVersion = r.ReadUInt32(); + Result.Movie.Comments.Add($"{MovieOrigin} .MMV"); + Result.Movie.Comments.Add($"{EmulationOrigin} Dega version {emuVersion}"); + + // 0008: 4-byte little endian unsigned int: frame count + uint frameCount = r.ReadUInt32(); + + // 000c: 4-byte little endian unsigned int: rerecord count + uint rerecordCount = r.ReadUInt32(); + Result.Movie.Rerecords = rerecordCount; + + + // 0010: 4-byte little endian flag: begin from reset? + uint reset = r.ReadUInt32(); + if (reset == 0) + { + Result.Errors.Add("Movies that begin with a savestate are not supported."); + return; + } + + // 0014: 4-byte little endian unsigned int: offset of state information + r.ReadUInt32(); + + // 0018: 4-byte little endian unsigned int: offset of input data + r.ReadUInt32(); + + // 001c: 4-byte little endian unsigned int: size of input packet + r.ReadUInt32(); + + // 0020-005f: string: author info (UTF-8) + string author = NullTerminated(new string(r.ReadChars(64))); + Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = author; + + // 0060: 4-byte little endian flags + byte flags = r.ReadByte(); + + // bit 0: unused + // bit 1: "PAL" + bool pal = ((flags >> 1) & 0x1) != 0; + Result.Movie.HeaderEntries[HeaderKeys.PAL] = pal.ToString(); + + // bit 2: Japan + bool japan = ((flags >> 2) & 0x1) != 0; + Result.Movie.HeaderEntries["Japan"] = japan.ToString(); + + // bit 3: Game Gear (version 1.16+) + bool isGameGear; + if (((flags >> 3) & 0x1) != 0) + { + isGameGear = true; + Result.Movie.HeaderEntries.Add("IsGGMode", "1"); + } + else + { + isGameGear = false; + } + + Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "SMS"; // System Id is still SMS even if game gear + + // bits 4-31: unused + r.ReadBytes(3); + + // 0064-00e3: string: rom name (ASCII) + string gameName = NullTerminated(new string(r.ReadChars(128))); + Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = gameName; + + // 00e4-00f3: binary: rom MD5 digest + byte[] md5 = r.ReadBytes(16); + Result.Movie.HeaderEntries[MD5] = md5.BytesToHexString().ToLower(); + + var ss = new SMS.SMSSyncSettings + { + ControllerType = "Standard" + }; + + var controllers = new SimpleController + { + Definition = isGameGear + ? SMS.SmsController + : SMS.GGController + }; + + /* + 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 = { "Up", "Down", "Left", "Right", "B1", "B2" }; + for (int frame = 1; frame <= frameCount; frame++) + { + /* + 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 + however both bytes are still present. + */ + for (int player = 1; player <= 2; player++) + { + byte controllerState = r.ReadByte(); + for (int button = 0; button < buttons.Length; button++) + { + controllers[$"P{player} {buttons[button]}"] = ((controllerState >> button) & 0x1) != 0; + } + + if (player == 1) + { + controllers["Pause"] = + (((controllerState >> 6) & 0x1) != 0 && (!isGameGear)) + || (((controllerState >> 7) & 0x1) != 0 && isGameGear); + } + } + + Result.Movie.AppendFrame(controllers); + } + + Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss); + } + } +} diff --git a/BizHawk.Client.Common/movie/import/MovieImport.cs b/BizHawk.Client.Common/movie/import/MovieImport.cs index ef39ec7339..0e634a051f 100644 --- a/BizHawk.Client.Common/movie/import/MovieImport.cs +++ b/BizHawk.Client.Common/movie/import/MovieImport.cs @@ -128,9 +128,6 @@ namespace BizHawk.Client.Common { switch (ext) { - case ".MMV": - m = ImportMmv(path, out errorMsg, out warningMsg); - break; case ".BKM": m.Filename = path; m.Load(false); @@ -171,154 +168,9 @@ namespace BizHawk.Client.Common { string[] extensions = { - "BKM", "MMV" + "BKM" }; return extensions.Any(ext => extension.ToUpper() == $".{ext}"); } - - // Ends the string where a NULL character is found. - private static string NullTerminated(string str) - { - int pos = str.IndexOf('\0'); - if (pos != -1) - { - str = str.Substring(0, pos); - } - - return str; - } - - // MMV file format: http://tasvideos.org/MMV.html - private static BkmMovie ImportMmv(string path, out string errorMsg, out string warningMsg) - { - errorMsg = warningMsg = ""; - BkmMovie m = new BkmMovie(path); - FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); - BinaryReader r = new BinaryReader(fs); - - // 0000: 4-byte signature: "MMV\0" - string signature = r.ReadStringFixedAscii(4); - 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 - uint emuVersion = r.ReadUInt32(); - m.Comments.Add($"{EMULATIONORIGIN} Dega version {emuVersion}"); - m.Comments.Add($"{MOVIEORIGIN} .MMV"); - - // 0008: 4-byte little endian unsigned int: frame count - uint frameCount = r.ReadUInt32(); - - // 000c: 4-byte little endian unsigned int: rerecord count - uint rerecordCount = r.ReadUInt32(); - m.Rerecords = rerecordCount; - - // 0010: 4-byte little endian flag: begin from reset? - uint reset = r.ReadUInt32(); - 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 - r.ReadUInt32(); - - // 0018: 4-byte little endian unsigned int: offset of input data - r.ReadUInt32(); - - // 001c: 4-byte little endian unsigned int: size of input packet - r.ReadUInt32(); - - // 0020-005f: string: author info (UTF-8) - string author = NullTerminated(r.ReadStringFixedAscii(64)); - m.Header[HeaderKeys.AUTHOR] = author; - - // 0060: 4-byte little endian flags - byte flags = r.ReadByte(); - - // bit 0: unused - // bit 1: "PAL" - bool pal = ((flags >> 1) & 0x1) != 0; - m.Header[HeaderKeys.PAL] = pal.ToString(); - - // bit 2: Japan - bool japan = ((flags >> 2) & 0x1) != 0; - m.Header[JAPAN] = japan.ToString(); - - // bit 3: Game Gear (version 1.16+) - bool gamegear; - if (((flags >> 3) & 0x1) != 0) - { - gamegear = true; - m.Header[HeaderKeys.PLATFORM] = "GG"; - } - else - { - gamegear = false; - m.Header[HeaderKeys.PLATFORM] = "SMS"; - } - - // bits 4-31: unused - r.ReadBytes(3); - - // 0064-00e3: string: rom name (ASCII) - string gameName = NullTerminated(r.ReadStringFixedAscii(128)); - m.Header[HeaderKeys.GAMENAME] = gameName; - - // 00e4-00f3: binary: rom MD5 digest - byte[] md5 = r.ReadBytes(16); - m.Header[MD5] = $"{md5.BytesToHexString().ToLower():x8}"; - var controllers = new SimpleController { Definition = new ControllerDefinition { 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 = { "Up", "Down", "Left", "Right", "B1", "B2" }; - for (int frame = 1; frame <= frameCount; frame++) - { - /* - 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 - however both bytes are still present. - */ - for (int player = 1; player <= 2; player++) - { - byte controllerState = r.ReadByte(); - for (int button = 0; button < buttons.Length; button++) - { - controllers[$"P{player} {buttons[button]}"] = ((controllerState >> button) & 0x1) != 0; - } - - if (player == 1) - { - controllers["Pause"] = - (((controllerState >> 6) & 0x1) != 0 && (!gamegear)) - || (((controllerState >> 7) & 0x1) != 0 && gamegear); - } - } - - m.AppendFrame(controllers); - } - - r.Close(); - fs.Close(); - return m; - } } }