diff --git a/.idea/.idea.osu/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
index 344301d4a7..2735f4ceb3 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
@@ -1,17 +1,20 @@
-
+
+
-
-
+
+
+
+
\ No newline at end of file
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index ab65541ecf..ad08f57c3a 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -1,7 +1,7 @@
- netcoreapp2.1
+ netcoreapp2.2
WinExe
AnyCPU
true
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 40f2375251..e875af5a30 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
@@ -9,7 +9,7 @@
WinExe
- netcoreapp2.1
+ netcoreapp2.2
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 12bf9759c4..0c6fbfa7d3 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
@@ -9,7 +9,7 @@
WinExe
- netcoreapp2.1
+ netcoreapp2.2
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 3e06aab0e5..35f137572d 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
@@ -9,7 +9,7 @@
WinExe
- netcoreapp2.1
+ netcoreapp2.2
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 e0097bf9c3..0fc01deed6 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
@@ -9,7 +9,7 @@
WinExe
- netcoreapp2.1
+ netcoreapp2.2
diff --git a/osu.Game.Tests/Visual/TestCasePollingComponent.cs b/osu.Game.Tests/Visual/TestCasePollingComponent.cs
new file mode 100644
index 0000000000..b4b9d465e5
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCasePollingComponent.cs
@@ -0,0 +1,143 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Logging;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual
+{
+ public class TestCasePollingComponent : OsuTestCase
+ {
+ private Container pollBox;
+ private TestPoller poller;
+
+ private const float safety_adjust = 1f;
+ private int count;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ count = 0;
+
+ Children = new Drawable[]
+ {
+ pollBox = new Container
+ {
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(0.4f),
+ Colour = Color4.LimeGreen,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = "Poll!",
+ }
+ }
+ }
+ };
+ });
+
+ [Test]
+ public void TestInstantPolling()
+ {
+ createPoller(true);
+
+ AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust);
+ checkCount(1);
+ checkCount(2);
+ checkCount(3);
+
+ AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5);
+ checkCount(4);
+ checkCount(4);
+ checkCount(4);
+
+ skip();
+
+ checkCount(5);
+ checkCount(5);
+
+ AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust);
+ checkCount(6);
+ checkCount(7);
+ }
+
+ [Test]
+ [Ignore("i have no idea how to fix the timing of this one")]
+ public void TestSlowPolling()
+ {
+ createPoller(false);
+
+ AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5);
+ checkCount(0);
+ skip();
+ checkCount(0);
+ skip();
+ skip();
+ checkCount(0);
+ skip();
+ skip();
+ checkCount(0);
+ }
+
+ private void skip() => AddStep("skip", () =>
+ {
+ // could be 4 or 5 at this point due to timing discrepancies (safety_adjust @ 0.2 * 5 ~= 1)
+ // easiest to just ignore the value at this point and move on.
+ });
+
+ private void checkCount(int checkValue)
+ {
+ Logger.Log($"value is {count}");
+ AddAssert($"count is {checkValue}", () => count == checkValue);
+ }
+
+ private void createPoller(bool instant) => AddStep("create poller", () =>
+ {
+ poller?.Expire();
+
+ Add(poller = instant ? new TestPoller() : new TestSlowPoller());
+ poller.OnPoll += () =>
+ {
+ pollBox.FadeOutFromOne(500);
+ count++;
+ };
+ });
+
+ protected override double TimePerAction => 500;
+
+ public class TestPoller : PollingComponent
+ {
+ public event Action OnPoll;
+
+ protected override Task Poll()
+ {
+ Schedule(() => OnPoll?.Invoke());
+ return base.Poll();
+ }
+ }
+
+ public class TestSlowPoller : TestPoller
+ {
+ protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll());
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index d45f1dd962..e6786dfd15 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -10,7 +10,7 @@
WinExe
- netcoreapp2.1
+ netcoreapp2.2
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 1dda257c95..a1376b8b94 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -199,6 +199,12 @@ namespace osu.Game.Online.API
try
{
Logger.Log($@"Performing request {req}", LoggingTarget.Network);
+ req.Failure += ex =>
+ {
+ if (ex is WebException we)
+ handleWebException(we);
+ };
+
req.Perform(this);
//we could still be in initialisation, at which point we don't want to say we're Online yet.
@@ -210,37 +216,12 @@ namespace osu.Game.Online.API
}
catch (WebException we)
{
- HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode
- ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
+ var removeFromQueue = handleWebException(we);
- // special cases for un-typed but useful message responses.
- switch (we.Message)
- {
- case "Unauthorized":
- statusCode = HttpStatusCode.Unauthorized;
- break;
- }
+ if (removeFromQueue)
+ req.Fail(we);
- switch (statusCode)
- {
- case HttpStatusCode.Unauthorized:
- Logout(false);
- return true;
- 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;
-
- State = APIState.Failing;
- flushQueue();
- return true;
- }
-
- req.Fail(we);
- return true;
+ return removeFromQueue;
}
catch (Exception e)
{
@@ -276,6 +257,41 @@ namespace osu.Game.Online.API
}
}
+ private bool handleWebException(WebException we)
+ {
+ HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode
+ ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
+
+ // special cases for un-typed but useful message responses.
+ switch (we.Message)
+ {
+ case "Unauthorized":
+ case "Forbidden":
+ statusCode = HttpStatusCode.Unauthorized;
+ break;
+ }
+
+ switch (statusCode)
+ {
+ case HttpStatusCode.Unauthorized:
+ Logout(false);
+ return true;
+ 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;
+
+ State = APIState.Failing;
+ flushQueue();
+ return true;
+ }
+
+ return true;
+ }
+
public bool IsLoggedIn => LocalUser.Value.Id > 1;
public void Queue(APIRequest request)
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 863ad3042f..a63af0f7a3 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -4,11 +4,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
-using osu.Framework.Graphics;
using osu.Framework.Logging;
-using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Users;
@@ -18,7 +17,7 @@ namespace osu.Game.Online.Chat
///
/// Manages everything channel related
///
- public class ChannelManager : Component, IOnlineComponent
+ public class ChannelManager : PollingComponent
{
///
/// The channels the player joins on startup
@@ -49,11 +48,14 @@ namespace osu.Game.Online.Chat
public IBindableCollection AvailableChannels => availableChannels;
private IAPIProvider api;
- private ScheduledDelegate fetchMessagesScheduleder;
+
+ public readonly BindableBool HighPollRate = new BindableBool();
public ChannelManager()
{
CurrentChannel.ValueChanged += currentChannelChanged;
+
+ HighPollRate.BindValueChanged(high => TimeBetweenPolls = high ? 1000 : 6000, true);
}
///
@@ -360,73 +362,60 @@ namespace osu.Game.Online.Chat
}
}
- public void APIStateChanged(APIAccess api, APIState state)
- {
- switch (state)
- {
- case APIState.Online:
- fetchUpdates();
- break;
- default:
- fetchMessagesScheduleder?.Cancel();
- fetchMessagesScheduleder = null;
- break;
- }
- }
-
private long lastMessageId;
- private const int update_poll_interval = 1000;
private bool channelsInitialised;
- private void fetchUpdates()
+ protected override Task Poll()
{
- fetchMessagesScheduleder?.Cancel();
- fetchMessagesScheduleder = Scheduler.AddDelayed(() =>
+ if (!api.IsLoggedIn)
+ return base.Poll();
+
+ var fetchReq = new GetUpdatesRequest(lastMessageId);
+
+ var tcs = new TaskCompletionSource();
+
+ fetchReq.Success += updates =>
{
- var fetchReq = new GetUpdatesRequest(lastMessageId);
-
- fetchReq.Success += updates =>
+ if (updates?.Presence != null)
{
- if (updates?.Presence != null)
+ foreach (var channel in updates.Presence)
{
- foreach (var channel in updates.Presence)
- {
- // we received this from the server so should mark the channel already joined.
- JoinChannel(channel, true);
- }
-
- //todo: handle left channels
-
- handleChannelMessages(updates.Messages);
-
- foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
- JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
-
- lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
+ // we received this from the server so should mark the channel already joined.
+ JoinChannel(channel, true);
}
- if (!channelsInitialised)
- {
- channelsInitialised = true;
- // we want this to run after the first presence so we can see if the user is in any channels already.
- initializeChannels();
- }
+ //todo: handle left channels
- fetchUpdates();
- };
+ handleChannelMessages(updates.Messages);
- fetchReq.Failure += delegate { fetchUpdates(); };
+ foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
+ JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
- api.Queue(fetchReq);
- }, update_poll_interval);
+ lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
+ }
+
+ if (!channelsInitialised)
+ {
+ channelsInitialised = true;
+ // we want this to run after the first presence so we can see if the user is in any channels already.
+ initializeChannels();
+ }
+
+ tcs.SetResult(true);
+ };
+
+ fetchReq.Failure += _ => tcs.SetResult(false);
+
+ api.Queue(fetchReq);
+
+ return tcs.Task;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
this.api = api;
- api.Register(this);
}
}
diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs
new file mode 100644
index 0000000000..9d0bed7595
--- /dev/null
+++ b/osu.Game/Online/PollingComponent.cs
@@ -0,0 +1,118 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Threading.Tasks;
+using osu.Framework.Graphics;
+using osu.Framework.Threading;
+
+namespace osu.Game.Online
+{
+ ///
+ /// A component which requires a constant polling process.
+ ///
+ public abstract class PollingComponent : Component
+ {
+ private double? lastTimePolled;
+
+ private ScheduledDelegate scheduledPoll;
+
+ private bool pollingActive;
+
+ private double timeBetweenPolls;
+
+ ///
+ /// The time in milliseconds to wait between polls.
+ /// Setting to zero stops all polling.
+ ///
+ public double TimeBetweenPolls
+ {
+ get => timeBetweenPolls;
+ set
+ {
+ timeBetweenPolls = value;
+ scheduledPoll?.Cancel();
+ pollIfNecessary();
+ }
+ }
+
+ ///
+ ///
+ ///
+ /// The initial time in milliseconds to wait between polls. Setting to zero stops al polling.
+ protected PollingComponent(double timeBetweenPolls = 0)
+ {
+ TimeBetweenPolls = timeBetweenPolls;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ pollIfNecessary();
+ }
+
+ private bool pollIfNecessary()
+ {
+ // we must be loaded so we have access to clock.
+ if (!IsLoaded) return false;
+
+ // there's already a poll process running.
+ if (pollingActive) return false;
+
+ // don't try polling if the time between polls hasn't been set.
+ if (timeBetweenPolls == 0) return false;
+
+ if (!lastTimePolled.HasValue)
+ {
+ doPoll();
+ return true;
+ }
+
+ if (Time.Current - lastTimePolled.Value > timeBetweenPolls)
+ {
+ doPoll();
+ return true;
+ }
+
+ // not ennough time has passed since the last poll. we do want to schedule a poll to happen, though.
+ scheduleNextPoll();
+ return false;
+ }
+
+ private void doPoll()
+ {
+ scheduledPoll = null;
+ pollingActive = true;
+ Poll().ContinueWith(_ => pollComplete());
+ }
+
+ ///
+ /// Perform the polling in this method. Call when done.
+ ///
+ protected virtual Task Poll()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Call when a poll operation has completed.
+ ///
+ private void pollComplete()
+ {
+ lastTimePolled = Time.Current;
+ pollingActive = false;
+
+ if (scheduledPoll == null)
+ scheduleNextPoll();
+ }
+
+ private void scheduleNextPoll()
+ {
+ scheduledPoll?.Cancel();
+
+ double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0;
+
+ scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration));
+ }
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 73ecbafb9e..31a00e68ac 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -418,6 +418,8 @@ namespace osu.Game
dependencies.Cache(notifications);
dependencies.Cache(dialogOverlay);
+ chatOverlay.StateChanged += state => channelManager.HighPollRate.Value = state == Visibility.Visible;
+
Add(externalLinkOpener = new ExternalLinkOpener());
var singleDisplaySideOverlays = new OverlayContainer[] { settings, notifications };
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 23f35d5d3a..e259996b7f 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections
public override FontAwesome Icon => FontAwesome.fa_paint_brush;
- private readonly Bindable dropdownBindable = new Bindable();
+ private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default };
private readonly Bindable configBindable = new Bindable();
private SkinManager skins;
diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs
index c5a4953c5e..5f7cf17a9d 100644
--- a/osu.Game/Overlays/WaveOverlayContainer.cs
+++ b/osu.Game/Overlays/WaveOverlayContainer.cs
@@ -25,13 +25,20 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
base.PopIn();
+
Waves.Show();
+
+ this.FadeIn();
}
protected override void PopOut()
{
base.PopOut();
+
Waves.Hide();
+
+ // this is required or we will remain present even though our waves are hidden.
+ this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
}
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index bf44e9e636..19b49b099c 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Screens.Play
{
Retries = RestartCount,
OnRetry = Restart,
- OnQuit = Exit,
+ OnQuit = performUserRequestedExit,
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded,
Children = new[]
{
@@ -211,7 +211,7 @@ namespace osu.Game.Screens.Play
failOverlay = new FailOverlay
{
OnRetry = Restart,
- OnQuit = Exit,
+ OnQuit = performUserRequestedExit,
},
new HotkeyRetryOverlay
{
@@ -225,7 +225,7 @@ namespace osu.Game.Screens.Play
}
};
- hudOverlay.HoldToQuit.Action = Exit;
+ hudOverlay.HoldToQuit.Action = performUserRequestedExit;
hudOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
RulesetContainer.IsPaused.BindTo(pauseContainer.IsPaused);
@@ -250,8 +250,16 @@ namespace osu.Game.Screens.Play
mod.ApplyToClock(sourceClock);
}
+ private void performUserRequestedExit()
+ {
+ if (!IsCurrentScreen) return;
+ Exit();
+ }
+
public void Restart()
{
+ if (!IsCurrentScreen) return;
+
sampleRestart?.Play();
ValidForResume = false;
RestartRequested?.Invoke();
diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/LocalSkinOverrideContainer.cs
index 2ab22af289..e555a53453 100644
--- a/osu.Game/Skinning/LocalSkinOverrideContainer.cs
+++ b/osu.Game/Skinning/LocalSkinOverrideContainer.cs
@@ -16,8 +16,8 @@ namespace osu.Game.Skinning
{
public event Action SourceChanged;
- private Bindable beatmapSkins = new Bindable();
- private Bindable beatmapHitsounds = new Bindable();
+ private readonly Bindable beatmapSkins = new Bindable();
+ private readonly Bindable beatmapHitsounds = new Bindable();
public Drawable GetDrawableComponent(string componentName)
{
@@ -84,11 +84,8 @@ namespace osu.Game.Skinning
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
- beatmapSkins = config.GetBindable(OsuSetting.BeatmapSkins);
- beatmapSkins.BindValueChanged(_ => onSourceChanged());
-
- beatmapHitsounds = config.GetBindable(OsuSetting.BeatmapHitsounds);
- beatmapHitsounds.BindValueChanged(_ => onSourceChanged(), true);
+ config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
+ config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds);
}
protected override void LoadComplete()
@@ -97,6 +94,9 @@ namespace osu.Game.Skinning
if (fallbackSource != null)
fallbackSource.SourceChanged += onSourceChanged;
+
+ beatmapSkins.BindValueChanged(_ => onSourceChanged());
+ beatmapHitsounds.BindValueChanged(_ => onSourceChanged(), true);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ed0e6c8417..305c9035b5 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,7 +18,7 @@
-
+