Rewrite Game Genie (GB) decoder

This commit is contained in:
James Groom 2024-01-21 15:01:20 +00:00 committed by GitHub
parent 2f9119834b
commit acf368e67f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 93 deletions

View File

@ -52,7 +52,8 @@ namespace BizHawk.Client.Common.cheats
private static IDecodeResult GameBoy(string code)
{
// Game Genie
if (code.LastIndexOf("-", StringComparison.Ordinal) == 7 && code.IndexOf("-", StringComparison.Ordinal) == 3)
if ((code.Length is 11 && code[3] is '-' && code[7] is '-')
|| (code.Length is 7 && code[3] is '-'))
{
return GbGgGameGenieDecoder.Decode(code);
}

View File

@ -1,109 +1,61 @@
using System;
using System.Collections.Generic;
using BizHawk.Common.NumberExtensions;
using BizHawk.Common.StringExtensions;
using static BizHawk.Common.StringExtensions.NumericStringExtensions;
namespace BizHawk.Client.Common.cheats
{
/// <summary>
/// Decodes Gameboy and Game Gear Game Genie codes
/// </summary>
/// <remarks><see href="https://www.devrs.com/gb/files/gg.html"/></remarks>
public static class GbGgGameGenieDecoder
{
private static readonly Dictionary<char, int> _gbGgGameGenieTable = new Dictionary<char, int>
public static IDecodeResult Decode(string code)
{
['0'] = 0,
['1'] = 1,
['2'] = 2,
['3'] = 3,
['4'] = 4,
['5'] = 5,
['6'] = 6,
['7'] = 7,
['8'] = 8,
['9'] = 9,
['A'] = 10,
['B'] = 11,
['C'] = 12,
['D'] = 13,
['E'] = 14,
['F'] = 15
};
public static IDecodeResult Decode(string _code)
{
if (_code == null)
if (code is null) throw new ArgumentNullException(nameof(code));
const string ERR_MSG_MALFORMED = "expecting code in the format XXX-XXX or XXX-XXX-XXX";
if (code.Length is not (7 or 11)) return new InvalidCheatCode(ERR_MSG_MALFORMED);
const char SEP = '-';
if (code[3] is not SEP || !(code[0].IsHex() && code[1].IsHex() && code[2].IsHex()
&& code[4].IsHex() && code[5].IsHex() && code[6].IsHex()))
{
throw new ArgumentNullException(nameof(_code));
return new InvalidCheatCode(ERR_MSG_MALFORMED);
}
if (_code.LastIndexOf("-", StringComparison.Ordinal) != 7 || _code.IndexOf("-", StringComparison.Ordinal) != 3)
DecodeResult result = new() { Size = WatchSize.Byte };
Span<char> toParse = stackalloc char[4];
if (code.Length is 11)
{
return new InvalidCheatCode("All Master System Game Genie Codes need to have a dash after the third character and seventh character.");
if (code[7] is not SEP || !(code[8].IsHex() && code[9].IsHex() && code[10].IsHex()))
{
return new InvalidCheatCode(ERR_MSG_MALFORMED);
}
// code is VVA-AAA-CCC, parse the compare value and fall through
toParse[0] = code[8];
_ = code[9]; // "undetermined but if you XOR it with encoded nibble A the result must not be any of the values 1 through 7 or else you will receive a bad code message"
toParse[1] = code[10];
var compareValue = ParseU8FromHex(toParse[..2]);
compareValue ^= 0xFF;
NumberExtensions.RotateRightU8(ref compareValue, 2);
result.Compare = compareValue ^ 0x45;
}
// No cypher on value
// Char # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
// 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|3|2|1|0|
// maps to| Value |A|B|C|D|E|F|G|H|I|J|K|L|XOR 0xF|a|b|c|c|NotUsed|e|f|g|h|
// proper | Value |XOR 0xF|A|B|C|D|E|F|G|H|I|J|K|L|g|h|a|b|Nothing|c|d|e|f|
var result = new DecodeResult { Size = WatchSize.Byte };
int x;
// Getting Value
if (_code.Length > 0)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[0], out x);
result.Value = x << 4;
}
if (_code.Length > 1)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[1], out x);
result.Value |= x;
}
// Address
if (_code.Length > 2)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[2], out x);
result.Value = x << 8;
}
else
{
result.Value = -1;
}
if (_code.Length > 3)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[3], out x);
result.Address |= x << 4;
}
if (_code.Length > 4)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[4], out x);
result.Address |= x;
}
if (_code.Length > 5)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[5], out x);
result.Address |= (x ^ 0xF) << 12;
}
// compare need to be full
if (_code.Length > 8)
{
_ = _gbGgGameGenieTable.TryGetValue(_code[6], out x);
var comp = x << 2;
// 8th character ignored
_ = _gbGgGameGenieTable.TryGetValue(_code[8], out x);
comp |= (x & 0xC) >> 2;
comp |= (x & 0x3) << 6;
result.Compare = comp ^ 0xBA;
}
// else code is VVA-AAA
toParse[0] = code[6];
toParse[1] = code[2];
toParse[2] = code[4];
toParse[3] = code[5];
result.Address = ParseU16FromHex(toParse) ^ 0xF000;
#pragma warning disable CA1846 // Span overload just calls ToString
result.Value = ParseU8FromHex(
#if NET7_0_OR_GREATER
code.AsSpan(start: 0, length: 2)
#else
code.Substring(startIndex: 0, length: 2)
#endif
);
#pragma warning restore CA1846
return result;
}
}

View File

@ -121,6 +121,14 @@ namespace BizHawk.Common.NumberExtensions
return val;
}
public static void RotateRightU8(ref byte b, int shift)
{
byte temp = b;
temp <<= 8 - shift;
b >>= shift;
b |= temp;
}
public static int RoundToInt(this double d) => (int) Math.Round(d);
public static int RoundToInt(this float f) => (int) Math.Round(f);

View File

@ -1,3 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
namespace BizHawk.Common.StringExtensions
@ -31,6 +33,26 @@ namespace BizHawk.Common.StringExtensions
/// </returns>
public static string OnlyHex(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsHex)).ToUpperInvariant();
#if NET7_0_OR_GREATER
public static ushort ParseU16FromHex(ReadOnlySpan<char> str)
=> ushort.Parse(str, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
public static byte ParseU8FromHex(ReadOnlySpan<char> str)
=> byte.Parse(str, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
#else
public static ushort ParseU16FromHex(ReadOnlySpan<char> str)
=> ParseU16FromHex(str.ToString());
public static ushort ParseU16FromHex(string str)
=> ushort.Parse(str, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
public static byte ParseU8FromHex(ReadOnlySpan<char> str)
=> ParseU8FromHex(str.ToString());
public static byte ParseU8FromHex(string str)
=> byte.Parse(str, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
#endif
/// <returns><see langword="true"/> iff <paramref name="str"/> is not <see langword="null"/> and all chars of <paramref name="str"/> are digits</returns>
public static bool IsUnsigned(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsUnsigned);
}