Exemple #1
0
        protected override void RunImport()
        {
            using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read);
            using var r  = new BinaryReader(fs);

            // 000 4-byte signature: 53 4D 56 1A "SMV\x1A"
            string signature = new string(r.ReadChars(4));

            if (signature != "SMV\x1A")
            {
                Result.Errors.Add("This is not a valid .SMV file.");
                return;
            }

            Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "SNES";

            // 004 4-byte little-endian unsigned int: version number
            uint   versionNumber = r.ReadUInt32();
            string version;

            switch (versionNumber)
            {
            case 1:
                version = "1.43";
                break;

            case 4:
                version = "1.51";
                break;

            case 5:
                version = "1.52";
                break;

            default:
                version = "Unknown";
                break;
            }

            Result.Movie.Comments.Add($"{EmulationOrigin} Snes9x version {version}");
            Result.Movie.Comments.Add($"{MovieOrigin} .SMV");

            /*
             * 008 4-byte little-endian integer: movie "uid" - identifies the movie-savestate relationship, also used as the
             * recording time in Unix epoch format
             */
            uint uid = r.ReadUInt32();

            // 00C 4-byte little-endian unsigned int: rerecord count
            Result.Movie.Rerecords = r.ReadUInt32();

            // 010 4-byte little-endian unsigned int: number of frames
            uint frameCount = r.ReadUInt32();

            // 014 1-byte flags "controller mask"
            byte controllerFlags = r.ReadByte();

            /*
             * bit 0: controller 1 in use
             * bit 1: controller 2 in use
             * bit 2: controller 3 in use
             * bit 3: controller 4 in use
             * bit 4: controller 5 in use
             * other: reserved, set to 0
             */
            bool[] controllersUsed = new bool[5];             // Eww, this is a clunky way to do this
            for (int controller = 1; controller <= controllersUsed.Length; controller++)
            {
                controllersUsed[controller - 1] = ((controllerFlags >> (controller - 1)) & 0x1) != 0;
            }

            var controllerCount = controllersUsed.Count(c => c);

            var ss = new LibsnesCore.SnesSyncSettings
            {
                LeftPort  = LibsnesControllerDeck.ControllerType.Gamepad,
                RightPort = LibsnesControllerDeck.ControllerType.Gamepad
            };

            if (controllerCount == 1)
            {
                ss.RightPort = LibsnesControllerDeck.ControllerType.Unplugged;
            }
            else if (controllerCount > 2)
            {
                // More than 2 controllers means a multi-tap on the first port
                // Snes9x only supported up to 5 controllers, so right port would never be multitap
                ss.LeftPort = LibsnesControllerDeck.ControllerType.Multitap;

                // Unless there are exactly 5, right port is unplugged, as the multitap will handle 4 controllers
                if (controllerCount < 5)
                {
                    ss.RightPort = LibsnesControllerDeck.ControllerType.Unplugged;
                }
            }

            _deck = new LibsnesControllerDeck(ss);

            // 015 1-byte flags "movie options"
            byte movieFlags = r.ReadByte();

            /*
             *       bit 0:
             *               if "0", movie begins from an embedded "quicksave" snapshot
             *               if "1", a SRAM is included instead of a quicksave; movie begins from reset
             */
            if ((movieFlags & 0x1) == 0)
            {
                Result.Errors.Add("Movies that begin with a savestate are not supported.");
                return;
            }

            // bit 1: if "0", movie is NTSC (60 fps); if "1", movie is PAL (50 fps)
            bool pal = ((movieFlags >> 1) & 0x1) != 0;

            Result.Movie.HeaderEntries[HeaderKeys.PAL] = pal.ToString();

            // other: reserved, set to 0

            /*
             * 016 1-byte flags "sync options":
             *       bit 0: MOVIE_SYNC2_INIT_FASTROM
             *       other: reserved, set to 0
             */
            r.ReadByte();

            /*
             * 017 1-byte flags "sync options":
             *       bit 0: MOVIE_SYNC_DATA_EXISTS
             *               if "1", all sync options flags are defined.
             *               if "0", all sync options flags have no meaning.
             *       bit 1: MOVIE_SYNC_WIP1TIMING
             *       bit 2: MOVIE_SYNC_LEFTRIGHT
             *       bit 3: MOVIE_SYNC_VOLUMEENVX
             *       bit 4: MOVIE_SYNC_FAKEMUTE
             *       bit 5: MOVIE_SYNC_SYNCSOUND
             *       bit 6: MOVIE_SYNC_HASROMINFO
             *               if "1", there is extra ROM info located right in between of the metadata and the savestate.
             *       bit 7: set to 0.
             */
            byte syncFlags = r.ReadByte();

            /*
             * Extra ROM info is always positioned right before the savestate. Its size is 30 bytes if MOVIE_SYNC_HASROMINFO
             * is used (and MOVIE_SYNC_DATA_EXISTS is set), 0 bytes otherwise.
             */
            int extraRomInfo = (((syncFlags >> 6) & 0x1) != 0 && (syncFlags & 0x1) != 0) ? 30 : 0;

            // 018 4-byte little-endian unsigned int: offset to the savestate inside file
            uint savestateOffset = r.ReadUInt32();

            // 01C 4-byte little-endian unsigned int: offset to the controller data inside file
            uint firstFrameOffset = r.ReadUInt32();

            int[] controllerTypes = new int[2];

            // The (.SMV 1.51 and up) header has an additional 32 bytes at the end
            if (version != "1.43")
            {
                // 020 4-byte little-endian unsigned int: number of input samples, primarily for peripheral-using games
                r.ReadBytes(4);

                /*
                 * 024 2 1-byte unsigned ints: what type of controller is plugged into ports 1 and 2 respectively: 0=NONE,
                 * 1=JOYPAD, 2=MOUSE, 3=SUPERSCOPE, 4=JUSTIFIER, 5=MULTITAP
                 */
                controllerTypes[0] = r.ReadByte();
                controllerTypes[1] = r.ReadByte();

                // 026 4 1-byte signed ints: controller IDs of port 1, or -1 for unplugged
                r.ReadBytes(4);

                // 02A 4 1-byte signed ints: controller IDs of port 2, or -1 for unplugged
                r.ReadBytes(4);

                // 02E 18 bytes: reserved for future use
                r.ReadBytes(18);
            }

            /*
             * After the header comes "metadata", which is UTF16-coded movie title string (author info). The metadata begins
             * from position 32 (0x20 (0x40 for 1.51 and up)) and ends at <savestate_offset -
             * length_of_extra_rom_info_in_bytes>.
             */
            byte[] metadata = r.ReadBytes((int)(savestateOffset - extraRomInfo - ((version != "1.43") ? 0x40 : 0x20)));
            string author   = NullTerminated(Encoding.Unicode.GetString(metadata).Trim());

            if (!string.IsNullOrWhiteSpace(author))
            {
                Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = author;
            }

            if (extraRomInfo == 30)
            {
                // 000 3 bytes of zero padding: 00 00 00 003 4-byte integer: CRC32 of the ROM 007 23-byte ascii string
                r.ReadBytes(3);
                int crc32 = r.ReadInt32();
                Result.Movie.HeaderEntries["CRC32"] = crc32.ToString();

                // the game name copied from the ROM, truncated to 23 bytes (the game name in the ROM is 21 bytes)
                string gameName = NullTerminated(Encoding.UTF8.GetString(r.ReadBytes(23)));
                Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = gameName;
            }

            SimpleController controllers = new SimpleController
            {
                Definition = _deck.Definition
            };

            r.BaseStream.Position = firstFrameOffset;

            /*
             * 01 00 (reserved)
             * 02 00 (reserved)
             * 04 00 (reserved)
             * 08 00 (reserved)
             * 10 00 R
             * 20 00 L
             * 40 00 X
             * 80 00 A
             * 00 01 Right
             * 00 02 Left
             * 00 04 Down
             * 00 08 Up
             * 00 10 Start
             * 00 20 Select
             * 00 40 Y
             * 00 80 B
             */
            string[] buttons =
            {
                "Right", "Left", "Down", "Up", "Start", "Select", "Y", "B", "R", "L", "X", "A"
            };

            for (int frame = 0; frame <= frameCount; frame++)
            {
                controllers["Reset"] = true;
                for (int player = 1; player <= controllersUsed.Length; player++)
                {
                    if (!controllersUsed[player - 1])
                    {
                        continue;
                    }

                    /*
                     * Each frame consists of 2 bytes per controller. So if there are 3 controllers, a frame is 6 bytes and
                     * if there is only 1 controller, a frame is 2 bytes.
                     */
                    byte controllerState1 = r.ReadByte();
                    byte controllerState2 = r.ReadByte();

                    /*
                     * In the reset-recording patch, a frame that contains the value FF FF for every controller denotes a
                     * reset. The reset is done through the S9xSoftReset routine.
                     */
                    if (controllerState1 != 0xFF || controllerState2 != 0xFF)
                    {
                        controllers["Reset"] = false;
                    }

                    /*
                     * While the meaning of controller data (for 1.51 and up) for a single standard SNES controller pad
                     * remains the same, each frame of controller data can contain additional bytes if input for peripherals
                     * is being recorded.
                     */
                    if (version != "1.43" && player <= controllerTypes.Length)
                    {
                        var peripheral = "";
                        switch (controllerTypes[player - 1])
                        {
                        case 0:                                 // NONE
                            continue;

                        case 1:                                 // JOYPAD
                            break;

                        case 2:                                 // MOUSE
                            peripheral = "Mouse";

                            // 5*num_mouse_ports
                            r.ReadBytes(5);
                            break;

                        case 3:                                 // SUPERSCOPE
                            peripheral = "Super Scope";         // 6*num_superscope_ports
                            r.ReadBytes(6);
                            break;

                        case 4:                                 // JUSTIFIER
                            peripheral = "Justifier";

                            // 11*num_justifier_ports
                            r.ReadBytes(11);
                            break;

                        case 5:                                 // MULTITAP
                            peripheral = "Multitap";
                            break;
                        }

                        if (peripheral != "" && !Result.Warnings.Any())
                        {
                            Result.Warnings.Add($"Unable to import {peripheral}. Not supported yet");
                        }
                    }

                    ushort controllerState = (ushort)(((controllerState1 << 4) & 0x0F00) | controllerState2);
                    if (player <= BkmMnemonicConstants.Players[controllers.Definition.Name])
                    {
                        for (int button = 0; button < buttons.Length; button++)
                        {
                            controllers[$"P{player} {buttons[button]}"] =
                                ((controllerState >> button) & 0x1) != 0;
                        }
                    }
                    else if (!Result.Warnings.Any())
                    {
                        Result.Warnings.Add($"Controller {player} not supported.");
                    }
                }

                // The controller data contains <number_of_frames + 1> frames.
                if (frame == 0)
                {
                    continue;
                }

                Result.Movie.AppendFrame(controllers);

                Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
                Global.Config.SNES_InSnes9x   = false;
            }
        }
Exemple #2
0
        protected override void RunImport()
        {
            Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.Bsnes;

            var hf = new HawkFile(SourceFile.FullName);

            // .LSMV movies are .zip files containing data files.
            if (!hf.IsArchive)
            {
                Result.Errors.Add("This is not an archive.");
                return;
            }

            var ss = new LibsnesCore.SnesSyncSettings
            {
                LeftPort  = LibsnesControllerDeck.ControllerType.Gamepad,
                RightPort = LibsnesControllerDeck.ControllerType.Gamepad
            };

            _deck = new LibsnesControllerDeck(ss);

            string platform = "SNES";

            foreach (var item in hf.ArchiveItems)
            {
                if (item.Name == "authors")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream     = hf.GetStream();
                    string authors    = Encoding.UTF8.GetString(stream.ReadAllBytes());
                    string authorList = "";
                    string authorLast = "";
                    using (var reader = new StringReader(authors))
                    {
                        string line;

                        // Each author is on a different line.
                        while ((line = reader.ReadLine()) != null)
                        {
                            string author = line.Trim();
                            if (author != "")
                            {
                                if (authorLast != "")
                                {
                                    authorList += $"{authorLast}, ";
                                }

                                authorLast = author;
                            }
                        }
                    }

                    if (authorList != "")
                    {
                        authorList += "and ";
                    }

                    if (authorLast != "")
                    {
                        authorList += authorLast;
                    }

                    Result.Movie.HeaderEntries[HeaderKeys.Author] = authorList;
                    hf.Unbind();
                }
                else if (item.Name == "coreversion")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream      = hf.GetStream();
                    string coreVersion = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.Comments.Add($"CoreOrigin {coreVersion}");
                    hf.Unbind();
                }
                else if (item.Name == "gamename")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream   = hf.GetStream();
                    string gameName = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.HeaderEntries[HeaderKeys.GameName] = gameName;
                    hf.Unbind();
                }
                else if (item.Name == "gametype")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream   = hf.GetStream();
                    string gametype = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();

                    // TODO: Handle the other types.
                    switch (gametype)
                    {
                    case "gdmg":
                        platform = "GB";
                        break;

                    case "ggbc":
                    case "ggbca":
                        platform = "GBC";
                        break;

                    case "sgb_ntsc":
                    case "sgb_pal":
                        platform       = "SNES";
                        Config.GbAsSgb = true;
                        break;
                    }

                    bool pal = gametype == "snes_pal" || gametype == "sgb_pal";
                    Result.Movie.HeaderEntries[HeaderKeys.Pal] = pal.ToString();
                    hf.Unbind();
                }
                else if (item.Name == "input")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream = hf.GetStream();
                    string input  = Encoding.UTF8.GetString(stream.ReadAllBytes());

                    int lineNum = 0;
                    using (var reader = new StringReader(input))
                    {
                        lineNum++;
                        string line;
                        while ((line = reader.ReadLine()) != null)
                        {
                            if (line == "")
                            {
                                continue;
                            }

                            // Insert an empty frame in lsmv snes movies
                            // https://github.com/TASVideos/BizHawk/issues/721
                            // Both emulators send the input to bsnes core at the same V interval, but:
                            // lsnes' frame boundary occurs at V = 241, after which the input is read;
                            // BizHawk's frame boundary is just before automatic polling;
                            // This isn't a great place to add this logic but this code is a mess
                            if (lineNum == 1 && platform == "SNES")
                            {
                                // Note that this logic assumes the first non-empty log entry is a valid input log entry
                                // and that it is NOT a subframe input entry.  It seems safe to assume subframe input would not be on the first line
                                Result.Movie.AppendFrame(EmptyLmsvFrame());
                            }

                            ImportTextFrame(line, platform);
                        }
                    }

                    hf.Unbind();
                }
                else if (item.Name.StartsWith("moviesram."))
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream    = hf.GetStream();
                    byte[] movieSram = stream.ReadAllBytes();
                    if (movieSram.Length != 0)
                    {
                        Result.Errors.Add("Movies that begin with SRAM are not supported.");
                        hf.Unbind();
                        return;
                    }
                }
                else if (item.Name == "port1")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream = hf.GetStream();
                    string port1  = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.HeaderEntries["port1"] = port1;
                    ss.LeftPort = LibsnesControllerDeck.ControllerType.Gamepad;
                    _deck       = new LibsnesControllerDeck(ss);
                    hf.Unbind();
                }
                else if (item.Name == "port2")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream = hf.GetStream();
                    string port2  = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.HeaderEntries["port2"] = port2;
                    ss.RightPort = LibsnesControllerDeck.ControllerType.Gamepad;
                    _deck        = new LibsnesControllerDeck(ss);
                    hf.Unbind();
                }
                else if (item.Name == "projectid")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream    = hf.GetStream();
                    string projectId = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.HeaderEntries["ProjectID"] = projectId;
                    hf.Unbind();
                }
                else if (item.Name == "rerecords")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream    = hf.GetStream();
                    string rerecords = Encoding.UTF8.GetString(stream.ReadAllBytes());
                    int    rerecordCount;

                    // Try to parse the re-record count as an integer, defaulting to 0 if it fails.
                    try
                    {
                        rerecordCount = int.Parse(rerecords);
                    }
                    catch
                    {
                        rerecordCount = 0;
                    }

                    Result.Movie.Rerecords = (ulong)rerecordCount;
                    hf.Unbind();
                }
                else if (item.Name.EndsWith(".sha256"))
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream = hf.GetStream();
                    string rom    = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    int    pos    = item.Name.LastIndexOf(".sha256");
                    string name   = item.Name.Substring(0, pos);
                    Result.Movie.HeaderEntries[$"SHA256_{name}"] = rom;
                    hf.Unbind();
                }
                else if (item.Name == "savestate")
                {
                    Result.Errors.Add("Movies that begin with a savestate are not supported.");
                    return;
                }
                else if (item.Name == "subtitles")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream    = hf.GetStream();
                    string subtitles = Encoding.UTF8.GetString(stream.ReadAllBytes());
                    using (var reader = new StringReader(subtitles))
                    {
                        string line;
                        while ((line = reader.ReadLine()) != null)
                        {
                            var subtitle = ImportTextSubtitle(line);
                            if (!string.IsNullOrEmpty(subtitle))
                            {
                                Result.Movie.Subtitles.AddFromString(subtitle);
                            }
                        }
                    }

                    hf.Unbind();
                }
                else if (item.Name == "starttime.second")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream      = hf.GetStream();
                    string startSecond = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.HeaderEntries["StartSecond"] = startSecond;
                    hf.Unbind();
                }
                else if (item.Name == "starttime.subsecond")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream         = hf.GetStream();
                    string startSubSecond = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.HeaderEntries["StartSubSecond"] = startSubSecond;
                    hf.Unbind();
                }
                else if (item.Name == "systemid")
                {
                    hf.BindArchiveMember(item.Index);
                    var    stream   = hf.GetStream();
                    string systemId = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
                    Result.Movie.Comments.Add($"{EmulationOrigin} {systemId}");
                    hf.Unbind();
                }
            }

            Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform;
            Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
            Config.PreferredCores["SNES"] = CoreNames.Bsnes;             // TODO: convert to snes9x if it is the user's preference
        }