diff --git a/src/BizHawk.Client.Common/cheats/GameSharkDecoder.cs b/src/BizHawk.Client.Common/cheats/GameSharkDecoder.cs index 0e932654264..e5a1316a0df 100644 --- a/src/BizHawk.Client.Common/cheats/GameSharkDecoder.cs +++ b/src/BizHawk.Client.Common/cheats/GameSharkDecoder.cs @@ -128,12 +128,7 @@ private static IDecodeResult Sms(string code) private static IDecodeResult Snes(string code) { - if (code.Contains("-") && code.Length == 9) - { - return new InvalidCheatCode("Game genie codes are not currently supported for SNES"); - ////return SnesGameGenieDecoder.Decode(code); - } - + if (SnesGameGenieDecoder.Decode(code) is DecodeResult validSNES) return validSNES; // decoder checks format, will not return invalid for any well-formed input if (code.Length == 8) { return SnesActionReplayDecoder.Decode(code); diff --git a/src/BizHawk.Client.Common/cheats/SnesGameGenieDecoder.cs b/src/BizHawk.Client.Common/cheats/SnesGameGenieDecoder.cs index e5e97ae5acc..393d993d8a7 100644 --- a/src/BizHawk.Client.Common/cheats/SnesGameGenieDecoder.cs +++ b/src/BizHawk.Client.Common/cheats/SnesGameGenieDecoder.cs @@ -1,31 +1,38 @@ using System.Collections.Generic; +using BizHawk.Common.StringExtensions; + namespace BizHawk.Client.Common.cheats { public static class SnesGameGenieDecoder { - // including transposition - // Code: D F 4 7 0 9 1 5 6 B C 8 A 2 3 E - // Hex: 0 1 2 3 4 5 6 7 8 9 A B C D E F - // This only applies to the SNES - private static readonly Dictionary SNESGameGenieTable = new Dictionary + private const string ERR_MSG_MALFORMED = "Game genie codes must be 9 characters with a format of xxyy-yyyy"; + + private static void RotLeft16(ref uint value, int offset) + => value = ((value << offset) & 0xFFFF) | (value >> (16 - offset)); + + /// + /// encr: D F 4 7 0 9 1 5 6 B C 8 A 2 3 E + /// decr: 0 1 2 3 4 5 6 7 8 9 A B C D E F + /// + private static readonly Dictionary NybbleDecodeLookup = new() { - ['D'] = 0, // 0000 - ['F'] = 1, // 0001 - ['4'] = 2, // 0010 - ['7'] = 3, // 0011 - ['0'] = 4, // 0100 - ['9'] = 5, // 0101 - ['1'] = 6, // 0110 - ['5'] = 7, // 0111 - ['6'] = 8, // 1000 - ['B'] = 9, // 1001 - ['C'] = 10, // 1010 - ['8'] = 11, // 1011 - ['A'] = 12, // 1100 - ['2'] = 13, // 1101 - ['3'] = 14, // 1110 - ['E'] = 15 // 1111 + ['0'] = 0x4, + ['1'] = 0x6, + ['2'] = 0xD, + ['3'] = 0xE, + ['4'] = 0x2, + ['5'] = 0x7, + ['6'] = 0x8, + ['7'] = 0x3, + ['8'] = 0xB, + ['9'] = 0x5, + ['A'] = 0xC, + ['B'] = 0x9, + ['C'] = 0xA, + ['D'] = 0x0, + ['E'] = 0xF, + ['F'] = 0x1, }; public static IDecodeResult Decode(string code) @@ -34,11 +41,9 @@ public static IDecodeResult Decode(string code) { throw new ArgumentNullException(nameof(code)); } - - if (!code.Contains("-") && code.Length != 9) - { - return new InvalidCheatCode("Game genie codes must be 9 characters with a format of xxyy-yyyy"); - } + if (code.Length is not 9 || code[4] is not '-') return new InvalidCheatCode(ERR_MSG_MALFORMED); + code = code.OnlyHex(); + if (code.Length is not 8) return new InvalidCheatCode(ERR_MSG_MALFORMED); // Code: D F 4 7 0 9 1 5 6 B C 8 A 2 3 E // Hex: 0 1 2 3 4 5 6 7 8 9 A B C D E F @@ -47,65 +52,25 @@ public static IDecodeResult Decode(string code) // Bit # |3|2|1|0|3|2|1|0|3|2|1|0|3|2|1|0|3|2|1|0|3|2|1|0|3|2|1|0|3|2|1|0| // maps to| Value |i|j|k|l|q|r|s|t|o|p|a|b|c|d|u|v|w|x|e|f|g|h|m|n| // order | Value |a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x| - var result = new DecodeResult { Size = WatchSize.Byte }; - - int x; - - // Value - if (code.Length > 0) + var addrBitsIJKL = (uint) NybbleDecodeLookup[code[2]]; + var addrBitsQRST = (uint) NybbleDecodeLookup[code[3]]; + var bunchaBits = unchecked((uint) ((NybbleDecodeLookup[code[4]] << 12) + | (NybbleDecodeLookup[code[5]] << 8) + | (NybbleDecodeLookup[code[6]] << 4) + | NybbleDecodeLookup[code[7]])); + RotLeft16(ref bunchaBits, 2); + var addr = ((bunchaBits & 0xF000U) << 8) // offset 12 to 20 + | ((bunchaBits & 0x00F0U) << 12) // offset 4 to 16 + | (addrBitsIJKL << 12) + | ((bunchaBits & 0x000FU) << 8) // offset 0 to 8 + | (addrBitsQRST << 4) + | ((bunchaBits & 0x0F00U) >> 8); // offset 8 to 0 + return new DecodeResult { - _ = SNESGameGenieTable.TryGetValue(code[0], out x); - result.Value = x << 4; - } - - if (code.Length > 1) - { - _ = SNESGameGenieTable.TryGetValue(code[1], out x); - result.Value |= x; - } - - // Address - if (code.Length > 2) - { - _ = SNESGameGenieTable.TryGetValue(code[2], out x); - result.Address = x << 12; - } - - if (code.Length > 3) - { - _ = SNESGameGenieTable.TryGetValue(code[3], out x); - result.Address |= x << 4; - } - - if (code.Length > 4) - { - _ = SNESGameGenieTable.TryGetValue(code[4], out x); - result.Address |= (x & 0xC) << 6; - result.Address |= (x & 0x3) << 22; - } - - if (code.Length > 5) - { - _ = SNESGameGenieTable.TryGetValue(code[5], out x); - result.Address |= (x & 0xC) << 18; - result.Address |= (x & 0x3) << 2; - } - - if (code.Length > 6) - { - _ = SNESGameGenieTable.TryGetValue(code[6], out x); - result.Address |= (x & 0xC) >> 2; - result.Address |= (x & 0x3) << 18; - } - - if (code.Length > 7) - { - _ = SNESGameGenieTable.TryGetValue(code[7], out x); - result.Address |= (x & 0xC) << 14; - result.Address |= (x & 0x3) << 10; - } - - return result; + Address = unchecked((int) addr), + Size = WatchSize.Byte, + Value = (NybbleDecodeLookup[code[0]] << 4) | NybbleDecodeLookup[code[1]], + }; } } } diff --git a/src/BizHawk.Tests/Client.Common/cheats/CheatDecoderTests.cs b/src/BizHawk.Tests/Client.Common/cheats/CheatDecoderTests.cs index 96d2913b029..2c605382296 100644 --- a/src/BizHawk.Tests/Client.Common/cheats/CheatDecoderTests.cs +++ b/src/BizHawk.Tests/Client.Common/cheats/CheatDecoderTests.cs @@ -52,6 +52,8 @@ private sealed class CheatcodeDataAttribute : Attribute, ITestDataSource new object?[] { VSystemID.Raw.GB, "BE0-37B-08C", 0x4037, 0xBE, 0xB9, WatchSize.Byte }, new object?[] { VSystemID.Raw.GBA, "4012F5B7 3B7801A6", 0x00000006, 0xB7, NO_COMPARE, WatchSize.Byte }, new object?[] { VSystemID.Raw.GBA, "686D7FC3 24B5B832", 0x00000032, 0x7FC3, NO_COMPARE, WatchSize.Word }, + new object?[] { VSystemID.Raw.SNES, "C264-64D7", 0x008E28, 0xAD, NO_COMPARE, WatchSize.Byte }, + new object?[] { VSystemID.Raw.SNES, "008E28AD", 0x008E28, 0xAD, NO_COMPARE, WatchSize.Byte }, new object?[] { VSystemID.Raw.SNES, "7E1F2801", 0x7E1F28, 0x01, NO_COMPARE, WatchSize.Byte }, };