protected override void RunImport() { var neshawkName = ((CoreAttribute)Attribute.GetCustomAttribute(typeof(NES), typeof(CoreAttribute))).CoreName; Result.Movie.HeaderEntries[HeaderKeys.Core] = neshawkName; const string emulator = "FCEUX"; var platform = "NES"; // TODO: FDS? var syncSettings = new NES.NESSyncSettings(); var controllerSettings = new NESControlSettings { NesLeftPort = nameof(UnpluggedNES), NesRightPort = nameof(UnpluggedNES) }; _deck = controllerSettings.Instantiate((x, y) => true); AddDeckControlButtons(); Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform; using var sr = SourceFile.OpenText(); string line; while ((line = sr.ReadLine()) != null) { if (line == "") { continue; } if (line[0] == '|') { ImportInputFrame(line); } 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($"{EmulationOrigin} {emulator} version {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.TryParse(ParseHeader(line, "rerecordCount"), out var rerecordCount); Result.Movie.Rerecords = (ulong)rerecordCount; } else if (line.ToLower().StartsWith("guid")) { // 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("port0")) { if (ParseHeader(line, "port0") == "1") { controllerSettings.NesLeftPort = nameof(ControllerNES); _deck = controllerSettings.Instantiate((x, y) => false); AddDeckControlButtons(); } } else if (line.ToLower().StartsWith("port1")) { if (ParseHeader(line, "port1") == "1") { controllerSettings.NesRightPort = nameof(ControllerNES); _deck = controllerSettings.Instantiate((x, y) => false); AddDeckControlButtons(); } } else if (line.ToLower().StartsWith("port2")) { if (ParseHeader(line, "port2") == "1") { Result.Errors.Add("Famicom port not yet supported"); break; } } else if (line.ToLower().StartsWith("fourscore")) { bool fourscore = ParseHeader(line, "fourscore") == "1"; if (fourscore) { // TODO: set controller config sync settings controllerSettings.NesLeftPort = nameof(FourScore); controllerSettings.NesRightPort = nameof(FourScore); } _deck = controllerSettings.Instantiate((x, y) => false); } else { Result.Movie.Comments.Add(line); // Everything not explicitly defined is treated as a comment. } } syncSettings.Controls = controllerSettings; Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(syncSettings); }
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 <filesize minus 144> divided by * <number of bytes per frame>. */ 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); }
protected override void RunImport() { var neshawkName = ((CoreAttribute)Attribute.GetCustomAttribute(typeof(NES), typeof(CoreAttribute))).CoreName; Result.Movie.HeaderEntries[HeaderKeys.Core] = neshawkName; 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 <header_size + length_of_metadata_in_bytes + padding>. 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 <byte>(); 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 <byte>(); 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; 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 = ConfigService.SaveWithType(syncSettings); }
protected override void RunImport() { Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.NesHawk; const string emulator = "FCEUX"; var platform = VSystemID.Raw.NES; // TODO: FDS? var syncSettings = new NES.NESSyncSettings(); var controllerSettings = new NESControlSettings { NesLeftPort = nameof(UnpluggedNES), NesRightPort = nameof(UnpluggedNES) }; _deck = controllerSettings.Instantiate((x, y) => true).AddSystemToControllerDef(); Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform; using var sr = SourceFile.OpenText(); string line; while ((line = sr.ReadLine()) != null) { if (line == "") { continue; } if (line[0] == '|') { ImportInputFrame(line); } 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($"{EmulationOrigin} {emulator} version {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")) { Result.Movie.Rerecords = (ulong)(int.TryParse(ParseHeader(line, "rerecordCount"), out var rerecordCount) ? rerecordCount : default);