several rcheevo updates, mostly for 3DS stuff

This commit is contained in:
CasualPokePlayer 2024-01-16 20:00:45 -08:00
parent 67b535d0f8
commit d03d076fa7
13 changed files with 267 additions and 43 deletions

Binary file not shown.

View File

@ -11,22 +11,13 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)
if(MSVC)
# MSVC targets don't export all symbols unless this is on
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
# max warnings, treat as errors
add_compile_options(/W4)
add_compile_options(/WX)
# ignore some warnings
# this differs between clang-cl and cl
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
# ignore some warnings on cl
if(CMAKE_C_COMPILER_ID MATCHES "MSVC")
add_compile_options(
-Wno-unused-parameter
)
else()
add_compile_options(
/wd4100 # "unreferenced formal parameter"
/wd4244 # "conversion from 'type1' to 'type2', possible loss of data"
/wd4245 # "conversion from 'type1' to 'type2', signed/unsigned mismatch"
)
@ -47,12 +38,6 @@ else()
add_compile_options(-Wall -Wextra)
add_compile_options(-Werror)
# ignore some warnings
add_compile_options(
-Wno-unused-parameter
-Wno-implicit-fallthrough
)
# strip in release, optimize for gdb usage in debug
add_link_options($<$<CONFIG:RELEASE>:-s>)
add_compile_options($<$<CONFIG:DEBUG>:-ggdb>)
@ -75,7 +60,7 @@ add_library(
${RC_SRC_DIR}/rc_compat.c
${RC_SRC_DIR}/rc_compat.h
${RC_SRC_DIR}/rc_util.c
${RC_SRC_DIR}/rc_util.h
${RC_SRC_DIR}/rc_version.c
${RC_SRC_DIR}/rc_version.h
${RC_SRC_DIR}/rapi/rc_api_common.c
${RC_SRC_DIR}/rapi/rc_api_common.h
@ -99,6 +84,8 @@ add_library(
${RC_SRC_DIR}/rcheevos/runtime_progress.c
${RC_SRC_DIR}/rcheevos/trigger.c
${RC_SRC_DIR}/rcheevos/value.c
${RC_SRC_DIR}/rhash/aes.c
${RC_SRC_DIR}/rhash/aes.h
${RC_SRC_DIR}/rhash/cdreader.c
${RC_SRC_DIR}/rhash/hash.c
${RC_SRC_DIR}/rhash/md5.c
@ -110,13 +97,15 @@ add_library(
${RC_INCLUDE_DIR}/rc_api_user.h
${RC_INCLUDE_DIR}/rc_consoles.h
${RC_INCLUDE_DIR}/rc_error.h
${RC_INCLUDE_DIR}/rc_export.h
${RC_INCLUDE_DIR}/rc_hash.h
${RC_INCLUDE_DIR}/rc_runtime.h
${RC_INCLUDE_DIR}/rc_runtime_types.h
${RC_INCLUDE_DIR}/rc_util.h
${RC_INCLUDE_DIR}/rcheevos.h
)
target_compile_definitions(${RC_TARGET} PRIVATE RC_DISABLE_LUA RC_NO_THREADS)
target_compile_definitions(${RC_TARGET} PRIVATE RC_DISABLE_LUA RC_NO_THREADS RC_SHARED)
target_include_directories(${RC_TARGET} PRIVATE ${RC_INCLUDE_DIR})
add_custom_command(

@ -1 +1 @@
Subproject commit 6fb3ebca22fe4f3a97e7a391e5e9c4623aa2286a
Subproject commit e7989c300280ba06d7621ae5b4e00ac7fe28d97a

View File

@ -17,6 +17,8 @@ namespace BizHawk.Client.EmuHawk
IMovieSession MovieSession { get; }
FirmwareManager FirmwareManager { get; }
event BeforeQuickLoadEventHandler QuicksaveLoad;
SettingsAdapter GetSettingsAdapterForLoadedCoreUntyped();

View File

@ -90,6 +90,14 @@ namespace BizHawk.Client.EmuHawk
RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL = 5,
}
public enum rc_runtime_achievement_type_t : uint
{
RC_ACHIEVEMENT_TYPE_STANDARD = 0,
RC_ACHIEVEMENT_TYPE_MISSABLE = 1,
RC_ACHIEVEMENT_TYPE_PROGRESSION = 2,
RC_ACHIEVEMENT_TYPE_WIN = 3,
}
[StructLayout(LayoutKind.Sequential)]
public struct rc_runtime_event_t
{
@ -163,13 +171,17 @@ namespace BizHawk.Client.EmuHawk
}
[StructLayout(LayoutKind.Sequential)]
public readonly record struct rc_api_start_session_request_t(string username, string api_token, uint game_id)
public readonly record struct rc_api_start_session_request_t(string username, string api_token, uint game_id, string game_hash, bool hardcore)
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
public readonly string username = username;
[MarshalAs(UnmanagedType.LPUTF8Str)]
public readonly string api_token = api_token;
public readonly uint game_id = game_id;
[MarshalAs(UnmanagedType.LPUTF8Str)]
public readonly string game_hash = game_hash;
[MarshalAs(UnmanagedType.Bool)]
public readonly bool hardcore = hardcore;
}
[StructLayout(LayoutKind.Sequential)]
@ -182,7 +194,8 @@ namespace BizHawk.Client.EmuHawk
[StructLayout(LayoutKind.Sequential)]
public readonly record struct rc_api_achievement_definition_t(uint id, uint points, rc_runtime_achievement_category_t category,
IntPtr title, IntPtr description, IntPtr definition, IntPtr author, IntPtr badge_name, long created, long updated)
IntPtr title, IntPtr description, IntPtr definition, IntPtr author, IntPtr badge_name, long created, long updated,
rc_runtime_achievement_type_t type, float rarity, float rarity_hardcore)
{
public string Title => Mershul.PtrToStringUtf8(title);
public string Description => Mershul.PtrToStringUtf8(description);
@ -252,7 +265,7 @@ namespace BizHawk.Client.EmuHawk
}
[StructLayout(LayoutKind.Sequential)]
public readonly record struct rc_api_ping_request_t(string username, string api_token, uint game_id, string rich_presence)
public readonly record struct rc_api_ping_request_t(string username, string api_token, uint game_id, string rich_presence, string game_hash, bool hardcore)
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
public readonly string username = username;
@ -260,7 +273,9 @@ namespace BizHawk.Client.EmuHawk
public readonly string api_token = api_token;
public readonly uint game_id = game_id;
[MarshalAs(UnmanagedType.LPUTF8Str)]
public readonly string rich_presence = rich_presence;
public readonly string game_hash = game_hash;
[MarshalAs(UnmanagedType.Bool)]
public readonly bool hardcore = hardcore;
}
[StructLayout(LayoutKind.Sequential)]
@ -344,6 +359,17 @@ namespace BizHawk.Client.EmuHawk
[UnmanagedFunctionPointer(cc)]
public delegate void rc_hash_message_callback([MarshalAs(UnmanagedType.LPUTF8Str)] string message);
[UnmanagedFunctionPointer(cc)]
[return: MarshalAs(UnmanagedType.Bool)]
public delegate bool rc_hash_3ds_get_cia_normal_key_func(byte common_key_index, IntPtr out_normal_key);
[UnmanagedFunctionPointer(cc)]
[return: MarshalAs(UnmanagedType.Bool)]
public delegate bool rc_hash_3ds_get_ncch_normal_keys_func(IntPtr primary_key_y, byte secondary_key_x_slot, IntPtr optional_program_id, IntPtr out_primary_key, IntPtr out_secondary_key);
[BizImport(cc)]
public abstract IntPtr rc_version_string();
[BizImport(cc)]
public abstract IntPtr rc_error_str(rc_error_t error_code);
@ -404,6 +430,12 @@ namespace BizHawk.Client.EmuHawk
[BizImport(cc, Compatibility = true)]
public abstract void rc_hash_init_custom_filereader(in rc_hash_filereader reader);
[BizImport(cc)]
public abstract void rc_hash_init_3ds_get_cia_normal_key_func(rc_hash_3ds_get_cia_normal_key_func func);
[BizImport(cc)]
public abstract void rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func);
[BizImport(cc)]
[return: MarshalAs(UnmanagedType.Bool)]
public abstract bool rc_hash_generate_from_buffer(byte[] hash, RetroAchievements.ConsoleID console_id, byte[] buffer, nuint buffer_size);

View File

@ -33,7 +33,7 @@ namespace BizHawk.Client.EmuHawk
_resolver = new("RA_Integration-x64.dll", hasLimitedLifetime: true);
_RA = BizInvoker.GetInvoker<RAInterface>(_resolver, DummyMonitor.Singleton, CallingConventionAdapters.Native);
_version = new(Marshal.PtrToStringAnsi(_RA.IntegrationVersion())!);
Console.WriteLine($"Loaded RetroAchievements v{_version}");
Console.WriteLine($"Loaded RAIntegration v{_version}");
}
private static void DetachDll()

View File

@ -55,6 +55,9 @@ namespace BizHawk.Client.EmuHawk
public DateTime Created { get; }
public DateTime Updated { get; }
public LibRCheevos.rc_runtime_achievement_type_t Type { get; }
public float Rarity { get; }
public float RarityHardcore { get; }
public bool IsSoftcoreUnlocked { get; set; }
public bool IsHardcoreUnlocked { get; set; }
@ -100,6 +103,9 @@ namespace BizHawk.Client.EmuHawk
BadgeName = cheevo.BadgeName;
Created = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.created).ToLocalTime();
Updated = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.updated).ToLocalTime();
Type = cheevo.type;
Rarity = cheevo.rarity;
RarityHardcore = cheevo.rarity_hardcore;
IsSoftcoreUnlocked = false;
IsHardcoreUnlocked = false;
IsPrimed = false;
@ -119,6 +125,9 @@ namespace BizHawk.Client.EmuHawk
BadgeName = cheevo.BadgeName;
Created = cheevo.Created;
Updated = cheevo.Updated;
Type = cheevo.Type;
Rarity = cheevo.Rarity;
RarityHardcore = cheevo.RarityHardcore;
IsSoftcoreUnlocked = false;
IsHardcoreUnlocked = false;
IsPrimed = false;

View File

@ -12,9 +12,9 @@ namespace BizHawk.Client.EmuHawk
{
private readonly LibRCheevos.rc_api_start_session_request_t _apiParams;
public StartGameSessionRequest(string username, string apiToken, uint gameId)
public StartGameSessionRequest(string username, string apiToken, uint gameId, string gameHash, bool hardcore)
{
_apiParams = new(username, apiToken, gameId);
_apiParams = new(username, apiToken, gameId, gameHash, hardcore);
}
public override void DoRequest()
@ -35,17 +35,15 @@ namespace BizHawk.Client.EmuHawk
}
private void StartGameSession()
{
PushRequest(new StartGameSessionRequest(Username, ApiToken, _gameData.GameID));
}
=> PushRequest(new StartGameSessionRequest(Username, ApiToken, _gameData.GameID, _gameHash, HardcoreMode));
private sealed class PingRequest : RCheevoHttpRequest
{
private readonly LibRCheevos.rc_api_ping_request_t _apiParams;
public PingRequest(string username, string apiToken, uint gameId, string richPresence)
public PingRequest(string username, string apiToken, uint gameId, string richPresence, string gameHash, bool hardcore)
{
_apiParams = new(username, apiToken, gameId, richPresence);
_apiParams = new(username, apiToken, gameId, richPresence, gameHash, hardcore);
}
public override void DoRequest()
@ -66,9 +64,7 @@ namespace BizHawk.Client.EmuHawk
}
private void SendPing()
{
PushRequest(new PingRequest(Username, ApiToken, _gameData.GameID, CurrentRichPresence));
}
=> PushRequest(new PingRequest(Username, ApiToken, _gameData.GameID, CurrentRichPresence, _gameHash, HardcoreMode));
private readonly byte[] _richPresenceBuffer = new byte[1024];

View File

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
@ -22,6 +23,9 @@ namespace BizHawk.Client.EmuHawk
OSTailoredCode.IsUnixHost ? "librcheevos.so" : "librcheevos.dll", hasLimitedLifetime: false);
_lib = BizInvoker.GetInvoker<LibRCheevos>(resolver, CallingConventionAdapters.Native);
var version = Marshal.PtrToStringAnsi(_lib.rc_version_string());
Console.WriteLine($"Loaded RCheevos v{version}");
// init message callbacks
_errorMessageCallback = ErrorMessageCallback;
_verboseMessageCallback = VerboseMessageCallback;

View File

@ -1,11 +1,16 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using BizHawk.Common;
using BizHawk.Client.Common;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.DiscSystem;
namespace BizHawk.Client.EmuHawk
@ -192,7 +197,7 @@ namespace BizHawk.Client.EmuHawk
(buf2352[j + bootLenOffset + 2] << 8) | buf2352[j + bootLenOffset + 3];
bootLba = startLba + i;
bootOff = j + bootLenOffset + 4;
byteswapped = false;
// byteswapped = false;
foundHeader = true;
break;
}
@ -284,6 +289,184 @@ namespace BizHawk.Client.EmuHawk
return IdentifyHash(hash);
}
// Stuff needed for 3DS hashing...
private readonly LibRCheevos.rc_hash_3ds_get_cia_normal_key_func _getCiaNormalKeyFunc;
private readonly LibRCheevos.rc_hash_3ds_get_ncch_normal_keys_func _getNcchNormalKeysFunc;
// https://github.com/citra-emu/citra/blob/2b20082581906fe973e26ed36bef695aa1f64527/src/core/hw/aes/key.cpp#L23-L30
private static readonly BigInteger GENERATOR_CONSTANT = BigInteger.Parse("1FF9E9AAC5FE0408024591DC5D52768A", NumberStyles.HexNumber, CultureInfo.InvariantCulture);
private static readonly BigInteger U128_MAX = BigInteger.Parse("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.HexNumber, CultureInfo.InvariantCulture);
private static byte[] Derive3DSNormalKey(BigInteger keyX, BigInteger keyY)
{
static BigInteger LeftRot128(BigInteger v, int rot)
{
var l = (v << rot) & U128_MAX;
var r = v >> (128 - rot);
return l | r;
}
static BigInteger Add128(BigInteger v1, BigInteger v2)
=> (v1 + v2) & U128_MAX;
var normalKey = LeftRot128(Add128(LeftRot128(keyX, 2) ^ keyY, GENERATOR_CONSTANT), 87);
var normalKeyBytes = normalKey.ToByteArray();
if (normalKeyBytes.Length > 17)
{
// this shoudn't ever happen
throw new InvalidOperationException();
}
// get rid of a final trailing 0
// but also make sure we have 0 paddng to 16 bytes
Array.Resize(ref normalKeyBytes, 16);
// .ToByteArray() is always in little endian order, but we want big endian order
Array.Reverse(normalKeyBytes);
return normalKeyBytes;
}
private MemoryStream GetFirmware(FirmwareID id)
{
var record = FirmwareDatabase.FirmwareRecords.First(fr => fr.ID == id);
var resolved = _mainForm.FirmwareManager.Resolve(_getConfig().PathEntries, _getConfig().FirmwareUserSpecifications, record);
if (resolved?.FilePath == null) throw new InvalidOperationException();
return new(File.ReadAllBytes(resolved.FilePath), writable: false);
}
private (BigInteger Key1, BigInteger Key2) FindAesKeys(string key1Prefix, string key2Prefix)
{
using var keys = new StreamReader(GetFirmware(new("3DS", "aes_keys")), Encoding.UTF8);
string key1Str = null, key2Str = null;
while ((key1Str is null || key2Str is null) && keys.ReadLine() is { } line)
{
if (line.Length == 0 || line.StartsWith('#'))
{
continue;
}
var eqpos = line.IndexOf('=');
if (eqpos == -1 || eqpos != line.LastIndexOf('='))
{
throw new InvalidOperationException("Malformed key list");
}
if (key1Str is null)
{
if (line.StartsWithOrdinal(key1Prefix))
{
key1Str = line[(eqpos + 1)..];
if (key1Str.Length != 32)
{
throw new InvalidOperationException("Invalid key length");
}
}
}
if (key2Str is null)
{
if (line.StartsWithOrdinal(key2Prefix))
{
key2Str = line[(eqpos + 1)..];
if (key2Str.Length != 32)
{
throw new InvalidOperationException("Invalid key length");
}
}
}
}
if (key1Str is null || key2Str is null)
{
throw new InvalidOperationException("Couldn't find requested keys");
}
var key1 = BigInteger.Parse($"0{key1Str}", NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var key2 = BigInteger.Parse($"0{key2Str}", NumberStyles.HexNumber, CultureInfo.InvariantCulture);
return (key1, key2);
}
private bool GetCiaNormalKeyFunc(byte common_key_index, IntPtr out_normal_key)
{
if (common_key_index > 5)
{
return false;
}
try
{
var (keyX, keyY) = FindAesKeys("slot0x3DKeyX=", $"common{common_key_index}=");
Marshal.Copy(Derive3DSNormalKey(keyX, keyY), 0, out_normal_key, 16);
return true;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return false;
}
}
private bool GetNcchNormalKeysFunc(IntPtr primary_key_y, byte secondary_key_x_slot, IntPtr optional_program_id, IntPtr out_primary_key, IntPtr out_secondary_key)
{
if (secondary_key_x_slot is not (0x2C or 0x25 or 0x18 or 0x1B))
{
return false;
}
try
{
var (primaryKeyX, secondaryKeyX) = FindAesKeys("slot0x2CKeyX=", $"slot0x{secondary_key_x_slot:X2}KeyX=");
var primaryKeyYBytes = new byte[17];
Marshal.Copy(primary_key_y, primaryKeyYBytes, 1, 16);
Array.Reverse(primaryKeyYBytes); // convert big endian to little endian
var primaryKeyY = new BigInteger(primaryKeyYBytes);
Marshal.Copy(Derive3DSNormalKey(primaryKeyX, primaryKeyY), 0, out_primary_key, 16);
if (optional_program_id == IntPtr.Zero)
{
Marshal.Copy(Derive3DSNormalKey(secondaryKeyX, primaryKeyY), 0, out_secondary_key, 16);
return true;
}
var programIdBytes = new byte[8];
Marshal.Copy(optional_program_id, programIdBytes, 0, 8);
var programId = BitConverter.ToUInt64(programIdBytes, 0);
using var seeddb = new BinaryReader(GetFirmware(new("3DS", "seeddb")));
var count = seeddb.ReadUInt32();
seeddb.BaseStream.Seek(12, SeekOrigin.Current); // apparently some padding bytes before actual seeds
for (long i = 0; i < count; i++)
{
var titleId = seeddb.ReadUInt64();
if (titleId != programId)
{
seeddb.BaseStream.Seek(24, SeekOrigin.Current);
continue;
}
var sha256Input = new byte[32];
Marshal.Copy(primary_key_y, sha256Input, 0, 16);
seeddb.BaseStream.Read(sha256Input, 16, 16);
var sha256Digest = SHA256Checksum.Compute(sha256Input);
var secondaryKeyYBytes = new byte[17];
Buffer.BlockCopy(sha256Digest, 0, secondaryKeyYBytes, 1, 16);
Array.Reverse(secondaryKeyYBytes); // convert big endian to little endian
var secondaryKeyY = new BigInteger(secondaryKeyYBytes);
Marshal.Copy(Derive3DSNormalKey(secondaryKeyX, secondaryKeyY), 0, out_secondary_key, 16);
return true;
}
return false;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return false;
}
}
private uint Hash3DS(string path)
{
// 3DS is too big to hash as a byte array...

View File

@ -362,17 +362,15 @@ namespace BizHawk.Client.EmuHawk
// these consoles will use the entire main memory domain
private static readonly ConsoleID[] UseFullMainMem =
{
ConsoleID.PlayStation, ConsoleID.Lynx, ConsoleID.Lynx, ConsoleID.NeoGeoPocket,
ConsoleID.Jaguar, ConsoleID.JaguarCD, ConsoleID.DS, ConsoleID.DSi,
ConsoleID.AppleII, ConsoleID.Vectrex, ConsoleID.Tic80, ConsoleID.PCEngine,
ConsoleID.Uzebox, ConsoleID.Nintendo3DS,
ConsoleID.PlayStation, ConsoleID.Lynx, ConsoleID.NeoGeoPocket, ConsoleID.Jaguar,
ConsoleID.JaguarCD, ConsoleID.DS, ConsoleID.DSi, ConsoleID.AppleII,
ConsoleID.Vectrex, ConsoleID.Tic80, ConsoleID.PCEngine, ConsoleID.Uzebox,
ConsoleID.Nintendo3DS,
};
// these consoles will use part of the system bus at an offset
private static readonly Dictionary<ConsoleID, (uint Start, uint Size)[]> UsePartialSysBus = new()
{
[ConsoleID.MasterSystem] = new[] { (0xC000u, 0x2000u) },
[ConsoleID.GameGear] = new[] { (0xC000u, 0x2000u) },
[ConsoleID.Colecovision] = new[] { (0x6000u, 0x400u) },
[ConsoleID.SG1000] = new[] { (0xC000u, 0x2000u), (0x2000u, 0x2000u), (0x8000u, 0x2000u) },
};
@ -426,6 +424,12 @@ namespace BizHawk.Client.EmuHawk
// our picodrive doesn't byteswap its SRAM, so...
TryAddDomain("SRAM", addressMangler: domains["SRAM"] is MemoryDomainIntPtrSwap16Monitor ? 1u : 0u);
break;
case ConsoleID.MasterSystem:
case ConsoleID.GameGear:
mfs.Add(new(domains["Main RAM"], 0, domains["Main RAM"].Size));
TryAddDomain("Cart (Volatile) RAM");
TryAddDomain("Save RAM");
break;
case ConsoleID.SNES:
mfs.Add(new(domains["WRAM"], 0, domains["WRAM"].Size));
TryAddDomain("CARTRAM");

View File

@ -41,6 +41,11 @@ namespace BizHawk.Client.EmuHawk
_getConfig = getConfig;
_raDropDownItems = raDropDownItems;
_shutdownRACallback = shutdownRACallback;
_getCiaNormalKeyFunc = GetCiaNormalKeyFunc;
_getNcchNormalKeysFunc = GetNcchNormalKeysFunc;
RCheevos._lib.rc_hash_init_3ds_get_cia_normal_key_func(_getCiaNormalKeyFunc);
RCheevos._lib.rc_hash_init_3ds_get_ncch_normal_keys_func(_getNcchNormalKeysFunc);
}
public static IRetroAchievements CreateImpl(

View File

@ -123,7 +123,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.N3DS
{
var message = new byte[1024];
var res = _core.Citra_InstallCIA(_context, romPath, message, message.Length);
var outMsg = Encoding.UTF8.GetString(message).TrimEnd();
var outMsg = Encoding.UTF8.GetString(message).TrimEnd('\0');
if (res)
{
romPath = outMsg;