From 6df12f368b6567e32da23e6616fd9fa2d0698e5f Mon Sep 17 00:00:00 2001 From: Patchzy <64382339+patchzyy@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:52:52 +0100 Subject: [PATCH] Add RRrating.pul VR/BR import and friend code utilities --- .../GameLicense/GameLicenseService.cs | 61 ++++++++++++++- .../WiiManagement/WiiManagementExtensions.cs | 2 + WheelWizard/Services/PathManager.cs | 1 + .../Generators/FriendCodeGenerator.cs | 71 +++++++++++++++++ .../Utilities/Generators/RRratingReader.cs | 76 +++++++++++++++++++ 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 WheelWizard/Utilities/Generators/RRratingReader.cs diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index fc657914..6e701353 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -78,15 +78,17 @@ public class GameLicenseSingletonService : RepeatedTaskManager, IGameLicenseSing private readonly IMiiDbService _miiService; private readonly IFileSystem _fileSystem; private readonly IWhWzDataSingletonService _whWzDataSingletonService; + private readonly IRRratingReader _rrratingReader; private LicenseCollection Licenses { get; } private byte[]? _rksysData; - public GameLicenseSingletonService(IMiiDbService miiService, IFileSystem fileSystem, IWhWzDataSingletonService whWzDataSingletonService) + public GameLicenseSingletonService(IMiiDbService miiService, IFileSystem fileSystem, IWhWzDataSingletonService whWzDataSingletonService, IRRratingReader rrratingReader) : base(40) { _miiService = miiService; _fileSystem = fileSystem; _whWzDataSingletonService = whWzDataSingletonService; + _rrratingReader = rrratingReader; Licenses = new(); } @@ -195,18 +197,54 @@ private OperationResult ParseLicenseUser(int rkpdOffset) if (_rksysData == null) return new ArgumentNullException(nameof(_rksysData)); + var profileId = BigEndianBinaryHelper.BufferToUint32(_rksysData, rkpdOffset + 0x5C); var friendCode = FriendCodeGenerator.GetFriendCode(_rksysData, rkpdOffset + 0x5C); var miiDataResult = ParseMiiData(rkpdOffset); var miiToUse = miiDataResult.IsFailure ? new() : miiDataResult.Value; var statistics = StatisticsSerializer.ParseStatistics(_rksysData, rkpdOffset); + // Try to read VR/BR from RRrating.pul file + var vrFromRksys = BigEndianBinaryHelper.BufferToUint16(_rksysData, rkpdOffset + 0xB0); + var brFromRksys = BigEndianBinaryHelper.BufferToUint16(_rksysData, rkpdOffset + 0xB2); + var vr = vrFromRksys; + var br = brFromRksys; + + if (profileId > 0 && !string.IsNullOrEmpty(friendCode)) + { + var rrRatingData = TryReadRRratingFile(); + if (rrRatingData != null) + { + // Try to find by profile_id first + var rating = _rrratingReader.ReadRatingFromFile(rrRatingData, profileId); + + // Verify friend code matches if rating found + if (rating.HasValue) + { + // Calculate friend code from profile_id and compare with rksys friend code + var calculatedFriendCode = FriendCodeGenerator.ProfileIdToFriendCode(profileId); + // Convert friend code string to ulong for comparison + var fcString = friendCode.Replace("-", ""); + if (ulong.TryParse(fcString, out var fcDec)) + { + // Compare friend codes - use rating if they match + if (fcDec == calculatedFriendCode) + { + // Convert float VR/BR to uint by multiplying by 100 (e.g., 258.62 -> 25862) + vr = (uint)Math.Round(rating.Value.vr * 100); + br = (uint)Math.Round(rating.Value.br * 100); + } + } + } + } + } + var user = new LicenseProfile { Mii = miiToUse, FriendCode = friendCode, - Vr = BigEndianBinaryHelper.BufferToUint16(_rksysData, rkpdOffset + 0xB0), - Br = BigEndianBinaryHelper.BufferToUint16(_rksysData, rkpdOffset + 0xB2), + Vr = vr, + Br = br, TotalRaceCount = BigEndianBinaryHelper.BufferToUint32(_rksysData, rkpdOffset + 0xB4), TotalWinCount = BigEndianBinaryHelper.BufferToUint32(_rksysData, rkpdOffset + 0xDC), BadgeVariants = _whWzDataSingletonService.GetBadges(friendCode), @@ -220,6 +258,23 @@ private OperationResult ParseLicenseUser(int rkpdOffset) return user; } + private byte[]? TryReadRRratingFile() + { + try + { + var rrRatingPath = PathManager.RRratingFilePath; + if (_fileSystem.File.Exists(rrRatingPath)) + { + return _fileSystem.File.ReadAllBytes(rrRatingPath); + } + } + catch + { + // Silently fail if file doesn't exist or can't be read + } + return null; + } + private OperationResult ParseMiiData(int rkpdOffset) { //https://wiki.tockdom.com/wiki/Rksys.dat#DWC_User_Data diff --git a/WheelWizard/Features/WiiManagement/WiiManagementExtensions.cs b/WheelWizard/Features/WiiManagement/WiiManagementExtensions.cs index 38355e86..32e01ae1 100644 --- a/WheelWizard/Features/WiiManagement/WiiManagementExtensions.cs +++ b/WheelWizard/Features/WiiManagement/WiiManagementExtensions.cs @@ -1,3 +1,4 @@ +using WheelWizard.Utilities.Generators; using WheelWizard.WiiManagement.GameLicense; using WheelWizard.WiiManagement.MiiManagement; @@ -7,6 +8,7 @@ public static class WiiManagementExtensions { public static IServiceCollection AddWiiManagement(this IServiceCollection services) { + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/WheelWizard/Services/PathManager.cs b/WheelWizard/Services/PathManager.cs index 1831cc36..169e46cc 100644 --- a/WheelWizard/Services/PathManager.cs +++ b/WheelWizard/Services/PathManager.cs @@ -75,6 +75,7 @@ public static bool IsUsingCustomWheelWizardAppdataPath public static string RetroRewindTempFile => Path.Combine(TempModsFolderPath, "RetroRewind.zip"); public static string WiiDbFolder => Path.Combine(WiiFolderPath, "shared2", "menu", "FaceLib"); public static string MiiDbFile => Path.Combine(WiiDbFolder, "RFL_DB.dat"); + public static string RRratingFilePath => Path.Combine(WiiFolderPath, "shared2", "Pulsar", "RetroRewind6", "RRrating.pul"); #region Wheel Wizard Appdata Override diff --git a/WheelWizard/Utilities/Generators/FriendCodeGenerator.cs b/WheelWizard/Utilities/Generators/FriendCodeGenerator.cs index b8090b64..16d843b9 100644 --- a/WheelWizard/Utilities/Generators/FriendCodeGenerator.cs +++ b/WheelWizard/Utilities/Generators/FriendCodeGenerator.cs @@ -6,6 +6,8 @@ namespace WheelWizard.Utilities.Generators; public class FriendCodeGenerator { + private const uint GameCodeInt = 0x524D434A; // "RMCJ" in big-endian + public static string GetFriendCode(byte[] data, int offset) { var pid = BigEndianBinaryHelper.BufferToUint32(data, offset); @@ -19,6 +21,70 @@ public static string GetFriendCode(byte[] data, int offset) return FormatFriendCode(fcDec); } + /// + /// Converts a profile ID to a friend code. + /// + public static ulong ProfileIdToFriendCode(uint profileId) + { + if (profileId == 0) + return 0; + + // Byte swap both values (same as Python _bswap32) + var swappedProfileId = BSwap32(profileId); + var swappedGameCode = BSwap32(GameCodeInt); + + // Pack as big-endian (">II" format): [swapped_profile_id_bytes] + [swapped_game_code_bytes] + var data = new byte[8]; + // Write swapped profile ID as big-endian + data[0] = (byte)(swappedProfileId >> 24); + data[1] = (byte)(swappedProfileId >> 16); + data[2] = (byte)(swappedProfileId >> 8); + data[3] = (byte)swappedProfileId; + // Write swapped game code as big-endian + data[4] = (byte)(swappedGameCode >> 24); + data[5] = (byte)(swappedGameCode >> 16); + data[6] = (byte)(swappedGameCode >> 8); + data[7] = (byte)swappedGameCode; + + // Calculate MD5 and get checksum (first byte >> 1) + using var md5 = MD5.Create(); + var hashBytes = md5.ComputeHash(data); + var checksum = (uint)(hashBytes[0] >> 1); + + // Return friend code: (checksum << 32) | profileId + return ((ulong)checksum << 32) | profileId; + } + + /// + /// Converts a friend code string (format: "XXXX-XXXX-XXXX") to a profile ID. + /// + public static uint FriendCodeToProfileId(string friendCode) + { + if (string.IsNullOrWhiteSpace(friendCode)) + return 0; + + // Remove dashes and parse as ulong + var fcString = friendCode.Replace("-", ""); + if (fcString.Length != 12 || !ulong.TryParse(fcString, out var fcDec)) + return 0; + + // Extract profile ID (lower 32 bits) + var profileId = (uint)(fcDec & 0xFFFFFFFF); + if (profileId == 0) + return 0; + + // Verify checksum + var expectedFc = ProfileIdToFriendCode(profileId); + var expectedChecksum = (uint)(expectedFc >> 32); + var actualChecksum = (uint)(fcDec >> 32); + + // Allow checksum to be 0 or match expected + if (actualChecksum != 0 && actualChecksum != expectedChecksum) + return 0; + + return profileId; + } + private static string GetMd5Hash(byte[] input) { using (var md5 = MD5.Create()) @@ -48,4 +114,9 @@ private static string FormatFriendCode(ulong fcDec) } private static string FcPartParse(int part) => part.ToString("D4"); + + private static uint BSwap32(uint value) + { + return ((value & 0xFF000000) >> 24) | ((value & 0x00FF0000) >> 8) | ((value & 0x0000FF00) << 8) | ((value & 0x000000FF) << 24); + } } diff --git a/WheelWizard/Utilities/Generators/RRratingReader.cs b/WheelWizard/Utilities/Generators/RRratingReader.cs new file mode 100644 index 00000000..3cff11d8 --- /dev/null +++ b/WheelWizard/Utilities/Generators/RRratingReader.cs @@ -0,0 +1,76 @@ +using WheelWizard.Helpers; + +namespace WheelWizard.Utilities.Generators; + +public interface IRRratingReader +{ + /// + /// Reads VR and BR values from RRrating.pul file for a given profile ID. + /// + (float vr, float br)? ReadRatingFromFile(byte[] fileData, uint profileId); + + /// + /// Reads VR and BR values from RRrating.pul file for a given friend code. + /// + (float vr, float br)? ReadRatingFromFileByFriendCode(byte[] fileData, string friendCode); +} + +public class RRratingReader : IRRratingReader +{ + private const uint Magic = 0x52525254; // 'RRRT' + private const ushort Version = 1; + private const int MaxProfiles = 100; + private const int HeaderSize = 8; // IHH = 4 + 2 + 2 + private const int EntrySize = 16; // iffI = 4 + 4 + 4 + 4 + private const uint FlagHasData = 0x1; + + /// + /// Reads VR and BR values from RRrating.pul file for a given profile ID. + /// + public (float vr, float br)? ReadRatingFromFile(byte[] fileData, uint profileId) + { + if (fileData == null || fileData.Length < HeaderSize + EntrySize * MaxProfiles) + return null; + + // Read header + var magic = BigEndianBinaryHelper.BufferToUint32(fileData, 0); + var version = BigEndianBinaryHelper.BufferToUint16(fileData, 4); + var count = BigEndianBinaryHelper.BufferToUint16(fileData, 6); + + if (magic != Magic || version != Version || count != MaxProfiles) + return null; + + // Search for matching profile ID + var offset = HeaderSize; + for (var i = 0; i < MaxProfiles; i++) + { + var entryProfileId = BigEndianBinaryHelper.BufferToUint32(fileData, offset); + var vr = BigEndianBinaryHelper.BufferToFloat(fileData, offset + 4); + var br = BigEndianBinaryHelper.BufferToFloat(fileData, offset + 8); + var flags = BigEndianBinaryHelper.BufferToUint32(fileData, offset + 12); + + var hasData = (flags & FlagHasData) != 0 && entryProfileId > 0; + + if (hasData && entryProfileId == profileId) + { + return (vr, br); + } + + offset += EntrySize; + } + + return null; + } + + /// + /// Reads VR and BR values from RRrating.pul file for a given friend code. + /// + public (float vr, float br)? ReadRatingFromFileByFriendCode(byte[] fileData, string friendCode) + { + var profileId = FriendCodeGenerator.FriendCodeToProfileId(friendCode); + if (profileId == 0) + return null; + + return ReadRatingFromFile(fileData, profileId); + } +}