Example #1
0
        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);
        }
Example #2
0
        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);
        }
Example #3
0
        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);
        }
Example #4
0
        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);