diff --git a/WheelWizard.Test/Features/ApplicationLifecycle/ApplicationStartupTests.cs b/WheelWizard.Test/Features/ApplicationLifecycle/ApplicationStartupTests.cs index 2edc8aaf5..deca09302 100644 --- a/WheelWizard.Test/Features/ApplicationLifecycle/ApplicationStartupTests.cs +++ b/WheelWizard.Test/Features/ApplicationLifecycle/ApplicationStartupTests.cs @@ -4,8 +4,8 @@ using WheelWizard.GameBanana.InstallRequests; using WheelWizard.Launching; using WheelWizard.Mods; +using WheelWizard.RrRooms; using WheelWizard.Settings; -using WheelWizard.WheelWizardData; namespace WheelWizard.Test.Features.ApplicationLifecycle; @@ -143,7 +143,7 @@ private sealed class Fixture public IModManager Mods { get; } = Substitute.For(); public IModInstallRequestHandler Requests { get; } = Substitute.For(); public IAutoUpdaterSingletonService Updates { get; } = Substitute.For(); - public IWhWzDataSingletonService Badges { get; } = Substitute.For(); + public IRrBadgeSingletonService Badges { get; } = Substitute.For(); public IApplicationLiveUpdates Live { get; } = Substitute.For(); public ISettingsManager Settings { get; } = Substitute.For(); public IRetroRewindLaunchService Launch { get; } = Substitute.For(); diff --git a/WheelWizard.Test/Features/RrRooms/RoomPresenceTests.cs b/WheelWizard.Test/Features/RrRooms/RoomPresenceTests.cs index 67b607318..862a195c9 100644 --- a/WheelWizard.Test/Features/RrRooms/RoomPresenceTests.cs +++ b/WheelWizard.Test/Features/RrRooms/RoomPresenceTests.cs @@ -5,7 +5,6 @@ using WheelWizard.RrRooms; using WheelWizard.Settings; using WheelWizard.Shared.Polling; -using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement.GameLicense; using WheelWizard.WiiManagement.GameLicense.Domain; using WheelWizard.WiiManagement.MiiManagement; @@ -34,7 +33,7 @@ public void Refresh_UpdatesBothLicenseAndFriends_AndOnlyNotifiesChanges() var service = new GameLicenseSingletonService( Substitute.For(), Substitute.For(), - Substitute.For(), + Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For(), @@ -104,7 +103,7 @@ public async Task FailedRoomRefresh_ClearsPresenceBeforeRefreshingProfiles() leaderboard.GetTopPlayersAsync(50).Returns(Ok(new List())); var licenses = Substitute.For(); licenses.ActiveCurrentFriends.Returns([]); - var service = new TestRooms(Substitute.For(), rooms, leaderboard, licenses, presence); + var service = new TestRooms(Substitute.For(), rooms, leaderboard, licenses, presence); rooms .GetRoomsAsync() .Returns( @@ -136,7 +135,7 @@ public async Task FailedRoomRefresh_ClearsPresenceBeforeRefreshingProfiles() } private sealed class TestRooms( - IWhWzDataSingletonService data, + IRrBadgeSingletonService data, IRrRoomsSingletonService rooms, IRrLeaderboardSingletonService leaderboard, IGameLicenseSingletonService licenses, diff --git a/WheelWizard.Test/Features/RrRooms/RrBadgeTests.cs b/WheelWizard.Test/Features/RrRooms/RrBadgeTests.cs new file mode 100644 index 000000000..d827c4669 --- /dev/null +++ b/WheelWizard.Test/Features/RrRooms/RrBadgeTests.cs @@ -0,0 +1,112 @@ +using System.Linq.Expressions; +using WheelWizard.RrRooms; +using WheelWizard.Shared; +using WheelWizard.Shared.Services; +using WheelWizard.WheelWizardData.Domain; + +namespace WheelWizard.Test.Features.RrRooms; + +public class RrBadgeTests +{ + // Real pid / friend code pairs from the RWFC leaderboard. + private const string Pid1 = "198760529"; + private const string FriendCode1 = "3137-3137-3137"; + private const string Pid2 = "4544000"; + private const string FriendCode2 = "0000-0454-4000"; + + private readonly IApiCaller _apiCaller; + private readonly RrBadgeSingletonService _service; + + public RrBadgeTests() + { + _apiCaller = Substitute.For>(); + _service = new(_apiCaller); + } + + private void SetupResponse(Dictionary badges) => + _apiCaller + .CallApiAsync(Arg.Any>>>()) + .Returns(Ok(new RwfcBadgesResponse { Badges = badges })); + + [Fact] + public async Task LoadBadgesAsync_MapsProfileIdsToFriendCodes() + { + SetupResponse(new() { { Pid1, [1, 1001] }, { Pid2, [2003] } }); + + var result = await _service.LoadBadgesAsync(); + + Assert.True(result.IsSuccess); + Assert.Equal([BadgeVariant.WheelWizardDeveloper, BadgeVariant.Translator], _service.GetBadges(FriendCode1)); + Assert.Equal([BadgeVariant.LeafStruckGold], _service.GetBadges(FriendCode2)); + } + + [Fact] + public async Task LoadBadgesAsync_ReturnsFailure_WhenApiCallFails() + { + var expectedError = new OperationError { Message = "API call failed" }; + _apiCaller.CallApiAsync(Arg.Any>>>()).Returns(expectedError); + + var result = await _service.LoadBadgesAsync(); + + Assert.True(result.IsFailure); + Assert.Equal(expectedError.Message, result.Error.Message); + Assert.Empty(_service.GetBadges(FriendCode1)); + } + + [Fact] + public async Task LoadBadgesAsync_SortsAndDeduplicatesBadges() + { + SetupResponse(new() { { Pid1, [2015, 1002, 100, 1002] } }); + + await _service.LoadBadgesAsync(); + + Assert.Equal([BadgeVariant.RWFCModerator, BadgeVariant.Supporter, BadgeVariant.EarthboundGold], _service.GetBadges(FriendCode1)); + } + + [Fact] + public async Task LoadBadgesAsync_SkipsUnknownBadgesAndInvalidEntries() + { + SetupResponse( + new() + { + { Pid1, [-1, 0, 999, 9999] }, + { Pid2, null }, + { "not-a-pid", [0] }, + { "0", [0] }, + } + ); + + var result = await _service.LoadBadgesAsync(); + + Assert.True(result.IsSuccess); + Assert.Equal([BadgeVariant.RetroRewindDeveloper], _service.GetBadges(FriendCode1)); + Assert.Empty(_service.GetBadges(FriendCode2)); + } + + [Theory] + [InlineData("")] + [InlineData("not a friend code")] + [InlineData("3137-3137-3138")] // checksum does not match the pid + [InlineData("0000-0000-0000")] + public async Task GetBadges_ReturnsEmpty_ForUnknownOrInvalidFriendCodes(string friendCode) + { + SetupResponse(new() { { Pid1, [1] } }); + await _service.LoadBadgesAsync(); + + Assert.Empty(_service.GetBadges(friendCode)); + } + + [Fact] + public async Task LoadBadgesAsync_ReplacesPreviousBadges() + { + SetupResponse(new() { { Pid1, [1] } }); + await _service.LoadBadgesAsync(); + Assert.Equal([BadgeVariant.WheelWizardDeveloper], _service.GetBadges(FriendCode1)); + + SetupResponse(new() { { Pid2, [1001] } }); + await _service.LoadBadgesAsync(); + + Assert.Empty(_service.GetBadges(FriendCode1)); + Assert.Equal([BadgeVariant.Translator], _service.GetBadges(FriendCode2)); + } +} diff --git a/WheelWizard.Test/Features/WhWzDataTests.cs b/WheelWizard.Test/Features/WhWzDataTests.cs index f1f9366be..d10ce7661 100644 --- a/WheelWizard.Test/Features/WhWzDataTests.cs +++ b/WheelWizard.Test/Features/WhWzDataTests.cs @@ -50,133 +50,4 @@ public async Task GetStatusAsync_ReturnsFailure_WhenApiCallFails() Assert.True(result.IsFailure); Assert.Equal(expectedError.Message, result.Error.Message); } - - [Fact] - public async Task LoadBadgesAsync_ReturnsSuccess_WhenApiCallSucceeds() - { - // Arrange - var badgeData = new Dictionary - { - { "FC1", [BadgeVariant.WhWzDev, BadgeVariant.Translator] }, - { "FC2", [BadgeVariant.RrDev] }, - { "FC3", [BadgeVariant.None, BadgeVariant.Firestarter_GoldWinner] }, - }; - - _apiCaller.CallApiAsync(Arg.Any>>>>()).Returns(Ok(badgeData)); - - // Act - var result = await _service.LoadBadgesAsync(); - - // Assert - Assert.True(result.IsSuccess); - } - - [Fact] - public async Task LoadBadgesAsync_ReturnsFailure_WhenApiCallFails() - { - // Arrange - var expectedError = new OperationError { Message = "API call failed" }; - - _apiCaller.CallApiAsync(Arg.Any>>>>()).Returns(expectedError); - - // Act - var result = await _service.LoadBadgesAsync(); - - // Assert - Assert.True(result.IsFailure); - Assert.Equal(expectedError.Message, result.Error.Message); - } - - [Fact] - public async Task GetBadges_ReturnsEmptyArray_WhenFriendCodeNotFound() - { - // Arrange - var badgeData = new Dictionary { { "FC1", [BadgeVariant.WhWzDev, BadgeVariant.Translator] } }; - - _apiCaller.CallApiAsync(Arg.Any>>>>()).Returns(Ok(badgeData)); - - await _service.LoadBadgesAsync(); - - // Act - var result = _service.GetBadges("NonExistentFC"); - - // Assert - Assert.Empty(result); - } - - [Fact] - public async Task GetBadges_ReturnsBadges_WhenFriendCodeExists() - { - // Arrange - var badgeData = new Dictionary { { "FC1", [BadgeVariant.WhWzDev, BadgeVariant.Translator] } }; - - _apiCaller.CallApiAsync(Arg.Any>>>>()).Returns(Ok(badgeData)); - - await _service.LoadBadgesAsync(); - - // Act - var result = _service.GetBadges("FC1"); - - // Assert - Assert.Equal(2, result.Length); - Assert.Contains(BadgeVariant.WhWzDev, result); - Assert.Contains(BadgeVariant.Translator, result); - } - - [Fact] - public async Task GetBadges_FiltersOutNoneBadges_WhenLoadingBadges() - { - // Arrange - var badgeData = new Dictionary { { "FC1", [BadgeVariant.None, BadgeVariant.WhWzDev, BadgeVariant.None] } }; - - _apiCaller.CallApiAsync(Arg.Any>>>>()).Returns(Ok(badgeData)); - - await _service.LoadBadgesAsync(); - - // Act - var result = _service.GetBadges("FC1"); - - // Assert - Assert.Single(result); - Assert.Contains(BadgeVariant.WhWzDev, result); - Assert.DoesNotContain(BadgeVariant.None, result); - } - - [Fact] - public async Task LoadBadgesAsync_OverwritesExistingBadges_WhenCalledMultipleTimes() - { - // Arrange - First load - var initialBadgeData = new Dictionary { { "FC1", [BadgeVariant.WhWzDev] } }; - - _apiCaller - .CallApiAsync(Arg.Any>>>>()) - .Returns(Ok(initialBadgeData)); - - await _service.LoadBadgesAsync(); - - // Verify initial state - var initialBadges = _service.GetBadges("FC1"); - Assert.Single(initialBadges); - Assert.Contains(BadgeVariant.WhWzDev, initialBadges); - - // Arrange - Second load with different data - var updatedBadgeData = new Dictionary - { - { "FC1", [BadgeVariant.Translator, BadgeVariant.Firestarter_GoldWinner] }, - }; - - _apiCaller - .CallApiAsync(Arg.Any>>>>()) - .Returns(Ok(updatedBadgeData)); - - // Act - await _service.LoadBadgesAsync(); - - // Assert - var updatedBadges = _service.GetBadges("FC1"); - Assert.Equal(2, updatedBadges.Length); - Assert.Contains(BadgeVariant.Translator, updatedBadges); - Assert.Contains(BadgeVariant.Firestarter_GoldWinner, updatedBadges); - Assert.DoesNotContain(BadgeVariant.WhWzDev, updatedBadges); - } } diff --git a/WheelWizard.Test/Features/WiiManagement/RegionLicenseTests.cs b/WheelWizard.Test/Features/WiiManagement/RegionLicenseTests.cs index cf68d7da8..ca16b7f42 100644 --- a/WheelWizard.Test/Features/WiiManagement/RegionLicenseTests.cs +++ b/WheelWizard.Test/Features/WiiManagement/RegionLicenseTests.cs @@ -8,7 +8,6 @@ using WheelWizard.Settings; using WheelWizard.Shared.Binary; using WheelWizard.Shared.Polling; -using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement.GameLicense; using WheelWizard.WiiManagement.MiiManagement; @@ -78,7 +77,7 @@ private static (GameLicenseSingletonService, MockFileSystem, ISettingsManager) C var service = new GameLicenseSingletonService( miis, fs, - Substitute.For(), + Substitute.For(), Substitute.For(), settings, Substitute.For(), diff --git a/WheelWizard.Test/Shared/Polling/LivePollingCancellationTests.cs b/WheelWizard.Test/Shared/Polling/LivePollingCancellationTests.cs index 5ae6ee2a1..ee0b162f4 100644 --- a/WheelWizard.Test/Shared/Polling/LivePollingCancellationTests.cs +++ b/WheelWizard.Test/Shared/Polling/LivePollingCancellationTests.cs @@ -27,7 +27,7 @@ public void StoppedRooms_DoNotPublishCompletedOrPendingRequest(bool completeBefo var completion = new TaskCompletionSource>>(); rooms.GetRoomsAsync().Returns(completion.Task); using var service = new LiveRoomsService( - Substitute.For(), + Substitute.For(), rooms, rankings, licenses, diff --git a/WheelWizard/Features/ApplicationLifecycle/ApplicationStartup.cs b/WheelWizard/Features/ApplicationLifecycle/ApplicationStartup.cs index 8fb555a79..d7e576c0f 100644 --- a/WheelWizard/Features/ApplicationLifecycle/ApplicationStartup.cs +++ b/WheelWizard/Features/ApplicationLifecycle/ApplicationStartup.cs @@ -3,8 +3,8 @@ using WheelWizard.GameBanana.InstallRequests; using WheelWizard.Launching; using WheelWizard.Mods; +using WheelWizard.RrRooms; using WheelWizard.Settings; -using WheelWizard.WheelWizardData; namespace WheelWizard.ApplicationLifecycle; @@ -17,7 +17,7 @@ public sealed class ApplicationStartup( IModManager mods, IModInstallRequestHandler installRequests, IAutoUpdaterSingletonService updates, - IWhWzDataSingletonService badges, + IRrBadgeSingletonService badges, IApplicationLiveUpdates liveUpdates, ISettingsManager settings, IRetroRewindLaunchService launch, diff --git a/WheelWizard/Features/RrRooms/Domain/IRwfcApi.cs b/WheelWizard/Features/RrRooms/Domain/IRwfcApi.cs index 13c20b5c0..97d331f55 100644 --- a/WheelWizard/Features/RrRooms/Domain/IRwfcApi.cs +++ b/WheelWizard/Features/RrRooms/Domain/IRwfcApi.cs @@ -16,4 +16,7 @@ public interface IRwfcApi [Get("/api/leaderboard/player/{friendCode}/history")] Task GetPlayerVrHistoryAsync(string friendCode, [AliasAs("days")] int days); + + [Get("/api/badges/all")] + Task GetAllBadgesAsync(); } diff --git a/WheelWizard/Features/RrRooms/Domain/RwfcBadgesResponse.cs b/WheelWizard/Features/RrRooms/Domain/RwfcBadgesResponse.cs new file mode 100644 index 000000000..c09426f5f --- /dev/null +++ b/WheelWizard/Features/RrRooms/Domain/RwfcBadgesResponse.cs @@ -0,0 +1,7 @@ +namespace WheelWizard.RrRooms; + +public sealed class RwfcBadgesResponse +{ + // Keyed by profile id (pid); values are RWFC badge ids. + public Dictionary Badges { get; set; } = []; +} diff --git a/WheelWizard/Features/RrRooms/LiveRoomsService.cs b/WheelWizard/Features/RrRooms/LiveRoomsService.cs index 19da72ae9..c4629a72c 100644 --- a/WheelWizard/Features/RrRooms/LiveRoomsService.cs +++ b/WheelWizard/Features/RrRooms/LiveRoomsService.cs @@ -2,7 +2,6 @@ using WheelWizard.Models.RRInfo; using WheelWizard.RrRooms; using WheelWizard.Shared.Polling; -using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement; using WheelWizard.WiiManagement.FriendCodes; using WheelWizard.WiiManagement.GameLicense; @@ -13,7 +12,7 @@ namespace WheelWizard.RrRooms; public class LiveRoomsService : ObservablePollingService { - private readonly IWhWzDataSingletonService _whWzService; + private readonly IRrBadgeSingletonService _badgeService; private readonly IRrRoomsSingletonService _roomsService; private readonly IRrLeaderboardSingletonService _leaderboardService; private readonly IGameLicenseSingletonService _gameLicenseService; @@ -25,7 +24,7 @@ public class LiveRoomsService : ObservablePollingService private readonly IRoomPresence _presence; public LiveRoomsService( - IWhWzDataSingletonService whWzService, + IRrBadgeSingletonService badgeService, IRrRoomsSingletonService roomsService, IRrLeaderboardSingletonService leaderboardService, IGameLicenseSingletonService gameLicenseService, @@ -36,7 +35,7 @@ ILogger logger ) : base(40, scheduler, timeProvider, logger) { - _whWzService = whWzService; + _badgeService = badgeService; _roomsService = roomsService; _leaderboardService = leaderboardService; _gameLicenseService = gameLicenseService; @@ -85,7 +84,7 @@ protected override async Task ExecuteTaskAsync(CancellationToken cancellationTok .ToHashSet(); var rrRooms = splitRaw - .Select(room => MapRoom(room, _whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds)) + .Select(room => MapRoom(room, _badgeService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds)) .ToList(); PublishRooms(rrRooms); @@ -100,7 +99,7 @@ private void PublishRooms(List rooms) private static RrRoom MapRoom( RwfcRoomStatusRoom room, - IWhWzDataSingletonService whWzService, + IRrBadgeSingletonService badgeService, IReadOnlyDictionary leaderboardByPid, IReadOnlyDictionary leaderboardByFriendCode, IReadOnlySet friendProfileIds @@ -114,14 +113,14 @@ IReadOnlySet friendProfileIds Suspend = room.Suspend, Rk = room.Rk, Players = room - .Players.Select(p => MapPlayer(p, whWzService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds)) + .Players.Select(p => MapPlayer(p, badgeService, leaderboardByPid, leaderboardByFriendCode, friendProfileIds)) .ToList(), }; } private static RrPlayer MapPlayer( RwfcRoomStatusPlayer p, - IWhWzDataSingletonService whWzService, + IRrBadgeSingletonService badgeService, IReadOnlyDictionary leaderboardByPid, IReadOnlyDictionary leaderboardByFriendCode, IReadOnlySet friendProfileIds @@ -159,7 +158,7 @@ IReadOnlySet friendProfileIds IsSuspended = p.IsSuspended, ConnectionMap = p.ConnectionMap ?? [], Mii = mii, - BadgeVariants = whWzService.GetBadges(friendCode), + BadgeVariants = badgeService.GetBadges(friendCode), LeaderboardRank = leaderboardEntry?.Rank ?? leaderboardEntry?.ActiveRank, IsFriend = profileId != 0 && friendProfileIds.Contains(profileId), }; diff --git a/WheelWizard/Features/RrRooms/RrBadgeSingletonService.cs b/WheelWizard/Features/RrRooms/RrBadgeSingletonService.cs new file mode 100644 index 000000000..7aaf67d82 --- /dev/null +++ b/WheelWizard/Features/RrRooms/RrBadgeSingletonService.cs @@ -0,0 +1,49 @@ +using WheelWizard.Shared.Services; +using WheelWizard.WheelWizardData.Domain; +using WheelWizard.WiiManagement.FriendCodes; + +namespace WheelWizard.RrRooms; + +public interface IRrBadgeSingletonService +{ + Task LoadBadgesAsync(); + BadgeVariant[] GetBadges(string friendCode); +} + +public class RrBadgeSingletonService(IApiCaller apiCaller) : IRrBadgeSingletonService +{ + private Dictionary BadgeData { get; set; } = new(); + + public async Task LoadBadgesAsync() + { + var badgeResult = await apiCaller.CallApiAsync(rwfcApi => rwfcApi.GetAllBadgesAsync()); + if (badgeResult.IsFailure) + return badgeResult; + + var badgeData = new Dictionary(); + foreach (var (pid, badgeIds) in badgeResult.Value.Badges ?? []) + { + if (!uint.TryParse(pid, out var profileId) || profileId == 0) + continue; + + // Badges the app does not know yet are skipped instead of rendering as blank. + var badges = (badgeIds ?? []) + .Where(id => id >= 0 && Enum.IsDefined(typeof(BadgeVariant), id)) + .Select(id => (BadgeVariant)id) + .Distinct() + .Order() + .ToArray(); + if (badges.Length != 0) + badgeData[profileId] = badges; + } + + BadgeData = badgeData; + return Ok(); + } + + public BadgeVariant[] GetBadges(string friendCode) + { + var profileId = FriendCode.FriendCodeToProfileId(friendCode); + return profileId != 0 && BadgeData.TryGetValue(profileId, out var badges) ? badges : []; + } +} diff --git a/WheelWizard/Features/RrRooms/RrRoomsExtensions.cs b/WheelWizard/Features/RrRooms/RrRoomsExtensions.cs index ca08c175e..34044df4f 100644 --- a/WheelWizard/Features/RrRooms/RrRoomsExtensions.cs +++ b/WheelWizard/Features/RrRooms/RrRoomsExtensions.cs @@ -13,6 +13,7 @@ public static IServiceCollection AddRrRooms(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/WheelWizard/Features/Settings/Views/AppInfo.axaml b/WheelWizard/Features/Settings/Views/AppInfo.axaml index c42c575e5..cd67b91fd 100644 --- a/WheelWizard/Features/Settings/Views/AppInfo.axaml +++ b/WheelWizard/Features/Settings/Views/AppInfo.axaml @@ -17,7 +17,7 @@ - @@ -33,7 +33,7 @@ - diff --git a/WheelWizard/Features/WheelWizardData/Domain/BadgeVariant.cs b/WheelWizard/Features/WheelWizardData/Domain/BadgeVariant.cs index 80877a9df..e046ccf55 100644 --- a/WheelWizard/Features/WheelWizardData/Domain/BadgeVariant.cs +++ b/WheelWizard/Features/WheelWizardData/Domain/BadgeVariant.cs @@ -1,20 +1,46 @@ namespace WheelWizard.WheelWizardData.Domain; +/// +/// Badge ids as returned by the RWFC API. The numeric values mirror wfc-bot's BadgeType +/// (src/commands/shared/badges.ts) and rwfc-web's BadgeId, so they must never be reordered. +/// public enum BadgeVariant { - None, // None is the default and should be the first enum value - WhWzDev, - RrDev, - Translator, - TranslatorLead, - Heart, - Firestarter_GoldWinner, - Firestarter_SilverWinner, - Firestarter_BronzeWinner, - SummitShowdown_GoldWinner, - SummitShowdown_SilverWinner, - SummitShowdown_BronzeWinner, - Leafstruck_GoldWinner, - Leafstruck_SilverWinner, - Leafstruck_BronzeWinner, + // None is not a real badge id, so it sits outside the RWFC id range. + None = -1, + + RetroRewindDeveloper = 0, + WheelWizardDeveloper = 1, + MajorContributor = 2, + + RWFCModerator = 100, + DiscordStaff = 101, + + Contributor = 1000, + Translator = 1001, + Supporter = 1002, + BetaTester = 1003, + Heart = 1004, + + FireStarterGold = 2000, + FireStarterSilver = 2001, + FireStarterBronze = 2002, + LeafStruckGold = 2003, + LeafStruckSilver = 2004, + LeafStruckBronze = 2005, + SummitShowdownGold = 2006, + SummitShowdownSilver = 2007, + SummitShowdownBronze = 2008, + HorizonGold = 2009, + HorizonSilver = 2010, + HorizonBronze = 2011, + SunblossomGold = 2012, + SunblossomSilver = 2013, + SunblossomBronze = 2014, + EarthboundGold = 2015, + EarthboundSilver = 2016, + EarthboundBronze = 2017, + BotBGold = 2018, + BotBSilver = 2019, + BotBBronze = 2020, } diff --git a/WheelWizard/Features/WheelWizardData/Domain/IWhWzDataApi.cs b/WheelWizard/Features/WheelWizardData/Domain/IWhWzDataApi.cs index 9dfa6dd5a..341706992 100644 --- a/WheelWizard/Features/WheelWizardData/Domain/IWhWzDataApi.cs +++ b/WheelWizard/Features/WheelWizardData/Domain/IWhWzDataApi.cs @@ -4,9 +4,6 @@ namespace WheelWizard.WheelWizardData.Domain; public interface IWhWzDataApi { - [Get("/badges.json")] - Task> GetBadgesAsync(); - [Get("/status.json")] Task GetStatusAsync(); } diff --git a/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml b/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml index bc20f373e..74e650b7a 100644 --- a/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml +++ b/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml @@ -4,49 +4,54 @@ xmlns:communityControls="clr-namespace:WheelWizard.WheelWizardData.Views"> - + - + - + - + - + - + - + - + - + + + + - - + - + - + + + + - + + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml.cs b/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml.cs index d947cbd57..7b329eaf9 100644 --- a/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml.cs +++ b/WheelWizard/Features/WheelWizardData/Views/CommunityBadge.axaml.cs @@ -1,4 +1,5 @@ using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using WheelWizard.WheelWizardData.Domain; @@ -9,21 +10,38 @@ public class CommunityBadge : TemplatedControl private static readonly Dictionary BadgeToolTip = new() { { BadgeVariant.None, "This is not a badge" }, - { BadgeVariant.WhWzDev, "Wheel Wizard Developer (hiii!)" }, - { BadgeVariant.RrDev, "Retro Rewind Developer" }, + { BadgeVariant.RetroRewindDeveloper, "Retro Rewind Developer" }, + { BadgeVariant.WheelWizardDeveloper, "Wheel Wizard Developer (hiii!)" }, + { BadgeVariant.MajorContributor, "Major Contributor" }, + { BadgeVariant.RWFCModerator, "Retro WFC Moderator" }, + { BadgeVariant.DiscordStaff, "Discord Staff" }, + { BadgeVariant.Contributor, "Contributor" }, { BadgeVariant.Translator, "Translator" }, - { BadgeVariant.TranslatorLead, "Translator Lead" }, + { BadgeVariant.Supporter, "Supporter" }, + { BadgeVariant.BetaTester, "Beta Tester" }, { BadgeVariant.Heart, "Heart of the Community" }, // winner badges - { BadgeVariant.Firestarter_GoldWinner, "Firestarter Tournament Winner" }, - { BadgeVariant.Firestarter_SilverWinner, "Firestarter Tournament Runner-Up" }, - { BadgeVariant.Firestarter_BronzeWinner, "Firestarter Tournament Runner-Up" }, - { BadgeVariant.SummitShowdown_GoldWinner, "Summit Showdown Tournament Winner" }, - { BadgeVariant.SummitShowdown_SilverWinner, "Summit Showdown Tournament Runner-Up" }, - { BadgeVariant.SummitShowdown_BronzeWinner, "Summit Showdown Tournament Runner-Up" }, - { BadgeVariant.Leafstruck_GoldWinner, "Leafstruck Tournament Winner" }, - { BadgeVariant.Leafstruck_SilverWinner, "Leafstruck Tournament Runner-Up" }, - { BadgeVariant.Leafstruck_BronzeWinner, "Leafstruck Tournament Runner-Up" }, + { BadgeVariant.FireStarterGold, "Firestarter Tournament Winner" }, + { BadgeVariant.FireStarterSilver, "Firestarter Tournament 2nd Place" }, + { BadgeVariant.FireStarterBronze, "Firestarter Tournament 3rd Place" }, + { BadgeVariant.LeafStruckGold, "Leafstruck Tournament Winner" }, + { BadgeVariant.LeafStruckSilver, "Leafstruck Tournament 2nd Place" }, + { BadgeVariant.LeafStruckBronze, "Leafstruck Tournament 3rd Place" }, + { BadgeVariant.SummitShowdownGold, "Summit Showdown Tournament Winner" }, + { BadgeVariant.SummitShowdownSilver, "Summit Showdown Tournament 2nd Place" }, + { BadgeVariant.SummitShowdownBronze, "Summit Showdown Tournament 3rd Place" }, + { BadgeVariant.HorizonGold, "Horizon Tournament Winner" }, + { BadgeVariant.HorizonSilver, "Horizon Tournament 2nd Place" }, + { BadgeVariant.HorizonBronze, "Horizon Tournament 3rd Place" }, + { BadgeVariant.SunblossomGold, "Sunblossom Tournament Winner" }, + { BadgeVariant.SunblossomSilver, "Sunblossom Tournament 2nd Place" }, + { BadgeVariant.SunblossomBronze, "Sunblossom Tournament 3rd Place" }, + { BadgeVariant.EarthboundGold, "Earthbound Tournament Winner" }, + { BadgeVariant.EarthboundSilver, "Earthbound Tournament 2nd Place" }, + { BadgeVariant.EarthboundBronze, "Earthbound Tournament 3rd Place" }, + { BadgeVariant.BotBGold, "Bottom of the Barrel Tournament Winner" }, + { BadgeVariant.BotBSilver, "Bottom of the Barrel Tournament 2nd Place" }, + { BadgeVariant.BotBBronze, "Bottom of the Barrel Tournament 3rd Place" }, }; public static readonly StyledProperty HoverTipProperty = AvaloniaProperty.Register( @@ -37,8 +55,10 @@ public string HoverTip set => SetValue(HoverTipProperty, value); } + // None has to be the explicit default, since 0 is a real badge id. public static readonly StyledProperty VariantProperty = AvaloniaProperty.Register( - nameof(Variant) + nameof(Variant), + BadgeVariant.None ); public BadgeVariant Variant @@ -47,9 +67,15 @@ public BadgeVariant Variant set => SetValue(VariantProperty, value); } - private void UpdateTooltip(BadgeVariant variant) + private void UpdateVariant(BadgeVariant variant) { HoverTip = BadgeToolTip.GetValueOrDefault(variant, BadgeToolTip[BadgeVariant.None]); + + // Tournament ids come in gold/silver/bronze triplets starting at 2000, so they all share one style per tier. + var tournamentTier = variant >= BadgeVariant.FireStarterGold ? ((int)variant - (int)BadgeVariant.FireStarterGold) % 3 : -1; + PseudoClasses.Set(":gold", tournamentTier == 0); + PseudoClasses.Set(":silver", tournamentTier == 1); + PseudoClasses.Set(":bronze", tournamentTier == 2); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -57,6 +83,6 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang base.OnPropertyChanged(change); if (change.Property == VariantProperty) - UpdateTooltip(change.GetNewValue()); + UpdateVariant(change.GetNewValue()); } } diff --git a/WheelWizard/Features/WheelWizardData/Views/LeaderboardPage.axaml.cs b/WheelWizard/Features/WheelWizardData/Views/LeaderboardPage.axaml.cs index c42e6abf1..32da25626 100644 --- a/WheelWizard/Features/WheelWizardData/Views/LeaderboardPage.axaml.cs +++ b/WheelWizard/Features/WheelWizardData/Views/LeaderboardPage.axaml.cs @@ -16,7 +16,6 @@ using WheelWizard.Views.Dialogs; using WheelWizard.Views.Shell; using WheelWizard.Views.Shell.Navigation; -using WheelWizard.WheelWizardData; using WheelWizard.WheelWizardData.Domain; using WheelWizard.WheelWizardData.Views.Dialogs; using WheelWizard.WiiManagement.FriendCodes; @@ -35,7 +34,7 @@ public sealed record LeaderboardPlayerItem public required string VrText { get; init; } public Mii? Mii { get; init; } public BadgeVariant[] BadgeVariants { get; init; } = []; - public BadgeVariant PrimaryBadge { get; init; } + public BadgeVariant PrimaryBadge { get; init; } = BadgeVariant.None; public bool HasBadge { get; init; } public bool IsSuspicious { get; init; } public bool IsEvenRow { get; init; } @@ -81,7 +80,7 @@ public partial class LeaderboardPage : UserControl, INotifyPropertyChanged private IRrLeaderboardSingletonService LeaderboardService { get; } - private IWhWzDataSingletonService BadgeService { get; } + private IRrBadgeSingletonService BadgeService { get; } private IGameLicenseSingletonService GameDataService { get; } @@ -188,7 +187,7 @@ public LeaderboardPage( INavigationService navigation, LiveRoomsService liveRooms, IRrLeaderboardSingletonService leaderboardService, - IWhWzDataSingletonService badgeService, + IRrBadgeSingletonService badgeService, IGameLicenseSingletonService gameDataService, ISettingsManager settingsManager, IMiiNativeRenderer miiRenderer, diff --git a/WheelWizard/Features/WheelWizardData/Views/LeaderboardPodiumStep.axaml b/WheelWizard/Features/WheelWizardData/Views/LeaderboardPodiumStep.axaml index 2bc3170a0..a5e2189c6 100644 --- a/WheelWizard/Features/WheelWizardData/Views/LeaderboardPodiumStep.axaml +++ b/WheelWizard/Features/WheelWizardData/Views/LeaderboardPodiumStep.axaml @@ -9,7 +9,7 @@ ShowBadge="True" BadgeVariant="Translator" /> + ShowBadge="True" BadgeVariant="WheelWizardDeveloper" /> diff --git a/WheelWizard/Features/WheelWizardData/Views/UserProfilePage.axaml.cs b/WheelWizard/Features/WheelWizardData/Views/UserProfilePage.axaml.cs index 758a1c520..6a4e8ac71 100644 --- a/WheelWizard/Features/WheelWizardData/Views/UserProfilePage.axaml.cs +++ b/WheelWizard/Features/WheelWizardData/Views/UserProfilePage.axaml.cs @@ -19,7 +19,6 @@ using WheelWizard.Views.Dialogs; using WheelWizard.Views.Shell; using WheelWizard.Views.Shell.Navigation; -using WheelWizard.WheelWizardData; using WheelWizard.WheelWizardData.Domain; using WheelWizard.WiiManagement; using WheelWizard.WiiManagement.GameLicense; @@ -53,7 +52,7 @@ public partial class UserProfilePage : UserControl, INotifyPropertyChanged private IGameLicenseSingletonService GameLicenseService { get; } - private IWhWzDataSingletonService BadgeService { get; } + private IRrBadgeSingletonService BadgeService { get; } private IMiiDbService MiiDbService { get; } @@ -231,7 +230,7 @@ public UserProfilePage( INavigationService navigation, LiveRoomsService liveRooms, IGameLicenseSingletonService gameLicenseService, - IWhWzDataSingletonService badgeService, + IRrBadgeSingletonService badgeService, IMiiDbService miiDbService, ISettingsManager settingsService ) diff --git a/WheelWizard/Features/WheelWizardData/WhWzDataExtensions.cs b/WheelWizard/Features/WheelWizardData/WhWzDataExtensions.cs index 7aa0496c2..b9d965240 100644 --- a/WheelWizard/Features/WheelWizardData/WhWzDataExtensions.cs +++ b/WheelWizard/Features/WheelWizardData/WhWzDataExtensions.cs @@ -1,6 +1,5 @@ using System.Text.Json; using System.Text.Json.Serialization; -using WheelWizard.Shared.JsonConverters; using WheelWizard.WheelWizardData.Domain; namespace WheelWizard.WheelWizardData; @@ -11,11 +10,7 @@ public static IServiceCollection AddWhWzData(this IServiceCollection services) { services.AddWhWzRefitApi( "https://raw.githubusercontent.com/TeamWheelWizard/WheelWizard-Data/main", - new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - Converters = { new EnumWithFallbackConverter(), new JsonStringEnumConverter() }, - } + new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, Converters = { new JsonStringEnumConverter() } } ); services.AddSingleton(); diff --git a/WheelWizard/Features/WheelWizardData/WhWzDataSingletonService.cs b/WheelWizard/Features/WheelWizardData/WhWzDataSingletonService.cs index 249da9cad..cbab0f7bf 100644 --- a/WheelWizard/Features/WheelWizardData/WhWzDataSingletonService.cs +++ b/WheelWizard/Features/WheelWizardData/WhWzDataSingletonService.cs @@ -6,29 +6,12 @@ namespace WheelWizard.WheelWizardData; public interface IWhWzDataSingletonService { Task> GetStatusAsync(); - Task LoadBadgesAsync(); - BadgeVariant[] GetBadges(string friendCode); } public class WhWzDataSingletonService(IApiCaller apiCaller) : IWhWzDataSingletonService { - private Dictionary BadgeData { get; set; } = new(); - public async Task> GetStatusAsync() { return await apiCaller.CallApiAsync(whWzDataApi => whWzDataApi.GetStatusAsync()); } - - public async Task LoadBadgesAsync() - { - var badgeResult = await apiCaller.CallApiAsync(whWzDataApi => whWzDataApi.GetBadgesAsync()); - if (badgeResult.IsFailure) - return badgeResult; - - BadgeData = badgeResult.Value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Where(b => b != BadgeVariant.None).ToArray()); - - return Ok(); - } - - public BadgeVariant[] GetBadges(string friendCode) => BadgeData.TryGetValue(friendCode, out var variants) ? variants : []; } diff --git a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs index 27dcf445a..06b95d005 100644 --- a/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs +++ b/WheelWizard/Features/WiiManagement/GameLicense/GameLicenseService.cs @@ -11,7 +11,6 @@ using WheelWizard.Shared.Binary; using WheelWizard.Shared.IO; using WheelWizard.Shared.Polling; -using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement.FriendCodes; using WheelWizard.WiiManagement.GameLicense.Domain; using WheelWizard.WiiManagement.MiiManagement; @@ -94,7 +93,7 @@ public class GameLicenseSingletonService : ObservablePollingService, IGameLicens { private readonly IMiiDbService _miiService; private readonly IFileSystem _fileSystem; - private readonly IWhWzDataSingletonService _whWzDataSingletonService; + private readonly IRrBadgeSingletonService _badgeService; private readonly IRrRatingReader _rrratingReader; private readonly ISettingsManager _settingsManager; private readonly ISaveRegionService _saveRegions; @@ -107,7 +106,7 @@ public class GameLicenseSingletonService : ObservablePollingService, IGameLicens public GameLicenseSingletonService( IMiiDbService miiService, IFileSystem fileSystem, - IWhWzDataSingletonService whWzDataSingletonService, + IRrBadgeSingletonService badgeService, IRrRatingReader rrratingReader, ISettingsManager settingsManager, ISaveRegionService saveRegions, @@ -122,7 +121,7 @@ ILogger logger { _miiService = miiService; _fileSystem = fileSystem; - _whWzDataSingletonService = whWzDataSingletonService; + _badgeService = badgeService; _rrratingReader = rrratingReader; _settingsManager = settingsManager; _saveRegions = saveRegions; @@ -332,7 +331,7 @@ private OperationResult ParseLicenseUser(int rkpdOffset) Br = br, TotalRaceCount = BigEndianBinary.BufferToUint32(_rksysData, rkpdOffset + 0xB4), TotalWinCount = BigEndianBinary.BufferToUint32(_rksysData, rkpdOffset + 0xDC), - BadgeVariants = _whWzDataSingletonService.GetBadges(friendCode), + BadgeVariants = _badgeService.GetBadges(friendCode), // Region is often found near offset 0x23308 + 0x3802 in RKGD. This code is a partial guess. // In practice, region might be read differently depending on your rksys layout. RegionId = BigEndianBinary.BufferToUint16(_rksysData, 0x23308 + 0x3802) / 4096, @@ -416,7 +415,7 @@ private void ParseFriends(LicenseProfile licenseProfile, int userOffset) Losses = BigEndianBinary.BufferToUint16(_rksysData, currentOffset + 0x12), CountryCode = _rksysData[currentOffset + 0x68], RegionId = _rksysData[currentOffset + 0x69], - BadgeVariants = _whWzDataSingletonService.GetBadges(friendCode), + BadgeVariants = _badgeService.GetBadges(friendCode), IsPending = isPending, Mii = miiResult.Value, }; diff --git a/WheelWizard/Views/Styles/Resources/Icons.axaml b/WheelWizard/Views/Styles/Resources/Icons.axaml index 5acb25a8c..94e6dfb45 100644 --- a/WheelWizard/Views/Styles/Resources/Icons.axaml +++ b/WheelWizard/Views/Styles/Resources/Icons.axaml @@ -161,6 +161,8 @@ M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L433.6 328.4l26.2 155.6c1.5 9-2.2 18.1-9.7 23.5s-17.3 6-25.3 1.7l-137-73.2L151 509.1c-8.1 4.3-17.9 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l26.2-155.6L31.1 218.2c-6.5-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L266.3 13.5C270.4 5.2 278.7 0 287.9 0zm0 79L235.4 187.2c-3.5 7.1-10.2 12.1-18.1 13.3L99 217.9 184.9 303c5.5 5.5 8.1 13.3 6.8 21L171.4 443.7l105.2-56.2c7.1-3.8 15.6-3.8 22.6 0l105.2 56.2L384.2 324.1c-1.3-7.7 1.2-15.5 6.8-21l85.9-85.1L358.6 200.5c-7.8-1.2-14.6-6.1-18.1-13.3L287.9 79z M173.8 5.5c11-7.3 25.4-7.3 36.4 0L228 17.2c6 3.9 13 5.8 20.1 5.4l21.3-1.3c13.2-.8 25.6 6.4 31.5 18.2l9.6 19.1c3.2 6.4 8.4 11.5 14.7 14.7L344.5 83c11.8 5.9 19 18.3 18.2 31.5l-1.3 21.3c-.4 7.1 1.5 14.2 5.4 20.1l11.8 17.8c7.3 11 7.3 25.4 0 36.4L366.8 228c-3.9 6-5.8 13-5.4 20.1l1.3 21.3c.8 13.2-6.4 25.6-18.2 31.5l-19.1 9.6c-6.4 3.2-11.5 8.4-14.7 14.7L301 344.5c-5.9 11.8-18.3 19-31.5 18.2l-21.3-1.3c-7.1-.4-14.2 1.5-20.1 5.4l-17.8 11.8c-11 7.3-25.4 7.3-36.4 0L156 366.8c-6-3.9-13-5.8-20.1-5.4l-21.3 1.3c-13.2 .8-25.6-6.4-31.5-18.2l-9.6-19.1c-3.2-6.4-8.4-11.5-14.7-14.7L39.5 301c-11.8-5.9-19-18.3-18.2-31.5l1.3-21.3c.4-7.1-1.5-14.2-5.4-20.1L5.5 210.2c-7.3-11-7.3-25.4 0-36.4L17.2 156c3.9-6 5.8-13 5.4-20.1l-1.3-21.3c-.8-13.2 6.4-25.6 18.2-31.5l19.1-9.6C65 70.2 70.2 65 73.4 58.6L83 39.5c5.9-11.8 18.3-19 31.5-18.2l21.3 1.3c7.1 .4 14.2-1.5 20.1-5.4L173.8 5.5zM272 192a80 80 0 1 0 -160 0 80 80 0 1 0 160 0zM1.3 441.8L44.4 339.3c.2 .1 .3 .2 .4 .4l9.6 19.1c11.7 23.2 36 37.3 62 35.8l21.3-1.3c.2 0 .5 0 .7 .2l17.8 11.8c5.1 3.3 10.5 5.9 16.1 7.7l-37.6 89.3c-2.3 5.5-7.4 9.2-13.3 9.7s-11.6-2.2-14.8-7.2L74.4 455.5l-56.1 8.3c-5.7 .8-11.4-1.5-15-6s-4.3-10.7-2.1-16zm248 60.4L211.7 413c5.6-1.8 11-4.3 16.1-7.7l17.8-11.8c.2-.1 .4-.2 .7-.2l21.3 1.3c26 1.5 50.3-12.6 62-35.8l9.6-19.1c.1-.2 .2-.3 .4-.4l43.2 102.5c2.2 5.3 1.4 11.4-2.1 16s-9.3 6.9-15 6l-56.1-8.3-32.2 49.2c-3.2 5-8.9 7.7-14.8 7.2s-11-4.3-13.3-9.7z + M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0z + M288 0H160 128C110.3 0 96 14.3 96 32s14.3 32 32 32V196.8c0 11.8-3.3 23.5-9.5 33.5L10.3 406.2C3.6 417.2 0 429.7 0 442.6C0 480.9 31.1 512 69.4 512H378.6c38.3 0 69.4-31.1 69.4-69.4c0-12.8-3.6-25.4-10.3-36.4L329.5 230.4c-6.2-10.1-9.5-21.7-9.5-33.5V64c17.7 0 32-14.3 32-32s-14.3-32-32-32H288zM192 196.8V64h64V196.8c0 23.7 6.6 46.9 19 67.1L309.5 320h-171L173 263.9c12.4-20.2 19-43.4 19-67.1z M39,18.67H35.42l-4.2,11.12A29,29,0,0,1,20.6,24.91a28.76,28.76,0,0,0,7.11-14.49h5.21a2,2,0,0,0,0-4H19.67V2a2,2,0,1,0-4,0V6.42H2.41a2,2,0,0,0,0,4H7.63a28.73,28.73,0,0,0,7.1,14.49A29.51,29.51,0,0,1,3.27,30a2,2,0,0,0,.43,4,1.61,1.61,0,0,0,.44-.05,32.56,32.56,0,0,0,13.53-6.25,32,32,0,0,0,12.13,5.9L22.83,52H28l2.7-7.76H43.64L46.37,52h5.22Zm-15.3-8.25a23.76,23.76,0,0,1-6,11.86,23.71,23.71,0,0,1-6-11.86Zm8.68,29.15,4.83-13.83L42,39.57Z