1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 19:22:54 +08:00

Merge branch 'master' into realtime-ready-button

This commit is contained in:
Bartłomiej Dach 2020-12-20 18:20:57 +01:00 committed by GitHub
commit bcd140b8df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 939 additions and 301 deletions

View File

@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components
{ {
createPoller(true); createPoller(true);
AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust);
checkCount(1); checkCount(1);
checkCount(2); checkCount(2);
checkCount(3); checkCount(3);
AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); AddStep("set poll interval to 5", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5);
checkCount(4); checkCount(4);
checkCount(4); checkCount(4);
checkCount(4); checkCount(4);
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components
checkCount(5); checkCount(5);
checkCount(5); checkCount(5);
AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust);
checkCount(6); checkCount(6);
checkCount(7); checkCount(7);
} }
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components
{ {
createPoller(false); createPoller(false);
AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5);
checkCount(0); checkCount(0);
skip(); skip();
checkCount(0); checkCount(0);
@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components
public class TestSlowPoller : TestPoller public class TestSlowPoller : TestPoller
{ {
protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls.Value / 2f / Clock.Rate)).ContinueWith(_ => base.Poll());
} }
} }
} }

View File

@ -10,6 +10,8 @@ using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osuTK; using osuTK;
@ -50,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay
cancel(); cancel();
complete(); complete();
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated);
} }
/// <summary> /// <summary>
@ -84,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
// wait to ensure there was no attempt of pushing the results screen. // wait to ensure there was no attempt of pushing the results screen.
AddWaitStep("wait", resultsDisplayWaitCount); AddWaitStep("wait", resultsDisplayWaitCount);
AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).ResultsCreated);
} }
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
@ -110,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay
public class FakeRankingPushPlayer : TestPlayer public class FakeRankingPushPlayer : TestPlayer
{ {
public bool GotoRankingInvoked; public bool ResultsCreated { get; private set; }
public FakeRankingPushPlayer() public FakeRankingPushPlayer()
: base(true, true) : base(true, true)
{ {
} }
protected override void GotoRanking() protected override ResultsScreen CreateResults(ScoreInfo score)
{ {
GotoRankingInvoked = true; var results = base.CreateResults(score);
ResultsCreated = true;
return results;
} }
} }
} }

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -26,7 +27,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(2), Scale = new Vector2(2),
RelativeSizeAxes = Axes.X,
}); });
} }
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
playerScore.Value = 1222333; playerScore.Value = 1222333;
}); });
AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" })); AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true));
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
} }
@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var player2Score = new BindableDouble(1234567); var player2Score = new BindableDouble(1234567);
var player3Score = new BindableDouble(1111111); var player3Score = new BindableDouble(1111111);
AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" })); AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" }));
AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" })); AddStep("add player 3", () => createLeaderboardScore(player3Score, new User { Username = "Player 3" }));
AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
@ -67,6 +67,30 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
} }
[Test]
public void TestRandomScores()
{
int playerNumber = 1;
AddRepeatStep("add player with random score", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 10);
}
[Test]
public void TestExistingUsers()
{
AddStep("add peppy", () => createRandomScore(new User { Username = "peppy", Id = 2 }));
AddStep("add smoogipoo", () => createRandomScore(new User { Username = "smoogipoo", Id = 1040328 }));
AddStep("add flyte", () => createRandomScore(new User { Username = "flyte", Id = 3103765 }));
AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 }));
}
private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false)
{
var leaderboardScore = leaderboard.AddPlayer(user, isTracked);
leaderboardScore.TotalScore.BindTo(score);
}
private class TestGameplayLeaderboard : GameplayLeaderboard private class TestGameplayLeaderboard : GameplayLeaderboard
{ {
public bool CheckPositionByUsername(string username, int? expectedPosition) public bool CheckPositionByUsername(string username, int? expectedPosition)

View File

@ -0,0 +1,155 @@
// 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 System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Online;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene
{
[Cached(typeof(SpectatorStreamingClient))]
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16);
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
private MultiplayerGameplayLeaderboard leaderboard;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
public TestSceneMultiplayerGameplayLeaderboard()
{
base.Content.Children = new Drawable[]
{
streamingClient,
lookupCache,
Content
};
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create leaderboard", () =>
{
OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
Children = new Drawable[]
{
scoreProcessor = new OsuScoreProcessor(),
};
scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}, Add);
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
}
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100);
}
public class TestMultiplayerStreaming : SpectatorStreamingClient
{
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private readonly int totalUsers;
public TestMultiplayerStreaming(int totalUsers)
{
this.totalUsers = totalUsers;
}
public void Start(int beatmapId)
{
for (int i = 0; i < totalUsers; i++)
{
((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
}
}
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
public void RandomlyUpdateState()
{
foreach (var userId in PlayingUsers)
{
if (RNG.NextBool())
continue;
if (!lastHeaders.TryGetValue(userId, out var header))
{
lastHeaders[userId] = header = new FrameHeader(new ScoreInfo
{
Statistics = new Dictionary<HitResult, int>
{
[HitResult.Miss] = 0,
[HitResult.Meh] = 0,
[HitResult.Great] = 0
}
});
}
switch (RNG.Next(0, 3))
{
case 0:
header.Combo = 0;
header.Statistics[HitResult.Miss]++;
break;
case 1:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Meh]++;
break;
default:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Great]++;
break;
}
((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty<LegacyReplayFrame>()));
}
}
protected override Task Connect() => Task.CompletedTask;
}
}
}

View File

@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSpectatorStreamingClient : SpectatorStreamingClient public class TestSpectatorStreamingClient : SpectatorStreamingClient
{ {
public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; public readonly User StreamingUser = new User { Id = 55, Username = "Test user" };
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers; public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public readonly BindableList<Room> Rooms = new BindableList<Room>(); public readonly BindableList<Room> Rooms = new BindableList<Room>();
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true); public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
IBindableList<Room> IRoomManager.Rooms => Rooms; IBindableList<Room> IRoomManager.Rooms => Rooms;

View File

@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
remove { } remove { }
} }
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true); public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindableList<Room> Rooms => null; public IBindableList<Room> Rooms => null;

View File

@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
remove => throw new NotImplementedException(); remove => throw new NotImplementedException();
} }
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true); public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindableList<Room> Rooms { get; } = new BindableList<Room>(); public IBindableList<Room> Rooms { get; } = new BindableList<Room>();

View File

@ -69,8 +69,32 @@ namespace osu.Game.Tests.Visual.Online
internal class TestUserLookupCache : UserLookupCache internal class TestUserLookupCache : UserLookupCache
{ {
private static readonly string[] usernames =
{
"fieryrage",
"Kerensa",
"MillhioreF",
"Player01",
"smoogipoo",
"Ephemeral",
"BTMC",
"Cilvery",
"m980",
"HappyStick",
"LittleEndu",
"frenzibyte",
"Zallius",
"BanchoBot",
"rocketminer210",
"pishifat"
};
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
=> Task.FromResult(new User { Username = "peppy", Id = 2 }); => Task.FromResult(new User
{
Id = lookup,
Username = usernames[lookup % usernames.Length],
});
} }
} }
} }

View File

@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
} }
[Test] [Test]
public void TestMultiplayerRoomPartedWhenAPIRoomJoined() public void TestMultiplayerRoomJoinedWhenAPIRoomJoined()
{ {
AddStep("create room manager with a room", () => AddStep("create room manager with a room", () =>
{ {
@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
}); });
}); });
AddAssert("multiplayer room parted", () => roomContainer.Client.Room != null); AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
} }
private TestRealtimeRoomManager createRoomManager() private TestRealtimeRoomManager createRoomManager()

View File

@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat
{ {
CurrentChannel.ValueChanged += currentChannelChanged; CurrentChannel.ValueChanged += currentChannelChanged;
HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true); HighPollRate.BindValueChanged(enabled => TimeBetweenPolls.Value = enabled.NewValue ? 1000 : 6000, true);
} }
/// <summary> /// <summary>

View File

@ -24,8 +24,8 @@ using osu.Game.Scoring;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Humanizer;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Utils;
namespace osu.Game.Online.Leaderboards namespace osu.Game.Online.Leaderboards
{ {
@ -358,7 +358,7 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, italics: true), Font = OsuFont.GetFont(size: 20, italics: true),
Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0), Text = rank == null ? "-" : rank.Value.FormatRank()
}; };
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -19,22 +20,11 @@ namespace osu.Game.Online
private bool pollingActive; private bool pollingActive;
private double timeBetweenPolls;
/// <summary> /// <summary>
/// The time in milliseconds to wait between polls. /// The time in milliseconds to wait between polls.
/// Setting to zero stops all polling. /// Setting to zero stops all polling.
/// </summary> /// </summary>
public double TimeBetweenPolls public readonly Bindable<double> TimeBetweenPolls = new Bindable<double>();
{
get => timeBetweenPolls;
set
{
timeBetweenPolls = value;
scheduledPoll?.Cancel();
pollIfNecessary();
}
}
/// <summary> /// <summary>
/// ///
@ -42,7 +32,13 @@ namespace osu.Game.Online
/// <param name="timeBetweenPolls">The initial time in milliseconds to wait between polls. Setting to zero stops all polling.</param> /// <param name="timeBetweenPolls">The initial time in milliseconds to wait between polls. Setting to zero stops all polling.</param>
protected PollingComponent(double timeBetweenPolls = 0) protected PollingComponent(double timeBetweenPolls = 0)
{ {
TimeBetweenPolls = timeBetweenPolls; TimeBetweenPolls.BindValueChanged(_ =>
{
scheduledPoll?.Cancel();
pollIfNecessary();
});
TimeBetweenPolls.Value = timeBetweenPolls;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -60,7 +56,7 @@ namespace osu.Game.Online
if (pollingActive) return false; if (pollingActive) return false;
// don't try polling if the time between polls hasn't been set. // don't try polling if the time between polls hasn't been set.
if (timeBetweenPolls == 0) return false; if (TimeBetweenPolls.Value == 0) return false;
if (!lastTimePolled.HasValue) if (!lastTimePolled.HasValue)
{ {
@ -68,7 +64,7 @@ namespace osu.Game.Online
return true; return true;
} }
if (Time.Current - lastTimePolled.Value > timeBetweenPolls) if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value)
{ {
doPoll(); doPoll();
return true; return true;
@ -99,7 +95,7 @@ namespace osu.Game.Online
/// </summary> /// </summary>
public void PollImmediately() public void PollImmediately()
{ {
lastTimePolled = Time.Current - timeBetweenPolls; lastTimePolled = Time.Current - TimeBetweenPolls.Value;
scheduleNextPoll(); scheduleNextPoll();
} }
@ -121,7 +117,7 @@ namespace osu.Game.Online
double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0; double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0;
scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration)); scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, TimeBetweenPolls.Value - lastPollDuration));
} }
} }
} }

View File

@ -9,11 +9,13 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -69,7 +71,9 @@ namespace osu.Game.Online.RealtimeMultiplayer
private RulesetStore rulesets { get; set; } = null!; private RulesetStore rulesets { get; set; } = null!;
private Room? apiRoom; private Room? apiRoom;
private int playlistItemId; // Todo: THIS IS SUPER TEMPORARY!!
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
private int playlistItemId;
/// <summary> /// <summary>
/// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>. /// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>.
@ -142,20 +146,14 @@ namespace osu.Game.Online.RealtimeMultiplayer
RulesetID = Room.Settings.RulesetID RulesetID = Room.Settings.RulesetID
}; };
var newSettings = new MultiplayerRoomSettings ChangeSettings(new MultiplayerRoomSettings
{ {
Name = name.GetOr(Room.Settings.Name), Name = name.GetOr(Room.Settings.Name),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID, RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
Mods = item.HasValue ? item.Value!.RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods
}; });
// Make sure there would be a meaningful change in settings.
if (newSettings.Equals(Room.Settings))
return;
ChangeSettings(newSettings);
} }
public abstract Task TransferHost(int userId); public abstract Task TransferHost(int userId);
@ -356,7 +354,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
if (Room == null) if (Room == null)
return; return;
// Update a few instantaneously properties of the room. // Update a few properties of the room instantaneously.
Schedule(() => Schedule(() =>
{ {
if (Room == null) if (Room == null)
@ -375,38 +373,36 @@ namespace osu.Game.Online.RealtimeMultiplayer
}); });
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res => req.Success += res => updatePlaylist(settings, res);
{
var beatmapSet = res.ToBeatmapSet(rulesets);
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); api.Queue(req);
beatmap.MD5Hash = settings.BeatmapChecksum; }
var ruleset = rulesets.GetRuleset(settings.RulesetID); private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
var mods = settings.Mods.Select(m => m.ToMod(ruleset.CreateInstance()));
PlaylistItem playlistItem = new PlaylistItem
{
ID = playlistItemId,
Beatmap = { Value = beatmap },
Ruleset = { Value = ruleset },
};
playlistItem.RequiredMods.AddRange(mods);
Schedule(() =>
{ {
if (Room == null || !Room.Settings.Equals(settings)) if (Room == null || !Room.Settings.Equals(settings))
return; return;
Debug.Assert(apiRoom != null); Debug.Assert(apiRoom != null);
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
apiRoom.Playlist.Add(playlistItem); var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
}); beatmap.MD5Hash = settings.BeatmapChecksum;
var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
var mods = settings.Mods.Select(m => m.ToMod(ruleset));
PlaylistItem playlistItem = new PlaylistItem
{
ID = playlistItemId,
Beatmap = { Value = beatmap },
Ruleset = { Value = ruleset.RulesetInfo },
}; };
api.Queue(req); playlistItem.RequiredMods.AddRange(mods);
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
apiRoom.Playlist.Add(playlistItem);
} }
} }
} }

View File

@ -68,7 +68,12 @@ namespace osu.Game.Rulesets.Scoring
private readonly double comboPortion; private readonly double comboPortion;
private int maxAchievableCombo; private int maxAchievableCombo;
/// <summary>
/// The maximum achievable base score.
/// </summary>
private double maxBaseScore; private double maxBaseScore;
private double rollingMaxBaseScore; private double rollingMaxBaseScore;
private double baseScore; private double baseScore;
@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Scoring
private double getScore(ScoringMode mode) private double getScore(ScoringMode mode)
{ {
return GetScore(mode, maxAchievableCombo, return GetScore(mode, maxAchievableCombo,
maxBaseScore > 0 ? baseScore / maxBaseScore : 0, calculateAccuracyRatio(baseScore),
maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, calculateComboRatio(HighestCombo.Value),
scoreResultCounts); scoreResultCounts);
} }
@ -227,6 +232,37 @@ namespace osu.Game.Rulesets.Scoring
} }
} }
/// <summary>
/// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param>
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param>
/// <param name="statistics">Statistics to be used for calculating accuracy, bonus score, etc.</param>
/// <returns>The computed score and accuracy for provided inputs.</returns>
public (double score, double accuracy) GetScoreAndAccuracy(ScoringMode mode, int maxCombo, Dictionary<HitResult, int> statistics)
{
// calculate base score from statistics pairs
int computedBaseScore = 0;
foreach (var pair in statistics)
{
if (!pair.Key.AffectsAccuracy())
continue;
computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value;
}
double accuracy = calculateAccuracyRatio(computedBaseScore);
double comboRatio = calculateComboRatio(maxCombo);
double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts);
return (score, accuracy);
}
private double calculateAccuracyRatio(double baseScore) => maxBaseScore > 0 ? baseScore / maxBaseScore : 0;
private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1;
private double getBonusScore(Dictionary<HitResult, int> statistics) private double getBonusScore(Dictionary<HitResult, int> statistics)
=> statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
+ statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;

View File

@ -92,6 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
private Container dragHandles;
private FillFlowContainer buttons; private FillFlowContainer buttons;
public const float BORDER_RADIUS = 3; public const float BORDER_RADIUS = 3;
@ -151,6 +152,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
}, },
} }
}, },
dragHandles = new Container
{
RelativeSizeAxes = Axes.Both,
// ensures that the centres of all drag handles line up with the middle of the selection box border.
Padding = new MarginPadding(BORDER_RADIUS / 2)
},
buttons = new FillFlowContainer buttons = new FillFlowContainer
{ {
Y = 20, Y = 20,
@ -232,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}); });
} }
private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle
{ {
Anchor = anchor, Anchor = anchor,
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),

View File

@ -25,6 +25,7 @@ namespace osu.Game.Screens.Multi.Components
{ {
currentFilter.BindValueChanged(_ => currentFilter.BindValueChanged(_ =>
{ {
NotifyRoomsReceived(null);
if (IsLoaded) if (IsLoaded)
PollImmediately(); PollImmediately();
}); });

View File

@ -23,11 +23,13 @@ namespace osu.Game.Screens.Multi.Components
private readonly BindableList<Room> rooms = new BindableList<Room>(); private readonly BindableList<Room> rooms = new BindableList<Room>();
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(); public IBindable<bool> InitialRoomsReceived => initialRoomsReceived;
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
public IBindableList<Room> Rooms => rooms; public IBindableList<Room> Rooms => rooms;
protected Room JoinedRoom { get; private set; } protected IBindable<Room> JoinedRoom => joinedRoom;
private readonly Bindable<Room> joinedRoom = new Bindable<Room>();
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
@ -44,7 +46,6 @@ namespace osu.Game.Screens.Multi.Components
InternalChildren = CreatePollingComponents().Select(p => InternalChildren = CreatePollingComponents().Select(p =>
{ {
p.InitialRoomsReceived.BindTo(InitialRoomsReceived);
p.RoomsReceived = onRoomsReceived; p.RoomsReceived = onRoomsReceived;
return p; return p;
}).ToList(); }).ToList();
@ -64,7 +65,7 @@ namespace osu.Game.Screens.Multi.Components
req.Success += result => req.Success += result =>
{ {
JoinedRoom = room; joinedRoom.Value = room;
update(room, result); update(room, result);
addRoom(room); addRoom(room);
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Multi.Components
currentJoinRoomRequest.Success += () => currentJoinRoomRequest.Success += () =>
{ {
JoinedRoom = room; joinedRoom.Value = room;
onSuccess?.Invoke(room); onSuccess?.Invoke(room);
}; };
@ -114,14 +115,20 @@ namespace osu.Game.Screens.Multi.Components
if (JoinedRoom == null) if (JoinedRoom == null)
return; return;
api.Queue(new PartRoomRequest(JoinedRoom)); api.Queue(new PartRoomRequest(joinedRoom.Value));
JoinedRoom = null; joinedRoom.Value = null;
} }
private readonly HashSet<int> ignoredRooms = new HashSet<int>(); private readonly HashSet<int> ignoredRooms = new HashSet<int>();
private void onRoomsReceived(List<Room> received) private void onRoomsReceived(List<Room> received)
{ {
if (received == null)
{
ClearRooms();
return;
}
// Remove past matches // Remove past matches
foreach (var r in rooms.ToList()) foreach (var r in rooms.ToList())
{ {
@ -155,6 +162,7 @@ namespace osu.Game.Screens.Multi.Components
} }
RoomsUpdated?.Invoke(); RoomsUpdated?.Invoke();
initialRoomsReceived.Value = true;
} }
protected void RemoveRoom(Room room) => rooms.Remove(room); protected void RemoveRoom(Room room) => rooms.Remove(room);
@ -162,7 +170,7 @@ namespace osu.Game.Screens.Multi.Components
protected void ClearRooms() protected void ClearRooms()
{ {
rooms.Clear(); rooms.Clear();
InitialRoomsReceived.Value = false; initialRoomsReceived.Value = false;
} }
/// <summary> /// <summary>
@ -191,6 +199,6 @@ namespace osu.Game.Screens.Multi.Components
existing.CopyFrom(room); existing.CopyFrom(room);
} }
protected abstract RoomPollingComponent[] CreatePollingComponents(); protected abstract IEnumerable<RoomPollingComponent> CreatePollingComponents();
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -13,29 +12,18 @@ namespace osu.Game.Screens.Multi.Components
{ {
public abstract class RoomPollingComponent : PollingComponent public abstract class RoomPollingComponent : PollingComponent
{ {
public Action<List<Room>> RoomsReceived;
/// <summary> /// <summary>
/// The time in milliseconds to wait between polls. /// Invoked when any <see cref="Room"/>s have been received from the API.
/// Setting to zero stops all polling. /// <para>
/// Any <see cref="Room"/>s present locally but not returned by this event are to be removed from display.
/// If null, the display of local rooms is reset to an initial state.
/// </para>
/// </summary> /// </summary>
public new readonly Bindable<double> TimeBetweenPolls = new Bindable<double>(); public Action<List<Room>> RoomsReceived;
public IBindable<bool> InitialRoomsReceived => initialRoomsReceived;
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
[Resolved] [Resolved]
protected IAPIProvider API { get; private set; } protected IAPIProvider API { get; private set; }
protected RoomPollingComponent() protected void NotifyRoomsReceived(List<Room> rooms) => RoomsReceived?.Invoke(rooms);
{
TimeBetweenPolls.BindValueChanged(time => base.TimeBetweenPolls = time.NewValue);
}
protected void NotifyRoomsReceived(List<Room> rooms)
{
initialRoomsReceived.Value = true;
RoomsReceived?.Invoke(rooms);
}
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Multi
/// <summary> /// <summary>
/// Whether an initial listing of rooms has been received. /// Whether an initial listing of rooms has been received.
/// </summary> /// </summary>
Bindable<bool> InitialRoomsReceived { get; } IBindable<bool> InitialRoomsReceived { get; }
/// <summary> /// <summary>
/// All the active <see cref="Room"/>s. /// All the active <see cref="Room"/>s.

View File

@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Lounge
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>(); private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private Container content; private Container content;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;

View File

@ -46,7 +46,7 @@ namespace osu.Game.Screens.Multi
private readonly IBindable<bool> isIdle = new BindableBool(); private readonly IBindable<bool> isIdle = new BindableBool();
[Cached(Type = typeof(IRoomManager))] [Cached(Type = typeof(IRoomManager))]
protected IRoomManager RoomManager { get; private set; } protected RoomManager RoomManager { get; private set; }
[Cached] [Cached]
private readonly Bindable<Room> selectedRoom = new Bindable<Room>(); private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi
InternalChild = waves = new MultiplayerWaveContainer InternalChild = waves = new MultiplayerWaveContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new[] Children = new Drawable[]
{ {
new Box new Box
{ {
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Multi
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Action = () => CreateRoom() Action = () => CreateRoom()
}, },
(Drawable)(RoomManager = CreateRoomManager()) RoomManager = CreateRoomManager()
} }
}; };
@ -353,7 +353,7 @@ namespace osu.Game.Screens.Multi
} }
} }
protected abstract IRoomManager CreateRoomManager(); protected abstract RoomManager CreateRoomManager();
private class MultiplayerWaveContainer : WaveContainer private class MultiplayerWaveContainer : WaveContainer
{ {

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -95,19 +96,36 @@ namespace osu.Game.Screens.Multi.Play
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
} }
protected override ScoreInfo CreateScore() protected override Score CreateScore()
{ {
var score = base.CreateScore(); var score = base.CreateScore();
score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
return score;
}
protected override async Task SubmitScore(Score score)
{
await base.SubmitScore(score);
Debug.Assert(token != null); Debug.Assert(token != null);
var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score); var tcs = new TaskCompletionSource<bool>();
request.Success += s => score.OnlineScoreID = s.ID; var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo);
request.Failure += e => Logger.Error(e, "Failed to submit score");
api.Queue(request);
return score; request.Success += s =>
{
score.ScoreInfo.OnlineScoreID = s.ID;
tcs.SetResult(true);
};
request.Failure += e =>
{
Logger.Error(e, "Failed to submit score");
tcs.SetResult(false);
};
api.Queue(request);
await tcs.Task;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -30,7 +31,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
base.LoadComplete(); base.LoadComplete();
isConnected.BindTo(multiplayerClient.IsConnected); isConnected.BindTo(multiplayerClient.IsConnected);
isConnected.BindValueChanged(_ => Schedule(updatePolling), true); isConnected.BindValueChanged(_ => Schedule(updatePolling));
JoinedRoom.BindValueChanged(_ => updatePolling());
updatePolling(); updatePolling();
} }
@ -46,15 +48,18 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
if (JoinedRoom == null) if (JoinedRoom == null)
return; return;
var joinedRoom = JoinedRoom; var joinedRoom = JoinedRoom.Value;
base.PartRoom(); base.PartRoom();
multiplayerClient.LeaveRoom(); multiplayerClient.LeaveRoom();
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
RemoveRoom(joinedRoom);
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
Schedule(() => listingPollingComponent.PollImmediately()); Schedule(() =>
{
RemoveRoom(joinedRoom);
listingPollingComponent.PollImmediately();
});
} }
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null) private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null)
@ -62,7 +67,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
Debug.Assert(room.RoomID.Value != null); Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room); var joinTask = multiplayerClient.JoinRoom(room);
joinTask.ContinueWith(_ => onSuccess?.Invoke(room)); joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t => joinTask.ContinueWith(t =>
{ {
PartRoom(); PartRoom();
@ -77,10 +82,10 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
ClearRooms(); ClearRooms();
// Don't poll when not connected or when a room has been joined. // Don't poll when not connected or when a room has been joined.
allowPolling.Value = isConnected.Value && JoinedRoom == null; allowPolling.Value = isConnected.Value && JoinedRoom.Value == null;
} }
protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
{ {
listingPollingComponent = new RealtimeListingPollingComponent listingPollingComponent = new RealtimeListingPollingComponent
{ {
@ -102,8 +107,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{ {
base.LoadComplete(); base.LoadComplete();
AllowPolling.BindValueChanged(_ => AllowPolling.BindValueChanged(allowPolling =>
{ {
if (!allowPolling.NewValue)
return;
if (IsLoaded) if (IsLoaded)
PollImmediately(); PollImmediately();
}); });
@ -120,8 +128,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{ {
base.LoadComplete(); base.LoadComplete();
AllowPolling.BindValueChanged(_ => AllowPolling.BindValueChanged(allowPolling =>
{ {
if (!allowPolling.NewValue)
return;
if (IsLoaded) if (IsLoaded)
PollImmediately(); PollImmediately();
}); });

View File

@ -3,6 +3,7 @@
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match;
@ -43,6 +44,6 @@ namespace osu.Game.Screens.Multi.Timeshift
Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})");
} }
protected override IRoomManager CreateRoomManager() => new TimeshiftRoomManager(); protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager();
} }
} }

View File

@ -1,6 +1,7 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Components;
@ -11,7 +12,7 @@ namespace osu.Game.Screens.Multi.Timeshift
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>(); public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>(); public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
{ {
new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } },
new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } }

View File

@ -1,9 +1,8 @@
// 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;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Users; using osu.Game.Users;
@ -15,8 +14,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
public GameplayLeaderboard() public GameplayLeaderboard()
{ {
RelativeSizeAxes = Axes.X; Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical; Direction = FillDirection.Vertical;
@ -29,32 +27,35 @@ namespace osu.Game.Screens.Play.HUD
/// <summary> /// <summary>
/// Adds a player to the leaderboard. /// Adds a player to the leaderboard.
/// </summary> /// </summary>
/// <param name="currentScore">The bindable current score of the player.</param>
/// <param name="user">The player.</param> /// <param name="user">The player.</param>
public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) /// <param name="isTracked">
/// Whether the player should be tracked on the leaderboard.
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
/// </param>
public ILeaderboardScore AddPlayer(User user, bool isTracked)
{ {
var scoreItem = addScore(currentScore.Value, user); var drawable = new GameplayLeaderboardScore(user, isTracked)
currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue;
}
private GameplayLeaderboardScore addScore(double totalScore, User user)
{ {
var scoreItem = new GameplayLeaderboardScore Anchor = Anchor.TopRight,
{ Origin = Anchor.TopRight,
User = user,
TotalScore = totalScore,
OnScoreChange = updateScores,
}; };
Add(scoreItem); base.Add(drawable);
updateScores(); drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true);
return scoreItem; Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y);
return drawable;
} }
private void updateScores() public sealed override void Add(GameplayLeaderboardScore drawable)
{ {
var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); throw new NotSupportedException($"Use {nameof(AddPlayer)} instead.");
}
private void sort()
{
var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList();
for (int i = 0; i < Count; i++) for (int i = 0; i < Count; i++)
{ {

View File

@ -1,25 +1,39 @@
// 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;
using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class GameplayLeaderboardScore : CompositeDrawable public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
{ {
private readonly OsuSpriteText positionText, positionSymbol, userString; public const float EXTENDED_WIDTH = 255f;
private readonly GlowingSpriteText scoreText;
public Action OnScoreChange; private const float regular_width = 235f;
public const float PANEL_HEIGHT = 35f;
public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear;
private const float panel_shear = 0.15f;
private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText;
public BindableDouble TotalScore { get; } = new BindableDouble();
public BindableDouble Accuracy { get; } = new BindableDouble(1);
public BindableInt Combo { get; } = new BindableInt();
private int? scorePosition; private int? scorePosition;
@ -28,109 +42,249 @@ namespace osu.Game.Screens.Play.HUD
get => scorePosition; get => scorePosition;
set set
{ {
if (value == scorePosition)
return;
scorePosition = value; scorePosition = value;
if (scorePosition.HasValue) if (scorePosition.HasValue)
positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; positionText.Text = $"#{scorePosition.Value.FormatRank()}";
positionText.FadeTo(scorePosition.HasValue ? 1 : 0); positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0); updateColour();
} }
} }
private double totalScore; public User User { get; }
public double TotalScore private readonly bool trackedPlayer;
{
get => totalScore;
set
{
totalScore = value;
scoreText.Text = totalScore.ToString("N0");
OnScoreChange?.Invoke(); private Container mainFillContainer;
} private Box centralFill;
}
private User user; /// <summary>
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary>
/// <param name="user">The score's player.</param>
/// <param name="trackedPlayer">Whether the player is the local user or a replay player.</param>
public GameplayLeaderboardScore(User user, bool trackedPlayer)
{
User = user;
this.trackedPlayer = trackedPlayer;
public User User Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT);
{
get => user;
set
{
user = value;
userString.Text = user?.Username;
}
}
public GameplayLeaderboardScore()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
{
Masking = true,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Right = 2.5f },
Spacing = new Vector2(2.5f),
Children = new[]
{
positionText = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
},
positionSymbol = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
Text = ">",
},
}
},
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = 2.5f },
Spacing = new Vector2(2.5f),
Children = new Drawable[]
{
userString = new OsuSpriteText
{
Size = new Vector2(80, 16),
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
},
scoreText = new GlowingSpriteText
{
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Font = OsuFont.Numeric.With(size: 14),
}
}
},
},
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
positionText.Colour = colours.YellowLight; InternalChildren = new Drawable[]
positionSymbol.Colour = colours.Yellow; {
mainFillContainer = new Container
{
Width = regular_width,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
Child = new Box
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
}
},
new GridContainer
{
Width = regular_width,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 35f),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 85f),
},
Content = new[]
{
new Drawable[]
{
positionText = new OsuSpriteText
{
Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold),
Shadow = false,
},
new Container
{
Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
RelativeSizeAxes = Axes.Both,
Children = new[]
{
centralFill = new Box
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("3399cc"),
},
}
},
new FillFlowContainer
{
Padding = new MarginPadding { Left = SHEAR_WIDTH },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4f, 0f),
Children = new Drawable[]
{
new CircularContainer
{
Masking = true,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(25f),
Children = new Drawable[]
{
new Box
{
Name = "Placeholder while avatar loads",
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
},
new UpdateableAvatar(User)
{
RelativeSizeAxes = Axes.Both,
},
}
},
usernameText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User.Username,
Truncate = true,
Shadow = false,
}
}
},
}
},
new Container
{
Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f },
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Children = new Drawable[]
{
scoreText = new OsuSpriteText
{
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
accuracyText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1f, 0f),
Shadow = false,
},
comboText = new OsuSpriteText
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
},
}
}
}
}
};
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true);
Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true);
}
protected override void LoadComplete()
{
base.LoadComplete();
updateColour();
FinishTransforms(true);
}
private const double panel_transition_duration = 500;
private void updateColour()
{
if (scorePosition == 1)
{
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
panelColour = Color4Extensions.FromHex("7fcc33");
textColour = Color4.White;
}
else if (trackedPlayer)
{
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
panelColour = Color4Extensions.FromHex("ffd966");
textColour = Color4Extensions.FromHex("2e576b");
}
else
{
mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
panelColour = Color4Extensions.FromHex("3399cc");
textColour = Color4.White;
}
}
private Color4 panelColour
{
set
{
mainFillContainer.FadeColour(value, panel_transition_duration, Easing.OutQuint);
centralFill.FadeColour(value, panel_transition_duration, Easing.OutQuint);
}
}
private const double text_transition_duration = 200;
private Color4 textColour
{
set
{
scoreText.FadeColour(value, text_transition_duration, Easing.OutQuint);
accuracyText.FadeColour(value, text_transition_duration, Easing.OutQuint);
comboText.FadeColour(value, text_transition_duration, Easing.OutQuint);
usernameText.FadeColour(value, text_transition_duration, Easing.OutQuint);
positionText.FadeColour(value, text_transition_duration, Easing.OutQuint);
}
} }
} }
} }

View File

@ -0,0 +1,14 @@
// 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 osu.Framework.Bindables;
namespace osu.Game.Screens.Play.HUD
{
public interface ILeaderboardScore
{
BindableDouble TotalScore { get; }
BindableDouble Accuracy { get; }
BindableInt Combo { get; }
}
}

View File

@ -0,0 +1,131 @@
// 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.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD
{
[LongRunningLoad]
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
{
private readonly ScoreProcessor scoreProcessor;
private readonly int[] userIds;
private readonly Dictionary<int, TrackedUserData> userScores = new Dictionary<int, TrackedUserData>();
/// <summary>
/// Construct a new leaderboard.
/// </summary>
/// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param>
/// <param name="userIds">IDs of all users in this match.</param>
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
{
// todo: this will eventually need to be created per user to support different mod combinations.
this.scoreProcessor = scoreProcessor;
// todo: this will likely be passed in as User instances.
this.userIds = userIds;
}
[Resolved]
private SpectatorStreamingClient streamingClient { get; set; }
[Resolved]
private UserLookupCache userLookupCache { get; set; }
private Bindable<ScoringMode> scoringMode;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
streamingClient.OnNewFrames += handleIncomingFrames;
foreach (var user in userIds)
{
streamingClient.WatchUser(user);
// probably won't be required in the final implementation.
var resolvedUser = userLookupCache.GetUserAsync(user).Result;
var trackedUser = new TrackedUserData();
userScores[user] = trackedUser;
var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id);
((IBindable<double>)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
((IBindable<double>)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
((IBindable<int>)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
}
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoringMode.BindValueChanged(updateAllScores, true);
}
private void updateAllScores(ValueChangedEvent<ScoringMode> mode)
{
foreach (var trackedData in userScores.Values)
trackedData.UpdateScore(scoreProcessor, mode.NewValue);
}
private void handleIncomingFrames(int userId, FrameDataBundle bundle)
{
if (userScores.TryGetValue(userId, out var trackedData))
{
trackedData.LastHeader = bundle.Header;
trackedData.UpdateScore(scoreProcessor, scoringMode.Value);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (streamingClient != null)
{
foreach (var user in userIds)
{
streamingClient.StopWatchingUser(user);
}
streamingClient.OnNewFrames -= handleIncomingFrames;
}
}
private class TrackedUserData
{
public IBindableNumber<double> Score => score;
private readonly BindableDouble score = new BindableDouble();
public IBindableNumber<double> Accuracy => accuracy;
private readonly BindableDouble accuracy = new BindableDouble(1);
public IBindableNumber<int> CurrentCombo => currentCombo;
private readonly BindableInt currentCombo = new BindableInt();
[CanBeNull]
public FrameHeader LastHeader;
public void UpdateScore(ScoreProcessor processor, ScoringMode mode)
{
if (LastHeader == null)
return;
(score.Value, accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics);
currentCombo.Value = LastHeader.Combo;
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -22,8 +23,10 @@ using osu.Game.Graphics.Containers;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -501,6 +504,7 @@ namespace osu.Game.Screens.Play
} }
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
private Task<ScoreInfo> scoreSubmissionTask;
private void updateCompletionState(ValueChangedEvent<bool> completionState) private void updateCompletionState(ValueChangedEvent<bool> completionState)
{ {
@ -527,33 +531,50 @@ namespace osu.Game.Screens.Play
if (!showResults) return; if (!showResults) return;
scoreSubmissionTask ??= Task.Run(async () =>
{
var score = CreateScore();
try
{
await SubmitScore(score);
}
catch (Exception ex)
{
Logger.Error(ex, "Score submission failed!");
}
try
{
await ImportScore(score);
}
catch (Exception ex)
{
Logger.Error(ex, "Score import failed!");
}
return score.ScoreInfo;
});
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
completionProgressDelegate = Schedule(GotoRanking); scheduleCompletion();
} }
protected virtual ScoreInfo CreateScore() private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
{ {
var score = new ScoreInfo if (!scoreSubmissionTask.IsCompleted)
{ {
Beatmap = Beatmap.Value.BeatmapInfo, scheduleCompletion();
Ruleset = rulesetInfo, return;
Mods = Mods.Value.ToArray(),
};
if (DrawableRuleset.ReplayScore != null)
score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
else
score.User = api.LocalUser.Value;
ScoreProcessor.PopulateScore(score);
return score;
} }
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(scoreSubmissionTask.Result));
});
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
#region Fail Logic #region Fail Logic
protected FailOverlay FailOverlay { get; private set; } protected FailOverlay FailOverlay { get; private set; }
@ -748,38 +769,73 @@ namespace osu.Game.Screens.Play
return base.OnExiting(next); return base.OnExiting(next);
} }
protected virtual void GotoRanking() /// <summary>
/// Creates the player's <see cref="Score"/>.
/// </summary>
/// <returns>The <see cref="Score"/>.</returns>
protected virtual Score CreateScore()
{ {
var score = new Score
{
ScoreInfo = new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
}
};
if (DrawableRuleset.ReplayScore != null) if (DrawableRuleset.ReplayScore != null)
{ {
// if a replay is present, we likely don't want to import into the local database. score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
this.Push(CreateResults(CreateScore())); score.Replay = DrawableRuleset.ReplayScore.Replay;
return; }
else
{
score.ScoreInfo.User = api.LocalUser.Value;
score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List<ReplayFrame>() };
} }
LegacyByteArrayReader replayReader = null; ScoreProcessor.PopulateScore(score.ScoreInfo);
var score = new Score { ScoreInfo = CreateScore() }; return score;
}
if (recordingScore?.Replay.Frames.Count > 0) /// <summary>
/// Imports the player's <see cref="Score"/> to the local database.
/// </summary>
/// <param name="score">The <see cref="Score"/> to import.</param>
/// <returns>The imported score.</returns>
protected virtual Task ImportScore(Score score)
{ {
score.Replay = recordingScore.Replay; // Replays are already populated and present in the game's database, so should not be re-imported.
if (DrawableRuleset.ReplayScore != null)
return Task.CompletedTask;
LegacyByteArrayReader replayReader;
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
} }
return scoreManager.Import(score.ScoreInfo, replayReader);
} }
scoreManager.Import(score.ScoreInfo, replayReader) /// <summary>
.ContinueWith(imported => Schedule(() => /// Submits the player's <see cref="Score"/>.
{ /// </summary>
// screen may be in the exiting transition phase. /// <param name="score">The <see cref="Score"/> to submit.</param>
if (this.IsCurrentScreen()) /// <returns>The submitted score.</returns>
this.Push(CreateResults(imported.Result)); protected virtual Task SubmitScore(Score score) => Task.CompletedTask;
}));
} /// <summary>
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to be displayed in the results screen.</param>
/// <returns>The <see cref="ResultsScreen"/>.</returns>
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
private void fadeOut(bool instant = false) private void fadeOut(bool instant = false)
{ {

View File

@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -26,18 +27,21 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(Score); DrawableRuleset?.SetReplayScore(Score);
} }
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); protected override Score CreateScore()
protected override ScoreInfo CreateScore()
{ {
var baseScore = base.CreateScore(); var baseScore = base.CreateScore();
// Since the replay score doesn't contain statistics, we'll pass them through here. // Since the replay score doesn't contain statistics, we'll pass them through here.
Score.ScoreInfo.HitEvents = baseScore.HitEvents; Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents;
return Score.ScoreInfo; return Score;
} }
// Don't re-import replay scores as they're already present in the database.
protected override Task ImportScore(Score score) => Task.CompletedTask;
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
{ {
switch (action) switch (action)

View File

@ -106,14 +106,14 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer
return Task.CompletedTask; return Task.CompletedTask;
} }
public override async Task StartMatch() public override Task StartMatch()
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
await ((IMultiplayerClient)this).LoadRequested(); return ((IMultiplayerClient)this).LoadRequested();
} }
} }
} }

View File

@ -1,6 +1,8 @@
// 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 Humanizer;
namespace osu.Game.Utils namespace osu.Game.Utils
{ {
public static class FormatUtils public static class FormatUtils
@ -18,5 +20,11 @@ namespace osu.Game.Utils
/// <param name="accuracy">The accuracy to be formatted</param> /// <param name="accuracy">The accuracy to be formatted</param>
/// <returns>formatted accuracy in percentage</returns> /// <returns>formatted accuracy in percentage</returns>
public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%"; public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%";
/// <summary>
/// Formats the supplied rank/leaderboard position in a consistent, simplified way.
/// </summary>
/// <param name="rank">The rank/position to be formatted.</param>
public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0);
} }
} }