diff --git a/FodyWeavers.xml b/FodyWeavers.xml
index cc07b89533..ea490e3297 100644
--- a/FodyWeavers.xml
+++ b/FodyWeavers.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 3c4380e355..c845d7f276 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index 1c89d9cd00..f89750a96e 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
private class TimeSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString("N0") + "ms";
+ public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index 48e4db11ca..5b476526c9 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
- public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
+ public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}
diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
new file mode 100644
index 0000000000..cf5b3a42a4
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
@@ -0,0 +1,241 @@
+// 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.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckFewHitsoundsTest
+ {
+ private CheckFewHitsounds check;
+
+ private List notHitsounded;
+ private List hitsounded;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckFewHitsounds();
+ notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+ hitsounded = new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
+ new HitSampleInfo(HitSampleInfo.HIT_FINISH)
+ };
+ }
+
+ [Test]
+ public void TestHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 16; ++i)
+ {
+ var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+
+ if ((i + 1) % 2 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ if ((i + 1) % 3 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
+ if ((i + 1) % 4 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertOk(hitObjects);
+ }
+
+ [Test]
+ public void TestHitsoundedWithBreak()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 32; ++i)
+ {
+ var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+
+ if ((i + 1) % 2 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ if ((i + 1) % 3 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
+ if ((i + 1) % 4 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+ // Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
+ if (i > 8 && i < 24)
+ continue;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertOk(hitObjects);
+ }
+
+ [Test]
+ public void TestLightlyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 30; ++i)
+ {
+ var samples = i % 8 == 0 ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertLongPeriodNegligible(hitObjects, count: 3);
+ }
+
+ [Test]
+ public void TestRarelyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 30; ++i)
+ {
+ var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ // Should prompt one warning between 1st and 16th, and another between 16th and 31st.
+ assertLongPeriodWarning(hitObjects, count: 2);
+ }
+
+ [Test]
+ public void TestExtremelyRarelyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 80; ++i)
+ {
+ var samples = i == 40 ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ // Should prompt one problem between 1st and 41st, and another between 41st and 81st.
+ assertLongPeriodProblem(hitObjects, count: 2);
+ }
+
+ [Test]
+ public void TestNotHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 20; ++i)
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
+
+ assertNoHitsounds(hitObjects);
+ }
+
+ [Test]
+ public void TestNestedObjectsHitsounded()
+ {
+ var ticks = new List();
+ for (int i = 1; i < 16; ++i)
+ ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
+ {
+ Samples = hitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertOk(new List { nested });
+ }
+
+ [Test]
+ public void TestNestedObjectsRarelyHitsounded()
+ {
+ var ticks = new List();
+ for (int i = 1; i < 16; ++i)
+ ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
+ {
+ Samples = hitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertLongPeriodWarning(new List { nested });
+ }
+
+ [Test]
+ public void TestConcurrentObjects()
+ {
+ var hitObjects = new List();
+
+ var ticks = new List();
+ for (int i = 1; i < 10; ++i)
+ ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
+ {
+ Samples = notHitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ hitObjects.Add(nested);
+
+ for (int i = 1; i <= 6; ++i)
+ hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
+
+ assertOk(hitObjects);
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertLongPeriodProblem(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
+ }
+
+ private void assertLongPeriodWarning(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
+ }
+
+ private void assertLongPeriodNegligible(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
+ }
+
+ private void assertNoHitsounds(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap { HitObjects = hitObjects };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
new file mode 100644
index 0000000000..41a8f72305
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
@@ -0,0 +1,289 @@
+// 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.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckMutedObjectsTest
+ {
+ private CheckMutedObjects check;
+ private ControlPointInfo cpi;
+
+ private const int volume_regular = 50;
+ private const int volume_low = 15;
+ private const int volume_muted = 5;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckMutedObjects();
+
+ cpi = new ControlPointInfo();
+ cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
+ cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
+ cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
+ }
+
+ [Test]
+ public void TestNormalControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestLowControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 1000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertLowVolume(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestNormalSampleVolume()
+ {
+ // The sample volume should take precedence over the control point volume.
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestLowSampleVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertLowVolume(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestMutedSampleVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestNormalSampleVolumeSlider()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedSampleVolumeSliderHead()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedSampleVolumeSliderTail()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMutedPassive(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolumeSliderHead()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 2250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolumeSliderTail()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ // Ends after the 5% control point.
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMutedPassive(new List { slider });
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertLowVolume(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
+ }
+
+ private void assertMuted(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
+ }
+
+ private void assertMutedPassive(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap
+ {
+ ControlPointInfo = cpi,
+ HitObjects = hitObjects
+ };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
new file mode 100644
index 0000000000..29938839d3
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
@@ -0,0 +1,36 @@
+// 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.Threading;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ public sealed class MockNestableHitObject : HitObject, IHasDuration
+ {
+ private readonly IEnumerable toBeNested;
+
+ public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime)
+ {
+ this.toBeNested = toBeNested;
+ StartTime = startTime;
+ EndTime = endTime;
+ }
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ foreach (var hitObject in toBeNested)
+ AddNested(hitObject);
+ }
+
+ public double EndTime { get; }
+
+ public double Duration
+ {
+ get => EndTime - StartTime;
+ set => throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
new file mode 100644
index 0000000000..25619de323
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
@@ -0,0 +1,95 @@
+// 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.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Rulesets;
+using osu.Game.Skinning;
+using osu.Game.Tests.Testing;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Rulesets
+{
+ public class TestSceneRulesetSkinProvidingContainer : OsuTestScene
+ {
+ private SkinRequester requester;
+
+ protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
+
+ [Cached(typeof(ISkinSource))]
+ private readonly ISkinSource testSource = new TestSkinProvider();
+
+ [Test]
+ public void TestEarlyAddedSkinRequester()
+ {
+ Texture textureOnLoad = null;
+
+ AddStep("setup provider", () =>
+ {
+ var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
+
+ rulesetSkinProvider.Add(requester = new SkinRequester());
+
+ requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture(TestSkinProvider.TEXTURE_NAME);
+
+ Child = rulesetSkinProvider;
+ });
+
+ AddAssert("requester got correct initial texture", () => textureOnLoad != null);
+ }
+
+ private class SkinRequester : Drawable, ISkin
+ {
+ private ISkinSource skin;
+
+ public event Action OnLoadAsync;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ this.skin = skin;
+
+ OnLoadAsync?.Invoke();
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName);
+
+ public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
+
+ public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup);
+ }
+
+ private class TestSkinProvider : ISkinSource
+ {
+ public const string TEXTURE_NAME = "some-texture";
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => componentName == TEXTURE_NAME ? Texture.WhitePixel : null;
+
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+
+ public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
+
+ public event Action SourceChanged
+ {
+ add { }
+ remove { }
+ }
+
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
+
+ public IEnumerable AllSources => new[] { this };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index 97087e31ab..fb50da32f3 100644
--- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Testing
Dependencies.Get() != null);
}
- private class TestRuleset : Ruleset
+ public class TestRuleset : Ruleset
{
public override string Description => string.Empty;
public override string ShortName => string.Empty;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 156d6b744e..5bfb676f81 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.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.Linq;
using NUnit.Framework;
@@ -14,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
@@ -23,6 +26,8 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingOverlay overlay;
+ private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single();
+
[BackgroundDependencyLoader]
private void load()
{
@@ -39,6 +44,16 @@ namespace osu.Game.Tests.Visual.Online
return true;
};
+
+ AddStep("initialize dummy", () =>
+ {
+ // non-supporter user
+ ((DummyAPIAccess)API).LocalUser.Value = new User
+ {
+ Username = "TestBot",
+ Id = API.LocalUser.Value.Id + 1,
+ };
+ });
}
[Test]
@@ -58,13 +73,164 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
}
+ [Test]
+ public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
+ {
+ AddStep("fetch for 0 beatmaps", () => fetchFor());
+ AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ notFoundPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+
+ // both RankAchieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+ }
+
+ [Test]
+ public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults()
+ {
+ AddStep("fetch for 0 beatmaps", () => fetchFor());
+ AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ notFoundPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ notFoundPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ notFoundPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+
+ // both Rank Achieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ notFoundPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+ }
+
+ [Test]
+ public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
+ {
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ noPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+
+ // both Rank Achieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+ }
+
+ [Test]
+ public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
+ {
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ noPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ noPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ noPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+
+ // both Rank Achieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ noPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+ }
+
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
- overlay.ChildrenOfType().Single().Query.TriggerChange();
+ searchControl.Query.TriggerChange();
+ }
+
+ private void setRankAchievedFilter(ScoreRank[] ranks)
+ {
+ AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () =>
+ {
+ searchControl.Ranks.Clear();
+ searchControl.Ranks.AddRange(ranks);
+ });
+ }
+
+ private void setPlayedFilter(SearchPlayed played)
+ {
+ AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played);
+ }
+
+ private void supporterRequiredPlaceholderShown()
+ {
+ AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+ }
+
+ private void notFoundPlaceholderShown()
+ {
+ AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+ }
+
+ private void noPlaceholderShown()
+ {
+ AddUntilStep("no placeholder shown", () =>
+ !overlay.ChildrenOfType().Any()
+ && !overlay.ChildrenOfType().Any());
}
private class TestAPIBeatmapSet : APIBeatmapSet
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs
index c5374d50ab..096bccae9e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
@@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private class Icon : Container, IHasTooltip
{
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public SpriteIcon SpriteIcon { get; }
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index 069ddfa4db..27ad6650d1 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
- private readonly Bindable beatmapId = new Bindable();
+ private readonly Bindable beatmapId = new Bindable();
private readonly Bindable mods = new Bindable();
@@ -220,14 +220,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- beatmapId.Value = Model.ID.ToString();
- beatmapId.BindValueChanged(idString =>
+ beatmapId.Value = Model.ID;
+ beatmapId.BindValueChanged(id =>
{
- int.TryParse(idString.NewValue, out var parsed);
+ Model.ID = id.NewValue ?? 0;
- Model.ID = parsed;
-
- if (idString.NewValue != idString.OldValue)
+ if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
if (Model.BeatmapInfo != null)
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 7bd8d3f6a0..6418bf97da 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
- private readonly Bindable beatmapId = new Bindable();
+ private readonly Bindable beatmapId = new Bindable();
private readonly Bindable score = new Bindable();
@@ -228,16 +228,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- beatmapId.Value = Model.ID.ToString();
- beatmapId.BindValueChanged(idString =>
+ beatmapId.Value = Model.ID;
+ beatmapId.BindValueChanged(id =>
{
- int parsed;
+ Model.ID = id.NewValue ?? 0;
- int.TryParse(idString.NewValue, out parsed);
-
- Model.ID = parsed;
-
- if (idString.NewValue != idString.OldValue)
+ if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
if (Model.BeatmapInfo != null)
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index aa1be143ea..0d2e64f300 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
private TournamentGameBase game { get; set; }
- private readonly Bindable userId = new Bindable();
+ private readonly Bindable userId = new Bindable();
private readonly Container drawableContainer;
@@ -278,14 +278,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load()
{
- userId.Value = user.Id.ToString();
- userId.BindValueChanged(idString =>
+ userId.Value = user.Id;
+ userId.BindValueChanged(id =>
{
- int.TryParse(idString.NewValue, out var parsed);
+ user.Id = id.NewValue ?? 0;
- user.Id = parsed;
-
- if (idString.NewValue != idString.OldValue)
+ if (id.NewValue != id.OldValue)
user.Username = string.Empty;
if (!string.IsNullOrEmpty(user.Username))
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index f854a5fecb..c558fb394c 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -191,8 +191,6 @@ namespace osu.Game.Beatmaps
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
- LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
-
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
{
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index 5dff4fe282..7824205257 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
- LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 55636495df..f373e59417 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Configuration
{
public LocalisableString Label { get; }
- public string Description { get; }
+ public LocalisableString Description { get; }
public int? OrderPosition { get; }
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 3798c0c6ae..f72a43fa01 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -788,7 +788,7 @@ namespace osu.Game.Database
/// The model to populate.
/// The archive to use as a reference for population. May be null.
/// An optional cancellation token.
- protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask;
+ protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default);
///
/// Perform any final actions before the import to database executes.
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs
index 75c73af0ce..ce8a9c8f9f 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs
@@ -4,12 +4,13 @@
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownImage : MarkdownImage, IHasTooltip
{
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public OsuMarkdownImage(LinkInline linkInline)
: base(linkInline.Url)
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 60ded8952d..0bc3c876e1 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.Containers
@@ -24,7 +25,7 @@ namespace osu.Game.Graphics.Containers
this.sampleSet = sampleSet;
}
- public virtual string TooltipText { get; set; }
+ public virtual LocalisableString TooltipText { get; set; }
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs
index 57f39bb8c7..81dca99ddd 100644
--- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs
+++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.Cursor
@@ -32,7 +33,7 @@ namespace osu.Game.Graphics.Cursor
public override bool SetContent(object content)
{
- if (!(content is string contentString))
+ if (!(content is LocalisableString contentString))
return false;
if (contentString == text.Text) return true;
diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
index 5a1eb53fe1..6ad88eaaba 100644
--- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
+++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Platform;
using osuTK;
using osuTK.Graphics;
@@ -58,6 +59,6 @@ namespace osu.Game.Graphics.UserInterface
return true;
}
- public string TooltipText => "view in browser";
+ public LocalisableString TooltipText => "view in browser";
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
index ac6f5ceb1b..8e82f4a7c1 100644
--- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Platform;
namespace osu.Game.Graphics.UserInterface
@@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface
private class CapsWarning : SpriteIcon, IHasTooltip
{
- public string TooltipText => @"caps lock is active";
+ public LocalisableString TooltipText => "caps lock is active";
public CapsWarning()
{
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index f58962f8e1..ae16169123 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface
{
@@ -34,7 +35,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box rightBox;
private readonly Container nubContainer;
- public virtual string TooltipText { get; private set; }
+ public virtual LocalisableString TooltipText { get; private set; }
///
/// Whether to format the tooltip as a percentage or the actual value.
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 795540b65d..e35d3d6461 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -295,7 +296,7 @@ namespace osu.Game.Online.Leaderboards
public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos);
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public ScoreComponentLabel(LeaderboardScoreStatistic statistic)
{
@@ -365,7 +366,7 @@ namespace osu.Game.Online.Leaderboards
};
}
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
}
public class LeaderboardScoreStatistic
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 1935a250b7..d80ef075e9 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -10,11 +10,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
+using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -23,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapListingFilterControl : CompositeDrawable
{
///
- /// Fired when a search finishes. Contains only new items in the case of pagination.
+ /// Fired when a search finishes.
///
- public Action> SearchFinished;
+ public Action SearchFinished;
///
/// Fired when search criteria change.
@@ -212,7 +214,25 @@ namespace osu.Game.Overlays.BeatmapListing
lastResponse = response;
getSetsRequest = null;
- SearchFinished?.Invoke(sets);
+ // check if a non-supporter used supporter-only filters
+ if (!api.LocalUser.Value.IsSupporter)
+ {
+ List filters = new List();
+
+ if (searchControl.Played.Value != SearchPlayed.Any)
+ filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed);
+
+ if (searchControl.Ranks.Any())
+ filters.Add(BeatmapsStrings.ListingSearchFiltersRank);
+
+ if (filters.Any())
+ {
+ SearchFinished?.Invoke(SearchResult.SupporterOnlyFilters(filters));
+ return;
+ }
+ }
+
+ SearchFinished?.Invoke(SearchResult.ResultsReturned(sets));
};
api.Queue(getSetsRequest);
@@ -237,5 +257,53 @@ namespace osu.Game.Overlays.BeatmapListing
base.Dispose(isDisposing);
}
+
+ ///
+ /// Indicates the type of result of a user-requested beatmap search.
+ ///
+ public enum SearchResultType
+ {
+ ///
+ /// Actual results have been returned from API.
+ ///
+ ResultsReturned,
+
+ ///
+ /// The user is not a supporter, but used supporter-only search filters.
+ ///
+ SupporterOnlyFilters
+ }
+
+ ///
+ /// Describes the result of a user-requested beatmap search.
+ ///
+ public struct SearchResult
+ {
+ public SearchResultType Type { get; private set; }
+
+ ///
+ /// Contains the beatmap sets returned from API.
+ /// Valid for read if and only if is .
+ ///
+ public List Results { get; private set; }
+
+ ///
+ /// Contains the names of supporter-only filters requested by the user.
+ /// Valid for read if and only if is .
+ ///
+ public List SupporterOnlyFiltersUsed { get; private set; }
+
+ public static SearchResult ResultsReturned(List results) => new SearchResult
+ {
+ Type = SearchResultType.ResultsReturned,
+ Results = results
+ };
+
+ public static SearchResult SupporterOnlyFilters(List filters) => new SearchResult
+ {
+ Type = SearchResultType.SupporterOnlyFilters,
+ SupporterOnlyFiltersUsed = filters
+ };
+ }
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 5e65cd9488..460b4ba4c9 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Localisation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -15,7 +16,9 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
+using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
@@ -33,6 +36,7 @@ namespace osu.Game.Overlays
private Container panelTarget;
private FillFlowContainer foundContent;
private NotFoundDrawable notFoundContent;
+ private SupporterRequiredDrawable supporterRequiredContent;
private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
@@ -76,6 +80,7 @@ namespace osu.Game.Overlays
{
foundContent = new FillFlowContainer(),
notFoundContent = new NotFoundDrawable(),
+ supporterRequiredContent = new SupporterRequiredDrawable(),
}
}
},
@@ -115,9 +120,16 @@ namespace osu.Game.Overlays
private Task panelLoadDelegate;
- private void onSearchFinished(List beatmaps)
+ private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult)
{
- var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b)
+ if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters)
+ {
+ supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed);
+ addContentToPlaceholder(supporterRequiredContent);
+ return;
+ }
+
+ var newPanels = searchResult.Results.Select(b => new GridBeatmapPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -128,7 +140,7 @@ namespace osu.Game.Overlays
//No matches case
if (!newPanels.Any())
{
- LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
+ addContentToPlaceholder(notFoundContent);
return;
}
@@ -170,9 +182,9 @@ namespace osu.Game.Overlays
{
var transform = lastContent.FadeOut(100, Easing.OutQuint);
- if (lastContent == notFoundContent)
+ if (lastContent == notFoundContent || lastContent == supporterRequiredContent)
{
- // not found display may be used multiple times, so don't expire/dispose it.
+ // the placeholders may be used multiple times, so don't expire/dispose them.
transform.Schedule(() => panelTarget.Remove(lastContent));
}
else
@@ -240,6 +252,67 @@ namespace osu.Game.Overlays
}
}
+ // TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside
+ // (https://github.com/ppy/osu-framework/issues/4530)
+ public class SupporterRequiredDrawable : CompositeDrawable
+ {
+ private LinkFlowContainer supporterRequiredText;
+
+ public SupporterRequiredDrawable()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 225;
+ Alpha = 0;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddInternal(new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Texture = textures.Get(@"Online/supporter-required"),
+ },
+ supporterRequiredText = new LinkFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Margin = new MarginPadding { Bottom = 10 },
+ },
+ }
+ });
+ }
+
+ public void UpdateText(List filters)
+ {
+ supporterRequiredText.Clear();
+
+ supporterRequiredText.AddText(
+ BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(),
+ t =>
+ {
+ t.Font = OsuFont.GetFont(size: 16);
+ t.Colour = Colour4.White;
+ }
+ );
+
+ supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag");
+ }
+ }
+
private const double time_between_fetches = 500;
private double lastFetchDisplayedTime;
diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
index cf74c0d4d3..b81c60a5b9 100644
--- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs
+++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
@@ -95,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
private readonly OsuSpriteText value;
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public LocalisableString Value
{
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs
index 7ad6906cea..bb87e7151b 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private readonly IBindable localUser = new Bindable();
- public string TooltipText
+ public LocalisableString TooltipText
{
get
{
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
index 6d27342049..cef623e59b 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -26,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private readonly bool noVideo;
- public string TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download";
+ public LocalisableString TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download";
private readonly IBindable localUser = new Bindable();
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 7c47ac655f..d94f8c4b8b 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -20,6 +20,7 @@ using System;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
+using osu.Framework.Localisation;
using osu.Game.Overlays.Comments.Buttons;
namespace osu.Game.Overlays.Comments
@@ -395,7 +396,7 @@ namespace osu.Game.Overlays.Comments
private class ParentUsername : FillFlowContainer, IHasTooltip
{
- public string TooltipText => getParentMessage();
+ public LocalisableString TooltipText => getParentMessage();
private readonly Comment parentComment;
@@ -427,7 +428,7 @@ namespace osu.Game.Overlays.Comments
if (parentComment == null)
return string.Empty;
- return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty;
+ return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? "deleted" : string.Empty;
}
}
}
diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
index 70424101fd..d0bd24496a 100644
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ b/osu.Game/Overlays/Mods/ModButton.cs
@@ -14,6 +14,7 @@ using System;
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Mods
///
public Action SelectionChanged;
- public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
+ public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
private const Easing mod_switch_easing = Easing.InOutSine;
private const double mod_switch_duration = 120;
diff --git a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs
index 87b9d89d4d..0ece96b56c 100644
--- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs
+++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs
@@ -11,6 +11,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
using osuTK.Graphics;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
namespace osu.Game.Overlays
{
@@ -56,7 +57,7 @@ namespace osu.Game.Overlays
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- public string TooltipText => $@"{Value} view";
+ public LocalisableString TooltipText => $@"{Value} view";
private readonly SpriteIcon icon;
diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs
index 7eed4d3b6b..74f3ed846b 100644
--- a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
using osu.Game.Users;
using osuTK;
@@ -42,6 +43,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
InternalChild.FadeInFromZero(200);
}
- public string TooltipText => badge.Description;
+ public LocalisableString TooltipText => badge.Description;
}
}
diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
index 29e13e4f51..527c70685f 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
@@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly BindableBool DetailsVisible = new BindableBool();
- public override string TooltipText => DetailsVisible.Value ? "collapse" : "expand";
+ public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand";
private SpriteIcon icon;
diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs
index bd8aa7b3bd..db94762efd 100644
--- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable User = new Bindable();
- public override string TooltipText => "followers";
+ public override LocalisableString TooltipText => "followers";
protected override IconUsage Icon => FontAwesome.Solid.User;
diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs
index 29471375b5..a0b8ef0f11 100644
--- a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
@@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable User = new Bindable();
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
private OsuSpriteText levelText;
diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs
index c97df3bc4d..528b05a80a 100644
--- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable User = new Bindable();
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
private Bar levelProgressBar;
private OsuSpriteText levelProgressText;
diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs
index b4d7c9a05c..ae3d024fbf 100644
--- a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable User = new Bindable();
- public override string TooltipText => "mapping subscribers";
+ public override LocalisableString TooltipText => "mapping subscribers";
protected override IconUsage Icon => FontAwesome.Solid.Bell;
diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs
index 228765ee1a..4c2cc768ce 100644
--- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Users;
@@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable User = new Bindable();
- public override string TooltipText => "send message";
+ public override LocalisableString TooltipText => "send message";
[Resolved(CanBeNull = true)]
private ChannelManager channelManager { get; set; }
diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs
index be96840217..aa7cb8636a 100644
--- a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable User = new Bindable();
- public string TooltipText { get; set; }
+ public LocalisableString TooltipText { get; set; }
private OverlinedInfoContainer info;
diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
index d581e2750c..9a43997030 100644
--- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Header.Components
@@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly FillFlowContainer iconContainer;
private readonly CircularContainer content;
- public string TooltipText => "osu!supporter";
+ public LocalisableString TooltipText => "osu!supporter";
public int SupportLevel
{
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
index 6d6ff32aac..6f1869966a 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private class PlayCountText : CompositeDrawable, IHasTooltip
{
- public string TooltipText => "times played";
+ public LocalisableString TooltipText => "times played";
public PlayCountText(int playCount)
{
diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs
index 213ad2ba68..fe36f6ba6d 100644
--- a/osu.Game/Overlays/RestoreDefaultValueButton.cs
+++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@@ -76,7 +77,7 @@ namespace osu.Game.Overlays
UpdateState();
}
- public string TooltipText => "revert to default";
+ public LocalisableString TooltipText => "revert to default";
protected override bool OnHover(HoverEvent e)
{
diff --git a/osu.Game/Overlays/Settings/OutlinedTextBox.cs b/osu.Game/Overlays/Settings/OutlinedTextBox.cs
new file mode 100644
index 0000000000..93eaf74b77
--- /dev/null
+++ b/osu.Game/Overlays/Settings/OutlinedTextBox.cs
@@ -0,0 +1,49 @@
+// 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.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Settings
+{
+ public class OutlinedTextBox : OsuTextBox
+ {
+ private const float border_thickness = 3;
+
+ private Color4 borderColourFocused;
+ private Color4 borderColourUnfocused;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ borderColourUnfocused = colour.Gray4.Opacity(0.5f);
+ borderColourFocused = BorderColour;
+
+ updateBorder();
+ }
+
+ protected override void OnFocus(FocusEvent e)
+ {
+ base.OnFocus(e);
+
+ updateBorder();
+ }
+
+ protected override void OnFocusLost(FocusLostEvent e)
+ {
+ base.OnFocusLost(e);
+
+ updateBorder();
+ }
+
+ private void updateBorder()
+ {
+ BorderThickness = border_thickness;
+ BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
index c9a81b955b..1ae297f2a9 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private class OffsetSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString(@"0ms");
+ public override LocalisableString TooltipText => Current.Value.ToString(@"0ms");
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 937bcc8abf..669753d2cb 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -233,7 +233,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private class UIScaleSlider : OsuSliderBar
{
- public override string TooltipText => base.TooltipText + "x";
+ public override LocalisableString TooltipText => base.TooltipText + "x";
}
private class ResolutionSettingsDropdown : SettingsDropdown
diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index fb908a7669..e87572e2ca 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
@@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private class SensitivitySlider : OsuSliderBar
{
- public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
+ public override LocalisableString TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs
index 101d8f43f7..8aeb440be1 100644
--- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs
+++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.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.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
@@ -10,6 +11,6 @@ namespace osu.Game.Overlays.Settings.Sections
///
internal class SizeSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString(@"0.##x");
+ public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x");
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs
index 19adfc5dd9..a6eb008623 100644
--- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -44,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
private class TimeSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString("N0") + "ms";
+ public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs
index c73a783d37..2470c0a6c5 100644
--- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -62,12 +63,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
private class MaximumStarsSlider : StarsSlider
{
- public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
+ public override LocalisableString TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
}
private class StarsSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString(@"0.## stars");
+ public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars");
}
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 088d69c031..87b1aa0e46 100644
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
@@ -17,14 +18,15 @@ namespace osu.Game.Overlays.Settings
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
}
- public string TooltipText { get; set; }
+ public LocalisableString TooltipText { get; set; }
public override IEnumerable FilterTerms
{
get
{
- if (TooltipText != null)
- return base.FilterTerms.Append(TooltipText);
+ if (TooltipText != default)
+ // TODO: this won't work as intended once the tooltip text is translated.
+ return base.FilterTerms.Append(TooltipText.ToString());
return base.FilterTerms;
}
diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index 807916e7f6..15a0a42d31 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings
public bool ShowsDefaultIndicator = true;
- public string TooltipText { get; set; }
+ public LocalisableString TooltipText { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@@ -142,4 +142,4 @@ namespace osu.Game.Overlays.Settings
labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
}
}
-}
\ No newline at end of file
+}
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
index ca9a8e9c08..2fbe522479 100644
--- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -1,19 +1,65 @@
// 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.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
- public class SettingsNumberBox : SettingsItem
+ public class SettingsNumberBox : SettingsItem
{
- protected override Drawable CreateControl() => new NumberBox
+ protected override Drawable CreateControl() => new NumberControl
{
- Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
+ Margin = new MarginPadding { Top = 5 }
};
- public class NumberBox : SettingsTextBox.TextBox
+ private sealed class NumberControl : CompositeDrawable, IHasCurrentValue
+ {
+ private readonly BindableWithCurrent current = new BindableWithCurrent();
+
+ public Bindable Current
+ {
+ get => current.Current;
+ set => current.Current = value;
+ }
+
+ public NumberControl()
+ {
+ AutoSizeAxes = Axes.Y;
+
+ OutlinedNumberBox numberBox;
+
+ InternalChildren = new[]
+ {
+ numberBox = new OutlinedNumberBox
+ {
+ Margin = new MarginPadding { Top = 5 },
+ RelativeSizeAxes = Axes.X,
+ CommitOnFocusLost = true
+ }
+ };
+
+ numberBox.Current.BindValueChanged(e =>
+ {
+ int? value = null;
+
+ if (int.TryParse(e.NewValue, out var intVal))
+ value = intVal;
+
+ current.Value = value;
+ });
+
+ Current.BindValueChanged(e =>
+ {
+ numberBox.Current.Value = e.NewValue?.ToString();
+ });
+ }
+ }
+
+ private class OutlinedNumberBox : OutlinedTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
}
diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs
index 25424e85a1..d28dbf1068 100644
--- a/osu.Game/Overlays/Settings/SettingsTextBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs
@@ -1,60 +1,17 @@
// 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.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
-using osuTK.Graphics;
namespace osu.Game.Overlays.Settings
{
public class SettingsTextBox : SettingsItem
{
- protected override Drawable CreateControl() => new TextBox
+ protected override Drawable CreateControl() => new OutlinedTextBox
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
- CommitOnFocusLost = true,
+ CommitOnFocusLost = true
};
-
- public class TextBox : OsuTextBox
- {
- private const float border_thickness = 3;
-
- private Color4 borderColourFocused;
- private Color4 borderColourUnfocused;
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colour)
- {
- borderColourUnfocused = colour.Gray4.Opacity(0.5f);
- borderColourFocused = BorderColour;
-
- updateBorder();
- }
-
- protected override void OnFocus(FocusEvent e)
- {
- base.OnFocus(e);
-
- updateBorder();
- }
-
- protected override void OnFocusLost(FocusLostEvent e)
- {
- base.OnFocusLost(e);
-
- updateBorder();
- }
-
- private void updateBorder()
- {
- BorderThickness = border_thickness;
- BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
- }
- }
}
}
diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
index d208c7fe07..706eec226c 100644
--- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
+++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Edit
// Audio
new CheckAudioPresence(),
new CheckAudioQuality(),
+ new CheckMutedObjects(),
+ new CheckFewHitsounds(),
// Compose
new CheckUnsnappedObjects(),
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
new file mode 100644
index 0000000000..5185ba6c99
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
@@ -0,0 +1,164 @@
+// 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.Game.Audio;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckFewHitsounds : ICheck
+ {
+ ///
+ /// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map.
+ /// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good.
+ ///
+ private const int negligible_threshold_time = 4000;
+
+ ///
+ /// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song.
+ /// This is ok if the section is a quiet intro, for example.
+ ///
+ private const int warning_threshold_time = 8000;
+
+ ///
+ /// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song.
+ ///
+ private const int problem_threshold_time = 24000;
+
+ // Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too).
+ private const int warning_threshold_objects = 4;
+ private const int problem_threshold_objects = 16;
+
+ public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateLongPeriodProblem(this),
+ new IssueTemplateLongPeriodWarning(this),
+ new IssueTemplateLongPeriodNegligible(this),
+ new IssueTemplateNoHitsounds(this)
+ };
+
+ private bool mapHasHitsounds;
+ private int objectsWithoutHitsounds;
+ private double lastHitsoundTime;
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ if (!context.Beatmap.HitObjects.Any())
+ yield break;
+
+ mapHasHitsounds = false;
+ objectsWithoutHitsounds = 0;
+ lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime;
+
+ var hitObjectsIncludingNested = new List();
+
+ foreach (var hitObject in context.Beatmap.HitObjects)
+ {
+ // Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat).
+ foreach (var nestedHitObject in hitObject.NestedHitObjects)
+ hitObjectsIncludingNested.Add(nestedHitObject);
+
+ hitObjectsIncludingNested.Add(hitObject);
+ }
+
+ var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList();
+ var hitObjectCount = hitObjectsByEndTime.Count;
+
+ for (int i = 0; i < hitObjectCount; ++i)
+ {
+ var hitObject = hitObjectsByEndTime[i];
+
+ // This is used to perform an update at the end so that the period after the last hitsounded object can be an issue.
+ bool isLastObject = i == hitObjectCount - 1;
+
+ foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject))
+ yield return issue;
+ }
+
+ if (!mapHasHitsounds)
+ yield return new IssueTemplateNoHitsounds(this).Create();
+ }
+
+ private IEnumerable applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false)
+ {
+ var time = hitObject.GetEndTime();
+ bool hasHitsound = hitObject.Samples.Any(isHitsound);
+ bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal);
+
+ // Only generating issues on hitsounded or last objects ensures we get one issue per long period.
+ // If there are no hitsounds we let the "No hitsounds" template take precedence.
+ if (hasHitsound || (isLastObject && mapHasHitsounds))
+ {
+ var timeWithoutHitsounds = time - lastHitsoundTime;
+
+ if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects)
+ yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds);
+ else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
+ yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds);
+ else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
+ yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds);
+ }
+
+ if (hasHitsound)
+ {
+ mapHasHitsounds = true;
+ objectsWithoutHitsounds = 0;
+ lastHitsoundTime = time;
+ }
+ else if (couldHaveHitsound)
+ ++objectsWithoutHitsounds;
+ }
+
+ private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains);
+ private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
+
+ public abstract class IssueTemplateLongPeriod : IssueTemplate
+ {
+ protected IssueTemplateLongPeriod(ICheck check, IssueType type)
+ : base(check, type, "Long period without hitsounds ({0:F1} seconds).")
+ {
+ }
+
+ public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time };
+ }
+
+ public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod
+ {
+ public IssueTemplateLongPeriodProblem(ICheck check)
+ : base(check, IssueType.Problem)
+ {
+ }
+ }
+
+ public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod
+ {
+ public IssueTemplateLongPeriodWarning(ICheck check)
+ : base(check, IssueType.Warning)
+ {
+ }
+ }
+
+ public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod
+ {
+ public IssueTemplateLongPeriodNegligible(ICheck check)
+ : base(check, IssueType.Negligible)
+ {
+ }
+ }
+
+ public class IssueTemplateNoHitsounds : IssueTemplate
+ {
+ public IssueTemplateNoHitsounds(ICheck check)
+ : base(check, IssueType.Problem, "There are no hitsounds.")
+ {
+ }
+
+ public Issue Create() => new Issue(this);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
new file mode 100644
index 0000000000..a4ff921b7e
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
@@ -0,0 +1,158 @@
+// 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.Utils;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckMutedObjects : ICheck
+ {
+ ///
+ /// Volume percentages lower than or equal to this are typically inaudible.
+ ///
+ private const int muted_threshold = 5;
+
+ ///
+ /// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume.
+ ///
+ private const int low_volume_threshold = 20;
+
+ private enum EdgeType
+ {
+ Head,
+ Repeat,
+ Tail,
+ None
+ }
+
+ public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateMutedActive(this),
+ new IssueTemplateLowVolumeActive(this),
+ new IssueTemplateMutedPassive(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ foreach (var hitObject in context.Beatmap.HitObjects)
+ {
+ // Worth keeping in mind: The samples of an object always play at its end time.
+ // Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this.
+ foreach (var nestedHitObject in hitObject.NestedHitObjects)
+ {
+ foreach (var issue in getVolumeIssues(hitObject, nestedHitObject))
+ yield return issue;
+ }
+
+ foreach (var issue in getVolumeIssues(hitObject))
+ yield return issue;
+ }
+ }
+
+ private IEnumerable getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null)
+ {
+ sampledHitObject ??= hitObject;
+ if (!sampledHitObject.Samples.Any())
+ yield break;
+
+ // Samples that allow themselves to be overridden by control points have a volume of 0.
+ int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
+ double samplePlayTime = sampledHitObject.GetEndTime();
+
+ EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);
+ // We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick.
+ if (edgeType == EdgeType.None)
+ yield break;
+
+ string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null;
+
+ if (maxVolume <= muted_threshold)
+ {
+ if (edgeType == EdgeType.Head)
+ yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
+ else
+ yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
+ }
+ else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head)
+ {
+ yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
+ }
+ }
+
+ private EdgeType getEdgeAtTime(HitObject hitObject, double time)
+ {
+ if (Precision.AlmostEquals(time, hitObject.StartTime, 1f))
+ return EdgeType.Head;
+ if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f))
+ return EdgeType.Tail;
+
+ if (hitObject is IHasRepeats hasRepeats)
+ {
+ double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount();
+ if (spanDuration <= 0)
+ // Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist.
+ return EdgeType.None;
+
+ double spans = (time - hitObject.StartTime) / spanDuration;
+ double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above.
+
+ if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) ||
+ Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference))
+ {
+ return EdgeType.Repeat;
+ }
+ }
+
+ return EdgeType.None;
+ }
+
+ public abstract class IssueTemplateMuted : IssueTemplate
+ {
+ protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage)
+ : base(check, type, unformattedMessage)
+ {
+ }
+
+ public Issue Create(HitObject hitobject, double volume, double time, string postfix = "")
+ {
+ string objectName = hitobject.GetType().Name;
+ if (!string.IsNullOrEmpty(postfix))
+ objectName += " " + postfix;
+
+ return new Issue(hitobject, this, objectName, volume) { Time = time };
+ }
+ }
+
+ public class IssueTemplateMutedActive : IssueTemplateMuted
+ {
+ public IssueTemplateMutedActive(ICheck check)
+ : base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.")
+ {
+ }
+ }
+
+ public class IssueTemplateLowVolumeActive : IssueTemplateMuted
+ {
+ public IssueTemplateLowVolumeActive(ICheck check)
+ : base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.")
+ {
+ }
+ }
+
+ public class IssueTemplateMutedPassive : IssueTemplateMuted
+ {
+ public IssueTemplateMutedPassive(ICheck check)
+ : base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.")
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs
index 61297c162d..1f7742b075 100644
--- a/osu.Game/Rulesets/Mods/ModRandom.cs
+++ b/osu.Game/Rulesets/Mods/ModRandom.cs
@@ -5,6 +5,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods
{
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.Dice;
public override double ScoreMultiplier => 1;
- [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))]
+ [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable Seed { get; } = new Bindable
{
Default = null,
diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs
deleted file mode 100644
index 5c57717d93..0000000000
--- a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-// 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.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.UserInterface;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays.Settings;
-
-namespace osu.Game.Rulesets.Mods
-{
- ///
- /// A settings control for use by mods which have a customisable seed value.
- ///
- public class SeedSettingsControl : SettingsItem
- {
- protected override Drawable CreateControl() => new SeedControl
- {
- RelativeSizeAxes = Axes.X,
- Margin = new MarginPadding { Top = 5 }
- };
-
- private sealed class SeedControl : CompositeDrawable, IHasCurrentValue
- {
- private readonly BindableWithCurrent current = new BindableWithCurrent();
-
- public Bindable Current
- {
- get => current;
- set
- {
- current.Current = value;
- seedNumberBox.Text = value.Value.ToString();
- }
- }
-
- private readonly OsuNumberBox seedNumberBox;
-
- public SeedControl()
- {
- AutoSizeAxes = Axes.Y;
-
- InternalChildren = new[]
- {
- new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.Absolute, 2),
- new Dimension(GridSizeMode.Relative, 0.25f)
- },
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.AutoSize)
- },
- Content = new[]
- {
- new Drawable[]
- {
- seedNumberBox = new OsuNumberBox
- {
- RelativeSizeAxes = Axes.X,
- CommitOnFocusLost = true
- }
- }
- }
- }
- };
-
- seedNumberBox.Current.BindValueChanged(e =>
- {
- int? value = null;
-
- if (int.TryParse(e.NewValue, out var intVal))
- value = intVal;
-
- current.Value = value;
- });
- }
-
- protected override void Update()
- {
- if (current.Value == null)
- seedNumberBox.Text = current.Current.Value.ToString();
- }
- }
- }
-}
diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs
index cae5da3d16..725cfa9c26 100644
--- a/osu.Game/Rulesets/UI/ModIcon.cs
+++ b/osu.Game/Rulesets/UI/ModIcon.cs
@@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.UI
{
@@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.UI
private const float size = 80;
- public virtual string TooltipText => showTooltip ? mod.IconTooltip : null;
+ public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null;
private Mod mod;
private readonly bool showTooltip;
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 9d3b952ada..d5bea0affc 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
+using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables;
@@ -72,6 +73,9 @@ namespace osu.Game.Scoring
}
}
+ protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
+ => Task.CompletedTask;
+
protected override void ExportModelTo(ScoreInfo model, Stream outputStream)
{
var file = model.Files.SingleOrDefault();
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs
index 3b1dae6c3d..3ac40fda0f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
@@ -58,6 +59,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs
index c4dc2a2b8f..ae1ca1b967 100644
--- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
@@ -16,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
private readonly GameType type;
- public string TooltipText => type.Name;
+ public LocalisableString TooltipText => type.Name;
public DrawableGameType(GameType type)
{
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index e1cf0cef4e..4a35202df2 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -431,7 +431,7 @@ namespace osu.Game.Screens.Select
public class InfoLabel : Container, IHasTooltip
{
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public InfoLabel(BeatmapStatistic statistic)
{
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index c48aeca99a..cb8b0fb3c8 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -42,27 +42,30 @@ namespace osu.Game.Skinning
};
}
- [Resolved]
- private ISkinSource skinSource { get; set; }
+ private ISkinSource parentSource;
- [BackgroundDependencyLoader]
- private void load()
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- UpdateSkins();
- skinSource.SourceChanged += OnSourceChanged;
+ parentSource = parent.Get();
+ parentSource.SourceChanged += OnSourceChanged;
+
+ // ensure sources are populated and ready for use before childrens' asynchronous load flow.
+ UpdateSkinSources();
+
+ return base.CreateChildDependencies(parent);
}
protected override void OnSourceChanged()
{
- UpdateSkins();
+ UpdateSkinSources();
base.OnSourceChanged();
}
- protected virtual void UpdateSkins()
+ protected virtual void UpdateSkinSources()
{
SkinSources.Clear();
- foreach (var skin in skinSource.AllSources)
+ foreach (var skin in parentSource.AllSources)
{
switch (skin)
{
@@ -93,8 +96,8 @@ namespace osu.Game.Skinning
{
base.Dispose(isDisposing);
- if (skinSource != null)
- skinSource.SourceChanged -= OnSourceChanged;
+ if (parentSource != null)
+ parentSource.SourceChanged -= OnSourceChanged;
}
}
}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 43cf6b6874..ea55fd28c2 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -144,16 +144,16 @@ namespace osu.Game.Skinning
return base.ComputeHash(item, reader);
}
- protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
+ protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
- await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
-
var instance = GetSkin(model);
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(model, instance);
+
+ return Task.CompletedTask;
}
private void populateMetadata(SkinInfo item, Skin instance)
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index c3bf740108..f73489ac61 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
namespace osu.Game.Users.Drawables
@@ -68,11 +69,11 @@ namespace osu.Game.Users.Drawables
private class ClickableArea : OsuClickableContainer
{
- private string tooltip = default_tooltip_text;
+ private LocalisableString tooltip = default_tooltip_text;
- public override string TooltipText
+ public override LocalisableString TooltipText
{
- get => Enabled.Value ? tooltip : null;
+ get => Enabled.Value ? tooltip : default;
set => tooltip = value;
}
diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs
index 1d648e46b6..aea40a01ae 100644
--- a/osu.Game/Users/Drawables/DrawableFlag.cs
+++ b/osu.Game/Users/Drawables/DrawableFlag.cs
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
namespace osu.Game.Users.Drawables
{
@@ -13,7 +14,7 @@ namespace osu.Game.Users.Drawables
{
private readonly Country country;
- public string TooltipText => country?.FullName;
+ public LocalisableString TooltipText => country?.FullName;
public DrawableFlag(Country country)
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f91620bd25..f047859dbb 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 22c4340ba2..304047ad12 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+