Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ public sealed class RwfcLeaderboardEntry

public RwfcLeaderboardVrStats? VrStats { get; set; }

public string? MiiImageBase64 { get; set; }
public string? MiiData { get; set; }
}
54 changes: 52 additions & 2 deletions WheelWizard/Features/RrRooms/RrLeaderboardSingletonService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using WheelWizard.Shared.Services;

namespace WheelWizard.RrRooms;
Expand All @@ -7,11 +9,59 @@ public interface IRrLeaderboardSingletonService
Task<OperationResult<List<RwfcLeaderboardEntry>>> GetTopPlayersAsync(int limit = 50);
}

public class RrLeaderboardSingletonService(IApiCaller<IRwfcApi> apiCaller) : IRrLeaderboardSingletonService
public class RrLeaderboardSingletonService(
IApiCaller<IRwfcApi> apiCaller,
IMemoryCache cache,
ILogger<RrLeaderboardSingletonService> logger
) : IRrLeaderboardSingletonService
{
private const string FreshCacheKey = "rrrooms:leaderboard:fresh";
private const string StaleCacheKey = "rrrooms:leaderboard:stale";
private static readonly TimeSpan CacheLifetime = TimeSpan.FromSeconds(90);

public async Task<OperationResult<List<RwfcLeaderboardEntry>>> GetTopPlayersAsync(int limit = 50)
{
var boundedLimit = Math.Clamp(limit, 1, 200);
return await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit));

if (TryGetCached(FreshCacheKey, boundedLimit, out var freshCache))
return freshCache;

var fetchResult = await apiCaller.CallApiAsync(api => api.GetTopLeaderboardAsync(boundedLimit));
if (fetchResult.IsFailure)
{
if (!TryGetCached(StaleCacheKey, boundedLimit, out var staleCache))
return fetchResult;

logger.LogWarning("RWFC leaderboard fetch failed; returning stale cached leaderboard for top {Limit}.", boundedLimit);
return staleCache;
}

var fetchedEntries = fetchResult.Value.ToList();

cache.Set(FreshCacheKey, fetchedEntries, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = CacheLifetime });
cache.Set(StaleCacheKey, fetchedEntries);

return TrimToLimit(fetchedEntries, boundedLimit);
}

private bool TryGetCached(string cacheKey, int limit, out OperationResult<List<RwfcLeaderboardEntry>> result)
{
if (
!cache.TryGetValue(cacheKey, out List<RwfcLeaderboardEntry>? cachedEntries)
|| cachedEntries == null
|| cachedEntries.Count < limit
)
{
result = default!;
return false;
}

result = TrimToLimit(cachedEntries, limit);
return true;
}

private static OperationResult<List<RwfcLeaderboardEntry>> TrimToLimit(IEnumerable<RwfcLeaderboardEntry> entries, int limit)
{
return entries.Take(limit).ToList();
}
}
1 change: 1 addition & 0 deletions WheelWizard/Models/RRInfo/RrPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class RrPlayer : IEquatable<RrPlayer>

public bool IsOpenHost { get; set; }
public bool IsSuspended { get; set; }
public bool IsFriend { get; set; }

public int? LeaderboardRank { get; set; }
public bool IsTopLeaderboardPlayer => LeaderboardRank.HasValue;
Expand Down
9 changes: 9 additions & 0 deletions WheelWizard/Resources/Languages/Common.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions WheelWizard/Resources/Languages/Common.resx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
<data name="PageTitle_Friends" xml:space="preserve">
<value>Friends</value>
</data>
<data name="PageTitle_Leaderboard" xml:space="preserve">
<value>Leaderboard</value>
</data>
<data name="Attribute_Status_Online" xml:space="preserve">
<value>Online</value>
</data>
Expand Down
28 changes: 23 additions & 5 deletions WheelWizard/Services/LiveData/RRLiveRooms.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using WheelWizard.Models.RRInfo;
using WheelWizard.RrRooms;
using WheelWizard.Utilities.Generators;
using WheelWizard.Utilities.RepeatedTasks;
using WheelWizard.Views;
using WheelWizard.WheelWizardData;
using WheelWizard.WiiManagement;
using WheelWizard.WiiManagement.GameLicense;
using WheelWizard.WiiManagement.MiiManagement;
using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;

Expand All @@ -14,6 +16,7 @@ public class RRLiveRooms : RepeatedTaskManager
private readonly IWhWzDataSingletonService _whWzService;
private readonly IRrRoomsSingletonService _roomsService;
private readonly IRrLeaderboardSingletonService _leaderboardService;
private readonly IGameLicenseSingletonService _gameLicenseService;

public List<RrRoom> CurrentRooms { get; private set; } = [];
public int PlayerCount => CurrentRooms.Sum(room => room.PlayerCount);
Expand All @@ -24,13 +27,15 @@ public class RRLiveRooms : RepeatedTaskManager
public RRLiveRooms(
IWhWzDataSingletonService whWzService,
IRrRoomsSingletonService roomsService,
IRrLeaderboardSingletonService leaderboardService
IRrLeaderboardSingletonService leaderboardService,
IGameLicenseSingletonService gameLicenseService
)
: base(40)
{
_whWzService = whWzService;
_roomsService = roomsService;
_leaderboardService = leaderboardService;
_gameLicenseService = gameLicenseService;
}

protected override async Task ExecuteTaskAsync()
Expand Down Expand Up @@ -67,7 +72,14 @@ protected override async Task ExecuteTaskAsync()
var raw = roomsResult.Value;
var splitRaw = SplitMergedRooms(raw);

var rrRooms = splitRaw.Select(room => MapRoom(room, _whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList();
var friendProfileIds = _gameLicenseService
.ActiveCurrentFriends.Select(friend => FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode))
.Where(profileId => profileId != 0)
.ToHashSet();

var rrRooms = splitRaw
.Select(room => MapRoom(room, _whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds))
.ToList();

CurrentRooms = rrRooms;
}
Expand All @@ -76,7 +88,8 @@ private static RrRoom MapRoom(
RwfcRoomStatusRoom room,
IWhWzDataSingletonService whWzService,
IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByPid,
IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode
IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode,
IReadOnlySet<uint> friendProfileIds
)
{
return new()
Expand All @@ -86,15 +99,18 @@ IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode
Type = room.Type,
Suspend = room.Suspend,
Rk = room.Rk,
Players = room.Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode)).ToList(),
Players = room
.Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds))
.ToList(),
};
}

private static RrPlayer MapPlayer(
RwfcRoomStatusPlayer p,
IWhWzDataSingletonService whWzService,
IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByPid,
IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode
IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode,
IReadOnlySet<uint> friendProfileIds
)
{
Mii? mii = null;
Expand All @@ -114,6 +130,7 @@ IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode
}

var friendCode = p.FriendCode ?? string.Empty;
var profileId = FriendCodeGenerator.FriendCodeToProfileId(friendCode);

var leaderboardEntry = GetLeaderboardEntry(p, friendCode, leaderboardByPid, leaderboardByFriendCode);

Expand All @@ -130,6 +147,7 @@ IReadOnlyDictionary<string, RwfcLeaderboardEntry> leaderboardByFriendCode
Mii = mii,
BadgeVariants = whWzService.GetBadges(friendCode),
LeaderboardRank = leaderboardEntry?.Rank ?? leaderboardEntry?.ActiveRank,
IsFriend = profileId != 0 && friendProfileIds.Contains(profileId),
};
}

Expand Down
3 changes: 2 additions & 1 deletion WheelWizard/Views/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@
<StyleInclude Source="Components/WhWzLibrary/Badge.axaml" />
<StyleInclude Source="Components/WhWzLibrary/MiiBlock.axaml" />
<StyleInclude Source="Components/WhWzLibrary/WheelTrail.axaml" />
<StyleInclude Source="Components/WhWzLibrary/LeaderboardPodiumCard.axaml" />
</Application.Styles>
</Application>
</Application>
Loading
Loading