From 641c3e9f82d027969dc5358f4254cb352fd573eb Mon Sep 17 00:00:00 2001 From: adelikat Date: Tue, 12 Nov 2019 14:58:58 -0600 Subject: [PATCH] convert .smv importer to new style, also convert to bsnes instead of snes9x --- .../BizHawk.Client.Common.csproj | 1 + .../movie/import/LsmvImport.cs | 4 +- .../movie/import/MovieImport.cs | 304 +--------------- .../movie/import/SmvImport.cs | 335 ++++++++++++++++++ .../Consoles/Nintendo/SNES9X/Snes9x.cs | 4 +- 5 files changed, 341 insertions(+), 307 deletions(-) create mode 100644 BizHawk.Client.Common/movie/import/SmvImport.cs diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj index 78618198a6..03718020c8 100644 --- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj +++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj @@ -160,6 +160,7 @@ + diff --git a/BizHawk.Client.Common/movie/import/LsmvImport.cs b/BizHawk.Client.Common/movie/import/LsmvImport.cs index c187c8ed63..a2f97b9e9c 100644 --- a/BizHawk.Client.Common/movie/import/LsmvImport.cs +++ b/BizHawk.Client.Common/movie/import/LsmvImport.cs @@ -27,8 +27,8 @@ namespace BizHawk.Client.Common.movie.import var ss = new LibsnesCore.SnesSyncSettings { - LeftPort = LibsnesControllerDeck.ControllerType.Gamepad - , RightPort = LibsnesControllerDeck.ControllerType.Gamepad + LeftPort = LibsnesControllerDeck.ControllerType.Gamepad, + RightPort = LibsnesControllerDeck.ControllerType.Gamepad }; _deck = new LibsnesControllerDeck(ss); diff --git a/BizHawk.Client.Common/movie/import/MovieImport.cs b/BizHawk.Client.Common/movie/import/MovieImport.cs index bcf7fcfa96..ef39ec7339 100644 --- a/BizHawk.Client.Common/movie/import/MovieImport.cs +++ b/BizHawk.Client.Common/movie/import/MovieImport.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.IO; using System.Reflection; using BizHawk.Common.BufferExtensions; @@ -10,14 +9,11 @@ using BizHawk.Common.IOExtensions; using BizHawk.Emulation.Common; using BizHawk.Client.Common.MovieConversionExtensions; -using BizHawk.Emulation.Cores.Nintendo.SNES9X; - namespace BizHawk.Client.Common { public static class MovieImport { // Movies 2.0 TODO: this is Movie.cs specific, can it be IMovie based? If not, needs to be refactored to a hardcoded 2.0 implementation, client needs to know what kind of type it imported to, or the mainform method needs to be moved here - private const string CRC32 = "CRC32"; private const string EMULATIONORIGIN = "emuOrigin"; private const string JAPAN = "Japan"; private const string MD5 = "MD5"; @@ -135,9 +131,6 @@ namespace BizHawk.Client.Common case ".MMV": m = ImportMmv(path, out errorMsg, out warningMsg); break; - case ".SMV": - m = ImportSmv(path, out errorMsg, out warningMsg); - break; case ".BKM": m.Filename = path; m.Load(false); @@ -178,7 +171,7 @@ namespace BizHawk.Client.Common { string[] extensions = { - "BKM", "MMV", "SMV" + "BKM", "MMV" }; return extensions.Any(ext => extension.ToUpper() == $".{ext}"); } @@ -327,300 +320,5 @@ namespace BizHawk.Client.Common fs.Close(); return m; } - - private static BkmMovie ImportSmv(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: 53 4D 56 1A "SMV\x1A" - string signature = r.ReadStringFixedAscii(4); - if (signature != "SMV\x1A") - { - errorMsg = "This is not a valid .SMV file."; - r.Close(); - fs.Close(); - return null; - } - - m.Header[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: - errorMsg = "SMV version not recognized. 1.43, 1.51, and 1.52 are currently supported."; - r.Close(); - fs.Close(); - return null; - } - - m.Comments.Add($"{EMULATIONORIGIN} Snes9x version {version}"); - m.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 - m.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 - */ - SimpleController controllers = new SimpleController { Definition = new ControllerDefinition { Name = "SNES Controller" } }; - bool[] controllersUsed = new bool[5]; - for (int controller = 1; controller <= controllersUsed.Length; controller++) - { - controllersUsed[controller - 1] = ((controllerFlags >> (controller - 1)) & 0x1) != 0; - } - - // 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) - { - errorMsg = "Movies that begin with a savestate are not supported."; - r.Close(); - fs.Close(); - return null; - } - - // bit 1: if "0", movie is NTSC (60 fps); if "1", movie is PAL (50 fps) - bool pal = ((movieFlags >> 1) & 0x1) != 0; - m.Header[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 . - */ - byte[] metadata = r.ReadBytes((int)(savestateOffset - extraRomInfo - ((version != "1.43") ? 0x40 : 0x20))); - string author = NullTerminated(Encoding.Unicode.GetString(metadata).Trim()); - if (author != "") - { - m.Header[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(); - m.Header[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))); - m.Header[HeaderKeys.GAMENAME] = gameName; - } - - 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 != "" && warningMsg == "") - { - warningMsg = $"Unable to import {peripheral}."; - } - } - - 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 (warningMsg == "") - { - warningMsg = $"Controller {player} not supported."; - } - } - - // The controller data contains frames. - if (frame == 0) - { - continue; - } - - m.AppendFrame(controllers); - } - - r.Close(); - fs.Close(); - - var ss = new Snes9x.SyncSettings(); - m.SyncSettingsJson = ConfigService.SaveWithType(ss); - Global.Config.SNES_InSnes9x = true; // This could be annoying to a user if they don't notice we set this preference, but the alternative is for the movie import to fail to load the movie - return m; - } } } diff --git a/BizHawk.Client.Common/movie/import/SmvImport.cs b/BizHawk.Client.Common/movie/import/SmvImport.cs new file mode 100644 index 0000000000..d4544c99a8 --- /dev/null +++ b/BizHawk.Client.Common/movie/import/SmvImport.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Nintendo.SNES; + +namespace BizHawk.Client.Common.movie.import +{ + // ReSharper disable once UnusedMember.Global + // http://tasvideos.org/EmulatorResources/Snes9x/SMV.html + [ImportExtension(".smv")] + public class SmvImport : MovieImporter + { + private LibsnesControllerDeck _deck; + + 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 . + */ + 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 frames. + if (frame == 0) + { + continue; + } + + Result.Movie.AppendFrame(controllers); + + Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss); + Global.Config.SNES_InSnes9x = false; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES9X/Snes9x.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES9X/Snes9x.cs index e71c8e995b..27169b6337 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES9X/Snes9x.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/SNES9X/Snes9x.cs @@ -32,8 +32,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.SNES9X SystemId = "SNES" }) { - settings = settings ?? new Settings(); - syncSettings = syncSettings ?? new SyncSettings(); + settings ??= new Settings(); + syncSettings ??= new SyncSettings(); _core = PreInit(new PeRunnerOptions {