Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -195,18 +197,54 @@ private OperationResult<LicenseProfile> 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),
Expand All @@ -220,6 +258,23 @@ private OperationResult<LicenseProfile> 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<Mii> ParseMiiData(int rkpdOffset)
{
//https://wiki.tockdom.com/wiki/Rksys.dat#DWC_User_Data
Expand Down
2 changes: 2 additions & 0 deletions WheelWizard/Features/WiiManagement/WiiManagementExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using WheelWizard.Utilities.Generators;
using WheelWizard.WiiManagement.GameLicense;
using WheelWizard.WiiManagement.MiiManagement;

Expand All @@ -7,6 +8,7 @@ public static class WiiManagementExtensions
{
public static IServiceCollection AddWiiManagement(this IServiceCollection services)
{
services.AddSingleton<IRRratingReader, RRratingReader>();
services.AddSingleton<IMiiDbService, MiiDbService>();
services.AddSingleton<IMiiRepositoryService, MiiRepositoryServiceService>();
services.AddSingleton<IGameLicenseSingletonService, GameLicenseSingletonService>();
Expand Down
1 change: 1 addition & 0 deletions WheelWizard/Services/PathManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
71 changes: 71 additions & 0 deletions WheelWizard/Utilities/Generators/FriendCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -19,6 +21,70 @@ public static string GetFriendCode(byte[] data, int offset)
return FormatFriendCode(fcDec);
}

/// <summary>
/// Converts a profile ID to a friend code.
/// </summary>
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;
}

/// <summary>
/// Converts a friend code string (format: "XXXX-XXXX-XXXX") to a profile ID.
/// </summary>
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())
Expand Down Expand Up @@ -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);
}
}
76 changes: 76 additions & 0 deletions WheelWizard/Utilities/Generators/RRratingReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using WheelWizard.Helpers;

namespace WheelWizard.Utilities.Generators;

public interface IRRratingReader
{
/// <summary>
/// Reads VR and BR values from RRrating.pul file for a given profile ID.
/// </summary>
(float vr, float br)? ReadRatingFromFile(byte[] fileData, uint profileId);

/// <summary>
/// Reads VR and BR values from RRrating.pul file for a given friend code.
/// </summary>
(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;

/// <summary>
/// Reads VR and BR values from RRrating.pul file for a given profile ID.
/// </summary>
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;
}

/// <summary>
/// Reads VR and BR values from RRrating.pul file for a given friend code.
/// </summary>
public (float vr, float br)? ReadRatingFromFileByFriendCode(byte[] fileData, string friendCode)
{
var profileId = FriendCodeGenerator.FriendCodeToProfileId(friendCode);
if (profileId == 0)
return null;

return ReadRatingFromFile(fileData, profileId);
}
}
Loading