diff --git a/README.md b/README.md
index 7c749f3422..86c42dae12 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,8 @@ If you are looking to install or test osu! without setting up a development envi
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
+- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
+
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
diff --git a/osu.Android.props b/osu.Android.props
index 2d531cf01e..a4bcbd289d 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index ab840e1c46..e8c2472c3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests
objects.Add(new Note { StartTime = time });
+ // don't hit the first note
if (i > 0)
{
frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1));
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
index fde42bec04..9bfb6aa839 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
diff --git a/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs
new file mode 100644
index 0000000000..21ec29b10b
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs
@@ -0,0 +1,296 @@
+// 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 NUnit.Framework;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class StreamingFramedReplayInputHandlerTest
+ {
+ private Replay replay;
+ private TestInputHandler handler;
+
+ [SetUp]
+ public void SetUp()
+ {
+ handler = new TestInputHandler(replay = new Replay
+ {
+ HasReceivedAllFrames = false,
+ Frames = new List
+ {
+ new TestReplayFrame(0),
+ new TestReplayFrame(1000),
+ new TestReplayFrame(2000),
+ new TestReplayFrame(3000, true),
+ new TestReplayFrame(4000, true),
+ new TestReplayFrame(5000, true),
+ new TestReplayFrame(7000, true),
+ new TestReplayFrame(8000),
+ }
+ });
+ }
+
+ [Test]
+ public void TestNormalPlayback()
+ {
+ Assert.IsNull(handler.CurrentFrame);
+
+ confirmCurrentFrame(null);
+ confirmNextFrame(0);
+
+ setTime(0, 0);
+ confirmCurrentFrame(0);
+ confirmNextFrame(1);
+
+ // if we hit the first frame perfectly, time should progress to it.
+ setTime(1000, 1000);
+ confirmCurrentFrame(1);
+ confirmNextFrame(2);
+
+ // in between non-important frames should progress based on input.
+ setTime(1200, 1200);
+ confirmCurrentFrame(1);
+
+ setTime(1400, 1400);
+ confirmCurrentFrame(1);
+
+ // progressing beyond the next frame should force time to that frame once.
+ setTime(2200, 2000);
+ confirmCurrentFrame(2);
+
+ // second attempt should progress to input time
+ setTime(2200, 2200);
+ confirmCurrentFrame(2);
+
+ // entering important section
+ setTime(3000, 3000);
+ confirmCurrentFrame(3);
+
+ // cannot progress within
+ setTime(3500, null);
+ confirmCurrentFrame(3);
+
+ setTime(4000, 4000);
+ confirmCurrentFrame(4);
+
+ // still cannot progress
+ setTime(4500, null);
+ confirmCurrentFrame(4);
+
+ setTime(5200, 5000);
+ confirmCurrentFrame(5);
+
+ // important section AllowedImportantTimeSpan allowance
+ setTime(5200, 5200);
+ confirmCurrentFrame(5);
+
+ setTime(7200, 7000);
+ confirmCurrentFrame(6);
+
+ setTime(7200, null);
+ confirmCurrentFrame(6);
+
+ // exited important section
+ setTime(8200, 8000);
+ confirmCurrentFrame(7);
+ confirmNextFrame(null);
+
+ setTime(8200, null);
+ confirmCurrentFrame(7);
+ confirmNextFrame(null);
+
+ setTime(8400, null);
+ confirmCurrentFrame(7);
+ confirmNextFrame(null);
+ }
+
+ [Test]
+ public void TestIntroTime()
+ {
+ setTime(-1000, -1000);
+ confirmCurrentFrame(null);
+ confirmNextFrame(0);
+
+ setTime(-500, -500);
+ confirmCurrentFrame(null);
+ confirmNextFrame(0);
+
+ setTime(0, 0);
+ confirmCurrentFrame(0);
+ confirmNextFrame(1);
+ }
+
+ [Test]
+ public void TestBasicRewind()
+ {
+ setTime(2800, 0);
+ setTime(2800, 1000);
+ setTime(2800, 2000);
+ setTime(2800, 2800);
+ confirmCurrentFrame(2);
+ confirmNextFrame(3);
+
+ // pivot without crossing a frame boundary
+ setTime(2700, 2700);
+ confirmCurrentFrame(2);
+ confirmNextFrame(1);
+
+ // cross current frame boundary; should not yet update frame
+ setTime(1980, 1980);
+ confirmCurrentFrame(2);
+ confirmNextFrame(1);
+
+ setTime(1200, 1200);
+ confirmCurrentFrame(2);
+ confirmNextFrame(1);
+
+ // ensure each frame plays out until start
+ setTime(-500, 1000);
+ confirmCurrentFrame(1);
+ confirmNextFrame(0);
+
+ setTime(-500, 0);
+ confirmCurrentFrame(0);
+ confirmNextFrame(null);
+
+ setTime(-500, -500);
+ confirmCurrentFrame(0);
+ confirmNextFrame(null);
+ }
+
+ [Test]
+ public void TestRewindInsideImportantSection()
+ {
+ fastForwardToPoint(3000);
+
+ setTime(4000, 4000);
+ confirmCurrentFrame(4);
+ confirmNextFrame(5);
+
+ setTime(3500, null);
+ confirmCurrentFrame(4);
+ confirmNextFrame(3);
+
+ setTime(3000, 3000);
+ confirmCurrentFrame(3);
+ confirmNextFrame(2);
+
+ setTime(3500, null);
+ confirmCurrentFrame(3);
+ confirmNextFrame(4);
+
+ setTime(4000, 4000);
+ confirmCurrentFrame(4);
+ confirmNextFrame(5);
+
+ setTime(4500, null);
+ confirmCurrentFrame(4);
+ confirmNextFrame(5);
+
+ setTime(4000, null);
+ confirmCurrentFrame(4);
+ confirmNextFrame(5);
+
+ setTime(3500, null);
+ confirmCurrentFrame(4);
+ confirmNextFrame(3);
+
+ setTime(3000, 3000);
+ confirmCurrentFrame(3);
+ confirmNextFrame(2);
+ }
+
+ [Test]
+ public void TestRewindOutOfImportantSection()
+ {
+ fastForwardToPoint(3500);
+
+ confirmCurrentFrame(3);
+ confirmNextFrame(4);
+
+ setTime(3200, null);
+ // next frame doesn't change even though direction reversed, because of important section.
+ confirmCurrentFrame(3);
+ confirmNextFrame(4);
+
+ setTime(3000, null);
+ confirmCurrentFrame(3);
+ confirmNextFrame(4);
+
+ setTime(2800, 2800);
+ confirmCurrentFrame(3);
+ confirmNextFrame(2);
+ }
+
+ private void fastForwardToPoint(double destination)
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ if (handler.SetFrameFromTime(destination) == null)
+ return;
+ }
+
+ throw new TimeoutException("Seek was never fulfilled");
+ }
+
+ private void setTime(double set, double? expect)
+ {
+ Assert.AreEqual(expect, handler.SetFrameFromTime(set));
+ }
+
+ private void confirmCurrentFrame(int? frame)
+ {
+ if (frame.HasValue)
+ {
+ Assert.IsNotNull(handler.CurrentFrame);
+ Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
+ }
+ else
+ {
+ Assert.IsNull(handler.CurrentFrame);
+ }
+ }
+
+ private void confirmNextFrame(int? frame)
+ {
+ if (frame.HasValue)
+ {
+ Assert.IsNotNull(handler.NextFrame);
+ Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
+ }
+ else
+ {
+ Assert.IsNull(handler.NextFrame);
+ }
+ }
+
+ private class TestReplayFrame : ReplayFrame
+ {
+ public readonly bool IsImportant;
+
+ public TestReplayFrame(double time, bool isImportant = false)
+ : base(time)
+ {
+ IsImportant = isImportant;
+ }
+ }
+
+ private class TestInputHandler : FramedReplayInputHandler
+ {
+ public TestInputHandler(Replay replay)
+ : base(replay)
+ {
+ FrameAccuratePlayback = true;
+ }
+
+ protected override double AllowedImportantTimeSpan => 1000;
+
+ protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 6ec673704c..f9914e0193 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -2,29 +2,23 @@
// 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.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestSceneHUDOverlay : SkinnableTestScene
+ public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
{
private HUDOverlay hudOverlay;
- private IEnumerable hudOverlays => CreatedDrawables.OfType();
-
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
@@ -37,17 +31,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
createNew();
- AddRepeatStep("increase combo", () =>
- {
- foreach (var hud in hudOverlays)
- hud.ComboCounter.Current.Value++;
- }, 10);
+ AddRepeatStep("increase combo", () => { hudOverlay.ComboCounter.Current.Value++; }, 10);
- AddStep("reset combo", () =>
- {
- foreach (var hud in hudOverlays)
- hud.ComboCounter.Current.Value = 0;
- });
+ AddStep("reset combo", () => { hudOverlay.ComboCounter.Current.Value = 0; });
}
[Test]
@@ -77,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
createNew();
- AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
+ AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
@@ -86,10 +72,32 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent);
}
+ [Test]
+ public void TestMomentaryShowHUD()
+ {
+ createNew();
+
+ HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay;
+
+ AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode));
+
+ AddStep("set hud to never show", () => config.Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
+
+ AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
+
+ AddStep("trigger momentary show", () => InputManager.PressKey(Key.ControlLeft));
+ AddUntilStep("wait for visible", () => hideTarget.IsPresent);
+
+ AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft));
+ AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
+
+ AddStep("set original config value", () => config.Set(OsuSetting.HUDVisibilityMode, originalConfigValue));
+ }
+
[Test]
public void TestExternalHideDoesntAffectConfig()
{
- HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks;
+ HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay;
createNew();
@@ -113,14 +121,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set keycounter visible false", () =>
{
config.Set(OsuSetting.KeyOverlay, false);
- hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
+ hudOverlay.KeyCounter.AlwaysVisible.Value = false;
});
- AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
+ AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
- AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
+ AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
@@ -131,22 +139,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create overlay", () =>
{
- SetContents(() =>
- {
- hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
+ hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
- // Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ // Add any key just to display the key counter visually.
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
- hudOverlay.ComboCounter.Current.Value = 1;
+ hudOverlay.ComboCounter.Current.Value = 1;
- action?.Invoke(hudOverlay);
+ action?.Invoke(hudOverlay);
- return hudOverlay;
- });
+ Child = hudOverlay;
});
}
-
- protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
new file mode 100644
index 0000000000..fec1610160
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -0,0 +1,99 @@
+// 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.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Configuration;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSkinnableHUDOverlay : SkinnableTestScene
+ {
+ private HUDOverlay hudOverlay;
+
+ private IEnumerable hudOverlays => CreatedDrawables.OfType();
+
+ // best way to check without exposing.
+ private Drawable hideTarget => hudOverlay.KeyCounter;
+ private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ [Test]
+ public void TestComboCounterIncrementing()
+ {
+ createNew();
+
+ AddRepeatStep("increase combo", () =>
+ {
+ foreach (var hud in hudOverlays)
+ hud.ComboCounter.Current.Value++;
+ }, 10);
+
+ AddStep("reset combo", () =>
+ {
+ foreach (var hud in hudOverlays)
+ hud.ComboCounter.Current.Value = 0;
+ });
+ }
+
+ [Test]
+ public void TestFadesInOnLoadComplete()
+ {
+ float? initialAlpha = null;
+
+ createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
+ AddUntilStep("wait for load", () => hudOverlay.IsAlive);
+ AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
+ }
+
+ [Test]
+ public void TestHideExternally()
+ {
+ createNew();
+
+ AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
+
+ AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
+ AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
+
+ // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above.
+ AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent);
+ }
+
+ private void createNew(Action action = null)
+ {
+ AddStep("create overlay", () =>
+ {
+ SetContents(() =>
+ {
+ hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
+
+ // Add any key just to display the key counter visually.
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+
+ hudOverlay.ComboCounter.Current.Value = 1;
+
+ action?.Invoke(hudOverlay);
+
+ return hudOverlay;
+ });
+ });
+ }
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
index 250fdc5ebd..5af55e99f8 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking
private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () =>
{
- Child = panel = new ScorePanel(score)
+ Child = panel = new ScorePanel(score, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index a4698a9a32..3f757031f8 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -27,6 +28,9 @@ namespace osu.Game.Tests.Visual.UserInterface
OsuSpriteText category;
OsuSpriteText genre;
OsuSpriteText language;
+ OsuSpriteText extra;
+ OsuSpriteText ranks;
+ OsuSpriteText played;
Add(control = new BeatmapListingSearchControl
{
@@ -46,6 +50,9 @@ namespace osu.Game.Tests.Visual.UserInterface
category = new OsuSpriteText(),
genre = new OsuSpriteText(),
language = new OsuSpriteText(),
+ extra = new OsuSpriteText(),
+ ranks = new OsuSpriteText(),
+ played = new OsuSpriteText()
}
});
@@ -54,6 +61,9 @@ namespace osu.Game.Tests.Visual.UserInterface
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
control.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true);
+ control.Extra.BindCollectionChanged((u, v) => extra.Text = $"Extra: {(control.Extra.Any() ? string.Join('.', control.Extra.Select(i => i.ToString().ToLowerInvariant())) : "")}", true);
+ control.Ranks.BindCollectionChanged((u, v) => ranks.Text = $"Ranks: {(control.Ranks.Any() ? string.Join('.', control.Ranks.Select(i => i.ToString())) : "")}", true);
+ control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true);
}
[Test]
diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs
index b0b55dd811..10f3f65355 100644
--- a/osu.Game/Configuration/HUDVisibilityMode.cs
+++ b/osu.Game/Configuration/HUDVisibilityMode.cs
@@ -12,9 +12,6 @@ namespace osu.Game.Configuration
[Description("Hide during gameplay")]
HideDuringGameplay,
- [Description("Hide during breaks")]
- HideDuringBreaks,
-
Always
}
}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index d22daf697c..ae081e5dcb 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -172,6 +172,7 @@ namespace osu.Game.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled")),
+ new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription())),
new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())),
};
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 41be4cfcc3..3de4bb1f9d 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -67,6 +67,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
+ new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
};
public IEnumerable AudioControlKeyBindings => new[]
@@ -187,5 +188,8 @@ namespace osu.Game.Input.Bindings
[Description("Timing Mode")]
EditorTimingMode,
+
+ [Description("Hold for HUD")]
+ HoldForHUD,
}
}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index dde45b5aeb..bbaa7e745f 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -1,11 +1,15 @@
// 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 JetBrains.Annotations;
using osu.Framework.IO.Network;
using osu.Game.Extensions;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests
{
@@ -21,6 +25,14 @@ namespace osu.Game.Online.API.Requests
public SearchLanguage Language { get; }
+ [CanBeNull]
+ public IReadOnlyCollection Extra { get; }
+
+ public SearchPlayed Played { get; }
+
+ [CanBeNull]
+ public IReadOnlyCollection Ranks { get; }
+
private readonly string query;
private readonly RulesetInfo ruleset;
private readonly Cursor cursor;
@@ -35,7 +47,10 @@ namespace osu.Game.Online.API.Requests
SortCriteria sortCriteria = SortCriteria.Ranked,
SortDirection sortDirection = SortDirection.Descending,
SearchGenre genre = SearchGenre.Any,
- SearchLanguage language = SearchLanguage.Any)
+ SearchLanguage language = SearchLanguage.Any,
+ IReadOnlyCollection extra = null,
+ IReadOnlyCollection ranks = null,
+ SearchPlayed played = SearchPlayed.Any)
{
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
@@ -46,6 +61,9 @@ namespace osu.Game.Online.API.Requests
SortDirection = sortDirection;
Genre = genre;
Language = language;
+ Extra = extra;
+ Ranks = ranks;
+ Played = played;
}
protected override WebRequest CreateWebRequest()
@@ -66,6 +84,15 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
+ if (Extra != null && Extra.Any())
+ req.AddParameter("e", string.Join('.', Extra.Select(e => e.ToString().ToLowerInvariant())));
+
+ if (Ranks != null && Ranks.Any())
+ req.AddParameter("r", string.Join('.', Ranks.Select(r => r.ToString())));
+
+ if (Played != SearchPlayed.Any)
+ req.AddParameter("played", Played.ToString().ToLowerInvariant());
+
req.AddCursor(cursor);
return req;
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 494a0df8f8..3be38e3c1d 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -130,6 +130,9 @@ namespace osu.Game.Overlays.BeatmapListing
searchControl.Category.BindValueChanged(_ => queueUpdateSearch());
searchControl.Genre.BindValueChanged(_ => queueUpdateSearch());
searchControl.Language.BindValueChanged(_ => queueUpdateSearch());
+ searchControl.Extra.CollectionChanged += (_, __) => queueUpdateSearch();
+ searchControl.Ranks.CollectionChanged += (_, __) => queueUpdateSearch();
+ searchControl.Played.BindValueChanged(_ => queueUpdateSearch());
sortCriteria.BindValueChanged(_ => queueUpdateSearch());
sortDirection.BindValueChanged(_ => queueUpdateSearch());
@@ -179,7 +182,10 @@ namespace osu.Game.Overlays.BeatmapListing
sortControl.Current.Value,
sortControl.SortDirection.Value,
searchControl.Genre.Value,
- searchControl.Language.Value);
+ searchControl.Language.Value,
+ searchControl.Extra,
+ searchControl.Ranks,
+ searchControl.Played.Value);
getSetsRequest.Success += response =>
{
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index 29c4fe0d2e..3694c9855e 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapListing
{
@@ -28,6 +29,12 @@ namespace osu.Game.Overlays.BeatmapListing
public Bindable Language => languageFilter.Current;
+ public BindableList Extra => extraFilter.Current;
+
+ public BindableList Ranks => ranksFilter.Current;
+
+ public Bindable Played => playedFilter.Current;
+
public BeatmapSetInfo BeatmapSet
{
set
@@ -48,6 +55,9 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly BeatmapSearchFilterRow categoryFilter;
private readonly BeatmapSearchFilterRow genreFilter;
private readonly BeatmapSearchFilterRow languageFilter;
+ private readonly BeatmapSearchMultipleSelectionFilterRow extraFilter;
+ private readonly BeatmapSearchScoreFilterRow ranksFilter;
+ private readonly BeatmapSearchFilterRow playedFilter;
private readonly Box background;
private readonly UpdateableBeatmapSetCover beatmapCover;
@@ -105,6 +115,9 @@ namespace osu.Game.Overlays.BeatmapListing
categoryFilter = new BeatmapSearchFilterRow(@"Categories"),
genreFilter = new BeatmapSearchFilterRow(@"Genre"),
languageFilter = new BeatmapSearchFilterRow(@"Language"),
+ extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"),
+ ranksFilter = new BeatmapSearchScoreFilterRow(),
+ playedFilter = new BeatmapSearchFilterRow(@"Played")
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
index 45ef793deb..b429a5277b 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
@@ -1,20 +1,16 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
-using osuTK.Graphics;
using Humanizer;
using osu.Game.Utils;
@@ -32,6 +28,7 @@ namespace osu.Game.Overlays.BeatmapListing
public BeatmapSearchFilterRow(string headerName)
{
+ Drawable filter;
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
AddInternal(new GridContainer
@@ -49,7 +46,7 @@ namespace osu.Game.Overlays.BeatmapListing
},
Content = new[]
{
- new Drawable[]
+ new[]
{
new OsuSpriteText
{
@@ -58,17 +55,17 @@ namespace osu.Game.Overlays.BeatmapListing
Font = OsuFont.GetFont(size: 13),
Text = headerName.Titleize()
},
- CreateFilter().With(f =>
- {
- f.Current = current;
- })
+ filter = CreateFilter()
}
}
});
+
+ if (filter is IHasCurrentValue filterWithValue)
+ Current = filterWithValue.Current;
}
[NotNull]
- protected virtual BeatmapSearchFilter CreateFilter() => new BeatmapSearchFilter();
+ protected virtual Drawable CreateFilter() => new BeatmapSearchFilter();
protected class BeatmapSearchFilter : TabControl
{
@@ -97,63 +94,7 @@ namespace osu.Game.Overlays.BeatmapListing
protected override Dropdown CreateDropdown() => new FilterDropdown();
- protected override TabItem CreateTabItem(T value) => new FilterTabItem(value);
-
- protected class FilterTabItem : TabItem
- {
- protected virtual float TextSize => 13;
-
- [Resolved]
- private OverlayColourProvider colourProvider { get; set; }
-
- private readonly OsuSpriteText text;
-
- public FilterTabItem(T value)
- : base(value)
- {
- AutoSizeAxes = Axes.Both;
- Anchor = Anchor.BottomLeft;
- Origin = Anchor.BottomLeft;
- AddRangeInternal(new Drawable[]
- {
- text = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Regular),
- Text = (value as Enum)?.GetDescription() ?? value.ToString()
- },
- new HoverClickSounds()
- });
-
- Enabled.Value = true;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- updateState();
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- base.OnHover(e);
- updateState();
- return true;
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- base.OnHoverLost(e);
- updateState();
- }
-
- protected override void OnActivated() => updateState();
-
- protected override void OnDeactivated() => updateState();
-
- private void updateState() => text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint);
-
- private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3;
- }
+ protected override TabItem CreateTabItem(T value) => new FilterTabItem(value);
private class FilterDropdown : OsuTabDropdown
{
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
new file mode 100644
index 0000000000..5dfa8e6109
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
@@ -0,0 +1,93 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osuTK;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow>
+ {
+ public new readonly BindableList Current = new BindableList();
+
+ private MultipleSelectionFilter filter;
+
+ public BeatmapSearchMultipleSelectionFilterRow(string headerName)
+ : base(headerName)
+ {
+ Current.BindTo(filter.Current);
+ }
+
+ protected sealed override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter();
+
+ ///
+ /// Creates a filter control that can be used to simultaneously select multiple values of type .
+ ///
+ protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter();
+
+ protected class MultipleSelectionFilter : FillFlowContainer
+ {
+ public readonly BindableList Current = new BindableList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.BottomLeft;
+ RelativeSizeAxes = Axes.X;
+ Height = 15;
+ Spacing = new Vector2(10, 0);
+
+ AddRange(GetValues().Select(CreateTabItem));
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ foreach (var item in Children)
+ item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue));
+ }
+
+ ///
+ /// Returns all values to be displayed in this filter row.
+ ///
+ protected virtual IEnumerable GetValues() => Enum.GetValues(typeof(T)).Cast();
+
+ ///
+ /// Creates a representing the supplied .
+ ///
+ protected virtual MultipleSelectionFilterTabItem CreateTabItem(T value) => new MultipleSelectionFilterTabItem(value);
+
+ private void toggleItem(T value, bool active)
+ {
+ if (active)
+ Current.Add(value);
+ else
+ Current.Remove(value);
+ }
+ }
+
+ protected class MultipleSelectionFilterTabItem : FilterTabItem
+ {
+ public MultipleSelectionFilterTabItem(T value)
+ : base(value)
+ {
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ base.OnClick(e);
+ Active.Toggle();
+ return true;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
index eebd896cf9..a8dc088e52 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing
@@ -13,7 +14,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
}
- protected override BeatmapSearchFilter CreateFilter() => new RulesetFilter();
+ protected override Drawable CreateFilter() => new RulesetFilter();
private class RulesetFilter : BeatmapSearchFilter
{
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
new file mode 100644
index 0000000000..804962adfb
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
@@ -0,0 +1,50 @@
+// 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 osu.Framework.Extensions;
+using osu.Game.Scoring;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow
+ {
+ public BeatmapSearchScoreFilterRow()
+ : base(@"Rank Achieved")
+ {
+ }
+
+ protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new RankFilter();
+
+ private class RankFilter : MultipleSelectionFilter
+ {
+ protected override MultipleSelectionFilterTabItem CreateTabItem(ScoreRank value) => new RankItem(value);
+
+ protected override IEnumerable GetValues() => base.GetValues().Reverse();
+ }
+
+ private class RankItem : MultipleSelectionFilterTabItem
+ {
+ public RankItem(ScoreRank value)
+ : base(value)
+ {
+ }
+
+ protected override string LabelFor(ScoreRank value)
+ {
+ switch (value)
+ {
+ case ScoreRank.XH:
+ return @"Silver SS";
+
+ case ScoreRank.SH:
+ return @"Silver S";
+
+ default:
+ return value.GetDescription();
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
new file mode 100644
index 0000000000..f02b515755
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
@@ -0,0 +1,79 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public class FilterTabItem : TabItem
+ {
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ private OsuSpriteText text;
+
+ public FilterTabItem(T value)
+ : base(value)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.BottomLeft;
+ AddRangeInternal(new Drawable[]
+ {
+ text = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
+ Text = LabelFor(Value)
+ },
+ new HoverClickSounds()
+ });
+
+ Enabled.Value = true;
+ updateState();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ base.OnHover(e);
+ updateState();
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ updateState();
+ }
+
+ protected override void OnActivated() => updateState();
+
+ protected override void OnDeactivated() => updateState();
+
+ ///
+ /// Returns the label text to be used for the supplied .
+ ///
+ protected virtual string LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
+
+ private void updateState()
+ {
+ text.FadeColour(IsHovered ? colourProvider.Light1 : getStateColour(), 200, Easing.OutQuint);
+ text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
+ }
+
+ private Color4 getStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs
new file mode 100644
index 0000000000..af37e3264f
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public enum SearchExtra
+ {
+ [Description("Has Video")]
+ Video,
+
+ [Description("Has Storyboard")]
+ Storyboard
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs
new file mode 100644
index 0000000000..eb7fb46158
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public enum SearchPlayed
+ {
+ Any,
+ Played,
+ Unplayed
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 4abdbfc244..f3816f6218 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -120,6 +120,11 @@ namespace osu.Game.Rulesets.Edit
///
public void Deselect() => State = SelectionState.NotSelected;
+ ///
+ /// Toggles the selection state of this .
+ ///
+ public void ToggleSelection() => State = IsSelected ? SelectionState.NotSelected : SelectionState.Selected;
+
public bool IsSelected => State == SelectionState.Selected;
///
diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
index 8a4451fdca..b43324bcfa 100644
--- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
+++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Replays
return null;
if (!currentFrameIndex.HasValue)
- return (TFrame)Frames[0];
+ return currentDirection > 0 ? (TFrame)Frames[0] : null;
int nextFrame = clampedNextFrameIndex;
@@ -109,30 +109,54 @@ namespace osu.Game.Rulesets.Replays
Debug.Assert(currentDirection != 0);
- TFrame next = NextFrame;
-
- // check if the next frame is valid for the current playback direction.
- // validity is if the next frame is equal or "earlier" than the current point in time (so we can change to it)
- int compare = time.CompareTo(next?.Time);
-
- if (next != null && (compare == 0 || compare == currentDirection))
+ if (!HasFrames)
{
- currentFrameIndex = clampedNextFrameIndex;
- return CurrentTime = CurrentFrame.Time;
+ // in the case all frames are received, allow time to progress regardless.
+ if (replay.HasReceivedAllFrames)
+ return CurrentTime = time;
+
+ return null;
}
- // at this point, the frame can't be advanced (in the replay).
- // even so, we may be able to move the clock forward due to being at the end of the replay or
- // moving towards the next valid frame.
+ TFrame next = NextFrame;
+
+ // if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so.
+ if (next != null)
+ {
+ int compare = time.CompareTo(next.Time);
+
+ if (compare == 0 || compare == currentDirection)
+ {
+ currentFrameIndex = clampedNextFrameIndex;
+ return CurrentTime = CurrentFrame.Time;
+ }
+ }
+
+ // at this point, the frame index can't be advanced.
+ // even so, we may be able to propose the clock progresses forward due to being at an extent of the replay,
+ // or moving towards the next valid frame (ie. interpolating in a non-important section).
// the exception is if currently in an important section, which is respected above all.
if (inImportantSection)
+ {
+ Debug.Assert(next != null || !replay.HasReceivedAllFrames);
return null;
+ }
- // in the case we have no next frames and haven't received the full replay, block.
- if (next == null && !replay.HasReceivedAllFrames) return null;
+ // if a next frame does exist, allow interpolation.
+ if (next != null)
+ return CurrentTime = time;
- return CurrentTime = time;
+ // if all frames have been received, allow playing beyond extents.
+ if (replay.HasReceivedAllFrames)
+ return CurrentTime = time;
+
+ // if not all frames are received but we are before the first frame, allow playing.
+ if (time < Frames[0].Time)
+ return CurrentTime = time;
+
+ // in the case we have no next frames and haven't received enough frame data, block.
+ return null;
}
private void updateDirection(double time)
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index 231c5110ea..e9865f6c8b 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -85,50 +85,46 @@ namespace osu.Game.Rulesets.UI
public override bool UpdateSubTree()
{
- double proposedTime = manualClock.CurrentTime;
-
- if (frameStableClock.WaitingOnFrames.Value)
- {
- // when waiting on frames, the update loop still needs to be run (at least once) to check for newly arrived frames.
- // time should not be sourced from the parent clock in this case.
- state = PlaybackState.Valid;
- }
- else if (!frameStableClock.IsPaused.Value)
- {
- state = PlaybackState.Valid;
-
- if (parentGameplayClock == null)
- setClock(); // LoadComplete may not be run yet, but we still want the clock.
-
- proposedTime = parentGameplayClock.CurrentTime;
- }
- else
- {
- // time should not advance while paused, not should anything run.
- state = PlaybackState.NotValid;
- return true;
- }
-
int loops = MaxCatchUpFrames;
- while (loops-- > 0)
+ do
{
- updateClock(ref proposedTime);
+ // update clock is always trying to approach the aim time.
+ // it should be provided as the original value each loop.
+ updateClock();
if (state == PlaybackState.NotValid)
break;
base.UpdateSubTree();
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
- }
+ } while (state == PlaybackState.RequiresCatchUp && loops-- > 0);
return true;
}
- private void updateClock(ref double proposedTime)
+ private void updateClock()
{
- // each update start with considering things in valid state.
- state = PlaybackState.Valid;
+ if (frameStableClock.WaitingOnFrames.Value)
+ {
+ // if waiting on frames, run one update loop to determine if frames have arrived.
+ state = PlaybackState.Valid;
+ }
+ else if (frameStableClock.IsPaused.Value)
+ {
+ // time should not advance while paused, nor should anything run.
+ state = PlaybackState.NotValid;
+ return;
+ }
+ else
+ {
+ state = PlaybackState.Valid;
+ }
+
+ if (parentGameplayClock == null)
+ setClock(); // LoadComplete may not be run yet, but we still want the clock.
+
+ double proposedTime = parentGameplayClock.CurrentTime;
if (FrameStablePlayback)
// if we require frame stability, the proposed time will be adjusted to move at most one known
@@ -143,22 +139,22 @@ namespace osu.Game.Rulesets.UI
state = PlaybackState.NotValid;
}
- if (proposedTime != manualClock.CurrentTime)
+ if (state == PlaybackState.Valid)
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
+ double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime);
+
+ frameStableClock.IsCatchingUp.Value = timeBehind > 200;
+ frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid;
+
manualClock.CurrentTime = proposedTime;
manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction;
manualClock.IsRunning = parentGameplayClock.IsRunning;
- double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime);
-
// determine whether catch-up is required.
if (state == PlaybackState.Valid && timeBehind > 0)
state = PlaybackState.RequiresCatchUp;
- frameStableClock.IsCatchingUp.Value = timeBehind > 200;
- frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid;
-
// The manual clock time has changed in the above code. The framed clock now needs to be updated
// to ensure that the its time is valid for our children before input is processed
framedClock.ProcessFrame();
@@ -253,14 +249,13 @@ namespace osu.Game.Rulesets.UI
NotValid,
///
- /// Whether we are running up-to-date with our parent clock.
- /// If not, we will need to keep processing children until we catch up.
+ /// Playback is running behind real-time. Catch-up will be attempted by processing more than once per
+ /// game loop (limited to a sane maximum to avoid frame drops).
///
RequiresCatchUp,
///
- /// Whether we are in a valid state (ie. should we keep processing children frames).
- /// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
+ /// In a valid state, progressing one child hierarchy loop per game loop.
///
Valid
}
diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs
index 9a0217a1eb..4cadfa9ad4 100644
--- a/osu.Game/Rulesets/UI/HitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.UI
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 7751df29cf..5ac360d029 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -298,13 +298,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
Debug.Assert(!clickSelectionBegan);
- // Deselections are only allowed for control + left clicks
- bool allowDeselection = e.ControlPressed && e.Button == MouseButton.Left;
-
- // Todo: This is probably incorrectly disallowing multiple selections on stacked objects
- if (!allowDeselection && SelectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
- return;
-
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
{
if (blueprint.IsHovered)
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 4caceedc5a..01e23bafc5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -24,6 +24,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
@@ -224,21 +225,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// The input state at the point of selection.
internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state)
{
- if (state.Keyboard.ControlPressed)
- {
- if (blueprint.IsSelected)
- blueprint.Deselect();
- else
- blueprint.Select();
- }
+ if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right))
+ EditorBeatmap.Remove(blueprint.HitObject);
+ else if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left))
+ blueprint.ToggleSelection();
else
- {
- if (blueprint.IsSelected)
- return;
+ ensureSelected(blueprint);
+ }
- DeselectAll?.Invoke();
- blueprint.Select();
- }
+ private void ensureSelected(SelectionBlueprint blueprint)
+ {
+ if (blueprint.IsSelected)
+ return;
+
+ DeselectAll?.Invoke();
+ blueprint.Select();
}
private void deleteSelected()
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 25ebd55f81..f95c7fe7a6 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -450,12 +450,21 @@ namespace osu.Game.Screens.Edit
if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog)
{
confirmExit();
- return true;
+ return false;
}
if (isNewBeatmap || HasUnsavedChanges)
{
- dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
+ dialogOverlay?.Push(new PromptForSaveDialog(() =>
+ {
+ confirmExit();
+ this.Exit();
+ }, () =>
+ {
+ confirmExitWithSave();
+ this.Exit();
+ }));
+
return true;
}
}
@@ -470,7 +479,6 @@ namespace osu.Game.Screens.Edit
{
exitConfirmed = true;
Save();
- this.Exit();
}
private void confirmExit()
@@ -489,7 +497,6 @@ namespace osu.Game.Screens.Edit
}
exitConfirmed = true;
- this.Exit();
}
private readonly Bindable clipboard = new Bindable();
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index b047d44f8a..e83dded075 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -8,8 +8,10 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
+using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
@@ -22,7 +24,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Play
{
[Cached]
- public class HUDOverlay : Container
+ public class HUDOverlay : Container, IKeyBindingHandler
{
public const float FADE_DURATION = 400;
@@ -67,6 +69,8 @@ namespace osu.Game.Screens.Play
internal readonly IBindable IsBreakTime = new Bindable();
+ private bool holdingForHUD;
+
private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods)
@@ -217,17 +221,18 @@ namespace osu.Game.Screens.Play
if (ShowHud.Disabled)
return;
+ if (holdingForHUD)
+ {
+ ShowHud.Value = true;
+ return;
+ }
+
switch (configVisibilityMode.Value)
{
case HUDVisibilityMode.Never:
ShowHud.Value = false;
break;
- case HUDVisibilityMode.HideDuringBreaks:
- // always show during replay as we want the seek bar to be visible.
- ShowHud.Value = replayLoaded.Value || !IsBreakTime.Value;
- break;
-
case HUDVisibilityMode.HideDuringGameplay:
// always show during replay as we want the seek bar to be visible.
ShowHud.Value = replayLoaded.Value || IsBreakTime.Value;
@@ -277,9 +282,21 @@ namespace osu.Game.Screens.Play
switch (e.Key)
{
case Key.Tab:
- configVisibilityMode.Value = configVisibilityMode.Value != HUDVisibilityMode.Never
- ? HUDVisibilityMode.Never
- : HUDVisibilityMode.HideDuringGameplay;
+ switch (configVisibilityMode.Value)
+ {
+ case HUDVisibilityMode.Never:
+ configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay;
+ break;
+
+ case HUDVisibilityMode.HideDuringGameplay:
+ configVisibilityMode.Value = HUDVisibilityMode.Always;
+ break;
+
+ case HUDVisibilityMode.Always:
+ configVisibilityMode.Value = HUDVisibilityMode.Never;
+ break;
+ }
+
return true;
}
}
@@ -351,5 +368,29 @@ namespace osu.Game.Screens.Play
HealthDisplay?.BindHealthProcessor(processor);
FailingLayer?.BindHealthProcessor(processor);
}
+
+ public bool OnPressed(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.HoldForHUD:
+ holdingForHUD = true;
+ updateVisibility();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.HoldForHUD:
+ holdingForHUD = false;
+ updateVisibility();
+ break;
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 30747438c3..cb4560802b 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Screens.Ranking.Expanded
private const float padding = 10;
private readonly ScoreInfo score;
+ private readonly bool withFlair;
+
private readonly List statisticDisplays = new List();
private FillFlowContainer starAndModDisplay;
@@ -41,9 +43,11 @@ namespace osu.Game.Screens.Ranking.Expanded
/// Creates a new .
///
/// The score to display.
- public ExpandedPanelMiddleContent(ScoreInfo score)
+ /// Whether to add flair for a new score being set.
+ public ExpandedPanelMiddleContent(ScoreInfo score, bool withFlair = false)
{
this.score = score;
+ this.withFlair = withFlair;
RelativeSizeAxes = Axes.Both;
Masking = true;
@@ -266,6 +270,9 @@ namespace osu.Game.Screens.Ranking.Expanded
delay += 200;
}
}
+
+ if (!withFlair)
+ FinishTransforms(true);
});
}
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 026ce01857..f8bdf0140c 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -149,7 +149,7 @@ namespace osu.Game.Screens.Ranking
};
if (Score != null)
- ScorePanelList.AddScore(Score);
+ ScorePanelList.AddScore(Score, true);
if (player != null && allowRetry)
{
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index ee97ee55eb..df710e4eb8 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -85,6 +85,8 @@ namespace osu.Game.Screens.Ranking
public readonly ScoreInfo Score;
+ private bool displayWithFlair;
+
private Container content;
private Container topLayerContainer;
@@ -97,9 +99,10 @@ namespace osu.Game.Screens.Ranking
private Container middleLayerContentContainer;
private Drawable middleLayerContent;
- public ScorePanel(ScoreInfo score)
+ public ScorePanel(ScoreInfo score, bool isNewLocalScore = false)
{
Score = score;
+ displayWithFlair = isNewLocalScore;
}
[BackgroundDependencyLoader]
@@ -188,7 +191,7 @@ namespace osu.Game.Screens.Ranking
state = value;
- if (LoadState >= LoadState.Ready)
+ if (IsLoaded)
updateState();
StateChanged?.Invoke(value);
@@ -209,7 +212,10 @@ namespace osu.Game.Screens.Ranking
middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
- middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0));
+ middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0));
+
+ // only the first expanded display should happen with flair.
+ displayWithFlair = false;
break;
case PanelState.Contracted:
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 0d7d339df0..77b3d8fc3b 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -95,9 +95,10 @@ namespace osu.Game.Screens.Ranking
/// Adds a to this list.
///
/// The to add.
- public ScorePanel AddScore(ScoreInfo score)
+ /// Whether this is a score that has just been achieved locally. Controls whether flair is added to the display or not.
+ public ScorePanel AddScore(ScoreInfo score, bool isNewLocalScore = false)
{
- var panel = new ScorePanel(score)
+ var panel = new ScorePanel(score, isNewLocalScore)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -117,19 +118,24 @@ namespace osu.Game.Screens.Ranking
d.Origin = Anchor.Centre;
}));
- if (SelectedScore.Value == score)
- selectedScoreChanged(new ValueChangedEvent(SelectedScore.Value, SelectedScore.Value));
- else
+ if (IsLoaded)
{
- // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
- // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
- if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
+ if (SelectedScore.Value == score)
{
- // A somewhat hacky property is used here because we need to:
- // 1) Scroll after the scroll container's visible range is updated.
- // 2) Scroll before the scroll container's scroll position is updated.
- // Without this, we would have a 1-frame positioning error which looks very jarring.
- scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
+ SelectedScore.TriggerChange();
+ }
+ else
+ {
+ // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
+ // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
+ if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
+ {
+ // A somewhat hacky property is used here because we need to:
+ // 1) Scroll after the scroll container's visible range is updated.
+ // 2) Scroll before the scroll container's scroll position is updated.
+ // Without this, we would have a 1-frame positioning error which looks very jarring.
+ scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
+ }
}
}
@@ -142,11 +148,15 @@ namespace osu.Game.Screens.Ranking
/// The to present.
private void selectedScoreChanged(ValueChangedEvent score)
{
- // Contract the old panel.
- foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue))
+ // avoid contracting panels unnecessarily when TriggerChange is fired manually.
+ if (score.OldValue != score.NewValue)
{
- t.Panel.State = PanelState.Contracted;
- t.Margin = new MarginPadding();
+ // Contract the old panel.
+ foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue))
+ {
+ t.Panel.State = PanelState.Contracted;
+ t.Margin = new MarginPadding();
+ }
}
// Find the panel corresponding to the new score.
@@ -162,12 +172,16 @@ namespace osu.Game.Screens.Ranking
expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing };
expandedPanel.State = PanelState.Expanded;
- // Scroll to the new panel. This is done manually since we need:
- // 1) To scroll after the scroll container's visible range is updated.
- // 2) To account for the centre anchor/origins of panels.
- // In the end, it's easier to compute the scroll position manually.
- float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing);
- scroll.ScrollTo(scrollOffset);
+ // requires schedule after children to ensure the flow (and thus ScrollContainer's ScrollableExtent) has been updated.
+ ScheduleAfterChildren(() =>
+ {
+ // Scroll to the new panel. This is done manually since we need:
+ // 1) To scroll after the scroll container's visible range is updated.
+ // 2) To account for the centre anchor/origins of panels.
+ // In the end, it's easier to compute the scroll position manually.
+ float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing);
+ scroll.ScrollTo(scrollOffset);
+ });
}
protected override void Update()
diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
index ab4fb38657..1e43e5d148 100644
--- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
+++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
@@ -180,9 +180,8 @@ namespace osu.Game.Tests.Beatmaps
private readonly BeatmapInfo skinBeatmapInfo;
private readonly IResourceStore resourceStore;
- public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio,
- double length = 60000)
- : base(beatmap, storyboard, referenceClock, audio, length)
+ public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
+ : base(beatmap, storyboard, referenceClock, audio)
{
this.skinBeatmapInfo = skinBeatmapInfo;
this.resourceStore = resourceStore;
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index b59a1db403..e32ed07863 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -23,6 +23,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens;
using osu.Game.Storyboards;
@@ -222,18 +223,23 @@ namespace osu.Game.Tests.Visual
/// The storyboard.
/// An optional clock which should be used instead of a stopwatch for virtual time progression.
/// Audio manager. Required if a reference clock isn't provided.
- /// The length of the returned virtual track.
- public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000)
+ public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
: base(beatmap, storyboard, audio)
{
+ double trackLength = 60000;
+
+ if (beatmap.HitObjects.Count > 0)
+ // add buffer after last hitobject to allow for final replay frames etc.
+ trackLength = Math.Max(trackLength, beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000);
+
if (referenceClock != null)
{
store = new TrackVirtualStore(referenceClock);
audio.AddItem(store);
- track = store.GetVirtual(length);
+ track = store.GetVirtual(trackLength);
}
else
- track = audio?.Tracks.GetVirtual(length);
+ track = audio?.Tracks.GetVirtual(trackLength);
}
~ClockBackedTestWorkingBeatmap()
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ca588b89d9..9be933c74a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 9c22dec330..e26f8cc8b4 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+