1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 01:52:55 +08:00

Merge branch 'master' into fix-texture-loader-usages

This commit is contained in:
Dean Herbert 2020-12-21 13:51:55 +09:00 committed by GitHub
commit bc212b2538
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 3181 additions and 765 deletions

View File

@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components
{
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(2);
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);
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components
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(7);
}
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components
{
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);
skip();
checkCount(0);
@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components
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.Rulesets;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Storyboards;
using osuTK;
@ -50,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay
cancel();
complete();
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked);
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated);
}
/// <summary>
@ -84,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
// wait to ensure there was no attempt of pushing the results screen.
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)
@ -110,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay
public class FakeRankingPushPlayer : TestPlayer
{
public bool GotoRankingInvoked;
public bool ResultsCreated { get; private set; }
public FakeRankingPushPlayer()
: 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.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osuTK;
@ -26,7 +27,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2),
RelativeSizeAxes = Axes.X,
});
}
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
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);
}
@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var player2Score = new BindableDouble(1234567);
var player3Score = new BindableDouble(1111111);
AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" }));
AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" }));
AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" }));
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 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));
}
[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
{
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 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;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
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;

View File

@ -4,7 +4,6 @@
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Users;
@ -14,10 +13,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneLoungeRoomInfo : MultiplayerTestScene
{
[SetUp]
public void Setup() => Schedule(() =>
public new void Setup() => Schedule(() =>
{
Room = new Room();
Child = new RoomInfo
{
Anchor = Anchor.Centre,

View File

@ -24,10 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesetStore { get; set; }
[SetUp]
public void Setup() => Schedule(() =>
public new void Setup() => Schedule(() =>
{
Room = new Room();
Child = new MatchBeatmapDetailArea
{
Anchor = Anchor.Centre,

View File

@ -1,6 +1,7 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
@ -14,7 +15,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public TestSceneMatchHeader()
{
Room = new Room();
Child = new Header();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.Playlist.Add(new PlaylistItem
{
Beatmap =
@ -41,8 +47,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
Room.Name.Value = "A very awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Child = new Header();
}
});
}
}

View File

@ -3,10 +3,10 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users;
using osuTK;
@ -19,8 +19,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestSceneMatchLeaderboard()
{
Room = new Room { RoomID = { Value = 3 } };
Add(new MatchLeaderboard
{
Origin = Anchor.Centre,
@ -40,6 +38,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
api.Queue(req);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.RoomID.Value = 3;
});
private class GetRoomScoresRequest : APIRequest<List<RoomScore>>
{
protected override string Target => "rooms/3/leaderboard";

View File

@ -23,10 +23,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestRoomSettings settings;
[SetUp]
public void Setup() => Schedule(() =>
public new void Setup() => Schedule(() =>
{
Room = new Room();
settings = new TestRoomSettings
{
RelativeSizeAxes = Axes.Both,
@ -133,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
remove { }
}
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindableList<Room> Rooms => null;

View File

@ -14,7 +14,6 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -94,12 +93,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
}
[SetUp]
public void Setup() => Schedule(() =>
{
Room = new Room();
});
[Test]
public void TestItemAddedIfEmptyOnStart()
{

View File

@ -45,12 +45,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
}
[SetUp]
public void Setup() => Schedule(() =>
{
Room = new Room();
});
[SetUpSteps]
public void SetupSteps()
{
@ -157,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
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>();

View File

@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Overlays;
using osu.Game.Screens.Multi.Timeshift;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestSceneMultiScreen()
{
Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer();
var multi = new TimeshiftMultiplayer();
AddStep("show", () => LoadScreen(multi));
AddUntilStep("wait for loaded", () => multi.IsLoaded);

View File

@ -1,50 +0,0 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneOverlinedParticipants : MultiplayerTestScene
{
protected override bool UseOnlineAPI => true;
[SetUp]
public void Setup() => Schedule(() =>
{
Room = new Room { RoomID = { Value = 7 } };
});
[Test]
public void TestHorizontalLayout()
{
AddStep("create component", () =>
{
Child = new ParticipantsDisplay(Direction.Horizontal)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
};
});
}
[Test]
public void TestVerticalLayout()
{
AddStep("create component", () =>
{
Child = new ParticipantsDisplay(Direction.Vertical)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500)
};
});
}
}
}

View File

@ -1,39 +0,0 @@
// 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.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneOverlinedPlaylist : MultiplayerTestScene
{
protected override bool UseOnlineAPI => true;
public TestSceneOverlinedPlaylist()
{
Room = new Room { RoomID = { Value = 7 } };
for (int i = 0; i < 10; i++)
{
Room.Playlist.Add(new PlaylistItem
{
ID = i,
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
});
}
Add(new DrawableRoomPlaylist(false, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500),
});
}
}
}

View File

@ -3,24 +3,55 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneParticipantsList : MultiplayerTestScene
{
protected override bool UseOnlineAPI => true;
[SetUp]
public void Setup() => Schedule(() =>
public new void Setup() => Schedule(() =>
{
Room = new Room { RoomID = { Value = 7 } };
Room.RoomID.Value = 7;
for (int i = 0; i < 50; i++)
{
Room.RecentParticipants.Add(new User
{
Username = "peppy",
Id = 2
});
}
});
public TestSceneParticipantsList()
[Test]
public void TestHorizontalLayout()
{
Add(new ParticipantsList { RelativeSizeAxes = Axes.Both });
AddStep("create component", () =>
{
Child = new ParticipantsDisplay(Direction.Horizontal)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.2f,
};
});
}
[Test]
public void TestVerticalLayout()
{
AddStep("create component", () =>
{
Child = new ParticipantsDisplay(Direction.Vertical)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.2f,
Height = 0.2f,
};
});
}
}
}

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer;
@ -22,18 +23,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
new DrawableRoom(new Room
{
Name = { Value = "Room 1" },
Status = { Value = new RoomStatusOpen() }
}),
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Room 2" },
Status = { Value = new RoomStatusPlaying() }
}),
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Room 3" },
Status = { Value = new RoomStatusEnded() }
}),
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
}
};
}

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Screens.Multi.Timeshift;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options;
@ -107,14 +108,14 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestExitMultiWithEscape()
{
PushAndConfirm(() => new Screens.Multi.Multiplayer());
PushAndConfirm(() => new TimeshiftMultiplayer());
exitViaEscapeAndConfirm();
}
[Test]
public void TestExitMultiWithBackButton()
{
PushAndConfirm(() => new Screens.Multi.Multiplayer());
PushAndConfirm(() => new TimeshiftMultiplayer());
exitViaBackButtonAndConfirm();
}

View File

@ -69,8 +69,32 @@ namespace osu.Game.Tests.Visual.Online
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)
=> Task.FromResult(new User { Username = "peppy", Id = 2 });
=> Task.FromResult(new User
{
Id = lookup,
Username = usernames[lookup % usernames.Length],
});
}
}
}

View File

@ -0,0 +1,116 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneParticipantsList : RealtimeMultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = new ParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
};
});
[Test]
public void TestAddUser()
{
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 1);
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddAssert("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 2);
}
[Test]
public void TestRemoveUser()
{
User secondUser = null;
AddStep("add a user", () =>
{
Client.AddUser(secondUser = new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
});
AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value));
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.User == secondUser);
}
[Test]
public void TestToggleReadyState()
{
AddAssert("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready));
AddUntilStep("ready mark visible", () => this.ChildrenOfType<ReadyMark>().Single().IsPresent);
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<ReadyMark>().Single().IsPresent);
}
[Test]
public void TestCrownChangesStateWhenHostTransferred()
{
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddUntilStep("first user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(0).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
AddUntilStep("second user crown hidden", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddStep("make second user host", () => Client.TransferHost(3));
AddUntilStep("first user crown hidden", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(0).ChildrenOfType<SpriteIcon>().First().Alpha == 0);
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
}
[Test]
public void TestManyUsers()
{
AddStep("add many users", () =>
{
for (int i = 0; i < 20; i++)
{
Client.AddUser(new User
{
Id = i,
Username = $"User {i}",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
if (i % 2 == 0)
Client.ChangeUserState(i, MultiplayerUserState.Ready);
}
});
}
}
}

View File

@ -0,0 +1,135 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene
{
private RealtimeReadyButton button;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
Child = button = new RealtimeReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
SelectedItem =
{
Value = new PlaylistItem
{
Beatmap = { Value = beatmap },
Ruleset = { Value = beatmap.Ruleset }
}
}
};
Client.AddUser(API.LocalUser.Value);
});
[Test]
public void TestToggleStateWhenNotHost()
{
AddStep("add second user as host", () =>
{
Client.AddUser(new User { Id = 2, Username = "Another user" });
Client.TransferHost(2);
});
addClickButtonStep();
AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
AddStep("setup", () =>
{
Client.TransferHost(Client.Room?.Users[0].UserID ?? 0);
if (!allReady)
Client.AddUser(new User { Id = 2, Username = "Another user" });
});
addClickButtonStep();
AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestBecomeHostWhileReady()
{
AddStep("add host", () =>
{
Client.AddUser(new User { Id = 2, Username = "Another user" });
Client.TransferHost(2);
});
addClickButtonStep();
AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0));
addClickButtonStep();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestLoseHostWhileReady()
{
AddStep("setup", () =>
{
Client.TransferHost(Client.Room?.Users[0].UserID ?? 0);
Client.AddUser(new User { Id = 2, Username = "Another user" });
});
addClickButtonStep();
AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0));
addClickButtonStep();
AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
private void addClickButtonStep() => AddStep("click button", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
}
}

View File

@ -0,0 +1,153 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
[HeadlessTest]
public class TestSceneRealtimeRoomManager : MultiplayerTestScene
{
private TestRealtimeRoomContainer roomContainer;
private TestRealtimeRoomManager roomManager => roomContainer.RoomManager;
[Test]
public void TestPollsInitially()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room { Name = { Value = "1" } });
roomManager.PartRoom();
roomManager.CreateRoom(new Room { Name = { Value = "2" } });
roomManager.PartRoom();
roomManager.ClearRooms();
});
});
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsClearedOnDisconnection()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
});
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsPolledOnReconnect()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
});
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddStep("connect", () => roomContainer.Client.Connect());
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsNotPolledWhenJoined()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.ClearRooms();
});
});
AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestMultiplayerRoomJoinedWhenCreated()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
});
});
AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
}
[Test]
public void TestMultiplayerRoomPartedWhenAPIRoomParted()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(new Room());
roomManager.PartRoom();
});
});
AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null);
}
[Test]
public void TestMultiplayerRoomJoinedWhenAPIRoomJoined()
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
var r = new Room();
roomManager.CreateRoom(r);
roomManager.PartRoom();
roomManager.JoinRoom(r);
});
});
AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
}
private TestRealtimeRoomManager createRoomManager()
{
Child = roomContainer = new TestRealtimeRoomContainer
{
RoomManager =
{
TimeBetweenListingPolls = { Value = 1 },
TimeBetweenSelectionPolls = { Value = 1 }
}
};
return roomManager;
}
}
}

View File

@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests
{
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
{
private readonly int id;
private readonly BeatmapSetLookupType type;
public readonly int ID;
public readonly BeatmapSetLookupType Type;
public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId)
{
this.id = id;
this.type = type;
ID = id;
Type = type;
}
protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}";
protected override string Target => Type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{ID}" : $@"beatmapsets/lookup?beatmap_id={ID}";
}
public enum BeatmapSetLookupType

View File

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

View File

@ -24,8 +24,8 @@ using osu.Game.Scoring;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
using Humanizer;
using osu.Game.Online.API;
using osu.Game.Utils;
namespace osu.Game.Online.Leaderboards
{
@ -358,7 +358,7 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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

@ -10,11 +10,11 @@ namespace osu.Game.Online.Multiplayer
{
public class CreateRoomRequest : APIRequest<APICreatedRoom>
{
private readonly Room room;
public readonly Room Room;
public CreateRoomRequest(Room room)
{
this.room = room;
Room = room;
}
protected override WebRequest CreateWebRequest()
@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer
req.ContentType = "application/json";
req.Method = HttpMethod.Post;
req.AddRaw(JsonConvert.SerializeObject(room));
req.AddRaw(JsonConvert.SerializeObject(Room));
return req;
}

View File

@ -7,13 +7,13 @@ namespace osu.Game.Online.Multiplayer
{
public class GetRoomRequest : APIRequest<Room>
{
private readonly int roomId;
public readonly int RoomId;
public GetRoomRequest(int roomId)
{
this.roomId = roomId;
RoomId = roomId;
}
protected override string Target => $"rooms/{roomId}";
protected override string Target => $"rooms/{RoomId}";
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
@ -19,22 +20,11 @@ namespace osu.Game.Online
private bool pollingActive;
private double timeBetweenPolls;
/// <summary>
/// The time in milliseconds to wait between polls.
/// Setting to zero stops all polling.
/// </summary>
public double TimeBetweenPolls
{
get => timeBetweenPolls;
set
{
timeBetweenPolls = value;
scheduledPoll?.Cancel();
pollIfNecessary();
}
}
public readonly Bindable<double> TimeBetweenPolls = new Bindable<double>();
/// <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>
protected PollingComponent(double timeBetweenPolls = 0)
{
TimeBetweenPolls = timeBetweenPolls;
TimeBetweenPolls.BindValueChanged(_ =>
{
scheduledPoll?.Cancel();
pollIfNecessary();
});
TimeBetweenPolls.Value = timeBetweenPolls;
}
protected override void LoadComplete()
@ -60,7 +56,7 @@ namespace osu.Game.Online
if (pollingActive) return false;
// 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)
{
@ -68,7 +64,7 @@ namespace osu.Game.Online
return true;
}
if (Time.Current - lastTimePolled.Value > timeBetweenPolls)
if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value)
{
doPoll();
return true;
@ -99,7 +95,7 @@ namespace osu.Game.Online
/// </summary>
public void PollImmediately()
{
lastTimePolled = Time.Current - timeBetweenPolls;
lastTimePolled = Time.Current - TimeBetweenPolls.Value;
scheduleNextPoll();
}
@ -121,7 +117,7 @@ namespace osu.Game.Online
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

@ -0,0 +1,408 @@
// 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.
#nullable enable
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Online.RealtimeMultiplayer
{
public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
{
/// <summary>
/// Invoked when any change occurs to the multiplayer room.
/// </summary>
public event Action? RoomChanged;
/// <summary>
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
/// </summary>
public event Action? LoadRequested;
/// <summary>
/// Invoked when the multiplayer server requests gameplay to be started.
/// </summary>
public event Action? MatchStarted;
/// <summary>
/// Invoked when the multiplayer server has finished collating results.
/// </summary>
public event Action? ResultsReady;
/// <summary>
/// Whether the <see cref="StatefulMultiplayerClient"/> is currently connected.
/// </summary>
public abstract IBindable<bool> IsConnected { get; }
/// <summary>
/// The joined <see cref="MultiplayerRoom"/>.
/// </summary>
public MultiplayerRoom? Room { get; private set; }
/// <summary>
/// The users currently in gameplay.
/// </summary>
public readonly BindableList<int> PlayingUsers = new BindableList<int>();
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private Room? apiRoom;
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
private int playlistItemId;
/// <summary>
/// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>.
/// </summary>
/// <param name="room">The API <see cref="Room"/>.</param>
public async Task JoinRoom(Room room)
{
if (Room != null)
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
Debug.Assert(room.RoomID.Value != null);
apiRoom = room;
playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0;
Room = await JoinRoom(room.RoomID.Value.Value);
Debug.Assert(Room != null);
foreach (var user in Room.Users)
await PopulateUser(user);
updateLocalRoomSettings(Room.Settings);
}
/// <summary>
/// Joins the <see cref="MultiplayerRoom"/> with a given ID.
/// </summary>
/// <param name="roomId">The room ID.</param>
/// <returns>The joined <see cref="MultiplayerRoom"/>.</returns>
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId);
public virtual Task LeaveRoom()
{
if (Room == null)
return Task.CompletedTask;
apiRoom = null;
Room = null;
Schedule(() => RoomChanged?.Invoke());
return Task.CompletedTask;
}
/// <summary>
/// Change the current <see cref="MultiplayerRoom"/> settings.
/// </summary>
/// <remarks>
/// A room must be joined for this to have any effect.
/// </remarks>
/// <param name="name">The new room name, if any.</param>
/// <param name="item">The new room playlist item, if any.</param>
public void ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
{
if (Room == null)
return;
// A dummy playlist item filled with the current room settings (except mods).
var existingPlaylistItem = new PlaylistItem
{
Beatmap =
{
Value = new BeatmapInfo
{
OnlineBeatmapID = Room.Settings.BeatmapID,
MD5Hash = Room.Settings.BeatmapChecksum
}
},
RulesetID = Room.Settings.RulesetID
};
ChangeSettings(new MultiplayerRoomSettings
{
Name = name.GetOr(Room.Settings.Name),
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods
});
}
public abstract Task TransferHost(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
public abstract Task ChangeState(MultiplayerUserState newState);
public abstract Task StartMatch();
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Room.State = state;
switch (state)
{
case MultiplayerRoomState.Open:
apiRoom.Status.Value = new RoomStatusOpen();
break;
case MultiplayerRoomState.Playing:
apiRoom.Status.Value = new RoomStatusPlaying();
break;
case MultiplayerRoomState.Closed:
apiRoom.Status.Value = new RoomStatusEnded();
break;
}
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
{
if (Room == null)
return;
await PopulateUser(user);
Schedule(() =>
{
if (Room == null)
return;
Room.Users.Add(user);
RoomChanged?.Invoke();
});
}
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Room.Users.Remove(user);
PlayingUsers.Remove(user.UserID);
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.HostChanged(int userId)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Debug.Assert(apiRoom != null);
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
Room.Host = user;
apiRoom.Host.Value = user?.User;
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
{
updateLocalRoomSettings(newSettings);
return Task.CompletedTask;
}
Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state)
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
Room.Users.Single(u => u.UserID == userId).State = state;
if (state != MultiplayerUserState.Playing)
PlayingUsers.Remove(userId);
RoomChanged?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.LoadRequested()
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
LoadRequested?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.MatchStarted()
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID));
MatchStarted?.Invoke();
});
return Task.CompletedTask;
}
Task IMultiplayerClient.ResultsReady()
{
if (Room == null)
return Task.CompletedTask;
Schedule(() =>
{
if (Room == null)
return;
ResultsReady?.Invoke();
});
return Task.CompletedTask;
}
/// <summary>
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
/// </summary>
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
/// <summary>
/// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.
/// </summary>
/// <remarks>
/// This updates both the joined <see cref="MultiplayerRoom"/> and the respective API <see cref="Room"/>.
/// </remarks>
/// <param name="settings">The new <see cref="MultiplayerRoomSettings"/> to update from.</param>
private void updateLocalRoomSettings(MultiplayerRoomSettings settings)
{
if (Room == null)
return;
// Update a few properties of the room instantaneously.
Schedule(() =>
{
if (Room == null)
return;
Debug.Assert(apiRoom != null);
Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name;
// The playlist update is delayed until an online beatmap lookup (below) succeeds.
// In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here.
apiRoom.Playlist.Clear();
RoomChanged?.Invoke();
});
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res => updatePlaylist(settings, res);
api.Queue(req);
}
private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
{
if (Room == null || !Room.Settings.Equals(settings))
return;
Debug.Assert(apiRoom != null);
var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
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 },
};
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 int maxAchievableCombo;
/// <summary>
/// The maximum achievable base score.
/// </summary>
private double maxBaseScore;
private double rollingMaxBaseScore;
private double baseScore;
@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Scoring
private double getScore(ScoringMode mode)
{
return GetScore(mode, maxAchievableCombo,
maxBaseScore > 0 ? baseScore / maxBaseScore : 0,
maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1,
calculateAccuracyRatio(baseScore),
calculateComboRatio(HighestCombo.Value),
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)
=> statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_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;
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
{
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,
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),

View File

@ -17,7 +17,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Timeshift;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.Menu
@ -104,7 +104,7 @@ namespace osu.Game.Screens.Menu
this.Push(new Editor());
},
OnSolo = onSolo,
OnMulti = delegate { this.Push(new Multiplayer()); },
OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); },
OnExit = confirmAndExit,
}
}

View File

@ -0,0 +1,69 @@
// 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.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Lounge.Components;
namespace osu.Game.Screens.Multi.Components
{
/// <summary>
/// A <see cref="RoomPollingComponent"/> that polls for the lounge listing.
/// </summary>
public class ListingPollingComponent : RoomPollingComponent
{
[Resolved]
private Bindable<FilterCriteria> currentFilter { get; set; }
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
[BackgroundDependencyLoader]
private void load()
{
currentFilter.BindValueChanged(_ =>
{
NotifyRoomsReceived(null);
if (IsLoaded)
PollImmediately();
});
}
private GetRoomsRequest pollReq;
protected override Task Poll()
{
if (!API.IsLoggedIn)
return base.Poll();
var tcs = new TaskCompletionSource<bool>();
pollReq?.Cancel();
pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category);
pollReq.Success += result =>
{
for (int i = 0; i < result.Count; i++)
{
if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value)
{
// The listing request always has less information than the opened room, so don't include it.
result[i] = selectedRoom.Value;
break;
}
}
NotifyRoomsReceived(result);
tcs.SetResult(true);
};
pollReq.Failure += _ => tcs.SetResult(false);
API.Queue(pollReq);
return tcs.Task;
}
}
}

View File

@ -11,28 +11,22 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.Multi.Match.Components
namespace osu.Game.Screens.Multi.Components
{
public class ReadyButton : TriangleButton
public abstract class ReadyButton : TriangleButton
{
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
[Resolved(typeof(Room), nameof(Room.EndDate))]
private Bindable<DateTimeOffset> endDate { get; set; }
public new readonly BindableBool Enabled = new BindableBool();
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; }
protected IBindable<WorkingBeatmap> GameBeatmap { get; private set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
private bool hasBeatmap;
public ReadyButton()
{
Text = "Start";
}
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
@ -45,10 +39,6 @@ namespace osu.Game.Screens.Multi.Match.Components
managerRemoved.BindValueChanged(beatmapRemoved);
SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true);
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
private void updateSelectedItem(PlaylistItem item)
@ -94,15 +84,13 @@ namespace osu.Game.Screens.Multi.Match.Components
private void updateEnabledState()
{
if (gameBeatmap.Value == null || SelectedItem.Value == null)
if (GameBeatmap.Value == null || SelectedItem.Value == null)
{
Enabled.Value = false;
base.Enabled.Value = false;
return;
}
bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value;
Enabled.Value = hasBeatmap && hasEnoughTime;
base.Enabled.Value = hasBeatmap && Enabled.Value;
}
}
}

View File

@ -0,0 +1,204 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
namespace osu.Game.Screens.Multi.Components
{
public abstract class RoomManager : CompositeDrawable, IRoomManager
{
public event Action RoomsUpdated;
private readonly BindableList<Room> rooms = new BindableList<Room>();
public IBindable<bool> InitialRoomsReceived => initialRoomsReceived;
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
public IBindableList<Room> Rooms => rooms;
protected IBindable<Room> JoinedRoom => joinedRoom;
private readonly Bindable<Room> joinedRoom = new Bindable<Room>();
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
protected RoomManager()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = CreatePollingComponents().Select(p =>
{
p.RoomsReceived = onRoomsReceived;
return p;
}).ToList();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
PartRoom();
}
public virtual void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
room.Host.Value = api.LocalUser.Value;
var req = new CreateRoomRequest(room);
req.Success += result =>
{
joinedRoom.Value = room;
update(room, result);
addRoom(room);
RoomsUpdated?.Invoke();
onSuccess?.Invoke(room);
};
req.Failure += exception =>
{
if (req.Result != null)
onError?.Invoke(req.Result.Error);
else
Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important);
};
api.Queue(req);
}
private JoinRoomRequest currentJoinRoomRequest;
public virtual void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = new JoinRoomRequest(room);
currentJoinRoomRequest.Success += () =>
{
joinedRoom.Value = room;
onSuccess?.Invoke(room);
};
currentJoinRoomRequest.Failure += exception =>
{
if (!(exception is OperationCanceledException))
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
onError?.Invoke(exception.ToString());
};
api.Queue(currentJoinRoomRequest);
}
public virtual void PartRoom()
{
currentJoinRoomRequest?.Cancel();
if (JoinedRoom == null)
return;
api.Queue(new PartRoomRequest(joinedRoom.Value));
joinedRoom.Value = null;
}
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
private void onRoomsReceived(List<Room> received)
{
if (received == null)
{
ClearRooms();
return;
}
// Remove past matches
foreach (var r in rooms.ToList())
{
if (received.All(e => e.RoomID.Value != r.RoomID.Value))
rooms.Remove(r);
}
for (int i = 0; i < received.Count; i++)
{
var room = received[i];
Debug.Assert(room.RoomID.Value != null);
if (ignoredRooms.Contains(room.RoomID.Value.Value))
continue;
room.Position.Value = i;
try
{
update(room, room);
addRoom(room);
}
catch (Exception ex)
{
Logger.Error(ex, $"Failed to update room: {room.Name.Value}.");
ignoredRooms.Add(room.RoomID.Value.Value);
rooms.Remove(room);
}
}
RoomsUpdated?.Invoke();
initialRoomsReceived.Value = true;
}
protected void RemoveRoom(Room room) => rooms.Remove(room);
protected void ClearRooms()
{
rooms.Clear();
initialRoomsReceived.Value = false;
}
/// <summary>
/// Updates a local <see cref="Room"/> with a remote copy.
/// </summary>
/// <param name="local">The local <see cref="Room"/> to update.</param>
/// <param name="remote">The remote <see cref="Room"/> to update with.</param>
private void update(Room local, Room remote)
{
foreach (var pi in remote.Playlist)
pi.MapObjects(beatmaps, rulesets);
local.CopyFrom(remote);
}
/// <summary>
/// Adds a <see cref="Room"/> to the list of available rooms.
/// </summary>
/// <param name="room">The <see cref="Room"/> to add.</param>
private void addRoom(Room room)
{
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
if (existing == null)
rooms.Add(room);
else
existing.CopyFrom(room);
}
protected abstract IEnumerable<RoomPollingComponent> CreatePollingComponents();
}
}

View File

@ -0,0 +1,29 @@
// 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 osu.Framework.Allocation;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.Multi.Components
{
public abstract class RoomPollingComponent : PollingComponent
{
/// <summary>
/// Invoked when any <see cref="Room"/>s have been received from the API.
/// <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>
public Action<List<Room>> RoomsReceived;
[Resolved]
protected IAPIProvider API { get; private set; }
protected void NotifyRoomsReceived(List<Room> rooms) => RoomsReceived?.Invoke(rooms);
}
}

View File

@ -0,0 +1,69 @@
// 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 System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.Multi.Components
{
/// <summary>
/// A <see cref="RoomPollingComponent"/> that polls for the currently-selected room.
/// </summary>
public class SelectionPollingComponent : RoomPollingComponent
{
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
[Resolved]
private IRoomManager roomManager { get; set; }
[BackgroundDependencyLoader]
private void load()
{
selectedRoom.BindValueChanged(_ =>
{
if (IsLoaded)
PollImmediately();
});
}
private GetRoomRequest pollReq;
protected override Task Poll()
{
if (!API.IsLoggedIn)
return base.Poll();
if (selectedRoom.Value?.RoomID.Value == null)
return base.Poll();
var tcs = new TaskCompletionSource<bool>();
pollReq?.Cancel();
pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value);
pollReq.Success += result =>
{
var rooms = new List<Room>(roomManager.Rooms);
int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value);
if (index < 0)
return;
rooms[index] = result;
NotifyRoomsReceived(rooms);
tcs.SetResult(true);
};
pollReq.Failure += _ => tcs.SetResult(false);
API.Queue(pollReq);
return tcs.Task;
}
}
}

View File

@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.Multi
{
[Cached(typeof(IRoomManager))]
public interface IRoomManager
{
/// <summary>
@ -17,7 +19,7 @@ namespace osu.Game.Screens.Multi
/// <summary>
/// Whether an initial listing of rooms has been received.
/// </summary>
Bindable<bool> InitialRoomsReceived { get; }
IBindable<bool> InitialRoomsReceived { get; }
/// <summary>
/// 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();
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private Container content;
private LoadingLayer loadingLayer;

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Timeshift;
using osuTK;
namespace osu.Game.Screens.Multi.Match.Components
@ -31,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
new ReadyButton
new TimeshiftReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
@ -30,7 +29,7 @@ using osuTK;
namespace osu.Game.Screens.Multi
{
[Cached]
public class Multiplayer : OsuScreen
public abstract class Multiplayer : OsuScreen
{
public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true;
@ -46,6 +45,9 @@ namespace osu.Game.Screens.Multi
private readonly IBindable<bool> isIdle = new BindableBool();
[Cached(Type = typeof(IRoomManager))]
protected RoomManager RoomManager { get; private set; }
[Cached]
private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
@ -55,9 +57,6 @@ namespace osu.Game.Screens.Multi
[Resolved(CanBeNull = true)]
private MusicController music { get; set; }
[Cached(Type = typeof(IRoomManager))]
private RoomManager roomManager;
[Resolved]
private OsuGameBase game { get; set; }
@ -70,7 +69,7 @@ namespace osu.Game.Screens.Multi
private readonly Drawable header;
private readonly Drawable headerBackground;
public Multiplayer()
protected Multiplayer()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -137,7 +136,7 @@ namespace osu.Game.Screens.Multi
Origin = Anchor.TopRight,
Action = () => CreateRoom()
},
roomManager = new RoomManager()
RoomManager = CreateRoomManager()
}
};
@ -168,7 +167,7 @@ namespace osu.Game.Screens.Multi
protected override void LoadComplete()
{
base.LoadComplete();
isIdle.BindValueChanged(idle => updatePollingRate(idle.NewValue), true);
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -178,36 +177,7 @@ namespace osu.Game.Screens.Multi
return dependencies;
}
private void updatePollingRate(bool idle)
{
if (!this.IsCurrentScreen())
{
roomManager.TimeBetweenListingPolls = 0;
roomManager.TimeBetweenSelectionPolls = 0;
}
else
{
switch (screenStack.CurrentScreen)
{
case LoungeSubScreen _:
roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000;
roomManager.TimeBetweenSelectionPolls = idle ? 120000 : 15000;
break;
case MatchSubScreen _:
roomManager.TimeBetweenListingPolls = 0;
roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000;
break;
default:
roomManager.TimeBetweenListingPolls = 0;
roomManager.TimeBetweenSelectionPolls = 0;
break;
}
}
Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})");
}
protected abstract void UpdatePollingRate(bool isIdle);
private void forcefullyExit()
{
@ -241,7 +211,7 @@ namespace osu.Game.Screens.Multi
beginHandlingTrack();
updatePollingRate(isIdle.Value);
UpdatePollingRate(isIdle.Value);
}
public override void OnSuspending(IScreen next)
@ -251,12 +221,12 @@ namespace osu.Game.Screens.Multi
endHandlingTrack();
updatePollingRate(isIdle.Value);
UpdatePollingRate(isIdle.Value);
}
public override bool OnExiting(IScreen next)
{
roomManager.PartRoom();
RoomManager.PartRoom();
waves.Hide();
@ -344,12 +314,14 @@ namespace osu.Game.Screens.Multi
if (newScreen is IOsuScreen newOsuScreen)
((IBindable<UserActivity>)Activity).BindTo(newOsuScreen.Activity);
updatePollingRate(isIdle.Value);
UpdatePollingRate(isIdle.Value);
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
updateTrack();
}
protected IScreen CurrentSubScreen => screenStack.CurrentScreen;
private void updateTrack(ValueChangedEvent<WorkingBeatmap> _ = null)
{
if (screenStack.CurrentScreen is MatchSubScreen)
@ -381,6 +353,8 @@ namespace osu.Game.Screens.Multi
}
}
protected abstract RoomManager CreateRoomManager();
private class MultiplayerWaveContainer : WaveContainer
{
protected override bool StartHidden => true;

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
@ -95,19 +96,36 @@ namespace osu.Game.Screens.Multi.Play
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
}
protected override ScoreInfo CreateScore()
protected override Score 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);
var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score);
request.Success += s => score.OnlineScoreID = s.ID;
request.Failure += e => Logger.Error(e, "Failed to submit score");
api.Queue(request);
var tcs = new TaskCompletionSource<bool>();
var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo);
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)

View File

@ -0,0 +1,189 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu
{
public readonly MultiplayerRoomUser User;
[Resolved]
private IAPIProvider api { get; set; }
private ReadyMark readyMark;
private SpriteIcon crown;
public ParticipantPanel(MultiplayerRoomUser user)
{
User = user;
RelativeSizeAxes = Axes.X;
Height = 40;
}
[BackgroundDependencyLoader]
private void load()
{
Debug.Assert(User.User != null);
var backgroundColour = Color4Extensions.FromHex("#33413C");
InternalChildren = new Drawable[]
{
crown = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Crown,
Size = new Vector2(14),
Colour = Color4Extensions.FromHex("#F7E65D"),
Alpha = 0
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 24 },
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
},
new UserCoverBackground
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
Width = 0.75f,
User = User.User,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f))
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new UpdateableAvatar
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
User = User.User
},
new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(30, 20),
Country = User.User.Country
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
Text = User.User.Username
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14),
Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty
}
}
},
readyMark = new ReadyMark
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 10 },
Alpha = 0
}
}
}
}
};
}
protected override void OnRoomChanged()
{
base.OnRoomChanged();
if (Room == null)
return;
const double fade_time = 50;
if (User.State == MultiplayerUserState.Ready)
readyMark.FadeIn(fade_time);
else
readyMark.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
else
crown.FadeOut(fade_time);
}
public MenuItem[] ContextMenuItems
{
get
{
if (Room == null)
return null;
// If the local user is targetted.
if (User.UserID == api.LocalUser.Value.Id)
return null;
// If the local user is not the host of the room.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
return null;
int targetUser = User.UserID;
return new MenuItem[]
{
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
return;
Client.TransferHost(targetUser);
})
};
}
}
}
}

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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ParticipantsList : RealtimeRoomComposite
{
private FillFlowContainer<ParticipantPanel> panels;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = panels = new FillFlowContainer<ParticipantPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2)
}
}
};
}
protected override void OnRoomChanged()
{
base.OnRoomChanged();
if (Room == null)
panels.Clear();
else
{
// Remove panels for users no longer in the room.
panels.RemoveAll(p => !Room.Users.Contains(p.User));
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user));
}
}
}
}

View File

@ -0,0 +1,51 @@
// 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.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants
{
public class ReadyMark : CompositeDrawable
{
public ReadyMark()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
Text = "ready",
Colour = Color4Extensions.FromHex("#DDFFFF")
},
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
Colour = Color4Extensions.FromHex("#AADD00")
}
}
};
}
}
}

View File

@ -0,0 +1,123 @@
// 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.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeReadyButton : RealtimeRoomComposite
{
public Bindable<PlaylistItem> SelectedItem => button.SelectedItem;
[Resolved]
private IAPIProvider api { get; set; }
[CanBeNull]
private MultiplayerRoomUser localUser;
[Resolved]
private OsuColour colours { get; set; }
private readonly ButtonWithTrianglesExposed button;
public RealtimeReadyButton()
{
InternalChild = button = new ButtonWithTrianglesExposed
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Enabled = { Value = true },
Action = onClick
};
}
protected override void OnRoomChanged()
{
base.OnRoomChanged();
localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id);
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open;
updateState();
}
private void updateState()
{
if (localUser == null)
return;
Debug.Assert(Room != null);
switch (localUser.State)
{
case MultiplayerUserState.Idle:
button.Text = "Ready";
updateButtonColour(true);
break;
case MultiplayerUserState.Ready:
if (Room?.Host?.Equals(localUser) == true)
{
int countReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
button.Text = $"Start match ({countReady} / {Room.Users.Count} ready)";
updateButtonColour(true);
}
else
{
button.Text = "Waiting for host...";
updateButtonColour(false);
}
break;
}
}
private void updateButtonColour(bool green)
{
if (green)
{
button.BackgroundColour = colours.Green;
button.Triangles.ColourDark = colours.Green;
button.Triangles.ColourLight = colours.GreenLight;
}
else
{
button.BackgroundColour = colours.YellowDark;
button.Triangles.ColourDark = colours.YellowDark;
button.Triangles.ColourLight = colours.Yellow;
}
}
private void onClick()
{
if (localUser == null)
return;
if (localUser.State == MultiplayerUserState.Idle)
Client.ChangeState(MultiplayerUserState.Ready);
else
{
if (Room?.Host?.Equals(localUser) == true)
Client.StartMatch();
else
Client.ChangeState(MultiplayerUserState.Idle);
}
}
private class ButtonWithTrianglesExposed : ReadyButton
{
public new Triangles Triangles => base.Triangles;
}
}
}

View File

@ -0,0 +1,38 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.RealtimeMultiplayer;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public abstract class RealtimeRoomComposite : MultiplayerComposite
{
[CanBeNull]
protected MultiplayerRoom Room => Client.Room;
[Resolved]
protected StatefulMultiplayerClient Client { get; private set; }
protected override void LoadComplete()
{
base.LoadComplete();
Client.RoomChanged += OnRoomChanged;
OnRoomChanged();
}
protected virtual void OnRoomChanged()
{
}
protected override void Dispose(bool isDisposing)
{
if (Client != null)
Client.RoomChanged -= OnRoomChanged;
base.Dispose(isDisposing);
}
}
}

View File

@ -0,0 +1,144 @@
// 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.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
{
public class RealtimeRoomManager : RoomManager
{
[Resolved]
private StatefulMultiplayerClient multiplayerClient { get; set; }
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
private readonly IBindable<bool> isConnected = new Bindable<bool>();
private readonly Bindable<bool> allowPolling = new Bindable<bool>();
private ListingPollingComponent listingPollingComponent;
protected override void LoadComplete()
{
base.LoadComplete();
isConnected.BindTo(multiplayerClient.IsConnected);
isConnected.BindValueChanged(_ => Schedule(updatePolling));
JoinedRoom.BindValueChanged(_ => updatePolling());
updatePolling();
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
public override void PartRoom()
{
if (JoinedRoom == null)
return;
var joinedRoom = JoinedRoom.Value;
base.PartRoom();
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.
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
Schedule(() =>
{
RemoveRoom(joinedRoom);
listingPollingComponent.PollImmediately();
});
}
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null)
{
Debug.Assert(room.RoomID.Value != null);
var joinTask = multiplayerClient.JoinRoom(room);
joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
joinTask.ContinueWith(t =>
{
PartRoom();
if (t.Exception != null)
Logger.Error(t.Exception, "Failed to join multiplayer room.");
}, TaskContinuationOptions.NotOnRanToCompletion);
}
private void updatePolling()
{
if (!isConnected.Value)
ClearRooms();
// Don't poll when not connected or when a room has been joined.
allowPolling.Value = isConnected.Value && JoinedRoom.Value == null;
}
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
{
listingPollingComponent = new RealtimeListingPollingComponent
{
TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls },
AllowPolling = { BindTarget = allowPolling }
},
new RealtimeSelectionPollingComponent
{
TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls },
AllowPolling = { BindTarget = allowPolling }
}
};
private class RealtimeListingPollingComponent : ListingPollingComponent
{
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
AllowPolling.BindValueChanged(allowPolling =>
{
if (!allowPolling.NewValue)
return;
if (IsLoaded)
PollImmediately();
});
}
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
private class RealtimeSelectionPollingComponent : SelectionPollingComponent
{
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
AllowPolling.BindValueChanged(allowPolling =>
{
if (!allowPolling.NewValue)
return;
if (IsLoaded)
PollImmediately();
});
}
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
}
}

View File

@ -1,337 +0,0 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Lounge.Components;
namespace osu.Game.Screens.Multi
{
public class RoomManager : CompositeDrawable, IRoomManager
{
public event Action RoomsUpdated;
private readonly BindableList<Room> rooms = new BindableList<Room>();
public Bindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>();
public IBindableList<Room> Rooms => rooms;
public double TimeBetweenListingPolls
{
get => listingPollingComponent.TimeBetweenPolls;
set => listingPollingComponent.TimeBetweenPolls = value;
}
public double TimeBetweenSelectionPolls
{
get => selectionPollingComponent.TimeBetweenPolls;
set => selectionPollingComponent.TimeBetweenPolls = value;
}
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
private readonly ListingPollingComponent listingPollingComponent;
private readonly SelectionPollingComponent selectionPollingComponent;
private Room joinedRoom;
public RoomManager()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
listingPollingComponent = new ListingPollingComponent
{
InitialRoomsReceived = { BindTarget = InitialRoomsReceived },
RoomsReceived = onListingReceived
},
selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived }
};
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
PartRoom();
}
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
room.Host.Value = api.LocalUser.Value;
var req = new CreateRoomRequest(room);
req.Success += result =>
{
joinedRoom = room;
update(room, result);
addRoom(room);
RoomsUpdated?.Invoke();
onSuccess?.Invoke(room);
};
req.Failure += exception =>
{
if (req.Result != null)
onError?.Invoke(req.Result.Error);
else
Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important);
};
api.Queue(req);
}
private JoinRoomRequest currentJoinRoomRequest;
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = new JoinRoomRequest(room);
currentJoinRoomRequest.Success += () =>
{
joinedRoom = room;
onSuccess?.Invoke(room);
};
currentJoinRoomRequest.Failure += exception =>
{
if (!(exception is OperationCanceledException))
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
onError?.Invoke(exception.ToString());
};
api.Queue(currentJoinRoomRequest);
}
public void PartRoom()
{
currentJoinRoomRequest?.Cancel();
if (joinedRoom == null)
return;
api.Queue(new PartRoomRequest(joinedRoom));
joinedRoom = null;
}
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
/// <summary>
/// Invoked when the listing of all <see cref="Room"/>s is received from the server.
/// </summary>
/// <param name="listing">The listing.</param>
private void onListingReceived(List<Room> listing)
{
// Remove past matches
foreach (var r in rooms.ToList())
{
if (listing.All(e => e.RoomID.Value != r.RoomID.Value))
rooms.Remove(r);
}
for (int i = 0; i < listing.Count; i++)
{
if (selectedRoom.Value?.RoomID?.Value == listing[i].RoomID.Value)
{
// The listing request contains less data than the selection request, so data from the selection request is always preferred while the room is selected.
continue;
}
var room = listing[i];
Debug.Assert(room.RoomID.Value != null);
if (ignoredRooms.Contains(room.RoomID.Value.Value))
continue;
room.Position.Value = i;
try
{
update(room, room);
addRoom(room);
}
catch (Exception ex)
{
Logger.Error(ex, $"Failed to update room: {room.Name.Value}.");
ignoredRooms.Add(room.RoomID.Value.Value);
rooms.Remove(room);
}
}
RoomsUpdated?.Invoke();
}
/// <summary>
/// Invoked when a <see cref="Room"/> is received from the server.
/// </summary>
/// <param name="toUpdate">The received <see cref="Room"/>.</param>
private void onSelectedRoomReceived(Room toUpdate)
{
foreach (var room in rooms)
{
if (room.RoomID.Value == toUpdate.RoomID.Value)
{
toUpdate.Position.Value = room.Position.Value;
update(room, toUpdate);
break;
}
}
}
/// <summary>
/// Updates a local <see cref="Room"/> with a remote copy.
/// </summary>
/// <param name="local">The local <see cref="Room"/> to update.</param>
/// <param name="remote">The remote <see cref="Room"/> to update with.</param>
private void update(Room local, Room remote)
{
foreach (var pi in remote.Playlist)
pi.MapObjects(beatmaps, rulesets);
local.CopyFrom(remote);
}
/// <summary>
/// Adds a <see cref="Room"/> to the list of available rooms.
/// </summary>
/// <param name="room">The <see cref="Room"/> to add.</param>
private void addRoom(Room room)
{
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
if (existing == null)
rooms.Add(room);
else
existing.CopyFrom(room);
}
private class SelectionPollingComponent : PollingComponent
{
public Action<Room> RoomReceived;
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
[BackgroundDependencyLoader]
private void load()
{
selectedRoom.BindValueChanged(_ =>
{
if (IsLoaded)
PollImmediately();
});
}
private GetRoomRequest pollReq;
protected override Task Poll()
{
if (!api.IsLoggedIn)
return base.Poll();
if (selectedRoom.Value?.RoomID.Value == null)
return base.Poll();
var tcs = new TaskCompletionSource<bool>();
pollReq?.Cancel();
pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value);
pollReq.Success += result =>
{
RoomReceived?.Invoke(result);
tcs.SetResult(true);
};
pollReq.Failure += _ => tcs.SetResult(false);
api.Queue(pollReq);
return tcs.Task;
}
}
private class ListingPollingComponent : PollingComponent
{
public Action<List<Room>> RoomsReceived;
public readonly Bindable<bool> InitialRoomsReceived = new Bindable<bool>();
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private Bindable<FilterCriteria> currentFilter { get; set; }
[BackgroundDependencyLoader]
private void load()
{
currentFilter.BindValueChanged(_ =>
{
InitialRoomsReceived.Value = false;
if (IsLoaded)
PollImmediately();
});
}
private GetRoomsRequest pollReq;
protected override Task Poll()
{
if (!api.IsLoggedIn)
return base.Poll();
var tcs = new TaskCompletionSource<bool>();
pollReq?.Cancel();
pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category);
pollReq.Success += result =>
{
InitialRoomsReceived.Value = true;
RoomsReceived?.Invoke(result);
tcs.SetResult(true);
};
pollReq.Failure += _ => tcs.SetResult(false);
api.Queue(pollReq);
return tcs.Task;
}
}
}
}

View File

@ -0,0 +1,49 @@
// 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.Logging;
using osu.Framework.Screens;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Lounge;
using osu.Game.Screens.Multi.Match;
namespace osu.Game.Screens.Multi.Timeshift
{
public class TimeshiftMultiplayer : Multiplayer
{
protected override void UpdatePollingRate(bool isIdle)
{
var timeshiftManager = (TimeshiftRoomManager)RoomManager;
if (!this.IsCurrentScreen())
{
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
}
else
{
switch (CurrentSubScreen)
{
case LoungeSubScreen _:
timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
case MatchSubScreen _:
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000;
break;
default:
timeshiftManager.TimeBetweenListingPolls.Value = 0;
timeshiftManager.TimeBetweenSelectionPolls.Value = 0;
break;
}
}
Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})");
}
protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager();
}
}

View File

@ -0,0 +1,38 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Screens.Multi.Timeshift
{
public class TimeshiftReadyButton : ReadyButton
{
[Resolved(typeof(Room), nameof(Room.EndDate))]
private Bindable<DateTimeOffset> endDate { get; set; }
public TimeshiftReadyButton()
{
Text = "Start";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
protected override void Update()
{
base.Update();
Enabled.Value = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value;
}
}
}

View File

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

View File

@ -1,9 +1,8 @@
// 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.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
@ -15,8 +14,7 @@ namespace osu.Game.Screens.Play.HUD
{
public GameplayLeaderboard()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
Direction = FillDirection.Vertical;
@ -29,32 +27,35 @@ namespace osu.Game.Screens.Play.HUD
/// <summary>
/// Adds a player to the leaderboard.
/// </summary>
/// <param name="currentScore">The bindable current score of 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);
currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue;
}
private GameplayLeaderboardScore addScore(double totalScore, User user)
{
var scoreItem = new GameplayLeaderboardScore
var drawable = new GameplayLeaderboardScore(user, isTracked)
{
User = user,
TotalScore = totalScore,
OnScoreChange = updateScores,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};
Add(scoreItem);
updateScores();
base.Add(drawable);
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++)
{

View File

@ -1,25 +1,39 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboardScore : CompositeDrawable
public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
{
private readonly OsuSpriteText positionText, positionSymbol, userString;
private readonly GlowingSpriteText scoreText;
public const float EXTENDED_WIDTH = 255f;
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;
@ -28,109 +42,249 @@ namespace osu.Game.Screens.Play.HUD
get => scorePosition;
set
{
if (value == scorePosition)
return;
scorePosition = value;
if (scorePosition.HasValue)
positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}";
positionText.Text = $"#{scorePosition.Value.FormatRank()}";
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;
private Container mainFillContainer;
private Box centralFill;
/// <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)
{
get => totalScore;
set
{
totalScore = value;
scoreText.Text = totalScore.ToString("N0");
User = user;
this.trackedPlayer = trackedPlayer;
OnScoreChange?.Invoke();
}
}
private User user;
public User User
{
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),
}
}
},
},
};
Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
positionText.Colour = colours.YellowLight;
positionSymbol.Colour = colours.Yellow;
InternalChildren = new Drawable[]
{
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.IO;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -22,8 +23,10 @@ using osu.Game.Graphics.Containers;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
@ -501,6 +504,7 @@ namespace osu.Game.Screens.Play
}
private ScheduledDelegate completionProgressDelegate;
private Task<ScoreInfo> scoreSubmissionTask;
private void updateCompletionState(ValueChangedEvent<bool> completionState)
{
@ -527,33 +531,50 @@ namespace osu.Game.Screens.Play
if (!showResults) return;
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
completionProgressDelegate = Schedule(GotoRanking);
}
protected virtual ScoreInfo CreateScore()
{
var score = new ScoreInfo
scoreSubmissionTask ??= Task.Run(async () =>
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
};
var score = CreateScore();
if (DrawableRuleset.ReplayScore != null)
score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
else
score.User = api.LocalUser.Value;
try
{
await SubmitScore(score);
}
catch (Exception ex)
{
Logger.Error(ex, "Score submission failed!");
}
ScoreProcessor.PopulateScore(score);
try
{
await ImportScore(score);
}
catch (Exception ex)
{
Logger.Error(ex, "Score import failed!");
}
return score;
return score.ScoreInfo;
});
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleCompletion();
}
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
{
if (!scoreSubmissionTask.IsCompleted)
{
scheduleCompletion();
return;
}
// 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 virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
#region Fail Logic
protected FailOverlay FailOverlay { get; private set; }
@ -748,39 +769,74 @@ namespace osu.Game.Screens.Play
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 a replay is present, we likely don't want to import into the local database.
this.Push(CreateResults(CreateScore()));
return;
score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
score.Replay = DrawableRuleset.ReplayScore.Replay;
}
LegacyByteArrayReader replayReader = null;
var score = new Score { ScoreInfo = CreateScore() };
if (recordingScore?.Replay.Frames.Count > 0)
else
{
score.Replay = recordingScore.Replay;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
score.ScoreInfo.User = api.LocalUser.Value;
score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List<ReplayFrame>() };
}
scoreManager.Import(score.ScoreInfo, replayReader)
.ContinueWith(imported => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(imported.Result));
}));
ScoreProcessor.PopulateScore(score.ScoreInfo);
return score;
}
/// <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)
{
// 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())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
return scoreManager.Import(score.ScoreInfo, replayReader);
}
/// <summary>
/// Submits the player's <see cref="Score"/>.
/// </summary>
/// <param name="score">The <see cref="Score"/> to submit.</param>
/// <returns>The submitted score.</returns>
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)
{
float fadeOutDuration = instant ? 0 : 250;

View File

@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Scoring;
@ -26,18 +27,21 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(Score);
}
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
protected override ScoreInfo CreateScore()
protected override Score CreateScore()
{
var baseScore = base.CreateScore();
// 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)
{
switch (action)

View File

@ -1,6 +1,7 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Multiplayer;
@ -12,11 +13,7 @@ namespace osu.Game.Tests.Visual
[Cached]
private readonly Bindable<Room> currentRoom = new Bindable<Room>();
protected Room Room
{
get => currentRoom.Value;
set => currentRoom.Value = value;
}
protected Room Room => currentRoom.Value;
private CachedModelDependencyContainer<Room> dependencies;
@ -26,5 +23,11 @@ namespace osu.Game.Tests.Visual
dependencies.Model.BindTo(currentRoom);
return dependencies;
}
[SetUp]
public void Setup() => Schedule(() =>
{
currentRoom.Value = new Room();
});
}
}

View File

@ -0,0 +1,50 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class RealtimeMultiplayerTestScene : MultiplayerTestScene
{
[Cached(typeof(StatefulMultiplayerClient))]
public TestRealtimeMultiplayerClient Client { get; }
[Cached(typeof(RealtimeRoomManager))]
public TestRealtimeRoomManager RoomManager { get; }
[Cached]
public Bindable<FilterCriteria> Filter { get; }
protected override Container<Drawable> Content => content;
private readonly TestRealtimeRoomContainer content;
private readonly bool joinRoom;
public RealtimeMultiplayerTestScene(bool joinRoom = true)
{
this.joinRoom = joinRoom;
base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both });
Client = content.Client;
RoomManager = content.RoomManager;
Filter = content.Filter;
}
[SetUp]
public new void Setup() => Schedule(() =>
{
RoomManager.Schedule(() => RoomManager.PartRoom());
if (joinRoom)
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
});
}
}

View File

@ -0,0 +1,119 @@
// 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.
#nullable enable
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient
{
public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
[Resolved]
private IAPIProvider api { get; set; } = null!;
public void Connect() => isConnected.Value = true;
public void Disconnect() => isConnected.Value = false;
public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user });
public void RemoveUser(User user)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user));
Schedule(() =>
{
if (Room.Users.Any())
TransferHost(Room.Users.First().UserID);
});
}
public void ChangeUserState(int userId, MultiplayerUserState newState)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).UserStateChanged(userId, newState);
Schedule(() =>
{
switch (newState)
{
case MultiplayerUserState.Loaded:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
((IMultiplayerClient)this).MatchStarted();
}
break;
case MultiplayerUserState.FinishedPlay:
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
ChangeUserState(u.UserID, MultiplayerUserState.Results);
((IMultiplayerClient)this).ResultsReady();
}
break;
}
});
}
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
{
var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
var room = new MultiplayerRoom(roomId);
room.Users.Add(user);
if (room.Users.Count == 1)
room.Host = user;
return Task.FromResult(room);
}
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
public override async Task ChangeSettings(MultiplayerRoomSettings settings)
{
Debug.Assert(Room != null);
await ((IMultiplayerClient)this).SettingsChanged(settings);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
}
public override Task ChangeState(MultiplayerUserState newState)
{
ChangeUserState(api.LocalUser.Value.Id, newState);
return Task.CompletedTask;
}
public override Task StartMatch()
{
Debug.Assert(Room != null);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
return ((IMultiplayerClient)this).LoadRequested();
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.RealtimeMultiplayer;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestRealtimeRoomContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
[Cached(typeof(StatefulMultiplayerClient))]
public readonly TestRealtimeMultiplayerClient Client;
[Cached(typeof(RealtimeRoomManager))]
public readonly TestRealtimeRoomManager RoomManager;
[Cached]
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
public TestRealtimeRoomContainer()
{
RelativeSizeAxes = Axes.Both;
AddRangeInternal(new Drawable[]
{
Client = new TestRealtimeMultiplayerClient(),
RoomManager = new TestRealtimeRoomManager(),
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
}
}

View File

@ -0,0 +1,116 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Screens.Multi.RealtimeMultiplayer;
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
{
public class TestRealtimeRoomManager : RealtimeRoomManager
{
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private OsuGameBase game { get; set; }
[Cached]
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
private readonly List<Room> rooms = new List<Room>();
protected override void LoadComplete()
{
base.LoadComplete();
int currentScoreId = 0;
int currentRoomId = 0;
((DummyAPIAccess)api).HandleRequest = req =>
{
switch (req)
{
case CreateRoomRequest createRoomRequest:
var createdRoom = new APICreatedRoom();
createdRoom.CopyFrom(createRoomRequest.Room);
createdRoom.RoomID.Value ??= currentRoomId++;
rooms.Add(createdRoom);
createRoomRequest.TriggerSuccess(createdRoom);
break;
case JoinRoomRequest joinRoomRequest:
joinRoomRequest.TriggerSuccess();
break;
case PartRoomRequest partRoomRequest:
partRoomRequest.TriggerSuccess();
break;
case GetRoomsRequest getRoomsRequest:
var roomsWithoutParticipants = new List<Room>();
foreach (var r in rooms)
{
var newRoom = new Room();
newRoom.CopyFrom(r);
newRoom.RecentParticipants.Clear();
roomsWithoutParticipants.Add(newRoom);
}
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
break;
case GetRoomRequest getRoomRequest:
getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
break;
case GetBeatmapSetRequest getBeatmapSetRequest:
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res);
onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e);
// Get the online API from the game's dependencies.
game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
break;
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
break;
case SubmitRoomScoreRequest submitRoomScoreRequest:
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.S,
MaxCombo = 1000,
TotalScore = 1000000,
User = api.LocalUser.Value,
Statistics = new Dictionary<HitResult, int>()
});
break;
}
};
}
public new void ClearRooms() => base.ClearRooms();
public new void Schedule(Action action) => base.Schedule(action);
}
}

View File

@ -1,6 +1,8 @@
// 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 Humanizer;
namespace osu.Game.Utils
{
public static class FormatUtils
@ -18,5 +20,11 @@ namespace osu.Game.Utils
/// <param name="accuracy">The accuracy to be formatted</param>
/// <returns>formatted accuracy in percentage</returns>
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);
}
}