1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-28 01:47:33 +08:00

Merge branch 'master' into async-deadlock-safety

This commit is contained in:
Dan Balasescu 2022-01-10 16:11:09 +09:00
commit af9fad00c3
20 changed files with 457 additions and 96 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.109.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.107.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.107.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">

View File

@ -95,7 +95,6 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode, Is.Not.Null); Assert.That(decodedAfterEncode, Is.Not.Null);
Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username)); Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username));
Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID));
Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset)); Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore)); Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore));
Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo)); Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo));

View File

@ -16,7 +16,8 @@ namespace osu.Game.Tests.NonVisual.Filtering
{ {
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
{ {
Ruleset = new RulesetInfo { OnlineID = 5 }, Ruleset = new RulesetInfo { OnlineID = 0 },
RulesetID = 0,
StarRating = 4.0d, StarRating = 4.0d,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestNoSubmissionOnResultsWithNoToken() public void TestNoSubmissionOnResultsWithNoToken()
{ {
prepareTokenResponse(false); prepareTestAPI(false);
createPlayerTest(); createPlayerTest();
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSubmissionOnResults() public void TestSubmissionOnResults()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(); createPlayerTest();
@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSubmissionForDifferentRuleset() public void TestSubmissionForDifferentRuleset()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(createRuleset: () => new TaikoRuleset()); createPlayerTest(createRuleset: () => new TaikoRuleset());
@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSubmissionForConvertedBeatmap() public void TestSubmissionForConvertedBeatmap()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(createRuleset: () => new ManiaRuleset(), createBeatmap: _ => createTestBeatmap(new OsuRuleset().RulesetInfo)); createPlayerTest(createRuleset: () => new ManiaRuleset(), createBeatmap: _ => createTestBeatmap(new OsuRuleset().RulesetInfo));
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestNoSubmissionOnExitWithNoToken() public void TestNoSubmissionOnExitWithNoToken()
{ {
prepareTokenResponse(false); prepareTestAPI(false);
createPlayerTest(); createPlayerTest();
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestNoSubmissionOnEmptyFail() public void TestNoSubmissionOnEmptyFail()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(true); createPlayerTest(true);
@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSubmissionOnFail() public void TestSubmissionOnFail()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(true); createPlayerTest(true);
@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestNoSubmissionOnEmptyExit() public void TestNoSubmissionOnEmptyExit()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(); createPlayerTest();
@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSubmissionOnExit() public void TestSubmissionOnExit()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(); createPlayerTest();
@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestSubmissionOnExitDuringImport() public void TestSubmissionOnExitDuringImport()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(); createPlayerTest();
AddStep("block imports", () => Player.AllowImportCompletion.Wait()); AddStep("block imports", () => Player.AllowImportCompletion.Wait());
@ -226,13 +226,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("exit", () => Player.Exit()); AddStep("exit", () => Player.Exit());
AddStep("allow import to proceed", () => Player.AllowImportCompletion.Release(1)); AddStep("allow import to proceed", () => Player.AllowImportCompletion.Release(1));
AddAssert("ensure submission", () => Player.SubmittedScore != null && Player.ImportedScore != null); AddUntilStep("ensure submission", () => Player.SubmittedScore != null && Player.ImportedScore != null);
} }
[Test] [Test]
public void TestNoSubmissionOnLocalBeatmap() public void TestNoSubmissionOnLocalBeatmap()
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(false, r => createPlayerTest(false, r =>
{ {
@ -253,7 +253,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(10)] [TestCase(10)]
public void TestNoSubmissionOnCustomRuleset(int? rulesetId) public void TestNoSubmissionOnCustomRuleset(int? rulesetId)
{ {
prepareTokenResponse(true); prepareTestAPI(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { OnlineID = rulesetId ?? -1 } }); createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { OnlineID = rulesetId ?? -1 } });
@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay
})); }));
} }
private void prepareTokenResponse(bool validToken) private void prepareTestAPI(bool validToken)
{ {
AddStep("Prepare test API", () => AddStep("Prepare test API", () =>
{ {
@ -289,6 +289,31 @@ namespace osu.Game.Tests.Visual.Gameplay
else else
tokenRequest.TriggerFailure(new APIException("something went wrong!", null)); tokenRequest.TriggerFailure(new APIException("something went wrong!", null));
return true; return true;
case SubmitSoloScoreRequest submissionRequest:
if (validToken)
{
var requestScore = submissionRequest.Score;
submissionRequest.TriggerSuccess(new MultiplayerScore
{
ID = 1234,
User = dummyAPI.LocalUser.Value,
Rank = requestScore.Rank,
TotalScore = requestScore.TotalScore,
Accuracy = requestScore.Accuracy,
MaxCombo = requestScore.MaxCombo,
Mods = requestScore.Mods,
Statistics = requestScore.Statistics,
Passed = requestScore.Passed,
EndedAt = DateTimeOffset.Now,
Position = 1
});
return true;
}
break;
} }
return false; return false;

View File

@ -1,13 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
@ -83,16 +95,60 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
} }
private void addItem(Func<BeatmapInfo> beatmap) [Test]
public void TestCorrectRulesetSelectedAfterNewItemAdded()
{ {
addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo);
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded);
AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddStep("exit player", () => CurrentScreen.Exit());
}
[Test]
public void TestCorrectModsSelectedAfterNewItemAdded()
{
addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() });
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded);
AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any());
AddStep("exit player", () => CurrentScreen.Exit());
}
private void addItem(Func<BeatmapInfo> beatmap, RulesetInfo? ruleset = null, IReadOnlyList<Mod>? mods = null)
{
Screens.Select.SongSelect? songSelect = null;
AddStep("click add button", () => AddStep("click add button", () =>
{ {
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single()); InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap())); AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded);
if (ruleset != null)
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);
if (mods != null)
AddStep($"set mods to {string.Join(",", mods.Select(m => m.Acronym))}", () => songSelect.AsNonNull().Mods.Value = mods);
AddStep("select other beatmap", () => songSelect.AsNonNull().FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
} }
} }

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
@ -23,6 +24,8 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
@ -439,6 +442,84 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
} }
[Test]
public void TestPlayStartsWithCorrectRulesetWhileAtSongSelect()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
pressReadyButton();
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == client.Room?.Playlist.First().RulesetID);
AddStep("Switch ruleset", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Ruleset.Value = new CatchRuleset().RulesetInfo);
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != client.Room?.Playlist.First().RulesetID);
AddStep("start match externally", () => client.StartMatch());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == client.Room?.Playlist.First().RulesetID);
}
[Test]
public void TestPlayStartsWithCorrectModsWhileAtSongSelect()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
pressReadyButton();
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => client.StartMatch());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
}
[Test] [Test]
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap() public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
{ {

View File

@ -1,38 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Rankings.Tables; using osu.Game.Overlays.Rankings.Tables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using System.Threading; using System.Threading;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Catch;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Rankings; using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneRankingsTables : OsuTestScene public class TestSceneRankingsTables : OsuTestScene
{ {
protected override bool UseOnlineAPI => true;
[Resolved]
private IAPIProvider api { get; set; }
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
private readonly BasicScrollContainer scrollFlow; private readonly BasicScrollContainer scrollFlow;
private readonly LoadingLayer loading; private readonly LoadingLayer loading;
private CancellationTokenSource cancellationToken; private CancellationTokenSource cancellationToken;
private APIRequest request;
public TestSceneRankingsTables() public TestSceneRankingsTables()
{ {
@ -53,73 +42,120 @@ namespace osu.Game.Tests.Visual.Online
{ {
base.LoadComplete(); base.LoadComplete();
AddStep("Osu performance", () => createPerformanceTable(new OsuRuleset().RulesetInfo, null)); AddStep("User performance", createPerformanceTable);
AddStep("Mania scores", () => createScoreTable(new ManiaRuleset().RulesetInfo)); AddStep("User scores", createScoreTable);
AddStep("Taiko country scores", () => createCountryTable(new TaikoRuleset().RulesetInfo)); AddStep("Country scores", createCountryTable);
AddStep("Catch US performance page 10", () => createPerformanceTable(new CatchRuleset().RulesetInfo, "US", 10));
AddStep("Osu spotlight table (chart 271)", () => createSpotlightTable(new OsuRuleset().RulesetInfo, 271));
} }
private void createCountryTable(RulesetInfo ruleset, int page = 1) private void createCountryTable()
{ {
onLoadStarted(); onLoadStarted();
request = new GetCountryRankingsRequest(ruleset, page); var countries = new List<CountryStatistics>
((GetCountryRankingsRequest)request).Success += rankings => Schedule(() =>
{ {
var table = new CountriesTable(page, rankings.Countries); new CountryStatistics
loadTable(table); {
}); Country = new Country { FlagName = "US", FullName = "United States" },
FlagName = "US",
ActiveUsers = 2_972_623,
PlayCount = 3_086_515_743,
RankedScore = 449_407_643_332_546,
Performance = 371_974_024
},
new CountryStatistics
{
Country = new Country { FlagName = "RU", FullName = "Russian Federation" },
FlagName = "RU",
ActiveUsers = 1_609_989,
PlayCount = 1_637_052_841,
RankedScore = 221_660_827_473_004,
Performance = 163_426_476
}
};
api.Queue(request); var table = new CountriesTable(1, countries);
loadTable(table);
} }
private void createPerformanceTable(RulesetInfo ruleset, string country, int page = 1) private static List<UserStatistics> createUserStatistics() => new List<UserStatistics>
{
new UserStatistics
{
User = new APIUser
{
Username = "first active user",
Country = new Country { FlagName = "JP" },
Active = true,
},
Accuracy = 0.9972,
PlayCount = 233_215,
TotalScore = 983_231_234_656,
RankedScore = 593_231_345_897,
PP = 23_934,
GradesCount = new UserStatistics.Grades
{
SS = 35_132,
S = 23_345,
A = 12_234
}
},
new UserStatistics
{
User = new APIUser
{
Username = "inactive user",
Country = new Country { FlagName = "AU" },
Active = false,
},
Accuracy = 0.9831,
PlayCount = 195_342,
TotalScore = 683_231_234_656,
RankedScore = 393_231_345_897,
PP = 20_934,
GradesCount = new UserStatistics.Grades
{
SS = 32_132,
S = 20_345,
A = 9_234
}
},
new UserStatistics
{
User = new APIUser
{
Username = "second active user",
Country = new Country { FlagName = "PL" },
Active = true,
},
Accuracy = 0.9584,
PlayCount = 100_903,
TotalScore = 97_242_983_434,
RankedScore = 3_156_345_897,
PP = 9_568,
GradesCount = new UserStatistics.Grades
{
SS = 13_152,
S = 24_375,
A = 9_960
}
},
};
private void createPerformanceTable()
{ {
onLoadStarted(); onLoadStarted();
loadTable(new PerformanceTable(1, createUserStatistics()));
request = new GetUserRankingsRequest(ruleset, country: country, page: page);
((GetUserRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new PerformanceTable(page, rankings.Users);
loadTable(table);
});
api.Queue(request);
} }
private void createScoreTable(RulesetInfo ruleset, int page = 1) private void createScoreTable()
{ {
onLoadStarted(); onLoadStarted();
loadTable(new ScoresTable(1, createUserStatistics()));
request = new GetUserRankingsRequest(ruleset, UserRankingsType.Score, page);
((GetUserRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new ScoresTable(page, rankings.Users);
loadTable(table);
});
api.Queue(request);
}
private void createSpotlightTable(RulesetInfo ruleset, int spotlight)
{
onLoadStarted();
request = new GetSpotlightRankingsRequest(ruleset, spotlight, RankingsSortCriteria.All);
((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new ScoresTable(1, rankings.Users);
loadTable(table);
});
api.Queue(request);
} }
private void onLoadStarted() private void onLoadStarted()
{ {
loading.Show(); loading.Show();
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
cancellationToken = new CancellationTokenSource(); cancellationToken = new CancellationTokenSource();
} }

View File

@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Overlays.BeatmapSet.Scores;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneScoreboardTime : OsuTestScene
{
private StopwatchClock stopwatch;
[Test]
public void TestVariousUnits()
{
AddStep("create various scoreboard times", () => Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Clock = new FramedClock(stopwatch = new StopwatchClock()), // prevent time from naturally elapsing.
Direction = FillDirection.Vertical,
ChildrenEnumerable = testCases.Select(dateTime => new ScoreboardTime(dateTime, 24).With(time => time.Anchor = time.Origin = Anchor.TopCentre))
});
AddStep("start stopwatch", () => stopwatch.Start());
}
private static IEnumerable<DateTimeOffset> testCases => new[]
{
DateTimeOffset.Now,
DateTimeOffset.Now.AddSeconds(-1),
DateTimeOffset.Now.AddSeconds(-25),
DateTimeOffset.Now.AddSeconds(-59),
DateTimeOffset.Now.AddMinutes(-1),
DateTimeOffset.Now.AddMinutes(-25),
DateTimeOffset.Now.AddMinutes(-59),
DateTimeOffset.Now.AddHours(-1),
DateTimeOffset.Now.AddHours(-13),
DateTimeOffset.Now.AddHours(-23),
DateTimeOffset.Now.AddDays(-1),
DateTimeOffset.Now.AddDays(-6),
DateTimeOffset.Now.AddDays(-16),
DateTimeOffset.Now.AddMonths(-1),
DateTimeOffset.Now.AddMonths(-11),
DateTimeOffset.Now.AddYears(-1),
DateTimeOffset.Now.AddYears(-5)
};
}
}

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>(); TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>();
FileBasedIPC ipc = null; FileBasedIPC ipc = null;
WaitForOrAssert(() => (ipc = osu.Dependencies.Get<MatchIPCInfo>() as FileBasedIPC) != null, @"ipc could not be populated in a reasonable amount of time"); WaitForOrAssert(() => (ipc = osu.Dependencies.Get<MatchIPCInfo>() as FileBasedIPC)?.IsLoaded == true, @"ipc could not be populated in a reasonable amount of time");
Assert.True(ipc.SetIPCLocation(testStableInstallDirectory)); Assert.True(ipc.SetIPCLocation(testStableInstallDirectory));
Assert.True(storage.AllTournaments.Exists("stable.json")); Assert.True(storage.AllTournaments.Exists("stable.json"));

View File

@ -26,6 +26,9 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Create a detached copy of the each item in the collection. /// Create a detached copy of the each item in the collection.
/// </summary> /// </summary>
/// <remarks>
/// Items which are already detached (ie. not managed by realm) will not be modified.
/// </remarks>
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param> /// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
/// <typeparam name="T">The type of object.</typeparam> /// <typeparam name="T">The type of object.</typeparam>
/// <returns>A list containing non-managed copies of provided items.</returns> /// <returns>A list containing non-managed copies of provided items.</returns>
@ -42,6 +45,9 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Create a detached copy of the item. /// Create a detached copy of the item.
/// </summary> /// </summary>
/// <remarks>
/// If the item if already detached (ie. not managed by realm) it will not be detached again and the original instance will be returned. This allows this method to be potentially called at multiple levels while only incurring the clone overhead once.
/// </remarks>
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param> /// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
/// <typeparam name="T">The type of object.</typeparam> /// <typeparam name="T">The type of object.</typeparam>
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns> /// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>

View File

@ -12,17 +12,17 @@ namespace osu.Game.Online.Solo
{ {
public class SubmitSoloScoreRequest : APIRequest<MultiplayerScore> public class SubmitSoloScoreRequest : APIRequest<MultiplayerScore>
{ {
public readonly SubmittableScore Score;
private readonly long scoreId; private readonly long scoreId;
private readonly int beatmapId; private readonly int beatmapId;
private readonly SubmittableScore score;
public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo) public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo)
{ {
this.beatmapId = beatmapId; this.beatmapId = beatmapId;
this.scoreId = scoreId; this.scoreId = scoreId;
score = new SubmittableScore(scoreInfo); Score = new SubmittableScore(scoreInfo);
} }
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
@ -33,7 +33,7 @@ namespace osu.Game.Online.Solo
req.Method = HttpMethod.Put; req.Method = HttpMethod.Put;
req.Timeout = 30000; req.Timeout = 30000;
req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings
{ {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore ReferenceLoopHandling = ReferenceLoopHandling.Ignore
})); }));

View File

@ -128,6 +128,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (showPerformancePoints) if (showPerformancePoints)
columns.Add(new TableColumn(BeatmapsetsStrings.ShowScoreboardHeaderspp, Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn(BeatmapsetsStrings.ShowScoreboardHeaderspp, Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30)));
columns.Add(new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersTime, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)));
columns.Add(new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersMods, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); columns.Add(new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersMods, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)));
return columns.ToArray(); return columns.ToArray();
@ -202,6 +203,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}); });
} }
content.Add(new ScoreboardTime(score.Date, text_size)
{
Margin = new MarginPadding { Right = 10 }
});
content.Add(new FillFlowContainer content.Add(new FillFlowContainer
{ {
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Humanizer;
using osu.Game.Graphics;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapSet.Scores
{
public class ScoreboardTime : DrawableDate
{
public ScoreboardTime(DateTimeOffset date, float textSize = OsuFont.DEFAULT_FONT_SIZE, bool italic = true)
: base(date, textSize, italic)
{
}
protected override string Format()
{
var now = DateTime.Now;
var difference = now - Date;
// web uses momentjs's custom locales to format the date for the purposes of the scoreboard.
// this is intended to be a best-effort, more legible approximation of that.
// compare:
// * https://github.com/ppy/osu-web/blob/a8f5a68fb435cb19a4faa4c7c4bce08c4f096933/resources/assets/lib/scoreboard-time.tsx
// * https://momentjs.com/docs/#/customization/ (reference for the customisation format)
// TODO: support localisation (probably via `CommonStrings.CountHours()` etc.)
// requires pluralisable string support framework-side
if (difference.TotalHours < 1)
return CommonStrings.TimeNow.ToString();
if (difference.TotalDays < 1)
return "hr".ToQuantity((int)difference.TotalHours);
// this is where this gets more complicated because of how the calendar works.
// since there's no `TotalMonths` / `TotalYears`, we have to iteratively add months/years
// and test against cutoff dates to determine how many months/years to show.
if (Date > now.AddMonths(-1))
return difference.TotalDays < 2 ? "1dy" : $"{(int)difference.TotalDays}dys";
for (int months = 1; months <= 11; ++months)
{
if (Date > now.AddMonths(-(months + 1)))
return months == 1 ? "1mo" : $"{months}mos";
}
int years = 1;
while (Date <= now.AddYears(-(years + 1)))
years += 1;
return years == 1 ? "1yr" : $"{years}yrs";
}
}
}

View File

@ -54,13 +54,15 @@ namespace osu.Game.Overlays.Rankings.Tables
Spacing = new Vector2(0, row_spacing), Spacing = new Vector2(0, row_spacing),
}); });
rankings.ForEach(_ => backgroundFlow.Add(new TableRowBackground { Height = row_height })); rankings.ForEach(s => backgroundFlow.Add(CreateRowBackground(s)));
Columns = mainHeaders.Concat(CreateAdditionalHeaders()).Cast<TableColumn>().ToArray(); Columns = mainHeaders.Concat(CreateAdditionalHeaders()).Cast<TableColumn>().ToArray();
Content = rankings.Select((s, i) => createContent((page - 1) * items_per_page + i, s)).ToArray().ToRectangular(); Content = rankings.Select((s, i) => CreateRowContent((page - 1) * items_per_page + i, s)).ToArray().ToRectangular();
} }
private Drawable[] createContent(int index, TModel item) => new Drawable[] { createIndexDrawable(index), createMainContent(item) }.Concat(CreateAdditionalContent(item)).ToArray(); protected virtual Drawable CreateRowBackground(TModel item) => new TableRowBackground { Height = row_height };
protected virtual Drawable[] CreateRowContent(int index, TModel item) => new Drawable[] { createIndexDrawable(index), createMainContent(item) }.Concat(CreateAdditionalContent(item)).ToArray();
private static RankingsTableColumn[] mainHeaders => new[] private static RankingsTableColumn[] mainHeaders => new[]
{ {

View File

@ -24,6 +24,31 @@ namespace osu.Game.Overlays.Rankings.Tables
protected virtual IEnumerable<LocalisableString> GradeColumns => new List<LocalisableString> { RankingsStrings.Statss, RankingsStrings.Stats, RankingsStrings.Stata }; protected virtual IEnumerable<LocalisableString> GradeColumns => new List<LocalisableString> { RankingsStrings.Statss, RankingsStrings.Stats, RankingsStrings.Stata };
protected override Drawable CreateRowBackground(UserStatistics item)
{
var background = base.CreateRowBackground(item);
// see: https://github.com/ppy/osu-web/blob/9de00a0b874c56893d98261d558d78d76259d81b/resources/views/multiplayer/rooms/_rankings_table.blade.php#L23
if (!item.User.Active)
background.Alpha = 0.5f;
return background;
}
protected override Drawable[] CreateRowContent(int index, UserStatistics item)
{
var content = base.CreateRowContent(index, item);
// see: https://github.com/ppy/osu-web/blob/9de00a0b874c56893d98261d558d78d76259d81b/resources/views/multiplayer/rooms/_rankings_table.blade.php#L23
if (!item.User.Active)
{
foreach (var d in content)
d.Alpha = 0.5f;
}
return content;
}
protected override RankingsTableColumn[] CreateAdditionalHeaders() => new[] protected override RankingsTableColumn[] CreateAdditionalHeaders() => new[]
{ {
new RankingsTableColumn(RankingsStrings.StatAccuracy, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), new RankingsTableColumn(RankingsStrings.StatAccuracy, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Globalization;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -9,6 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
@ -66,7 +68,8 @@ namespace osu.Game.Screens.Edit.Timing
Current.BindValueChanged(val => Current.BindValueChanged(val =>
{ {
textBox.Text = val.NewValue.ToString(); decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
}, true); }, true);
} }

View File

@ -300,6 +300,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
updateWorkingBeatmap(); updateWorkingBeatmap();
beginHandlingTrack(); beginHandlingTrack();
Scheduler.AddOnce(UpdateMods); Scheduler.AddOnce(UpdateMods);
Scheduler.AddOnce(updateRuleset);
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
@ -353,8 +354,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
.ToList(); .ToList();
UpdateMods(); UpdateMods();
updateRuleset();
Ruleset.Value = rulesets.GetRuleset(selected.RulesetID);
if (!selected.AllowedMods.Any()) if (!selected.AllowedMods.Any())
{ {
@ -387,6 +387,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList();
} }
private void updateRuleset()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
Ruleset.Value = rulesets.GetRuleset(SelectedItem.Value.RulesetID);
}
private void beginHandlingTrack() private void beginHandlingTrack()
{ {
Beatmap.BindValueChanged(applyLoopingToTrack, true); Beatmap.BindValueChanged(applyLoopingToTrack, true);

View File

@ -28,8 +28,8 @@ namespace osu.Game.Screens.Select.Carousel
bool match = bool match =
criteria.Ruleset == null || criteria.Ruleset == null ||
BeatmapInfo.RulesetID == criteria.Ruleset.ID || BeatmapInfo.RulesetID == criteria.Ruleset.OnlineID ||
(BeatmapInfo.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.OnlineID > 0 && criteria.AllowConvertedBeatmaps);
if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
{ {

View File

@ -37,7 +37,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.7.1" /> <PackageReference Include="Realm" Version="10.7.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.107.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.107.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.109.0" />
<PackageReference Include="Sentry" Version="3.12.1" /> <PackageReference Include="Sentry" Version="3.12.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -61,7 +61,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.107.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.107.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.109.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup> <PropertyGroup>