From 6d535a11eec9295d85bfa5498038bf56bf08a744 Mon Sep 17 00:00:00 2001 From: adelikat Date: Sun, 10 Nov 2019 09:55:27 -0600 Subject: [PATCH] Convert fcm importer to new style, input parsing seems to have been broken this whole time, this commit doesn't fix that, just converts it --- .../BizHawk.Client.Common.csproj | 1 + .../movie/import/FcmImport.cs | 305 ++++++++++++++++++ .../movie/import/MovieImport.cs | 287 +--------------- BizHawk.sln.DotSettings | 2 + 4 files changed, 309 insertions(+), 286 deletions(-) create mode 100644 BizHawk.Client.Common/movie/import/FcmImport.cs diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj index a0dae32cbd..1dd074c780 100644 --- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj +++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj @@ -154,6 +154,7 @@ Bk2Movie.cs + diff --git a/BizHawk.Client.Common/movie/import/FcmImport.cs b/BizHawk.Client.Common/movie/import/FcmImport.cs new file mode 100644 index 0000000000..28104a2b3f --- /dev/null +++ b/BizHawk.Client.Common/movie/import/FcmImport.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using BizHawk.Common.BufferExtensions; +using BizHawk.Emulation.Cores.Nintendo.NES; + +namespace BizHawk.Client.Common.movie.import +{ + // FCM file format: http://code.google.com/p/fceu/wiki/FCM + // ReSharper disable once UnusedMember.Global + [ImportExtension(".fcm")] + public class FcmImport : MovieImporter + { + private IControllerDeck _deck; + + protected override void RunImport() + { + + + using var r = new BinaryReader(SourceFile.Open(FileMode.Open, FileAccess.Read)); + var signature = new string(r.ReadChars(4)); + if (signature != "FCM\x1A") + { + Result.Errors.Add("This is not a valid .FCM file."); + return; + } + + Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "NES"; + + var syncSettings = new NES.NESSyncSettings(); + + var controllerSettings = new NESControlSettings + { + NesLeftPort = nameof(ControllerNES), + NesRightPort = nameof(ControllerNES) + }; + _deck = controllerSettings.Instantiate((x, y) => true); + AddDeckControlButtons(); + + // 004 4-byte little-endian unsigned int: version number, must be 2 + uint version = r.ReadUInt32(); + if (version != 2) + { + Result.Errors.Add(".FCM movie version must always be 2."); + return; + } + + Result.Movie.Comments.Add($"{MovieOrigin} .FCM version {version}"); + + // 008 1-byte flags + byte flags = r.ReadByte(); + + /* + * bit 0: reserved, set to 0 + * bit 1: + * if "0", movie begins from an embedded "quicksave" snapshot + * if "1", movie begins from reset or power-on[1] + */ + if (((flags >> 1) & 0x1) == 0) + { + Result.Errors.Add("Movies that begin with a savestate are not supported."); + return; + } + + /* + bit 2: + * if "0", NTSC timing + * if "1", "PAL" timing + Starting with version 0.98.12 released on September 19, 2004, a "PAL" flag was added to the header but + unfortunately it is not reliable - the emulator does not take the "PAL" setting from the ROM, but from a user + preference. This means that this site cannot calculate movie lengths reliably. + */ + bool pal = ((flags >> 2) & 0x1) != 0; + Result.Movie.HeaderEntries[HeaderKeys.PAL] = pal.ToString(); + + // other: reserved, set to 0 + bool syncHack = ((flags >> 4) & 0x1) != 0; + Result.Movie.Comments.Add($"SyncHack {syncHack}"); + + // 009 1-byte flags: reserved, set to 0 + r.ReadByte(); + + // 00A 1-byte flags: reserved, set to 0 + r.ReadByte(); + + // 00B 1-byte flags: reserved, set to 0 + r.ReadByte(); + + // 00C 4-byte little-endian unsigned int: number of frames + uint frameCount = r.ReadUInt32(); + + // 010 4-byte little-endian unsigned int: rerecord count + uint rerecordCount = r.ReadUInt32(); + Result.Movie.Rerecords = rerecordCount; + /* + 018 4-byte little-endian unsigned int: offset to the savestate inside file + The savestate offset is . 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 + byte[] md5 = r.ReadBytes(16); + Result.Movie.HeaderEntries[MD5] = md5.BytesToHexString().ToLower(); + + // 030 4-byte little-endian unsigned int: version of the emulator used + uint emuVersion = r.ReadUInt32(); + Result.Movie.Comments.Add($"{EmulationOrigin} FCEU {emuVersion}"); + + // 034 name of the ROM used - UTF8 encoded nul-terminated string. + var gameBytes = new List(); + while (r.PeekChar() != 0) + { + gameBytes.Add(r.ReadByte()); + } + + // Advance past null byte. + r.ReadByte(); + string gameName = Encoding.UTF8.GetString(gameBytes.ToArray()); + Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = gameName; + + /* + After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM + name and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the + emulator. + */ + var authorBytes = new List(); + while (r.PeekChar() != 0) + { + authorBytes.Add(r.ReadByte()); + } + + // Advance past null byte. + r.ReadByte(); + string author = Encoding.UTF8.GetString(authorBytes.ToArray()); + Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = author; + + // Advance to first byte of input data. + r.BaseStream.Position = firstFrameOffset; + + var controllers = new SimpleController + { + Definition = _deck.GetDefinition() + }; + + string[] buttons = { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }; + bool fds = false; + bool fourscore = false; + int frame = 1; + while (frame <= frameCount) + { + byte update = r.ReadByte(); + + // aa: Number of delta bytes to follow + int delta = (update >> 5) & 0x3; + int frames = 0; + + /* + The delta byte(s) indicate the number of emulator frames between this update and the next update. It is + encoded in little-endian format and its size depends on the magnitude of the delta: + Delta of: Number of bytes: + 0 0 + 1-255 1 + 256-65535 2 + 65536-(2^24-1) 3 + */ + for (int b = 0; b < delta; b++) + { + frames += r.ReadByte() * (int)Math.Pow(2, b * 8); + } + + frame += frames; + while (frames > 0) + { + Result.Movie.AppendFrame(controllers); + if (controllers["Reset"]) + { + controllers["Reset"] = false; + } + + frames--; + } + + if (((update >> 7) & 0x1) != 0) + { + // Control update: 0x1aabbbbb + bool reset = false; + int command = update & 0x1F; + + // 0xbbbbb: + controllers["Reset"] = command == 1; + switch (command) + { + case 0: // Do nothing + break; + case 1: // Reset + reset = true; + break; + case 2: // Power cycle + reset = true; + if (frame != 1) + { + controllers["Power"] = true; + } + + break; + case 7: // VS System Insert Coin + Result.Warnings.Add($"Unsupported command: VS System Insert Coin at frame {frame}"); + break; + case 8: // VS System Dipswitch 0 Toggle + Result.Warnings.Add($"Unsupported command: VS System Dipswitch 0 Toggle at frame {frame}"); + break; + case 24: // FDS Insert + fds = true; + Result.Warnings.Add($"Unsupported command: FDS Insert at frame {frame}"); + break; + case 25: // FDS Eject + fds = true; + Result.Warnings.Add($"Unsupported command: FDS Eject at frame {frame}"); + break; + case 26: // FDS Select Side + fds = true; + Result.Warnings.Add($"Unsupported command: FDS Select Side at frame {frame}"); + break; + default: + Result.Warnings.Add($"Unknown command: {command} detected at frame {frame}"); + break; + } + + /* + 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) + { + Result.Errors.Add("Movies that begin with a savestate are not supported."); + return; + } + } + else + { + /* + Controller update: 0aabbccc + * bb: Gamepad number minus one (?) + */ + int player = ((update >> 3) & 0x3) + 1; + if (player > 2) + { + Result.Errors.Add("Four score not yet supported."); + return; + } + + /* + ccc: + * 0 A + * 1 B + * 2 Select + * 3 Start + * 4 Up + * 5 Down + * 6 Left + * 7 Right + */ + int button = update & 0x7; + + /* + The controller update toggles the affected input. Controller update data is emitted to the movie file + only when the state of the controller changes. + */ + controllers[$"P{player} {buttons[button]}"] = !controllers[$"P{player} {buttons[button]}"]; + } + + Result.Movie.AppendFrame(controllers); + } + + if (fds) + { + Result.Movie.HeaderEntries[HeaderKeys.BOARDNAME] = "FDS"; + } + + syncSettings.Controls = controllerSettings; + Result.Movie.SyncSettingsJson = ToJson(syncSettings); + } + + private void AddDeckControlButtons() + { + var controllers = new SimpleController + { + Definition = _deck.GetDefinition() + }; + + // TODO: FDS + // Yes, this adds them to the deck definition too + controllers.Definition.BoolButtons.Add("Reset"); + controllers.Definition.BoolButtons.Add("Power"); + } + } +} diff --git a/BizHawk.Client.Common/movie/import/MovieImport.cs b/BizHawk.Client.Common/movie/import/MovieImport.cs index 8072a5ba06..70bba101a9 100644 --- a/BizHawk.Client.Common/movie/import/MovieImport.cs +++ b/BizHawk.Client.Common/movie/import/MovieImport.cs @@ -148,9 +148,6 @@ namespace BizHawk.Client.Common { switch (ext) { - case ".FCM": - m = ImportFcm(path, out errorMsg, out warningMsg); - break; case ".FMV": m = ImportFmv(path, out errorMsg, out warningMsg); break; @@ -227,7 +224,7 @@ namespace BizHawk.Client.Common { string[] extensions = { - "BKM", "FCM", "FMV", "GMV", "MCM", "MC2", "MMV", "NMV", "LSMV", "SMV", "VBM", "VMV", "YMV", "ZMV" + "BKM", "FMV", "GMV", "MCM", "MC2", "MMV", "NMV", "LSMV", "SMV", "VBM", "VMV", "YMV", "ZMV" }; return extensions.Any(ext => extension.ToUpper() == $".{ext}"); } @@ -428,10 +425,6 @@ namespace BizHawk.Client.Common var platform = ""; switch (Path.GetExtension(path).ToUpper()) { - case ".FM2": - emulator = "FCEUX"; - platform = "NES"; - break; case ".MC2": emulator = "Mednafen/PCEjin"; platform = "PCE"; @@ -609,284 +602,6 @@ namespace BizHawk.Client.Common return str; } - // FCM file format: http://code.google.com/p/fceu/wiki/FCM - private static BkmMovie ImportFcm(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); - - // 000 4-byte signature: 46 43 4D 1A "FCM\x1A" - string signature = r.ReadStringFixedAscii(4); - 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 - uint version = r.ReadUInt32(); - if (version != 2) - { - errorMsg = ".FCM movie version must always be 2."; - r.Close(); - fs.Close(); - return null; - } - - m.Comments.Add($"{MOVIEORIGIN} .FCM version {version}"); - - // 008 1-byte flags - byte flags = r.ReadByte(); - - /* - * bit 0: reserved, set to 0 - * bit 1: - * if "0", movie begins from an embedded "quicksave" snapshot - * if "1", movie begins from reset or power-on[1] - */ - if (((flags >> 1) & 0x1) == 0) - { - errorMsg = "Movies that begin with a savestate are not supported."; - r.Close(); - fs.Close(); - return null; - } - - /* - bit 2: - * if "0", NTSC timing - * if "1", "PAL" timing - Starting with version 0.98.12 released on September 19, 2004, a "PAL" flag was added to the header but - unfortunately it is not reliable - the emulator does not take the "PAL" setting from the ROM, but from a user - preference. This means that this site cannot calculate movie lengths reliably. - */ - bool pal = ((flags >> 2) & 0x1) != 0; - m.Header[HeaderKeys.PAL] = pal.ToString(); - - // other: reserved, set to 0 - bool syncHack = ((flags >> 4) & 0x1) != 0; - m.Comments.Add($"{SYNCHACK} {syncHack}"); - - // 009 1-byte flags: reserved, set to 0 - r.ReadByte(); - - // 00A 1-byte flags: reserved, set to 0 - r.ReadByte(); - - // 00B 1-byte flags: reserved, set to 0 - r.ReadByte(); - - // 00C 4-byte little-endian unsigned int: number of frames - uint frameCount = r.ReadUInt32(); - - // 010 4-byte little-endian unsigned int: rerecord count - uint rerecordCount = r.ReadUInt32(); - m.Rerecords = rerecordCount; - /* - 018 4-byte little-endian unsigned int: offset to the savestate inside file - The savestate offset is . 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 - byte[] md5 = r.ReadBytes(16); - m.Header[MD5] = md5.BytesToHexString().ToLower(); - - // 030 4-byte little-endian unsigned int: version of the emulator used - uint emuVersion = r.ReadUInt32(); - m.Comments.Add($"{EMULATIONORIGIN} FCEU {emuVersion}"); - - // 034 name of the ROM used - UTF8 encoded nul-terminated string. - List gameBytes = new List(); - while (r.PeekChar() != 0) - { - gameBytes.Add(r.ReadByte()); - } - - // Advance past null byte. - r.ReadByte(); - string gameName = Encoding.UTF8.GetString(gameBytes.ToArray()); - m.Header[HeaderKeys.GAMENAME] = gameName; - - /* - After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM - name and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the - emulator. - */ - List authorBytes = new List(); - while (r.PeekChar() != 0) - { - authorBytes.Add(r.ReadByte()); - } - - // Advance past null byte. - r.ReadByte(); - string author = Encoding.UTF8.GetString(authorBytes.ToArray()); - m.Header[HeaderKeys.AUTHOR] = author; - - // Advance to first byte of input data. - r.BaseStream.Position = firstFrameOffset; - SimpleController controllers = new SimpleController { Definition = new ControllerDefinition { Name = "NES Controller" } }; - string[] buttons = { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }; - bool fds = false; - bool fourscore = false; - int frame = 1; - while (frame <= frameCount) - { - byte update = r.ReadByte(); - - // aa: Number of delta bytes to follow - int delta = (update >> 5) & 0x3; - int frames = 0; - - /* - The delta byte(s) indicate the number of emulator frames between this update and the next update. It is - encoded in little-endian format and its size depends on the magnitude of the delta: - Delta of: Number of bytes: - 0 0 - 1-255 1 - 256-65535 2 - 65536-(2^24-1) 3 - */ - for (int b = 0; b < delta; b++) - { - frames += r.ReadByte() * (int)Math.Pow(2, b * 8); - } - - frame += frames; - while (frames > 0) - { - m.AppendFrame(controllers); - if (controllers["Reset"]) - { - controllers["Reset"] = false; - } - - frames--; - } - - if (((update >> 7) & 0x1) != 0) - { - // Control update: 1aabbbbb - bool reset = false; - int command = update & 0x1F; - - // bbbbb: - controllers["Reset"] = command == 1; - if (warningMsg == "") - { - switch (command) - { - case 0: // Do nothing - break; - case 1: // Reset - reset = true; - break; - case 2: // Power cycle - reset = true; - if (frame != 1) - { - warningMsg = "hard reset"; - } - - break; - case 7: // VS System Insert Coin - warningMsg = "VS System Insert Coin"; - break; - case 8: // VS System Dipswitch 0 Toggle - warningMsg = "VS System Dipswitch 0 Toggle"; - break; - case 24: // FDS Insert - fds = true; - warningMsg = "FDS Insert"; - break; - case 25: // FDS Eject - fds = true; - warningMsg = "FDS Eject"; - break; - case 26: // FDS Select Side - fds = true; - 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) & 0x3) + 1; - if (player > 2) - { - fourscore = true; - } - - /* - ccc: - * 0 A - * 1 B - * 2 Select - * 3 Start - * 4 Up - * 5 Down - * 6 Left - * 7 Right - */ - int button = update & 0x7; - - /* - The controller update toggles the affected input. Controller update data is emitted to the movie file - only when the state of the controller changes. - */ - controllers[$"P{player} {buttons[button]}"] = !controllers[$"P{player} {buttons[button]}"]; - } - } - - m.Header[HeaderKeys.PLATFORM] = "NES"; - if (fds) - { - m.Header[HeaderKeys.BOARDNAME] = "FDS"; - } - - m.Header[HeaderKeys.FOURSCORE] = fourscore.ToString(); - r.Close(); - fs.Close(); - return m; - } - // FMV file format: http://tasvideos.org/FMV.html private static BkmMovie ImportFmv(string path, out string errorMsg, out string warningMsg) { diff --git a/BizHawk.sln.DotSettings b/BizHawk.sln.DotSettings index fc3b174d2b..1148d786bc 100644 --- a/BizHawk.sln.DotSettings +++ b/BizHawk.sln.DotSettings @@ -201,6 +201,7 @@ True True True + True True True True @@ -264,6 +265,7 @@ True True True + True True True True