diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index dd53eefd23..58c24181d3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,7 +15,7 @@
]
},
"jetbrains.resharper.globaltools": {
- "version": "2020.2.4",
+ "version": "2020.3.2",
"commands": [
"jb"
]
diff --git a/Directory.Build.props b/Directory.Build.props
index 551cb75077..9ec442aafa 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,7 +16,7 @@
-
+
diff --git a/Gemfile.lock b/Gemfile.lock
index a4b49af7e4..8ac863c9a8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,27 +1,27 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.2)
+ CFPropertyList (3.0.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
- aws-partitions (1.354.0)
- aws-sdk-core (3.104.3)
+ aws-partitions (1.413.0)
+ aws-sdk-core (3.110.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.36.0)
- aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sdk-kms (1.40.0)
+ aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.78.0)
- aws-sdk-core (~> 3, >= 3.104.3)
+ aws-sdk-s3 (1.87.0)
+ aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.2.1)
+ aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
- babosa (1.0.3)
+ babosa (1.0.4)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
@@ -29,22 +29,23 @@ GEM
highline (~> 1.7.2)
declarative (0.0.20)
declarative-option (0.1.0)
- digest-crc (0.6.1)
- rake (~> 13.0)
+ digest-crc (0.6.3)
+ rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
- emoji_regex (3.0.0)
- excon (0.76.0)
- faraday (1.0.1)
+ emoji_regex (3.2.1)
+ excon (0.78.1)
+ faraday (1.2.0)
multipart-post (>= 1.2, < 3)
- faraday-cookie_jar (0.0.6)
- faraday (>= 0.7.4)
+ ruby2_keywords
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
- fastimage (2.2.0)
- fastlane (2.156.0)
+ fastimage (2.2.1)
+ fastlane (2.170.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
@@ -96,17 +97,17 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.3)
+ google-cloud-env (1.4.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
- google-cloud-storage (1.27.0)
+ google-cloud-storage (1.29.2)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.13.1)
+ googleauth (0.14.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -118,10 +119,10 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
- json (2.3.1)
- jwt (2.2.1)
+ json (2.5.1)
+ jwt (2.2.2)
memoist (0.16.2)
- mini_magick (4.10.1)
+ mini_magick (4.11.0)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
multi_json (1.15.0)
@@ -132,14 +133,15 @@ GEM
mini_portile2 (~> 2.4.0)
os (1.1.1)
plist (3.5.0)
- public_suffix (4.0.5)
- rake (13.0.1)
+ public_suffix (4.0.6)
+ rake (13.0.3)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
+ ruby2_keywords (0.0.2)
rubyzip (2.3.0)
security (0.1.3)
signet (0.14.0)
@@ -168,7 +170,7 @@ GEM
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.18.0)
+ xcodeproj (1.19.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
diff --git a/osu.Android.props b/osu.Android.props
index fc01f9bf1d..611f0d05f4 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 7f2ef82d12..42d9ad33ab 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -18,7 +18,9 @@ using osu.Game.Database;
namespace osu.Android
{
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
- [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
+ [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 61ecd79e3d..51d2032795 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index fa7bfd7169..3261f632f2 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index d6a03da807..32243e0bc3 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
index f18d3191ca..af9ea99232 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
@@ -20,17 +20,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private double lastTrailTime;
private IBindable cursorSize;
- public LegacyCursorTrail()
- {
- Blending = BlendingParameters.Additive;
- }
-
[BackgroundDependencyLoader]
private void load(ISkinSource skin, OsuConfigManager config)
{
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
+ Blending = !disjointTrail ? BlendingParameters.Additive : BlendingParameters.Inherit;
+
if (Texture != null)
{
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index a89645d881..210f81d111 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
new file mode 100644
index 0000000000..a2ad37cf4a
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using Humanizer;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Tests.Visual.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.NonVisual.Multiplayer
+{
+ [HeadlessTest]
+ public class StatefulMultiplayerClientTest : MultiplayerTestScene
+ {
+ [Test]
+ public void TestPlayingUserTracking()
+ {
+ int id = 2000;
+
+ AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5);
+ checkPlayingUserCount(0);
+
+ changeState(3, MultiplayerUserState.WaitingForLoad);
+ checkPlayingUserCount(3);
+
+ changeState(3, MultiplayerUserState.Playing);
+ checkPlayingUserCount(3);
+
+ changeState(3, MultiplayerUserState.Results);
+ checkPlayingUserCount(0);
+
+ changeState(6, MultiplayerUserState.WaitingForLoad);
+ checkPlayingUserCount(6);
+
+ AddStep("another user left", () => Client.RemoveUser(Client.Room?.Users.Last().User));
+ checkPlayingUserCount(5);
+
+ AddStep("leave room", () => Client.LeaveRoom());
+ checkPlayingUserCount(0);
+ }
+
+ private void checkPlayingUserCount(int expectedCount)
+ => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount);
+
+ private void changeState(int userCount, MultiplayerUserState state)
+ => AddStep($"{"user".ToQuantity(userCount)} in {state}", () =>
+ {
+ for (int i = 0; i < userCount; ++i)
+ {
+ var userId = Client.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!");
+ Client.ChangeUserState(userId, state);
+ }
+ });
+ }
+}
diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
index a3db20ce83..9a999a4931 100644
--- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
+++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
@@ -8,7 +8,6 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
-using static osu.Game.Tests.Visual.Components.TestScenePreviewTrackManager.TestPreviewTrackManager;
namespace osu.Game.Tests.Visual.Components
{
@@ -100,7 +99,7 @@ namespace osu.Game.Tests.Visual.Components
[Test]
public void TestNonPresentTrack()
{
- TestPreviewTrack track = null;
+ TestPreviewTrackManager.TestPreviewTrack track = null;
AddStep("get non-present track", () =>
{
@@ -182,9 +181,9 @@ namespace osu.Game.Tests.Visual.Components
AddAssert("track stopped", () => !track.IsRunning);
}
- private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null);
+ private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(null);
- private TestPreviewTrack getOwnedTrack()
+ private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack()
{
var track = getTrack();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
index c0a021436e..17fe09f2c6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
@@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public bool CheckPositionByUsername(string username, int? expectedPosition)
{
- var scoreItem = this.FirstOrDefault(i => i.User.Username == username);
+ var scoreItem = this.FirstOrDefault(i => i.User?.Username == username);
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs
index e1b0820662..5bac8582d7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
-using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
similarity index 89%
rename from osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs
rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 8078c7b994..d016accc25 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -22,12 +22,14 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Online;
-namespace osu.Game.Tests.Visual.Gameplay
+namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene
+ public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene
{
+ private const int users = 16;
+
[Cached(typeof(SpectatorStreamingClient))]
- private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16);
+ private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users);
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
@@ -47,10 +49,12 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[SetUpSteps]
- public void SetUpSteps()
+ public override void SetUpSteps()
{
AddStep("create leaderboard", () =>
{
+ leaderboard?.Expire();
+
OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
@@ -58,6 +62,9 @@ namespace osu.Game.Tests.Visual.Gameplay
streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
+ Client.CurrentMatchPlayingUserIds.Clear();
+ Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers);
+
Children = new Drawable[]
{
scoreProcessor = new OsuScoreProcessor(),
@@ -81,6 +88,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100);
}
+ [Test]
+ public void TestUserQuit()
+ {
+ AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
+ }
+
public class TestMultiplayerStreaming : SpectatorStreamingClient
{
public new BindableList PlayingUsers => (BindableList)base.PlayingUsers;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
new file mode 100644
index 0000000000..95c333e9f4
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -0,0 +1,145 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Extensions;
+using osu.Framework.Platform;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Rulesets.Taiko.Mods;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Screens.Select;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerMatchSongSelect : RoomTestScene
+ {
+ private BeatmapManager manager;
+ private RulesetStore rulesets;
+
+ private List beatmaps;
+
+ private TestMultiplayerMatchSongSelect songSelect;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
+
+ beatmaps = new List();
+
+ for (int i = 0; i < 8; ++i)
+ {
+ int beatmapId = 10 * 10 + i;
+
+ int length = RNG.Next(30000, 200000);
+ double bpm = RNG.NextSingle(80, 200);
+
+ beatmaps.Add(new BeatmapInfo
+ {
+ Ruleset = rulesets.GetRuleset(i % 4),
+ OnlineBeatmapID = beatmapId,
+ Length = length,
+ BPM = bpm,
+ BaseDifficulty = new BeatmapDifficulty()
+ });
+ }
+
+ manager.Import(new BeatmapSetInfo
+ {
+ OnlineBeatmapSetID = 10,
+ Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "Some Artist",
+ Title = "Some Beatmap",
+ AuthorString = "Some Author"
+ },
+ Beatmaps = beatmaps,
+ DateAdded = DateTimeOffset.UtcNow
+ }).Wait();
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("reset", () =>
+ {
+ Ruleset.Value = new OsuRuleset().RulesetInfo;
+ Beatmap.SetDefault();
+ SelectedMods.SetDefault();
+ });
+
+ AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect()));
+ AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
+ }
+
+ [Test]
+ public void TestBeatmapRevertedOnExitIfNoSelection()
+ {
+ BeatmapInfo selectedBeatmap = null;
+
+ AddStep("select beatmap",
+ () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.Where(beatmap => beatmap.RulesetID == new OsuRuleset().LegacyID).ElementAt(1)));
+ AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
+
+ AddStep("exit song select", () => songSelect.Exit());
+ AddAssert("beatmap reverted", () => Beatmap.IsDefault);
+ }
+
+ [Test]
+ public void TestModsRevertedOnExitIfNoSelection()
+ {
+ AddStep("change mods", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
+
+ AddStep("exit song select", () => songSelect.Exit());
+ AddAssert("mods reverted", () => SelectedMods.Value.Count == 0);
+ }
+
+ [Test]
+ public void TestRulesetRevertedOnExitIfNoSelection()
+ {
+ AddStep("change ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
+
+ AddStep("exit song select", () => songSelect.Exit());
+ AddAssert("ruleset reverted", () => Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
+ }
+
+ [Test]
+ public void TestBeatmapConfirmed()
+ {
+ BeatmapInfo selectedBeatmap = null;
+
+ AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
+ AddStep("select beatmap",
+ () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.RulesetID == new TaikoRuleset().LegacyID)));
+ AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
+ AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
+
+ AddStep("confirm selection", () => songSelect.FinaliseSelection());
+ AddStep("exit song select", () => songSelect.Exit());
+
+ AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
+ AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
+ AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime);
+ }
+
+ private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect
+ {
+ public new BeatmapCarousel Carousel => base.Carousel;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 9181170bee..968a869532 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -43,6 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2);
}
+ [Test]
+ public void TestAddNullUser()
+ {
+ AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1);
+
+ AddStep("add non-resolvable user", () => Client.AddNullUser(-3));
+
+ AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2);
+ }
+
[Test]
public void TestRemoveUser()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
index 7a3845cbf3..80d1acd145 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
@@ -143,7 +143,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
RoomManager =
{
TimeBetweenListingPolls = { Value = 1 },
- TimeBetweenSelectionPolls = { Value = 1 }
}
};
diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
index dcfe0432a8..3d65e7e4ba 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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;
@@ -13,13 +14,12 @@ namespace osu.Game.Tests.Visual.Online
public class TestSceneAccountCreationOverlay : OsuTestScene
{
private readonly Container userPanelArea;
+ private readonly AccountCreationOverlay accountCreation;
private IBindable localUser;
public TestSceneAccountCreationOverlay()
{
- AccountCreationOverlay accountCreation;
-
Children = new Drawable[]
{
accountCreation = new AccountCreationOverlay(),
@@ -31,8 +31,6 @@ namespace osu.Game.Tests.Visual.Online
Origin = Anchor.TopRight,
},
};
-
- AddStep("show", () => accountCreation.Show());
}
[BackgroundDependencyLoader]
@@ -42,8 +40,19 @@ namespace osu.Game.Tests.Visual.Online
localUser = API.LocalUser.GetBoundCopy();
localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true);
+ }
- AddStep("logout", API.Logout);
+ [Test]
+ public void TestOverlayVisibility()
+ {
+ AddStep("start hidden", () => accountCreation.Hide());
+ AddStep("log out", API.Logout);
+
+ AddStep("show manually", () => accountCreation.Show());
+ AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible);
+
+ AddStep("log back in", () => API.Login("dummy", "password"));
+ AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
index 998e42b478..cd2c4e9346 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
@@ -1,8 +1,13 @@
// Copyright (c) ppy Pty Ltd . 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 Humanizer;
using NUnit.Framework;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Changelog;
@@ -12,13 +17,61 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public class TestSceneChangelogOverlay : OsuTestScene
{
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ private readonly Dictionary streams;
+ private readonly Dictionary builds;
+
+ private APIChangelogBuild requestedBuild;
private TestChangelogOverlay changelog;
- protected override bool UseOnlineAPI => true;
+ public TestSceneChangelogOverlay()
+ {
+ streams = APIUpdateStream.KNOWN_STREAMS.Keys.Select((stream, id) => new APIUpdateStream
+ {
+ Id = id + 1,
+ Name = stream,
+ DisplayName = stream.Humanize(), // not quite there, but good enough.
+ }).ToDictionary(stream => stream.Name);
+
+ string version = DateTimeOffset.Now.ToString("yyyy.Mdd.0");
+ builds = APIUpdateStream.KNOWN_STREAMS.Keys.Select(stream => new APIChangelogBuild
+ {
+ Version = version,
+ DisplayVersion = version,
+ UpdateStream = streams[stream],
+ ChangelogEntries = new List()
+ }).ToDictionary(build => build.UpdateStream.Name);
+
+ foreach (var stream in streams.Values)
+ stream.LatestBuild = builds[stream.Name];
+ }
[SetUp]
public void SetUp() => Schedule(() =>
{
+ requestedBuild = null;
+
+ dummyAPI.HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case GetChangelogRequest changelogRequest:
+ var changelogResponse = new APIChangelogIndex
+ {
+ Streams = streams.Values.ToList(),
+ Builds = builds.Values.ToList()
+ };
+ changelogRequest.TriggerSuccess(changelogResponse);
+ break;
+
+ case GetChangelogBuildRequest buildRequest:
+ if (requestedBuild != null)
+ buildRequest.TriggerSuccess(requestedBuild);
+ break;
+ }
+ };
+
Child = changelog = new TestChangelogOverlay();
});
@@ -41,26 +94,60 @@ namespace osu.Game.Tests.Visual.Online
}
[Test]
- [Ignore("needs to be updated to not be so server dependent")]
public void ShowWithBuild()
{
- AddStep(@"Show with Lazer 2018.712.0", () =>
+ showBuild(() => new APIChangelogBuild
{
- changelog.ShowBuild(new APIChangelogBuild
+ Version = "2018.712.0",
+ DisplayVersion = "2018.712.0",
+ UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME],
+ ChangelogEntries = new List
{
- Version = "2018.712.0",
- DisplayVersion = "2018.712.0",
- UpdateStream = new APIUpdateStream { Id = 5, Name = OsuGameBase.CLIENT_STREAM_NAME },
- ChangelogEntries = new List
+ new APIChangelogEntry
{
- new APIChangelogEntry
+ Type = ChangelogEntryType.Fix,
+ Category = "osu!",
+ Title = "Fix thing",
+ MessageHtml = "Additional info goes here.",
+ Repository = "osu",
+ GithubPullRequestId = 11100,
+ GithubUser = new APIChangelogUser
{
- Category = "Test",
- Title = "Title",
- MessageHtml = "Message",
+ OsuUsername = "smoogipoo",
+ UserId = 1040328
}
+ },
+ new APIChangelogEntry
+ {
+ Type = ChangelogEntryType.Add,
+ Category = "osu!",
+ Title = "Add thing",
+ Major = true,
+ Repository = "ppy/osu-framework",
+ GithubPullRequestId = 4444,
+ GithubUser = new APIChangelogUser
+ {
+ DisplayName = "frenzibyte",
+ GithubUrl = "https://github.com/frenzibyte"
+ }
+ },
+ new APIChangelogEntry
+ {
+ Type = ChangelogEntryType.Misc,
+ Category = "Code quality",
+ Title = "Clean up thing",
+ GithubUser = new APIChangelogUser
+ {
+ DisplayName = "some dude"
+ }
+ },
+ new APIChangelogEntry
+ {
+ Type = ChangelogEntryType.Misc,
+ Category = "Code quality",
+ Title = "Clean up another thing"
}
- });
+ }
});
AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0);
@@ -71,35 +158,38 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestHTMLUnescaping()
{
- AddStep(@"Ensure HTML string unescaping", () =>
+ showBuild(() => new APIChangelogBuild
{
- changelog.ShowBuild(new APIChangelogBuild
+ Version = "2019.920.0",
+ DisplayVersion = "2019.920.0",
+ UpdateStream = new APIUpdateStream
{
- Version = "2019.920.0",
- DisplayVersion = "2019.920.0",
- UpdateStream = new APIUpdateStream
+ Name = "Test",
+ DisplayName = "Test"
+ },
+ ChangelogEntries = new List
+ {
+ new APIChangelogEntry
{
- Name = "Test",
- DisplayName = "Test"
- },
- ChangelogEntries = new List
- {
- new APIChangelogEntry
+ Category = "Testing HTML strings unescaping",
+ Title = "Ensuring HTML strings are being unescaped",
+ MessageHtml = """"This text should appear triple-quoted""" >_<",
+ GithubUser = new APIChangelogUser
{
- Category = "Testing HTML strings unescaping",
- Title = "Ensuring HTML strings are being unescaped",
- MessageHtml = """"This text should appear triple-quoted""" >_<",
- GithubUser = new APIChangelogUser
- {
- DisplayName = "Dummy",
- OsuUsername = "Dummy",
- }
- },
- }
- });
+ DisplayName = "Dummy",
+ OsuUsername = "Dummy",
+ }
+ },
+ }
});
}
+ private void showBuild(Func build)
+ {
+ AddStep("set up build", () => requestedBuild = build.Invoke());
+ AddStep("show build", () => changelog.ShowBuild(requestedBuild));
+ }
+
private class TestChangelogOverlay : ChangelogOverlay
{
public new List Streams => base.Streams;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
index 1666c9cde4..1baa07f208 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
@@ -90,11 +90,17 @@ namespace osu.Game.Tests.Visual.Online
};
protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
- => Task.FromResult(new User
+ {
+ // tests against failed lookups
+ if (lookup == 13)
+ return Task.FromResult(null);
+
+ return Task.FromResult(new User
{
Id = lookup,
Username = usernames[lookup % usernames.Length],
});
+ }
}
}
}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 83d7b4135a..9049b67f90 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index bc6b994988..dc4f22788d 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index 05d6930992..568726199c 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@@ -17,6 +18,13 @@ namespace osu.Game.Database
[Resolved]
private IAPIProvider api { get; set; }
+ ///
+ /// Perform an API lookup on the specified user, populating a model.
+ ///
+ /// The user to lookup.
+ /// An optional cancellation token.
+ /// The populated user, or null if the user does not exist or the request could not be satisfied.
+ [ItemCanBeNull]
public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
@@ -72,6 +80,7 @@ namespace osu.Game.Database
var request = new GetUsersRequest(userTasks.Keys.ToArray());
// rather than queueing, we maintain our own single-threaded request stream.
+ // todo: we probably want retry logic here.
api.Perform(request);
// Create a new request task if there's still more users to query.
@@ -82,14 +91,19 @@ namespace osu.Game.Database
createNewTask();
}
- foreach (var user in request.Result.Users)
- {
- if (userTasks.TryGetValue(user.Id, out var tasks))
- {
- foreach (var task in tasks)
- task.SetResult(user);
+ List foundUsers = request.Result?.Users;
- userTasks.Remove(user.Id);
+ if (foundUsers != null)
+ {
+ foreach (var user in foundUsers)
+ {
+ if (userTasks.TryGetValue(user.Id, out var tasks))
+ {
+ foreach (var task in tasks)
+ task.SetResult(user);
+
+ userTasks.Remove(user.Id);
+ }
}
}
diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs
index a1215d786b..4138c2757a 100644
--- a/osu.Game/Extensions/TaskExtensions.cs
+++ b/osu.Game/Extensions/TaskExtensions.cs
@@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
+using System;
using System.Threading.Tasks;
+using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
namespace osu.Game.Extensions
@@ -13,13 +17,19 @@ namespace osu.Game.Extensions
/// Avoids unobserved exceptions from being fired.
///
/// The task.
- /// Whether errors should be logged as important, or silently ignored.
- public static void CatchUnobservedExceptions(this Task task, bool logOnError = false)
+ ///
+ /// Whether errors should be logged as errors visible to users, or as debug messages.
+ /// Logging as debug will essentially silence the errors on non-release builds.
+ ///
+ public static void CatchUnobservedExceptions(this Task task, bool logAsError = false)
{
task.ContinueWith(t =>
{
- if (logOnError)
- Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important);
+ Exception? exception = t.Exception?.AsSingular();
+ if (logAsError)
+ Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true);
+ else
+ Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug);
}, TaskContinuationOptions.NotOnRanToCompletion);
}
}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 133ba22406..2aaea22155 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
+using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
@@ -293,8 +294,21 @@ namespace osu.Game.Online.API
failureCount = 0;
return true;
}
+ catch (HttpRequestException re)
+ {
+ log.Add($"{nameof(HttpRequestException)} while performing request {req}: {re.Message}");
+ handleFailure();
+ return false;
+ }
+ catch (SocketException se)
+ {
+ log.Add($"{nameof(SocketException)} while performing request {req}: {se.Message}");
+ handleFailure();
+ return false;
+ }
catch (WebException we)
{
+ log.Add($"{nameof(WebException)} while performing request {req}: {we.Message}");
handleWebException(we);
return false;
}
@@ -312,7 +326,7 @@ namespace osu.Game.Online.API
///
public IBindable State => state;
- private bool handleWebException(WebException we)
+ private void handleWebException(WebException we)
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode
?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
@@ -330,26 +344,24 @@ namespace osu.Game.Online.API
{
case HttpStatusCode.Unauthorized:
Logout();
- return true;
+ break;
case HttpStatusCode.RequestTimeout:
- failureCount++;
- log.Add($@"API failure count is now {failureCount}");
-
- if (failureCount < 3)
- // we might try again at an api level.
- return false;
-
- if (State.Value == APIState.Online)
- {
- state.Value = APIState.Failing;
- flushQueue();
- }
-
- return true;
+ handleFailure();
+ break;
}
+ }
- return true;
+ private void handleFailure()
+ {
+ failureCount++;
+ log.Add($@"API failure count is now {failureCount}");
+
+ if (failureCount >= 3 && State.Value == APIState.Online)
+ {
+ state.Value = APIState.Failing;
+ flushQueue();
+ }
}
public bool IsLoggedIn => localUser.Value.Id > 1;
diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs
index d9e48373bb..5af7d6a01c 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Framework.Graphics.Colour;
using osuTK.Graphics;
@@ -27,34 +28,16 @@ namespace osu.Game.Online.API.Requests.Responses
public bool Equals(APIUpdateStream other) => Id == other?.Id;
- public ColourInfo Colour
+ internal static readonly Dictionary KNOWN_STREAMS = new Dictionary
{
- get
- {
- switch (Name)
- {
- case "stable40":
- return new Color4(102, 204, 255, 255);
+ ["stable40"] = new Color4(102, 204, 255, 255),
+ ["stable"] = new Color4(34, 153, 187, 255),
+ ["beta40"] = new Color4(255, 221, 85, 255),
+ ["cuttingedge"] = new Color4(238, 170, 0, 255),
+ [OsuGameBase.CLIENT_STREAM_NAME] = new Color4(237, 18, 33, 255),
+ ["web"] = new Color4(136, 102, 238, 255)
+ };
- case "stable":
- return new Color4(34, 153, 187, 255);
-
- case "beta40":
- return new Color4(255, 221, 85, 255);
-
- case "cuttingedge":
- return new Color4(238, 170, 0, 255);
-
- case OsuGameBase.CLIENT_STREAM_NAME:
- return new Color4(237, 18, 33, 255);
-
- case "web":
- return new Color4(136, 102, 238, 255);
-
- default:
- return new Color4(0, 0, 0, 255);
- }
- }
- }
+ public ColourInfo Colour => KNOWN_STREAMS.TryGetValue(Name, out var colour) ? colour : new Color4(0, 0, 0, 255);
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 24ea6abc4a..7cd1ef78f7 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -88,11 +88,12 @@ namespace osu.Game.Online.Multiplayer
{
isConnected.Value = false;
- if (ex != null)
- {
- Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network);
+ Logger.Log(ex != null
+ ? $"Multiplayer client lost connection: {ex}"
+ : "Multiplayer client disconnected", LoggingTarget.Network);
+
+ if (connection != null)
await tryUntilConnected();
- }
};
await tryUntilConnected();
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
index 2134e50d72..12fcf25ace 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
@@ -5,9 +5,7 @@
using System;
using System.Collections.Generic;
-using System.Threading;
using Newtonsoft.Json;
-using osu.Framework.Allocation;
namespace osu.Game.Online.Multiplayer
{
@@ -42,35 +40,12 @@ namespace osu.Game.Online.Multiplayer
///
public MultiplayerRoomUser? Host { get; set; }
- private object writeLock = new object();
-
[JsonConstructor]
public MultiplayerRoom(in long roomId)
{
RoomID = roomId;
}
- private object updateLock = new object();
-
- private ManualResetEventSlim freeForWrite = new ManualResetEventSlim(true);
-
- ///
- /// Request a lock on this room to perform a thread-safe update.
- ///
- public IDisposable LockForUpdate()
- {
- // ReSharper disable once InconsistentlySynchronizedField
- freeForWrite.Wait();
-
- lock (updateLock)
- {
- freeForWrite.Wait();
- freeForWrite.Reset();
-
- return new ValueInvokeOnDisposal(this, r => freeForWrite.Set());
- }
- }
-
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
}
}
diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
index fcb0977f53..dc80488d39 100644
--- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
@@ -61,9 +61,9 @@ namespace osu.Game.Online.Multiplayer
public MultiplayerRoom? Room { get; private set; }
///
- /// The users currently in gameplay.
+ /// The users in the joined which are participating in the current gameplay loop.
///
- public readonly BindableList PlayingUsers = new BindableList();
+ public readonly BindableList CurrentMatchPlayingUserIds = new BindableList();
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
@@ -84,7 +84,7 @@ namespace osu.Game.Online.Multiplayer
IsConnected.BindValueChanged(connected =>
{
// clean up local room state on server disconnect.
- if (!connected.NewValue)
+ if (!connected.NewValue && Room != null)
{
Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
LeaveRoom().CatchUnobservedExceptions();
@@ -133,6 +133,7 @@ namespace osu.Game.Online.Multiplayer
apiRoom = null;
Room = null;
+ CurrentMatchPlayingUserIds.Clear();
RoomUpdated?.Invoke();
}, false);
@@ -253,7 +254,7 @@ namespace osu.Game.Online.Multiplayer
return;
Room.Users.Remove(user);
- PlayingUsers.Remove(user.UserID);
+ CurrentMatchPlayingUserIds.Remove(user.UserID);
RoomUpdated?.Invoke();
}, false);
@@ -302,8 +303,7 @@ namespace osu.Game.Online.Multiplayer
Room.Users.Single(u => u.UserID == userId).State = state;
- if (state != MultiplayerUserState.Playing)
- PlayingUsers.Remove(userId);
+ updateUserPlayingState(userId, state);
RoomUpdated?.Invoke();
}, false);
@@ -337,8 +337,6 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
- PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID));
-
MatchStarted?.Invoke();
}, false);
@@ -454,5 +452,24 @@ namespace osu.Game.Online.Multiplayer
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
apiRoom.Playlist.Add(playlistItem);
}
+
+ ///
+ /// For the provided user ID, update whether the user is included in .
+ ///
+ /// The user's ID.
+ /// The new state of the user.
+ private void updateUserPlayingState(int userId, MultiplayerUserState state)
+ {
+ bool wasPlaying = CurrentMatchPlayingUserIds.Contains(userId);
+ bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay;
+
+ if (isPlaying == wasPlaying)
+ return;
+
+ if (isPlaying)
+ CurrentMatchPlayingUserIds.Add(userId);
+ else
+ CurrentMatchPlayingUserIds.Remove(userId);
+ }
}
}
diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs
index 58ede5502a..3084c7475a 100644
--- a/osu.Game/Overlays/AccountCreationOverlay.cs
+++ b/osu.Game/Overlays/AccountCreationOverlay.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
+using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
@@ -93,6 +94,11 @@ namespace osu.Game.Overlays
if (welcomeScreen.GetChildScreen() != null)
welcomeScreen.MakeCurrent();
+
+ // there might be a stale scheduled hide from a previous API state change.
+ // cancel it here so that the overlay is not hidden again after one frame.
+ scheduledHide?.Cancel();
+ scheduledHide = null;
}
protected override void PopOut()
@@ -101,7 +107,9 @@ namespace osu.Game.Overlays
this.FadeOut(100);
}
- private void apiStateChanged(ValueChangedEvent state) => Schedule(() =>
+ private ScheduledDelegate scheduledHide;
+
+ private void apiStateChanged(ValueChangedEvent state)
{
switch (state.NewValue)
{
@@ -113,9 +121,10 @@ namespace osu.Game.Overlays
break;
case APIState.Online:
- Hide();
+ scheduledHide?.Cancel();
+ scheduledHide = Schedule(Hide);
break;
}
- });
+ }
}
}
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 65ff0fef92..2d071b7345 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -9,14 +9,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using System;
using System.Linq;
-using System.Text.RegularExpressions;
using osu.Game.Graphics.Sprites;
-using osu.Game.Users;
-using osuTK.Graphics;
using osu.Framework.Allocation;
-using System.Net;
-using osuTK;
-using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Overlays.Changelog
{
@@ -63,126 +57,7 @@ namespace osu.Game.Overlays.Changelog
Margin = new MarginPadding { Top = 35, Bottom = 15 },
});
- var fontLarge = OsuFont.GetFont(size: 16);
- var fontMedium = OsuFont.GetFont(size: 12);
-
- foreach (var entry in categoryEntries)
- {
- var entryColour = entry.Major ? colours.YellowLight : Color4.White;
-
- LinkFlowContainer title;
-
- var titleContainer = new Container
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Margin = new MarginPadding { Vertical = 5 },
- Children = new Drawable[]
- {
- new SpriteIcon
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreRight,
- Size = new Vector2(10),
- Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus,
- Colour = entryColour.Opacity(0.5f),
- Margin = new MarginPadding { Right = 5 },
- },
- title = new LinkFlowContainer
- {
- Direction = FillDirection.Full,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- TextAnchor = Anchor.BottomLeft,
- }
- }
- };
-
- title.AddText(entry.Title, t =>
- {
- t.Font = fontLarge;
- t.Colour = entryColour;
- });
-
- if (!string.IsNullOrEmpty(entry.Repository))
- {
- title.AddText(" (", t =>
- {
- t.Font = fontLarge;
- t.Colour = entryColour;
- });
- title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
- creationParameters: t =>
- {
- t.Font = fontLarge;
- t.Colour = entryColour;
- });
- title.AddText(")", t =>
- {
- t.Font = fontLarge;
- t.Colour = entryColour;
- });
- }
-
- title.AddText("by ", t =>
- {
- t.Font = fontMedium;
- t.Colour = entryColour;
- t.Padding = new MarginPadding { Left = 10 };
- });
-
- if (entry.GithubUser != null)
- {
- if (entry.GithubUser.UserId != null)
- {
- title.AddUserLink(new User
- {
- Username = entry.GithubUser.OsuUsername,
- Id = entry.GithubUser.UserId.Value
- }, t =>
- {
- t.Font = fontMedium;
- t.Colour = entryColour;
- });
- }
- else if (entry.GithubUser.GithubUrl != null)
- {
- title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
- {
- t.Font = fontMedium;
- t.Colour = entryColour;
- });
- }
- else
- {
- title.AddText(entry.GithubUser.DisplayName, t =>
- {
- t.Font = fontMedium;
- t.Colour = entryColour;
- });
- }
- }
-
- ChangelogEntries.Add(titleContainer);
-
- if (!string.IsNullOrEmpty(entry.MessageHtml))
- {
- var message = new TextFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- };
-
- // todo: use markdown parsing once API returns markdown
- message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t =>
- {
- t.Font = fontMedium;
- t.Colour = colourProvider.Foreground1;
- });
-
- ChangelogEntries.Add(message);
- }
- }
+ ChangelogEntries.AddRange(categoryEntries.Select(entry => new ChangelogEntry(entry)));
}
}
diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs
new file mode 100644
index 0000000000..55edb40283
--- /dev/null
+++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs
@@ -0,0 +1,202 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Net;
+using System.Text.RegularExpressions;
+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.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Users;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Changelog
+{
+ public class ChangelogEntry : FillFlowContainer
+ {
+ private readonly APIChangelogEntry entry;
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ private FontUsage fontLarge;
+ private FontUsage fontMedium;
+
+ public ChangelogEntry(APIChangelogEntry entry)
+ {
+ this.entry = entry;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Direction = FillDirection.Vertical;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ fontLarge = OsuFont.GetFont(size: 16);
+ fontMedium = OsuFont.GetFont(size: 12);
+
+ Children = new[]
+ {
+ createTitle(),
+ createMessage()
+ };
+ }
+
+ private Drawable createTitle()
+ {
+ var entryColour = entry.Major ? colours.YellowLight : Color4.White;
+
+ LinkFlowContainer title;
+
+ var titleContainer = new Container
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Margin = new MarginPadding { Vertical = 5 },
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(10),
+ Icon = getIconForChangelogEntry(entry.Type),
+ Colour = entryColour.Opacity(0.5f),
+ Margin = new MarginPadding { Right = 5 },
+ },
+ title = new LinkFlowContainer
+ {
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ TextAnchor = Anchor.BottomLeft,
+ }
+ }
+ };
+
+ title.AddText(entry.Title, t =>
+ {
+ t.Font = fontLarge;
+ t.Colour = entryColour;
+ });
+
+ if (!string.IsNullOrEmpty(entry.Repository))
+ addRepositoryReference(title, entryColour);
+
+ if (entry.GithubUser != null)
+ addGithubAuthorReference(title, entryColour);
+
+ return titleContainer;
+ }
+
+ private void addRepositoryReference(LinkFlowContainer title, Color4 entryColour)
+ {
+ title.AddText(" (", t =>
+ {
+ t.Font = fontLarge;
+ t.Colour = entryColour;
+ });
+ title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
+ t =>
+ {
+ t.Font = fontLarge;
+ t.Colour = entryColour;
+ });
+ title.AddText(")", t =>
+ {
+ t.Font = fontLarge;
+ t.Colour = entryColour;
+ });
+ }
+
+ private void addGithubAuthorReference(LinkFlowContainer title, Color4 entryColour)
+ {
+ title.AddText("by ", t =>
+ {
+ t.Font = fontMedium;
+ t.Colour = entryColour;
+ t.Padding = new MarginPadding { Left = 10 };
+ });
+
+ if (entry.GithubUser.UserId != null)
+ {
+ title.AddUserLink(new User
+ {
+ Username = entry.GithubUser.OsuUsername,
+ Id = entry.GithubUser.UserId.Value
+ }, t =>
+ {
+ t.Font = fontMedium;
+ t.Colour = entryColour;
+ });
+ }
+ else if (entry.GithubUser.GithubUrl != null)
+ {
+ title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
+ {
+ t.Font = fontMedium;
+ t.Colour = entryColour;
+ });
+ }
+ else
+ {
+ title.AddText(entry.GithubUser.DisplayName, t =>
+ {
+ t.Font = fontMedium;
+ t.Colour = entryColour;
+ });
+ }
+ }
+
+ private Drawable createMessage()
+ {
+ if (string.IsNullOrEmpty(entry.MessageHtml))
+ return Empty();
+
+ var message = new TextFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ };
+
+ // todo: use markdown parsing once API returns markdown
+ message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t =>
+ {
+ t.Font = fontMedium;
+ t.Colour = colourProvider.Foreground1;
+ });
+
+ return message;
+ }
+
+ private static IconUsage getIconForChangelogEntry(ChangelogEntryType entryType)
+ {
+ // compare: https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/_components/changelog-entry.coffee#L8-L11
+ switch (entryType)
+ {
+ case ChangelogEntryType.Add:
+ return FontAwesome.Solid.Plus;
+
+ case ChangelogEntryType.Fix:
+ return FontAwesome.Solid.Check;
+
+ case ChangelogEntryType.Misc:
+ return FontAwesome.Regular.Circle;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(entryType), $"Unrecognised entry type {entryType}");
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs
index 509a6dabae..aa36a5c8fd 100644
--- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs
@@ -7,6 +7,11 @@ namespace osu.Game.Overlays.Changelog
{
public class ChangelogUpdateStreamControl : OverlayStreamControl
{
+ public ChangelogUpdateStreamControl()
+ {
+ SelectFirstTabByDefault = false;
+ }
+
protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value);
}
}
diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index e2d4fc6051..7999023998 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -73,15 +73,19 @@ namespace osu.Game
// find closest valid target
IScreen current = getCurrentScreen();
+ if (current == null)
+ return;
+
// a dialog may be blocking the execution for now.
if (checkForDialog(current)) return;
game?.CloseAllOverlays(false);
// we may already be at the target screen type.
- if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled)
+ if (validScreens.Contains(current.GetType()) && !beatmap.Disabled)
{
- complete();
+ finalAction(current);
+ Cancel();
return;
}
@@ -135,11 +139,5 @@ namespace osu.Game
lastEncounteredDialogScreen = current;
return true;
}
-
- private void complete()
- {
- finalAction(getCurrentScreen());
- Cancel();
- }
}
}
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 474cbde192..fd4d74dc6b 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -156,11 +156,11 @@ namespace osu.Game.Screens.Menu
private void onMultiplayer()
{
- if (!api.IsLoggedIn)
+ if (api.State.Value != APIState.Online)
{
notifications?.Post(new SimpleNotification
{
- Text = "You gotta be logged in to multi 'yo!",
+ Text = "You gotta be online to multi 'yo!",
Icon = FontAwesome.Solid.Globe,
Activated = () =>
{
@@ -177,11 +177,11 @@ namespace osu.Game.Screens.Menu
private void onPlaylists()
{
- if (!api.IsLoggedIn)
+ if (api.State.Value != APIState.Online)
{
notifications?.Post(new SimpleNotification
{
- Text = "You gotta be logged in to multi 'yo!",
+ Text = "You gotta be online to view playlists 'yo!",
Icon = FontAwesome.Solid.Globe,
Activated = () =>
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
index 76f5c74433..310617a0bc 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
@@ -33,7 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!this.IsCurrentScreen())
{
multiplayerRoomManager.TimeBetweenListingPolls.Value = 0;
- multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0;
}
else
{
@@ -41,18 +40,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
case LoungeSubScreen _:
multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
- multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
// Don't poll inside the match or anywhere else.
default:
multiplayerRoomManager.TimeBetweenListingPolls.Value = 0;
- multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0;
break;
}
}
- Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})");
+ Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value})");
}
protected override Room CreateNewRoom()
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
index 0842574f54..72539a2e3a 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
@@ -8,9 +9,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Screens;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
@@ -29,6 +33,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private LoadingLayer loadingLayer;
+ private WorkingBeatmap initialBeatmap;
+ private RulesetInfo initialRuleset;
+ private IReadOnlyList initialMods;
+
+ private bool itemSelected;
+
public MultiplayerMatchSongSelect()
{
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
@@ -38,10 +48,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void load()
{
AddInternal(loadingLayer = new LoadingLayer(Carousel));
+ initialBeatmap = Beatmap.Value;
+ initialRuleset = Ruleset.Value;
+ initialMods = Mods.Value.ToList();
}
protected override bool OnStart()
{
+ itemSelected = true;
var item = new PlaylistItem();
item.Beatmap.Value = Beatmap.Value.BeatmapInfo;
@@ -82,6 +96,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
+ public override bool OnExiting(IScreen next)
+ {
+ if (!itemSelected)
+ {
+ Beatmap.Value = initialBeatmap;
+ Ruleset.Value = initialRuleset;
+ Mods.Value = initialMods;
+ }
+
+ return base.OnExiting(next);
+ }
+
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 58314c3774..ffa36ecfdb 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -200,7 +200,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
Debug.Assert(client.Room != null);
- int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray();
+ int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs
index 3cb263298f..5c327266a3 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private StatefulMultiplayerClient multiplayerClient { get; set; }
public readonly Bindable TimeBetweenListingPolls = new Bindable();
- public readonly Bindable TimeBetweenSelectionPolls = new Bindable();
+
private readonly IBindable isConnected = new Bindable();
private readonly Bindable allowPolling = new Bindable();
@@ -119,11 +119,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls },
AllowPolling = { BindTarget = allowPolling }
},
- new MultiplayerSelectionPollingComponent
- {
- TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls },
- AllowPolling = { BindTarget = allowPolling }
- }
};
private class MultiplayerListingPollingComponent : ListingPollingComponent
@@ -146,26 +141,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
-
- private class MultiplayerSelectionPollingComponent : SelectionPollingComponent
- {
- public readonly IBindable AllowPolling = new Bindable();
-
- 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();
- }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index de3069b2f6..f99655e305 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . 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;
@@ -45,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[BackgroundDependencyLoader]
private void load()
{
- Debug.Assert(User.User != null);
+ var user = User.User;
var backgroundColour = Color4Extensions.FromHex("#33413C");
@@ -82,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
Width = 0.75f,
- User = User.User,
+ User = user,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f))
},
new FillFlowContainer
@@ -98,28 +97,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
- User = User.User
+ User = user
},
new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(30, 20),
- Country = User.User.Country
+ Country = user?.Country
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
- Text = User.User.Username
+ Text = 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
+ Text = user?.CurrentModeRank != null ? $"#{user.CurrentModeRank}" : string.Empty
}
}
},
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 4074dd1573..75612516a9 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -8,6 +8,7 @@ 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.Drawables;
using osu.Game.Graphics.Containers;
@@ -165,7 +166,10 @@ namespace osu.Game.Screens.OnlinePlay
private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
{
if (state.NewValue != APIState.Online)
+ {
+ Logger.Log("API connection was lost, can't continue with online play", LoggingTarget.Network, LogLevel.Important);
Schedule(forcefullyExit);
+ }
});
protected override void LoadComplete()
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
index e33cc05e64..7b94bf19ec 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -42,7 +43,7 @@ namespace osu.Game.Screens.Play.HUD
/// Whether the player should be tracked on the leaderboard.
/// Set to true for the local player or a player whose replay is currently being played.
///
- public ILeaderboardScore AddPlayer(User user, bool isTracked)
+ public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked)
{
var drawable = new GameplayLeaderboardScore(user, isTracked)
{
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 51b19a8d45..cb20deb272 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -34,6 +35,7 @@ namespace osu.Game.Screens.Play.HUD
public BindableDouble TotalScore { get; } = new BindableDouble();
public BindableDouble Accuracy { get; } = new BindableDouble(1);
public BindableInt Combo { get; } = new BindableInt();
+ public BindableBool HasQuit { get; } = new BindableBool();
private int? scorePosition;
@@ -51,10 +53,11 @@ namespace osu.Game.Screens.Play.HUD
positionText.Text = $"#{scorePosition.Value.FormatRank()}";
positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
- updateColour();
+ updateState();
}
}
+ [CanBeNull]
public User User { get; }
private readonly bool trackedPlayer;
@@ -67,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD
///
/// The score's player.
/// Whether the player is the local user or a replay player.
- public GameplayLeaderboardScore(User user, bool trackedPlayer)
+ public GameplayLeaderboardScore([CanBeNull] User user, bool trackedPlayer)
{
User = user;
this.trackedPlayer = trackedPlayer;
@@ -179,7 +182,7 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
- Text = User.Username,
+ Text = User?.Username,
Truncate = true,
Shadow = false,
}
@@ -230,20 +233,31 @@ namespace osu.Game.Screens.Play.HUD
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);
+ HasQuit.BindValueChanged(_ => updateState());
}
protected override void LoadComplete()
{
base.LoadComplete();
- updateColour();
+ updateState();
FinishTransforms(true);
}
private const double panel_transition_duration = 500;
- private void updateColour()
+ private void updateState()
{
+ if (HasQuit.Value)
+ {
+ // we will probably want to display this in a better way once we have a design.
+ // and also show states other than quit.
+ mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
+ panelColour = Color4.Gray;
+ textColour = Color4.White;
+ return;
+ }
+
if (scorePosition == 1)
{
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
index bc1a03c5aa..83b6f6621b 100644
--- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
@@ -10,5 +10,7 @@ namespace osu.Game.Screens.Play.HUD
BindableDouble TotalScore { get; }
BindableDouble Accuracy { get; }
BindableInt Combo { get; }
+
+ BindableBool HasQuit { get; }
}
}
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index c10ec9e004..d4ce542a67 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
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.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
@@ -18,10 +21,21 @@ namespace osu.Game.Screens.Play.HUD
{
private readonly ScoreProcessor scoreProcessor;
- private readonly int[] userIds;
-
private readonly Dictionary userScores = new Dictionary();
+ [Resolved]
+ private SpectatorStreamingClient streamingClient { get; set; }
+
+ [Resolved]
+ private StatefulMultiplayerClient multiplayerClient { get; set; }
+
+ [Resolved]
+ private UserLookupCache userLookupCache { get; set; }
+
+ private Bindable scoringMode;
+
+ private readonly BindableList playingUsers;
+
///
/// Construct a new leaderboard.
///
@@ -33,43 +47,68 @@ namespace osu.Game.Screens.Play.HUD
this.scoreProcessor = scoreProcessor;
// todo: this will likely be passed in as User instances.
- this.userIds = userIds;
+ playingUsers = new BindableList(userIds);
}
- [Resolved]
- private SpectatorStreamingClient streamingClient { get; set; }
-
- [Resolved]
- private UserLookupCache userLookupCache { get; set; }
-
- private Bindable scoringMode;
-
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
streamingClient.OnNewFrames += handleIncomingFrames;
- foreach (var user in userIds)
+ foreach (var userId in playingUsers)
{
- streamingClient.WatchUser(user);
+ streamingClient.WatchUser(userId);
// probably won't be required in the final implementation.
- var resolvedUser = userLookupCache.GetUserAsync(user).Result;
+ var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
var trackedUser = new TrackedUserData();
- userScores[user] = trackedUser;
- var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id);
+ userScores[userId] = trackedUser;
+ var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id);
((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
+ ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit);
}
scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
scoringMode.BindValueChanged(updateAllScores, true);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
+ foreach (int userId in playingUsers)
+ {
+ if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId))
+ usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId }));
+ }
+
+ playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
+ playingUsers.BindCollectionChanged(usersChanged);
+ }
+
+ private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var userId in e.OldItems.OfType())
+ {
+ streamingClient.StopWatchingUser(userId);
+
+ if (userScores.TryGetValue(userId, out var trackedData))
+ trackedData.MarkUserQuit();
+ }
+
+ break;
+ }
+ }
+
private void updateAllScores(ValueChangedEvent mode)
{
foreach (var trackedData in userScores.Values)
@@ -91,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD
if (streamingClient != null)
{
- foreach (var user in userIds)
+ foreach (var user in playingUsers)
{
streamingClient.StopWatchingUser(user);
}
@@ -114,9 +153,15 @@ namespace osu.Game.Screens.Play.HUD
private readonly BindableInt currentCombo = new BindableInt();
+ public IBindable UserQuit => userQuit;
+
+ private readonly BindableBool userQuit = new BindableBool();
+
[CanBeNull]
public FrameHeader LastHeader;
+ public void MarkUserQuit() => userQuit.Value = true;
+
public void UpdateScore(ScoreProcessor processor, ScoringMode mode)
{
if (LastHeader == null)
diff --git a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs
similarity index 95%
rename from osu.Game/Screens/Play/SkinnableHealthDisplay.cs
rename to osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs
index d35d15d665..1f91f5e50f 100644
--- a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs
@@ -5,10 +5,9 @@ using System;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay
{
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 9a839c8d22..2ce5211757 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user });
+ public void AddNullUser(int userId) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(userId));
+
public void RemoveUser(User user)
{
Debug.Assert(Room != null);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index cbf9f6f1bd..6c220a5c21 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -21,12 +21,12 @@
-
-
+
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index adbcc0ef1c..5445adb3fb 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -88,7 +88,7 @@
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 22ea73858e..aa8f8739c1 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -106,6 +106,7 @@
HINT
WARNING
WARNING
+ DO_NOT_SHOW
WARNING
WARNING
WARNING