using System.IO; using BizHawk.Emulation.Cores.Nintendo.NES; namespace BizHawk.Client.Common.movie.import { // FMV file format: http://tasvideos.org/FMV.html // ReSharper disable once UnusedMember.Global [ImporterFor("Famtasia", ".fmv")] internal class FmvImport : MovieImporter { private IControllerDeck _deck; protected override void RunImport() { using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read); using var r = new BinaryReader(fs); // 000 4-byte signature: 46 4D 56 1A "FMV\x1A" var signature = new string(r.ReadChars(4)); if (signature != "FMV\x1A") { Result.Errors.Add("This is not a valid .FMV file."); return; } // 004 1-byte flags: byte flags = r.ReadByte(); // bit 7: 0=reset-based, 1=savestate-based if (((flags >> 2) & 0x1) != 0) { Result.Errors.Add("Movies that begin with a savestate are not supported."); return; } Result.Movie.HeaderEntries[HeaderKeys.Platform] = "NES"; var syncSettings = new NES.NESSyncSettings(); // other bits: unknown, set to 0 // 005 1-byte flags: flags = r.ReadByte(); // bit 5: is a FDS recording bool fds; if (((flags >> 5) & 0x1) != 0) { fds = true; Result.Movie.HeaderEntries[HeaderKeys.BoardName] = "FDS"; } else { fds = false; } // bit 6: uses controller 2 bool controller2 = ((flags >> 6) & 0x1) != 0; // bit 7: uses controller 1 bool controller1 = ((flags >> 7) & 0x1) != 0; // other bits: unknown, set to 0 // 006 4-byte little-endian unsigned int: unknown, set to 00000000 r.ReadInt32(); // 00A 4-byte little-endian unsigned int: rerecord count minus 1 uint rerecordCount = r.ReadUInt32(); /* The rerecord count stored in the file is the number of times a savestate was loaded. If a savestate was never loaded, the number is 0. Famtasia however displays "1" in such case. It always adds 1 to the number found in the file. */ Result.Movie.Rerecords = rerecordCount + 1; // 00E 2-byte little-endian unsigned int: unknown, set to 0000 r.ReadInt16(); // 010 64-byte zero-terminated emulator identifier string string emuVersion = NullTerminated(new string(r.ReadChars(64))); Result.Movie.Comments.Add($"{EmulationOrigin} Famtasia version {emuVersion}"); Result.Movie.Comments.Add($"{MovieOrigin} .FMV"); // 050 64-byte zero-terminated movie title string string description = NullTerminated(new string(r.ReadChars(64))); Result.Movie.Comments.Add(description); if (!controller1 && !controller2 && !fds) { Result.Warnings.Add("No input recorded."); } var controllerSettings = new NESControlSettings { NesLeftPort = controller1 ? nameof(ControllerNES) : nameof(UnpluggedNES), NesRightPort = controller2 ? nameof(ControllerNES) : nameof(UnpluggedNES) }; _deck = controllerSettings.Instantiate((x, y) => true); syncSettings.Controls.NesLeftPort = controllerSettings.NesLeftPort; syncSettings.Controls.NesRightPort = controllerSettings.NesRightPort; AddDeckControlButtons(); var controllers = new SimpleController { Definition = _deck.GetDefinition() }; /* * 01 Right * 02 Left * 04 Up * 08 Down * 10 B * 20 A * 40 Select * 80 Start */ string[] buttons = { "Right", "Left", "Up", "Down", "B", "A", "Select", "Start" }; bool[] masks = { controller1, controller2, fds }; /* The file has no terminator byte or frame count. The number of frames is the divided by . */ int bytesPerFrame = 0; for (int player = 1; player <= masks.Length; player++) { if (masks[player - 1]) { bytesPerFrame++; } } long frameCount = (fs.Length - 144) / bytesPerFrame; for (long frame = 1; frame <= frameCount; frame++) { /* Each frame consists of 1 or more bytes. Controller 1 takes 1 byte, controller 2 takes 1 byte, and the FDS data takes 1 byte. If all three exist, the frame is 3 bytes. For example, if the movie is a regular NES game with only controller 1 data, a frame is 1 byte. */ for (int player = 1; player <= masks.Length; player++) { if (!masks[player - 1]) { continue; } byte controllerState = r.ReadByte(); if (player != 3) { for (int button = 0; button < buttons.Length; button++) { controllers[$"P{player} {buttons[button]}"] = ((controllerState >> button) & 0x1) != 0; } } else { Result.Warnings.Add("FDS commands are not properly supported."); } } Result.Movie.AppendFrame(controllers); } Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(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"); } } }