diff --git a/osu.Android.props b/osu.Android.props
index 723844155f..d2bdbc8b61 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
new file mode 100644
index 0000000000..40bb83aece
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
@@ -0,0 +1,51 @@
+// 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.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Replays;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaLegacyReplayTest
+ {
+ [TestCase(ManiaAction.Key1)]
+ [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
+ [TestCase(ManiaAction.Special1)]
+ [TestCase(ManiaAction.Key8)]
+ public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
+ {
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
+
+ var frame = new ManiaReplayFrame(0, actions);
+ var legacyFrame = frame.ToLegacy(beatmap);
+
+ var decodedFrame = new ManiaReplayFrame();
+ decodedFrame.FromLegacy(legacyFrame, beatmap);
+
+ Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
+ }
+
+ [TestCase(ManiaAction.Key1)]
+ [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
+ [TestCase(ManiaAction.Special1)]
+ [TestCase(ManiaAction.Special2)]
+ [TestCase(ManiaAction.Special1, ManiaAction.Special2)]
+ [TestCase(ManiaAction.Special1, ManiaAction.Key5)]
+ [TestCase(ManiaAction.Key8)]
+ public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
+ {
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
+ beatmap.Stages.Add(new StageDefinition { Columns = 5 });
+
+ var frame = new ManiaReplayFrame(0, actions);
+ var legacyFrame = frame.ToLegacy(beatmap);
+
+ var decodedFrame = new ManiaReplayFrame();
+ decodedFrame.FromLegacy(legacyFrame, beatmap);
+
+ Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
index 8c73c36e99..dbab54d1d0 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
@@ -1,8 +1,8 @@
// 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.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
while (activeColumns > 0)
{
- var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter);
+ bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction);
@@ -58,33 +58,87 @@ namespace osu.Game.Rulesets.Mania.Replays
int keys = 0;
- var specialColumns = new List();
-
- for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
- {
- if (maniaBeatmap.Stages.First().IsSpecialColumn(i))
- specialColumns.Add(i);
- }
-
foreach (var action in Actions)
{
switch (action)
{
case ManiaAction.Special1:
- keys |= 1 << specialColumns[0];
+ keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
break;
case ManiaAction.Special2:
- keys |= 1 << specialColumns[1];
+ keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
break;
default:
- keys |= 1 << (action - ManiaAction.Key1);
+ // the index in lazer, which doesn't include special keys.
+ int nonSpecialKeyIndex = action - ManiaAction.Key1;
+
+ // the index inclusive of special keys.
+ int overallIndex = 0;
+
+ // iterate to find the index including special keys.
+ for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
+ {
+ // skip over special columns.
+ if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
+ continue;
+ // found a non-special column to use.
+ if (nonSpecialKeyIndex == 0)
+ break;
+ // found a non-special column but not ours.
+ nonSpecialKeyIndex--;
+ }
+
+ keys |= 1 << overallIndex;
break;
}
}
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
+
+ ///
+ /// Find the overall index (across all stages) for a specified special key.
+ ///
+ /// The beatmap.
+ /// The special key offset (0 is S1).
+ /// The overall index for the special column.
+ private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
+ {
+ for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
+ {
+ if (isColumnAtIndexSpecial(maniaBeatmap, i))
+ {
+ if (specialOffset == 0)
+ return i;
+
+ specialOffset--;
+ }
+ }
+
+ throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
+ }
+
+ ///
+ /// Check whether the column at an overall index (across all stages) is a special column.
+ ///
+ /// The beatmap.
+ /// The overall index to check.
+ private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
+ {
+ foreach (var stage in beatmap.Stages)
+ {
+ if (index >= stage.Columns)
+ {
+ index -= stage.Columns;
+ continue;
+ }
+
+ return stage.IsSpecialColumn(index);
+ }
+
+ throw new ArgumentException("Column index is too high.", nameof(index));
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 397888bb11..2f90f3b96c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
/// Moves to a layer proxied above the playfield.
- /// Does nothing is content is already proxied.
+ /// Does nothing if content is already proxied.
///
protected void ProxyContent()
{
diff --git a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
index ef16976130..9613f250c4 100644
--- a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
+++ b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
@@ -15,15 +15,18 @@ namespace osu.Game.Tests.Editor
{
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
- Assert.That(handler.HasUndoState, Is.False);
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.False);
handler.SaveState();
- Assert.That(handler.HasUndoState, Is.True);
+ Assert.That(handler.CanUndo.Value, Is.True);
+ Assert.That(handler.CanRedo.Value, Is.False);
handler.RestoreState(-1);
- Assert.That(handler.HasUndoState, Is.False);
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.True);
}
[Test]
@@ -31,20 +34,20 @@ namespace osu.Game.Tests.Editor
{
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
- Assert.That(handler.HasUndoState, Is.False);
+ Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
handler.SaveState();
- Assert.That(handler.HasUndoState, Is.True);
+ Assert.That(handler.CanUndo.Value, Is.True);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
- Assert.That(handler.HasUndoState, Is.True);
+ Assert.That(handler.CanUndo.Value, Is.True);
handler.RestoreState(-1);
}
- Assert.That(handler.HasUndoState, Is.False);
+ Assert.That(handler.CanUndo.Value, Is.False);
}
[Test]
@@ -52,20 +55,20 @@ namespace osu.Game.Tests.Editor
{
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
- Assert.That(handler.HasUndoState, Is.False);
+ Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
handler.SaveState();
- Assert.That(handler.HasUndoState, Is.True);
+ Assert.That(handler.CanUndo.Value, Is.True);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
- Assert.That(handler.HasUndoState, Is.True);
+ Assert.That(handler.CanUndo.Value, Is.True);
handler.RestoreState(-1);
}
- Assert.That(handler.HasUndoState, Is.False);
+ Assert.That(handler.CanUndo.Value, Is.False);
}
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
new file mode 100644
index 0000000000..f611f2717e
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -0,0 +1,346 @@
+// 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.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.IO.Stores;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneHitObjectSamples : PlayerTestScene
+ {
+ private readonly SkinInfo userSkinInfo = new SkinInfo();
+
+ private readonly BeatmapInfo beatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo(),
+ Metadata = new BeatmapMetadata
+ {
+ Author = User.SYSTEM_USER
+ }
+ };
+
+ private readonly TestResourceStore userSkinResourceStore = new TestResourceStore();
+ private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore();
+
+ protected override bool HasCustomSteps => true;
+
+ public TestSceneHitObjectSamples()
+ : base(new OsuRuleset())
+ {
+ }
+
+ private SkinSourceDependencyContainer dependencies;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent)));
+
+ ///
+ /// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin.
+ ///
+ [Test]
+ public void TestDefaultSampleFromUserSkin()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("hitobject-skin-sample.osu");
+
+ assertUserLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a sample set of 1 retrieves samples from the beatmap skin.
+ ///
+ [Test]
+ public void TestDefaultSampleFromBeatmap()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("hitobject-beatmap-sample.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a sample set of 1 retrieves samples from the user skin when the beatmap does not contain the sample.
+ ///
+ [Test]
+ public void TestDefaultSampleFromUserSkinFallback()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(null, expected_sample);
+
+ createTestWithBeatmap("hitobject-beatmap-sample.osu");
+
+ assertUserLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
+ /// normal-hitnormal2
+ /// normal-hitnormal
+ ///
+ [TestCase("normal-hitnormal2")]
+ [TestCase("normal-hitnormal")]
+ public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
+ {
+ setupSkins(expectedSample, expectedSample);
+
+ createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ assertBeatmapLookup(expectedSample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
+ /// normal-hitnormal2
+ /// normal-hitnormal
+ ///
+ [TestCase("normal-hitnormal2")]
+ [TestCase("normal-hitnormal")]
+ public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
+ {
+ setupSkins(string.Empty, expectedSample);
+
+ createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ assertUserLookup(expectedSample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
+ ///
+ [Test]
+ public void TestFileSampleFromBeatmap()
+ {
+ const string expected_sample = "hit_1.wav";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("file-beatmap-sample.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a default hitobject and control point causes .
+ ///
+ [Test]
+ public void TestControlPointSampleFromSkin()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("controlpoint-skin-sample.osu");
+
+ assertUserLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a control point that provides a custom sample set of 1 causes .
+ ///
+ [Test]
+ public void TestControlPointSampleFromBeatmap()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("controlpoint-beatmap-sample.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a control point that provides a custom sample of 2 causes .
+ ///
+ [TestCase("normal-hitnormal2")]
+ [TestCase("normal-hitnormal")]
+ public void TestControlPointCustomSampleFromBeatmap(string sampleName)
+ {
+ setupSkins(sampleName, sampleName);
+
+ createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu");
+
+ assertBeatmapLookup(sampleName);
+ }
+
+ ///
+ /// Tests that a hitobject's custom sample overrides the control point's.
+ ///
+ [Test]
+ public void TestHitObjectCustomSampleOverride()
+ {
+ const string expected_sample = "normal-hitnormal3";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap;
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+ => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
+
+ private IBeatmap currentTestBeatmap;
+
+ private void createTestWithBeatmap(string filename)
+ {
+ CreateTest(() =>
+ {
+ AddStep("clear performed lookups", () =>
+ {
+ userSkinResourceStore.PerformedLookups.Clear();
+ beatmapSkinResourceStore.PerformedLookups.Clear();
+ });
+
+ AddStep($"load {filename}", () =>
+ {
+ using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}")))
+ currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader);
+ });
+ });
+ }
+
+ private void setupSkins(string beatmapFile, string userFile)
+ {
+ AddStep("setup skins", () =>
+ {
+ userSkinInfo.Files = new List
+ {
+ new SkinFileInfo
+ {
+ Filename = userFile,
+ FileInfo = new IO.FileInfo { Hash = userFile }
+ }
+ };
+
+ beatmapInfo.BeatmapSet.Files = new List
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = beatmapFile,
+ FileInfo = new IO.FileInfo { Hash = beatmapFile }
+ }
+ };
+
+ // Need to refresh the cached skin source to refresh the skin resource store.
+ dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
+ });
+ }
+
+ private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin",
+ () => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name));
+
+ private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin",
+ () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name));
+
+ private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
+ {
+ public ISkinSource SkinSource;
+
+ private readonly IReadOnlyDependencyContainer fallback;
+
+ public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback)
+ {
+ this.fallback = fallback;
+ }
+
+ public object Get(Type type)
+ {
+ if (type == typeof(ISkinSource))
+ return SkinSource;
+
+ return fallback.Get(type);
+ }
+
+ public object Get(Type type, CacheInfo info)
+ {
+ if (type == typeof(ISkinSource))
+ return SkinSource;
+
+ return fallback.Get(type, info);
+ }
+
+ public void Inject(T instance) where T : class
+ {
+ // Never used directly
+ }
+ }
+
+ private class TestResourceStore : IResourceStore
+ {
+ public readonly List PerformedLookups = new List();
+
+ public byte[] Get(string name)
+ {
+ markLookup(name);
+ return Array.Empty();
+ }
+
+ public Task GetAsync(string name)
+ {
+ markLookup(name);
+ return Task.FromResult(Array.Empty());
+ }
+
+ public Stream GetStream(string name)
+ {
+ markLookup(name);
+ return new MemoryStream();
+ }
+
+ private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1));
+
+ public IEnumerable GetAvailableResources() => Enumerable.Empty();
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ 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)
+ {
+ this.skinBeatmapInfo = skinBeatmapInfo;
+ this.resourceStore = resourceStore;
+ }
+
+ protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 2782e902fe..158954106d 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -29,11 +29,17 @@ namespace osu.Game.Tests.NonVisual
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
- cpi.Add(1000, new TimingControlPoint()); // is redundant
+ cpi.Add(1000, new TimingControlPoint()); // is also not redundant, due to change of offset
- Assert.That(cpi.Groups.Count, Is.EqualTo(1));
- Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+
+ cpi.Add(1000, new TimingControlPoint()); //is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
}
[Test]
@@ -86,11 +92,12 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
+ cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
+ cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
- Assert.That(cpi.Groups.Count, Is.EqualTo(1));
- Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
}
[Test]
diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
new file mode 100644
index 0000000000..1e77d50115
--- /dev/null
+++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
@@ -0,0 +1,117 @@
+// 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.Testing;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+using osu.Game.Tests.Visual;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Online
+{
+ [HeadlessTest]
+ public class TestDummyAPIRequestHandling : OsuTestScene
+ {
+ [Test]
+ public void TestGenericRequestHandling()
+ {
+ AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case CommentVoteRequest cRequest:
+ cRequest.TriggerSuccess(new CommentBundle());
+ break;
+ }
+ });
+
+ CommentVoteRequest request = null;
+ CommentBundle response = null;
+
+ AddStep("fire request", () =>
+ {
+ response = null;
+ request = new CommentVoteRequest(1, CommentVoteAction.Vote);
+ request.Success += res => response = res;
+ API.Queue(request);
+ });
+
+ AddAssert("response event fired", () => response != null);
+
+ AddAssert("request has response", () => request.Result == response);
+ }
+
+ [Test]
+ public void TestQueueRequestHandling()
+ {
+ registerHandler();
+
+ LeaveChannelRequest request;
+ bool gotResponse = false;
+
+ AddStep("fire request", () =>
+ {
+ gotResponse = false;
+ request = new LeaveChannelRequest(new Channel(), new User());
+ request.Success += () => gotResponse = true;
+ API.Queue(request);
+ });
+
+ AddAssert("response event fired", () => gotResponse);
+ }
+
+ [Test]
+ public void TestPerformRequestHandling()
+ {
+ registerHandler();
+
+ LeaveChannelRequest request;
+ bool gotResponse = false;
+
+ AddStep("fire request", () =>
+ {
+ gotResponse = false;
+ request = new LeaveChannelRequest(new Channel(), new User());
+ request.Success += () => gotResponse = true;
+ API.Perform(request);
+ });
+
+ AddAssert("response event fired", () => gotResponse);
+ }
+
+ [Test]
+ public void TestPerformAsyncRequestHandling()
+ {
+ registerHandler();
+
+ LeaveChannelRequest request;
+ bool gotResponse = false;
+
+ AddStep("fire request", () =>
+ {
+ gotResponse = false;
+ request = new LeaveChannelRequest(new Channel(), new User());
+ request.Success += () => gotResponse = true;
+ API.PerformAsync(request);
+ });
+
+ AddAssert("response event fired", () => gotResponse);
+ }
+
+ private void registerHandler()
+ {
+ AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case LeaveChannelRequest cRequest:
+ cRequest.TriggerSuccess();
+ break;
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu
new file mode 100644
index 0000000000..91dbc6a60e
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu
new file mode 100644
index 0000000000..3274820100
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,1,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu
new file mode 100644
index 0000000000..c53ec465fb
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,0,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu
new file mode 100644
index 0000000000..65b5ea8707
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+255,193,2170,1,0,0:0:0:0:hit_1.wav
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu
new file mode 100644
index 0000000000..13dc2faab1
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:3:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu
new file mode 100644
index 0000000000..4ab672dbb0
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,0,0:0:2:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu
new file mode 100644
index 0000000000..33bc34949a
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,0,0:0:1:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu
new file mode 100644
index 0000000000..47f5b44c90
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
new file mode 100644
index 0000000000..64d1024efb
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.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.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Rulesets.Scoring
+{
+ public class ScoreProcessorTest
+ {
+ private ScoreProcessor scoreProcessor;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void SetUp()
+ {
+ scoreProcessor = new ScoreProcessor();
+ beatmap = new TestBeatmap(new RulesetInfo())
+ {
+ HitObjects = new List
+ {
+ new HitCircle()
+ }
+ };
+ }
+
+ [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
+ [TestCase(ScoringMode.Classic, HitResult.Good, 100)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, 300)]
+ public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
+ {
+ scoreProcessor.Mode.Value = scoringMode;
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
+ {
+ Type = hitResult
+ };
+ scoreProcessor.ApplyResult(judgementResult);
+
+ Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
new file mode 100644
index 0000000000..a95e806862
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
@@ -0,0 +1,73 @@
+// 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.Testing;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneFailingLayer : OsuTestScene
+ {
+ private FailingLayer layer;
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create layer", () =>
+ {
+ Child = layer = new FailingLayer();
+ layer.BindHealthProcessor(new DrainingHealthProcessor(1));
+ });
+
+ AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ AddUntilStep("layer is visible", () => layer.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerFading()
+ {
+ AddSliderStep("current health", 0.0, 1.0, 1.0, val =>
+ {
+ if (layer != null)
+ layer.Current.Value = val;
+ });
+
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f);
+ AddStep("set health to 1", () => layer.Current.Value = 1f);
+ AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerDisabledViaConfig()
+ {
+ AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddUntilStep("layer is not visible", () => !layer.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerVisibilityWithAccumulatingProcessor()
+ {
+ AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new AccumulatingHealthProcessor(1)));
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddUntilStep("layer is not visible", () => !layer.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerVisibilityWithDrainingProcessor()
+ {
+ AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new DrainingHealthProcessor(1)));
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddWaitStep("wait for potential fade", 10);
+ AddAssert("layer is still visible", () => layer.IsPresent);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
index 5b0c2d3c67..f612992bf6 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
@@ -149,8 +149,8 @@ namespace osu.Game.Tests.Visual.Online
public DownloadState DownloadState => State.Value;
- public TestDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
- : base(beatmapSet, noVideo)
+ public TestDownloadButton(BeatmapSetInfo beatmapSet)
+ : base(beatmapSet)
{
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 76a8ee9914..f68ed4154b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -54,6 +54,35 @@ namespace osu.Game.Tests.Visual.SongSelect
this.rulesets = rulesets;
}
+ [Test]
+ public void TestRecommendedSelection()
+ {
+ loadBeatmaps();
+
+ AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
+
+ // check recommended was selected
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(1, 3);
+
+ // change away from recommended
+ advanceSelection(direction: -1, diff: true);
+ waitForSelection(1, 2);
+
+ // next set, check recommended
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(2, 3);
+
+ // next set, check recommended
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(3, 3);
+
+ // go back to first set and ensure user selection was retained
+ advanceSelection(direction: -1, diff: false);
+ advanceSelection(direction: -1, diff: false);
+ waitForSelection(1, 2);
+ }
+
///
/// Test keyboard traversal
///
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs
new file mode 100644
index 0000000000..9ea76c2c7b
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs
@@ -0,0 +1,91 @@
+// 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.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneOsuMenu : OsuManualInputManagerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OsuMenu),
+ typeof(DrawableOsuMenuItem)
+ };
+
+ private OsuMenu menu;
+ private bool actionPerformed;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ actionPerformed = false;
+
+ Child = menu = new OsuMenu(Direction.Vertical, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Items = new[]
+ {
+ new OsuMenuItem("standard", MenuItemType.Standard, performAction),
+ new OsuMenuItem("highlighted", MenuItemType.Highlighted, performAction),
+ new OsuMenuItem("destructive", MenuItemType.Destructive, performAction),
+ }
+ };
+ });
+
+ [Test]
+ public void TestClickEnabledMenuItem()
+ {
+ AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First()));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("action performed", () => actionPerformed);
+ }
+
+ [Test]
+ public void TestDisableMenuItemsAndClick()
+ {
+ AddStep("disable menu items", () =>
+ {
+ foreach (var item in menu.Items)
+ ((OsuMenuItem)item).Action.Disabled = true;
+ });
+
+ AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First()));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("action not performed", () => !actionPerformed);
+ }
+
+ [Test]
+ public void TestEnableMenuItemsAndClick()
+ {
+ AddStep("disable menu items", () =>
+ {
+ foreach (var item in menu.Items)
+ ((OsuMenuItem)item).Action.Disabled = true;
+ });
+
+ AddStep("enable menu items", () =>
+ {
+ foreach (var item in menu.Items)
+ ((OsuMenuItem)item).Action.Disabled = false;
+ });
+
+ AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First()));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("action performed", () => actionPerformed);
+ }
+
+ private void performAction() => actionPerformed = true;
+ }
+}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 39a0e6f6d4..a1822a1163 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -5,7 +5,7 @@ using System;
namespace osu.Game.Beatmaps.ControlPoints
{
- public abstract class ControlPoint : IComparable, IEquatable
+ public abstract class ControlPoint : IComparable
{
///
/// The time at which the control point takes effect.
@@ -19,12 +19,10 @@ namespace osu.Game.Beatmaps.ControlPoints
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
///
- /// Whether this control point is equivalent to another, ignoring time.
+ /// Determines whether this results in a meaningful change when placed alongside another.
///
- /// Another control point to compare with.
- /// Whether equivalent.
- public abstract bool EquivalentTo(ControlPoint other);
-
- public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other);
+ /// An existing control point to compare with.
+ /// Whether this is redundant when placed alongside .
+ public abstract bool IsRedundant(ControlPoint existing);
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index df68d8acd2..d33a922a32 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps.ControlPoints
break;
}
- return existing?.EquivalentTo(newPoint) == true;
+ return newPoint?.IsRedundant(existing) == true;
}
private void groupItemAdded(ControlPoint controlPoint)
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 8b21098a51..2448b2b25c 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -27,7 +27,8 @@ namespace osu.Game.Beatmaps.ControlPoints
set => SpeedMultiplierBindable.Value = value;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
+ public override bool IsRedundant(ControlPoint existing)
+ => existing is DifficultyControlPoint existingDifficulty
+ && SpeedMultiplier == existingDifficulty.SpeedMultiplier;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 369b93ff3d..9b69147468 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -35,8 +35,10 @@ namespace osu.Game.Beatmaps.ControlPoints
set => KiaiModeBindable.Value = value;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is EffectControlPoint otherTyped &&
- KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
+ public override bool IsRedundant(ControlPoint existing)
+ => !OmitFirstBarLine
+ && existing is EffectControlPoint existingEffect
+ && KiaiMode == existingEffect.KiaiMode
+ && OmitFirstBarLine == existingEffect.OmitFirstBarLine;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 393bcfdb3c..61851a00d7 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -68,8 +68,9 @@ namespace osu.Game.Beatmaps.ControlPoints
return newSampleInfo;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is SampleControlPoint otherTyped &&
- SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume;
+ public override bool IsRedundant(ControlPoint existing)
+ => existing is SampleControlPoint existingSample
+ && SampleBank == existingSample.SampleBank
+ && SampleVolume == existingSample.SampleVolume;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 51b3377394..1927dd6575 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -48,8 +48,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public double BPM => 60000 / BeatLength;
- public override bool EquivalentTo(ControlPoint other) =>
- other is TimingControlPoint otherTyped
- && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
+ // Timing points are never redundant as they can change the time signature.
+ public override bool IsRedundant(ControlPoint existing) => false;
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 113526f9dd..6406bd88a5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -8,6 +8,7 @@ using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO;
+using osu.Game.Rulesets.Objects.Legacy;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Formats
@@ -169,15 +170,19 @@ namespace osu.Game.Beatmaps.Formats
{
var baseInfo = base.ApplyTo(hitSampleInfo);
- if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 1)
- baseInfo.Suffix = CustomSampleBank.ToString();
+ if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy
+ && legacy.CustomSampleBank == 0)
+ {
+ legacy.CustomSampleBank = CustomSampleBank;
+ }
return baseInfo;
}
- public override bool EquivalentTo(ControlPoint other) =>
- base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
- CustomSampleBank == otherTyped.CustomSampleBank;
+ public override bool IsRedundant(ControlPoint existing)
+ => base.IsRedundant(existing)
+ && existing is LegacySampleControlPoint existingSample
+ && CustomSampleBank == existingSample.CustomSampleBank;
}
}
}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index ab5a652a94..9d31bc9bba 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -49,6 +49,7 @@ namespace osu.Game.Configuration
};
Set(OsuSetting.ExternalLinkWarning, true);
+ Set(OsuSetting.PreferNoVideo, false);
// Audio
Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
@@ -87,6 +88,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowProgressGraph, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
+ Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
Set(OsuSetting.KeyOverlay, false);
Set(OsuSetting.PositionalHitSounds, true);
Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
@@ -183,6 +185,7 @@ namespace osu.Game.Configuration
ShowInterface,
ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
+ FadePlayfieldWhenHealthLow,
MouseDisableButtons,
MouseDisableWheel,
AudioOffset,
@@ -214,6 +217,7 @@ namespace osu.Game.Configuration
IncreaseFirstObjectVisibility,
ScoreDisplayMode,
ExternalLinkWarning,
+ PreferNoVideo,
Scaling,
ScalingPositionX,
ScalingPositionY,
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index f36079682e..5a613d1a54 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
- if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat)
+ if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
return;
using (BeginDelayedSequence(-TimeSinceLastBeat, true))
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index a3ca851341..abaae7b43c 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
updateTextColour();
+
+ Item.Action.BindDisabledChanged(_ => updateState(), true);
}
private void updateTextColour()
@@ -65,19 +67,33 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
- sampleHover.Play();
- text.BoldText.FadeIn(transition_length, Easing.OutQuint);
- text.NormalText.FadeOut(transition_length, Easing.OutQuint);
+ updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- text.BoldText.FadeOut(transition_length, Easing.OutQuint);
- text.NormalText.FadeIn(transition_length, Easing.OutQuint);
+ updateState();
base.OnHoverLost(e);
}
+ private void updateState()
+ {
+ Alpha = Item.Action.Disabled ? 0.2f : 1;
+
+ if (IsHovered && !Item.Action.Disabled)
+ {
+ sampleHover.Play();
+ text.BoldText.FadeIn(transition_length, Easing.OutQuint);
+ text.NormalText.FadeOut(transition_length, Easing.OutQuint);
+ }
+ else
+ {
+ text.BoldText.FadeOut(transition_length, Easing.OutQuint);
+ text.NormalText.FadeIn(transition_length, Easing.OutQuint);
+ }
+ }
+
protected override bool OnClick(ClickEvent e)
{
sampleClick.Play();
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 6a6c7b72a8..47600e4f68 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -16,20 +16,27 @@ namespace osu.Game.Online.API
{
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri);
- public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
+ public T Result { get; private set; }
protected APIRequest()
{
- base.Success += onSuccess;
+ base.Success += () => TriggerSuccess(((OsuJsonWebRequest)WebRequest)?.ResponseObject);
}
- private void onSuccess() => Success?.Invoke(Result);
-
///
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
///
public new event APISuccessHandler Success;
+
+ internal void TriggerSuccess(T result)
+ {
+ if (Result != null)
+ throw new InvalidOperationException("Attempted to trigger success more than once");
+
+ Result = result;
+ Success?.Invoke(result);
+ }
}
///
@@ -96,10 +103,15 @@ namespace osu.Game.Online.API
{
if (cancelled) return;
- Success?.Invoke();
+ TriggerSuccess();
});
}
+ internal void TriggerSuccess()
+ {
+ Success?.Invoke();
+ }
+
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index a1c3475fd9..7800241904 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.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;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -30,6 +31,11 @@ namespace osu.Game.Online.API
private readonly List components = new List();
+ ///
+ /// Provide handling logic for an arbitrary API request.
+ ///
+ public Action HandleRequest;
+
public APIState State
{
get => state;
@@ -55,11 +61,16 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request)
{
+ HandleRequest?.Invoke(request);
}
- public void Perform(APIRequest request) { }
+ public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
- public Task PerformAsync(APIRequest request) => Task.CompletedTask;
+ public Task PerformAsync(APIRequest request)
+ {
+ HandleRequest?.Invoke(request);
+ return Task.CompletedTask;
+ }
public void Register(IOnlineComponent component)
{
diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs
index 08e3ed9b38..387ced6acb 100644
--- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs
+++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
@@ -16,8 +17,6 @@ namespace osu.Game.Overlays.Direct
{
protected bool DownloadEnabled => button.Enabled.Value;
- private readonly bool noVideo;
-
///
/// Currently selected beatmap. Used to present the correct difficulty after completing a download.
///
@@ -25,12 +24,11 @@ namespace osu.Game.Overlays.Direct
private readonly ShakeContainer shakeContainer;
private readonly DownloadButton button;
+ private Bindable noVideoSetting;
- public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
+ public PanelDownloadButton(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
- this.noVideo = noVideo;
-
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
@@ -50,7 +48,7 @@ namespace osu.Game.Overlays.Direct
}
[BackgroundDependencyLoader(true)]
- private void load(OsuGame game, BeatmapManager beatmaps)
+ private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{
@@ -59,6 +57,8 @@ namespace osu.Game.Overlays.Direct
return;
}
+ noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo);
+
button.Action = () =>
{
switch (State.Value)
@@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Direct
break;
default:
- beatmaps.Download(BeatmapSet.Value, noVideo);
+ beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value);
break;
}
};
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index ef03c0622a..93a02ea0e4 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -53,6 +53,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Keywords = new[] { "hp", "bar" }
},
new SettingsCheckbox
+ {
+ LabelText = "Fade playfield to red when health is low",
+ Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow),
+ },
+ new SettingsCheckbox
{
LabelText = "Always show key overlay",
Bindable = config.GetBindable(OsuSetting.KeyOverlay)
diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs
index a8b3e45a83..23513eade8 100644
--- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs
@@ -21,6 +21,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online
LabelText = "Warn about opening external links",
Bindable = config.GetBindable(OsuSetting.ExternalLinkWarning)
},
+ new SettingsCheckbox
+ {
+ LabelText = "Prefer downloads without video",
+ Keywords = new[] { "no-video" },
+ Bindable = config.GetBindable(OsuSetting.PreferNoVideo)
+ },
};
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 8d3ad5984f..9a60a0a75c 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -409,22 +409,34 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
- private class LegacyHitSampleInfo : HitSampleInfo
+ internal class LegacyHitSampleInfo : HitSampleInfo
{
+ private int customSampleBank;
+
public int CustomSampleBank
{
+ get => customSampleBank;
set
{
- if (value > 1)
+ customSampleBank = value;
+
+ if (value >= 2)
Suffix = value.ToString();
}
}
}
- private class FileHitSampleInfo : HitSampleInfo
+ private class FileHitSampleInfo : LegacyHitSampleInfo
{
public string Filename;
+ public FileHitSampleInfo()
+ {
+ // Make sure that the LegacyBeatmapSkin does not fall back to the user skin.
+ // Note that this does not change the lookup names, as they are overridden locally.
+ CustomSampleBank = 1;
+ }
+
public override IEnumerable LookupNames => new[]
{
Filename,
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 8eafaa88ec..1f40f44dce 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Scoring
case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
- return bonusScore + baseScore * ((1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier) / 25);
+ return bonusScore + baseScore * (1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier / 25);
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 14a227eb07..9a1f450dc6 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -107,6 +107,8 @@ namespace osu.Game.Screens.Edit
dependencies.CacheAs(changeHandler);
EditorMenuBar menuBar;
+ OsuMenuItem undoMenuItem;
+ OsuMenuItem redoMenuItem;
var fileMenuItems = new List
public class EditorChangeHandler : IEditorChangeHandler
{
- private readonly LegacyEditorBeatmapPatcher patcher;
+ public readonly Bindable CanUndo = new Bindable();
+ public readonly Bindable CanRedo = new Bindable();
+ private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List savedStates = new List();
private int currentState = -1;
@@ -45,8 +48,6 @@ namespace osu.Game.Screens.Edit
SaveState();
}
- public bool HasUndoState => currentState > 0;
-
private void hitObjectAdded(HitObject obj) => SaveState();
private void hitObjectRemoved(HitObject obj) => SaveState();
@@ -90,6 +91,8 @@ namespace osu.Game.Screens.Edit
}
currentState = savedStates.Count - 1;
+
+ updateBindables();
}
///
@@ -114,6 +117,14 @@ namespace osu.Game.Screens.Edit
currentState = newState;
isRestoring = false;
+
+ updateBindables();
+ }
+
+ private void updateBindables()
+ {
+ CanUndo.Value = savedStates.Count > 0 && currentState > 0;
+ CanRedo.Value = currentState < savedStates.Count - 1;
}
}
}
diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs
index ed3f9af8e2..d7dcca9809 100644
--- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs
+++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs
@@ -212,8 +212,8 @@ namespace osu.Game.Screens.Multi
private class PlaylistDownloadButton : PanelDownloadButton
{
- public PlaylistDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
- : base(beatmapSet, noVideo)
+ public PlaylistDownloadButton(BeatmapSetInfo beatmapSet)
+ : base(beatmapSet)
{
Alpha = 0;
}
diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs
new file mode 100644
index 0000000000..a49aa89a7c
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs
@@ -0,0 +1,117 @@
+// 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.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Scoring;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by .
+ ///
+ public class FailingLayer : HealthDisplay
+ {
+ private const float max_alpha = 0.4f;
+ private const int fade_time = 400;
+ private const float gradient_size = 0.3f;
+
+ ///
+ /// The threshold under which the current player life should be considered low and the layer should start fading in.
+ ///
+ public double LowHealthThreshold = 0.20f;
+
+ private readonly Bindable enabled = new Bindable();
+ private readonly Container boxes;
+
+ private Bindable configEnabled;
+ private HealthProcessor healthProcessor;
+
+ public FailingLayer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Children = new Drawable[]
+ {
+ boxes = new Container
+ {
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)),
+ Height = gradient_size,
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = gradient_size,
+ Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ },
+ }
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour color, OsuConfigManager config)
+ {
+ boxes.Colour = color.Red;
+
+ configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow);
+ enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateBindings();
+ }
+
+ public override void BindHealthProcessor(HealthProcessor processor)
+ {
+ base.BindHealthProcessor(processor);
+
+ healthProcessor = processor;
+ updateBindings();
+ }
+
+ private void updateBindings()
+ {
+ if (LoadState < LoadState.Ready)
+ return;
+
+ enabled.UnbindBindings();
+
+ // Don't display ever if the ruleset is not using a draining health display.
+ if (healthProcessor is DrainingHealthProcessor)
+ enabled.BindTo(configEnabled);
+ else
+ enabled.Value = false;
+ }
+
+ protected override void Update()
+ {
+ double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha);
+
+ boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f);
+
+ base.Update();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs
index 37038ad58c..edc9dedf24 100644
--- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs
@@ -3,15 +3,29 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Play.HUD
{
+ ///
+ /// A container for components displaying the current player health.
+ /// Gets bound automatically to the when inserted to hierarchy.
+ ///
public abstract class HealthDisplay : Container
{
- public readonly BindableDouble Current = new BindableDouble
+ public readonly BindableDouble Current = new BindableDouble(1)
{
MinValue = 0,
MaxValue = 1
};
+
+ ///
+ /// Bind the tracked fields of to this health display.
+ ///
+ public virtual void BindHealthProcessor(HealthProcessor processor)
+ {
+ Current.BindTo(processor.Health);
+ }
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index e06f6d19c2..5114efd9a9 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -37,6 +37,7 @@ namespace osu.Game.Screens.Play
public readonly HitErrorDisplay HitErrorDisplay;
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
+ public readonly FailingLayer FailingLayer;
public Bindable ShowHealthbar = new Bindable(true);
@@ -75,6 +76,7 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
+ FailingLayer = CreateFailingLayer(),
visibilityContainer = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -260,6 +262,8 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20 }
};
+ protected virtual FailingLayer CreateFailingLayer() => new FailingLayer();
+
protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
{
Anchor = Anchor.BottomRight,
@@ -304,7 +308,8 @@ namespace osu.Game.Screens.Play
protected virtual void BindHealthProcessor(HealthProcessor processor)
{
- HealthDisplay?.Current.BindTo(processor.Health);
+ HealthDisplay?.BindHealthProcessor(processor);
+ FailingLayer?.BindHealthProcessor(processor);
}
}
}
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 59dddc2baa..a8225ba1ec 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -48,6 +48,11 @@ namespace osu.Game.Screens.Select
///
public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet;
+ ///
+ /// A function to optionally decide on a recommended difficulty from a beatmap set.
+ ///
+ public Func, BeatmapInfo> GetRecommendedBeatmap;
+
private CarouselBeatmapSet selectedBeatmapSet;
///
@@ -116,6 +121,7 @@ namespace osu.Game.Screens.Select
private readonly Stack randomSelectedBeatmaps = new Stack();
protected List Items = new List();
+
private CarouselRoot root;
public BeatmapCarousel()
@@ -579,7 +585,10 @@ namespace osu.Game.Screens.Select
b.Metadata = beatmapSet.Metadata;
}
- var set = new CarouselBeatmapSet(beatmapSet);
+ var set = new CarouselBeatmapSet(beatmapSet)
+ {
+ GetRecommendedBeatmap = beatmaps => GetRecommendedBeatmap?.Invoke(beatmaps)
+ };
foreach (var c in set.Beatmaps)
{
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
index 8e323c66e2..92ccfde14b 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Screens.Select.Carousel
public BeatmapSetInfo BeatmapSet;
+ public Func, BeatmapInfo> GetRecommendedBeatmap;
+
public CarouselBeatmapSet(BeatmapSetInfo beatmapSet)
{
BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet));
@@ -28,6 +30,17 @@ namespace osu.Game.Screens.Select.Carousel
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this);
+ protected override CarouselItem GetNextToSelect()
+ {
+ if (LastSelected == null)
+ {
+ if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended)
+ return Children.OfType().First(b => b.Beatmap == recommended);
+ }
+
+ return base.GetNextToSelect();
+ }
+
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
{
if (!(other is CarouselBeatmapSet otherSet))
diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
index 6ce12f7b89..262bea9c71 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
@@ -90,11 +90,15 @@ namespace osu.Game.Screens.Select.Carousel
PerformSelection();
}
+ protected virtual CarouselItem GetNextToSelect()
+ {
+ return Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
+ Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ }
+
protected virtual void PerformSelection()
{
- CarouselItem nextToSelect =
- Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
- Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ CarouselItem nextToSelect = GetNextToSelect();
if (nextToSelect != null)
nextToSelect.State.Value = CarouselItemState.Selected;
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
index 2520c70989..3e4798a812 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
@@ -49,12 +49,13 @@ namespace osu.Game.Screens.Select.Carousel
}
[BackgroundDependencyLoader(true)]
- private void load(SongSelect songSelect, BeatmapManager manager)
+ private void load(BeatmapManager manager, SongSelect songSelect)
{
if (songSelect != null)
{
startRequested = b => songSelect.FinaliseSelection(b);
- editRequested = songSelect.Edit;
+ if (songSelect.AllowEditing)
+ editRequested = songSelect.Edit;
}
if (manager != null)
@@ -187,15 +188,19 @@ namespace osu.Game.Screens.Select.Carousel
{
get
{
- List
public virtual bool AllowEditing => true;
- [Resolved(canBeNull: true)]
- private NotificationOverlay notificationOverlay { get; set; }
-
[Resolved]
private Bindable> selectedMods { get; set; }
@@ -81,6 +77,8 @@ namespace osu.Game.Screens.Select
protected BeatmapCarousel Carousel { get; private set; }
+ private DifficultyRecommender recommender;
+
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@@ -109,6 +107,7 @@ namespace osu.Game.Screens.Select
AddRangeInternal(new Drawable[]
{
+ recommender = new DifficultyRecommender(),
new ResetScrollContainer(() => Carousel.ScrollToSelected())
{
RelativeSizeAxes = Axes.Y,
@@ -156,6 +155,7 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.Both,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
+ GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
},
}
},
@@ -325,10 +325,7 @@ namespace osu.Game.Screens.Select
public void Edit(BeatmapInfo beatmap = null)
{
if (!AllowEditing)
- {
- notificationOverlay?.Post(new SimpleNotification { Text = "Editing is not available from the current mode." });
- return;
- }
+ throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
this.Push(new Editor());
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index 1190a330fe..21533e58cd 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.IO.Stores;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Skinning
{
@@ -33,6 +36,17 @@ namespace osu.Game.Skinning
return base.GetConfig(lookup);
}
+ public override SampleChannel GetSample(ISampleInfo sampleInfo)
+ {
+ if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
+ {
+ // When no custom sample bank is provided, always fall-back to the default samples.
+ return null;
+ }
+
+ return base.GetSample(sampleInfo);
+ }
+
private static SkinInfo createSkinInfo(BeatmapInfo beatmap) =>
new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() };
}
diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs
index a1ddafbacf..d13c874ee2 100644
--- a/osu.Game/Storyboards/Storyboard.cs
+++ b/osu.Game/Storyboards/Storyboard.cs
@@ -47,9 +47,6 @@ namespace osu.Game.Storyboards
if (backgroundPath == null)
return false;
- if (GetLayer("Video").Elements.Any())
- return true;
-
return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath);
}
}
diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
index 6db34af20c..8f8afb87d4 100644
--- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.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 osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
@@ -18,8 +19,9 @@ namespace osu.Game.Tests.Beatmaps
///
/// The beatmap.
/// An optional storyboard.
- public TestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- : base(beatmap.BeatmapInfo, null)
+ /// The .
+ public TestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null, AudioManager audioManager = null)
+ : base(beatmap.BeatmapInfo, audioManager)
{
this.beatmap = beatmap;
this.storyboard = storyboard;
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index d1d8059cb1..5dc8714c07 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual
/// 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)
- : base(beatmap, storyboard)
+ : base(beatmap, storyboard, audio)
{
if (referenceClock != null)
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 76f7a030f9..5facb04117 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7a487a6430..dda1ee5c42 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+