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;
- }
}
}