diff --git a/osu.Android.props b/osu.Android.props
index 2c3c8bcaad..93a9a073a4 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -61,6 +61,6 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 7c282f449b..c527a81f51 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 4dcfc1b81f..af10d5e06e 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png
new file mode 100755
index 0000000000..75f9ba5ea6
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png
new file mode 100755
index 0000000000..ebf59c18ba
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
index 02716dc1d5..29e5146ff1 100644
--- a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
@@ -37,10 +37,21 @@ namespace osu.Game.Rulesets.Osu.Tests
public void SetContents(Func creationFunction)
{
- Cell(0).Child = new LocalSkinOverrideContainer(null) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
- Cell(1).Child = new LocalSkinOverrideContainer(metricsSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
- Cell(2).Child = new LocalSkinOverrideContainer(defaultSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
- Cell(3).Child = new LocalSkinOverrideContainer(specialSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
+ Cell(0).Child = createProvider(null, creationFunction);
+ Cell(1).Child = createProvider(metricsSkin, creationFunction);
+ Cell(2).Child = createProvider(defaultSkin, creationFunction);
+ Cell(3).Child = createProvider(specialSkin, creationFunction);
+ }
+
+ private Drawable createProvider(Skin skin, Func creationFunction)
+ {
+ var mainProvider = new SkinProvidingContainer(skin);
+
+ return mainProvider
+ .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
+ {
+ Child = creationFunction()
+ });
}
private class TestLegacySkin : LegacySkin
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 1b1cfa89c0..ebb6cd3a5a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -6,29 +6,23 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Cursor;
-using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Rulesets.UI;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneGameplayCursor : OsuTestScene, IProvideCursor
+ public class TestSceneGameplayCursor : SkinnableTestScene
{
- private GameplayCursorContainer cursorContainer;
-
public override IReadOnlyList RequiredTypes => new[] { typeof(CursorTrail) };
- public CursorContainer Cursor => cursorContainer;
-
- public bool ProvidingUserCursor => true;
-
[BackgroundDependencyLoader]
private void load()
{
- Add(cursorContainer = new OsuCursorContainer { RelativeSizeAxes = Axes.Both });
+ SetContents(() => new OsuCursorContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ });
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
new file mode 100644
index 0000000000..731b0a84e9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -0,0 +1,157 @@
+// 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.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Timing;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class TestSceneSkinFallbacks : PlayerTestScene
+ {
+ private readonly TestSource testUserSkin;
+ private readonly TestSource testBeatmapSkin;
+
+ public TestSceneSkinFallbacks()
+ : base(new OsuRuleset())
+ {
+ testUserSkin = new TestSource("user");
+ testBeatmapSkin = new TestSource("beatmap");
+ }
+
+ [Test]
+ public void TestBeatmapSkinDefault()
+ {
+ AddStep("enable user provider", () => testUserSkin.Enabled = true);
+
+ AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true));
+ checkNextHitObject("beatmap");
+
+ AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false));
+ checkNextHitObject("user");
+
+ AddStep("disable user provider", () => testUserSkin.Enabled = false);
+ checkNextHitObject(null);
+ }
+
+ private void checkNextHitObject(string skin) =>
+ AddUntilStep($"check skin from {skin}", () =>
+ {
+ var firstObject = ((TestPlayer)Player).DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault();
+
+ if (firstObject == null)
+ return false;
+
+ var skinnable = firstObject.ApproachCircle.Child as SkinnableDrawable;
+
+ if (skin == null && skinnable?.Drawable is Sprite)
+ // check for default skin provider
+ return true;
+
+ var text = skinnable?.Drawable as SpriteText;
+
+ return text?.Text == skin;
+ });
+
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin);
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) => new CustomSkinWorkingBeatmap(beatmap, Clock, audio, testBeatmapSkin);
+
+ public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ private readonly ISkinSource skin;
+
+ public CustomSkinWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin)
+ : base(beatmap, frameBasedClock, audio)
+ {
+ this.skin = skin;
+ }
+
+ protected override ISkin GetSkin() => skin;
+ }
+
+ public class SkinProvidingPlayer : TestPlayer
+ {
+ private readonly TestSource userSkin;
+
+ public SkinProvidingPlayer(TestSource userSkin)
+ {
+ this.userSkin = userSkin;
+ }
+
+ private DependencyContainer dependencies;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ dependencies.CacheAs(userSkin);
+
+ return dependencies;
+ }
+ }
+
+ public class TestSource : ISkinSource
+ {
+ private readonly string identifier;
+
+ public TestSource(string identifier)
+ {
+ this.identifier = identifier;
+ }
+
+ public Drawable GetDrawableComponent(string componentName)
+ {
+ if (!enabled) return null;
+
+ return new SpriteText
+ {
+ Text = identifier,
+ Font = OsuFont.Default.With(size: 30),
+ };
+ }
+
+ public Texture GetTexture(string componentName) => null;
+
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
+
+ public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
+
+ public event Action SourceChanged;
+
+ private bool enabled = true;
+
+ public bool Enabled
+ {
+ get => enabled;
+ set
+ {
+ if (value == enabled)
+ return;
+
+ enabled = value;
+ SourceChanged?.Invoke();
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 197309c7c4..c331c811d2 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 3bbfc25d22..49676933e1 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -23,7 +23,9 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Difficulty;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Scoring;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu
{
@@ -163,6 +165,8 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkin(source);
+
public override int? LegacyID => 0;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
new file mode 100644
index 0000000000..470ba3acae
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
@@ -0,0 +1,42 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class LegacyCursor : CompositeDrawable
+ {
+ public LegacyCursor()
+ {
+ Size = new Vector2(50);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new NonPlayfieldSprite
+ {
+ Texture = skin.GetTexture("cursormiddle"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new NonPlayfieldSprite
+ {
+ Texture = skin.GetTexture("cursor"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
new file mode 100644
index 0000000000..a7906ddd24
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -0,0 +1,81 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class LegacyMainCirclePiece : CompositeDrawable
+ {
+ public LegacyMainCirclePiece()
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+ }
+
+ private readonly IBindable state = new Bindable();
+
+ private readonly Bindable accentColour = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ {
+ Sprite hitCircleSprite;
+
+ InternalChildren = new Drawable[]
+ {
+ hitCircleSprite = new Sprite
+ {
+ Texture = skin.GetTexture("hitcircle"),
+ Colour = drawableObject.AccentColour.Value,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
+ {
+ Font = OsuFont.Numeric.With(size: 40),
+ UseFullGlyphHeight = false,
+ }, confineMode: ConfineMode.NoScaling)
+ {
+ Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
+ },
+ new Sprite
+ {
+ Texture = skin.GetTexture("hitcircleoverlay"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+
+ state.BindTo(drawableObject.State);
+ state.BindValueChanged(updateState, true);
+
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+ }
+
+ private void updateState(ValueChangedEvent state)
+ {
+ const double legacy_fade_duration = 240;
+
+ switch (state.NewValue)
+ {
+ case ArmedState.Hit:
+ this.FadeOut(legacy_fade_duration, Easing.Out);
+ this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
new file mode 100644
index 0000000000..ec838c596d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
@@ -0,0 +1,44 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class LegacySliderBall : CompositeDrawable
+ {
+ private readonly Drawable animationContent;
+
+ public LegacySliderBall(Drawable animationContent)
+ {
+ this.animationContent = animationContent;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, DrawableHitObject drawableObject)
+ {
+ animationContent.Colour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White;
+
+ InternalChildren = new[]
+ {
+ new Sprite
+ {
+ Texture = skin.GetTexture("sliderb-nd"),
+ Colour = new Color4(5, 5, 5, 255),
+ },
+ animationContent,
+ new Sprite
+ {
+ Texture = skin.GetTexture("sliderb-spec"),
+ Blending = BlendingParameters.Additive,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/NonPlayfieldSprite.cs b/osu.Game.Rulesets.Osu/Skinning/NonPlayfieldSprite.cs
new file mode 100644
index 0000000000..55257106e2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/NonPlayfieldSprite.cs
@@ -0,0 +1,28 @@
+// 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.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// A sprite which is displayed within the playfield, but historically was not considered part of the playfield.
+ /// Performs scale adjustment to undo the scale applied by (osu! ruleset specifically).
+ ///
+ public class NonPlayfieldSprite : Sprite
+ {
+ public override Texture Texture
+ {
+ get => base.Texture;
+ set
+ {
+ if (value != null)
+ // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
+ value.ScaleAdjust *= 1.6f;
+ base.Texture = value;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs
new file mode 100644
index 0000000000..904064e2f0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs
@@ -0,0 +1,122 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class OsuLegacySkin : ISkin
+ {
+ private readonly ISkin source;
+
+ private Lazy configuration;
+
+ private Lazy hasHitCircle;
+
+ ///
+ /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
+ /// Their hittable area is 128px, but the actual circle portion is 118px.
+ /// We must account for some gameplay elements such as slider bodies, where this padding is not present.
+ ///
+ private const float legacy_circle_radius = 64 - 5;
+
+ public OsuLegacySkin(ISkinSource source)
+ {
+ this.source = source;
+
+ source.SourceChanged += sourceChanged;
+ sourceChanged();
+ }
+
+ private void sourceChanged()
+ {
+ // these need to be lazy in order to ensure they aren't called before the dependencies have been loaded into our source.
+ configuration = new Lazy(() =>
+ {
+ var config = new SkinConfiguration();
+ if (hasHitCircle.Value)
+ config.SliderPathRadius = legacy_circle_radius;
+
+ // defaults should only be applied for non-beatmap skins (which are parsed via this constructor).
+ config.CustomColours["SliderBall"] =
+ source.GetValue(s => s.CustomColours.TryGetValue("SliderBall", out var val) ? val : (Color4?)null)
+ ?? new Color4(2, 170, 255, 255);
+
+ return config;
+ });
+
+ hasHitCircle = new Lazy(() => source.GetTexture("hitcircle") != null);
+ }
+
+ public Drawable GetDrawableComponent(string componentName)
+ {
+ switch (componentName)
+ {
+ case "Play/osu/sliderfollowcircle":
+ return this.GetAnimation(componentName, true, true);
+
+ case "Play/osu/sliderball":
+ var sliderBallContent = this.GetAnimation("sliderb", true, true, "");
+
+ if (sliderBallContent != null)
+ {
+ var size = sliderBallContent.Size;
+
+ sliderBallContent.RelativeSizeAxes = Axes.Both;
+ sliderBallContent.Size = Vector2.One;
+
+ return new LegacySliderBall(sliderBallContent)
+ {
+ Size = size
+ };
+ }
+
+ return null;
+
+ case "Play/osu/hitcircle":
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece();
+
+ return null;
+
+ case "Play/osu/cursor":
+ if (source.GetTexture("cursor") != null)
+ return new LegacyCursor();
+
+ return null;
+
+ case "Play/osu/number-text":
+
+ string font = GetValue(config => config.HitCircleFont);
+ var overlap = GetValue(config => config.HitCircleOverlap);
+
+ return !hasFont(font)
+ ? null
+ : new LegacySpriteText(source, font)
+ {
+ // Spacing value was reverse-engineered from the ratio of the rendered sprite size in the visual inspector vs the actual texture size
+ Scale = new Vector2(0.96f),
+ Spacing = new Vector2(-overlap * 0.89f, 0)
+ };
+ }
+
+ return null;
+ }
+
+ public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+
+ public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+
+ public TValue GetValue(Func query) where TConfiguration : SkinConfiguration
+ => configuration.Value is TConfiguration conf ? query.Invoke(conf) : source.GetValue(query);
+
+ private bool hasFont(string fontName) => source.GetTexture($"{fontName}-0") != null;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 9037faf606..ea7eee8bb8 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Osu.UI
},
// Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
// Todo: Remove when hitobjects are properly pooled
- new LocalSkinOverrideContainer(null)
+ new SkinProvidingContainer(null)
{
- RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer,
},
approachCircles = new ApproachCircleProxyContainer
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
new file mode 100644
index 0000000000..f27e329e8e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
@@ -0,0 +1,74 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Judgements;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneSwellJudgements : PlayerTestScene
+ {
+ protected new TestPlayer Player => (TestPlayer)base.Player;
+
+ public TestSceneSwellJudgements()
+ : base(new TaikoRuleset())
+ {
+ }
+
+ [Test]
+ public void TestZeroTickTimeOffsets()
+ {
+ AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted);
+ AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.Judgement is TaikoSwellTickJudgement).All(r => r.TimeOffset == 0));
+ }
+
+ protected override bool Autoplay => true;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Swell
+ {
+ StartTime = 1000,
+ Duration = 1000,
+ }
+ }
+ };
+
+ return beatmap;
+ }
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer();
+
+ protected class TestPlayer : Player
+ {
+ public readonly List Results = new List();
+
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public TestPlayer()
+ : base(false, false)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ScoreProcessor.NewJudgement += r => Results.Add(r);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index a5db1625d9..d2a0a8fa6f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
index 8b27d78101..ce875ebba8 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.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.Graphics;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -14,7 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
}
- public void TriggerResult(HitResult type) => ApplyResult(r => r.Type = type);
+ protected override void UpdateInitialTransforms() => this.FadeOut();
+
+ public void TriggerResult(HitResult type)
+ {
+ HitObject.StartTime = Time.Current;
+ ApplyResult(r => r.Type = type);
+ }
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs
new file mode 100644
index 0000000000..f20440249b
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs
@@ -0,0 +1,119 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Catch.Objects;
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Framework.MathUtils;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneBarHitErrorMeter : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(HitErrorMeter),
+ };
+
+ private HitErrorMeter meter;
+ private HitErrorMeter meter2;
+ private HitWindows hitWindows;
+
+ public TestSceneBarHitErrorMeter()
+ {
+ recreateDisplay(new OsuHitWindows(), 5);
+
+ AddRepeatStep("New random judgement", () => newJudgement(), 40);
+
+ AddRepeatStep("New max negative", () => newJudgement(-hitWindows.HalfWindowFor(HitResult.Meh)), 20);
+ AddRepeatStep("New max positive", () => newJudgement(hitWindows.HalfWindowFor(HitResult.Meh)), 20);
+ AddStep("New fixed judgement (50ms)", () => newJudgement(50));
+ }
+
+ [Test]
+ public void TestOsu()
+ {
+ AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new OsuHitWindows(), 10));
+ }
+
+ [Test]
+ public void TestTaiko()
+ {
+ AddStep("OD 1", () => recreateDisplay(new TaikoHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new TaikoHitWindows(), 10));
+ }
+
+ [Test]
+ public void TestMania()
+ {
+ AddStep("OD 1", () => recreateDisplay(new ManiaHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new ManiaHitWindows(), 10));
+ }
+
+ [Test]
+ public void TestCatch()
+ {
+ AddStep("OD 1", () => recreateDisplay(new CatchHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new CatchHitWindows(), 10));
+ }
+
+ private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
+ {
+ this.hitWindows = hitWindows;
+
+ hitWindows?.SetDifficulty(overallDifficulty);
+
+ Clear();
+
+ Add(new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new SpriteText { Text = $@"Great: {hitWindows?.Great}" },
+ new SpriteText { Text = $@"Good: {hitWindows?.Good}" },
+ new SpriteText { Text = $@"Meh: {hitWindows?.Meh}" },
+ }
+ });
+
+ Add(meter = new BarHitErrorMeter(hitWindows, true)
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ });
+
+ Add(meter2 = new BarHitErrorMeter(hitWindows, false)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ });
+ }
+
+ private void newJudgement(double offset = 0)
+ {
+ var judgement = new JudgementResult(new Judgement())
+ {
+ TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
+ Type = HitResult.Perfect,
+ };
+
+ meter.OnNewJudgement(judgement);
+ meter2.OnNewJudgement(judgement);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 6c003e62ec..96dc864577 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("setup layout larger source", () =>
{
- Child = new LocalSkinOverrideContainer(new SizedSource(50))
+ Child = new SkinProvidingContainer(new SizedSource(50))
{
RelativeSizeAxes = Axes.Both,
Child = fill = new FillFlowContainer
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("setup layout larger source", () =>
{
- Child = new LocalSkinOverrideContainer(new SizedSource(30))
+ Child = new SkinProvidingContainer(new SizedSource(30))
{
RelativeSizeAxes = Axes.Both,
Child = fill = new FillFlowContainer
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = new SkinSourceContainer
{
RelativeSizeAxes = Axes.Both,
- Child = new LocalSkinOverrideContainer(secondarySource)
+ Child = new SkinProvidingContainer(secondarySource)
{
RelativeSizeAxes = Axes.Both,
Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true)
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = new SkinSourceContainer
{
RelativeSizeAxes = Axes.Both,
- Child = target = new LocalSkinOverrideContainer(secondarySource)
+ Child = target = new SkinProvidingContainer(secondarySource)
{
RelativeSizeAxes = Axes.Both,
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 8f19df65a9..ee9e088dcc 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -176,6 +176,8 @@ namespace osu.Game.Tests.Visual.Online
HasVideo = true,
HasStoryboard = true,
Covers = new BeatmapSetOnlineCovers(),
+ Language = new BeatmapSetOnlineLanguage { Id = 3, Name = "English" },
+ Genre = new BeatmapSetOnlineGenre { Id = 4, Name = "Rock" },
},
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = new List
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 4a9d88f3a6..84f67c9319 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 2a8bd393da..bba3c92245 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -7,7 +7,7 @@
-
+
WinExe
diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
index 2d8a0b1249..5bbffc2f77 100644
--- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps
return storyboard;
}
- protected override Skin GetSkin()
+ protected override ISkin GetSkin()
{
try
{
diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs
index df3a45d1cc..06dee4d3f5 100644
--- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs
@@ -75,6 +75,28 @@ namespace osu.Game.Beatmaps
/// The availability of this beatmap set.
///
public BeatmapSetOnlineAvailability Availability { get; set; }
+
+ ///
+ /// The song genre of this beatmap set.
+ ///
+ public BeatmapSetOnlineGenre Genre { get; set; }
+
+ ///
+ /// The song language of this beatmap set.
+ ///
+ public BeatmapSetOnlineLanguage Language { get; set; }
+ }
+
+ public class BeatmapSetOnlineGenre
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class BeatmapSetOnlineLanguage
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
}
public class BeatmapSetOnlineCovers
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index aea3751bb5..44071d9cc1 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
///
/// Retrieves the which this provides.
///
- Skin Skin { get; }
+ ISkin Skin { get; }
///
/// Constructs a playable from using the applicable converters for a specific .
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 90dde4239c..d8ab411beb 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Beatmaps
background = new RecyclableLazy(GetBackground, BackgroundStillValid);
waveform = new RecyclableLazy(GetWaveform);
storyboard = new RecyclableLazy(GetStoryboard);
- skin = new RecyclableLazy(GetSkin);
+ skin = new RecyclableLazy(GetSkin);
total_count.Value++;
}
@@ -202,10 +202,10 @@ namespace osu.Game.Beatmaps
private readonly RecyclableLazy storyboard;
public bool SkinLoaded => skin.IsResultAvailable;
- public Skin Skin => skin.Value;
+ public ISkin Skin => skin.Value;
- protected virtual Skin GetSkin() => new DefaultSkin();
- private readonly RecyclableLazy skin;
+ protected virtual ISkin GetSkin() => new DefaultSkin();
+ private readonly RecyclableLazy skin;
///
/// Transfer pieces of a beatmap to a new one, where possible, to save on loading.
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index fb472f3f89..0cecbb225f 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -83,6 +83,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.KeyOverlay, false);
+ Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
Set(OsuSetting.FloatingComments, false);
@@ -136,6 +137,7 @@ namespace osu.Game.Configuration
BlurLevel,
ShowStoryboard,
KeyOverlay,
+ ScoreMeter,
FloatingComments,
ShowInterface,
ShowHealthDisplayWhenCantFail,
diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs
index 21a63fb3ed..b85ef9309d 100644
--- a/osu.Game/Configuration/ScoreMeterType.cs
+++ b/osu.Game/Configuration/ScoreMeterType.cs
@@ -1,12 +1,22 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.ComponentModel;
+
namespace osu.Game.Configuration
{
public enum ScoreMeterType
{
+ [Description("None")]
None,
- Colour,
- Error
+
+ [Description("Hit Error (left)")]
+ HitErrorLeft,
+
+ [Description("Hit Error (right)")]
+ HitErrorRight,
+
+ [Description("Hit Error (both)")]
+ HitErrorBoth,
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
index e5bfde8f8f..1ca14256e5 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
@@ -69,6 +69,12 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"availability")]
private BeatmapSetOnlineAvailability availability { get; set; }
+ [JsonProperty(@"genre")]
+ private BeatmapSetOnlineGenre genre { get; set; }
+
+ [JsonProperty(@"language")]
+ private BeatmapSetOnlineLanguage language { get; set; }
+
[JsonProperty(@"beatmaps")]
private IEnumerable beatmaps { get; set; }
@@ -95,6 +101,8 @@ namespace osu.Game.Online.API.Requests.Responses
LastUpdated = lastUpdated,
Availability = availability,
HasFavourited = hasFavourited,
+ Genre = genre,
+ Language = language
},
Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(),
};
diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs
index 44827f0a0c..16d6236051 100644
--- a/osu.Game/Overlays/BeatmapSet/Info.cs
+++ b/osu.Game/Overlays/BeatmapSet/Info.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Overlays.BeatmapSet
public Info()
{
- MetadataSection source, tags;
+ MetadataSection source, tags, genre, language;
RelativeSizeAxes = Axes.X;
Height = 220;
Masking = true;
@@ -83,11 +83,12 @@ namespace osu.Game.Overlays.BeatmapSet
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- LayoutDuration = transition_duration,
+ Direction = FillDirection.Full,
Children = new[]
{
source = new MetadataSection("Source"),
+ genre = new MetadataSection("Genre") { Width = 0.5f },
+ language = new MetadataSection("Language") { Width = 0.5f },
tags = new MetadataSection("Tags"),
},
},
@@ -119,6 +120,8 @@ namespace osu.Game.Overlays.BeatmapSet
{
source.Text = b.NewValue?.Metadata.Source ?? string.Empty;
tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty;
+ genre.Text = b.NewValue?.OnlineInfo?.Genre?.Name ?? string.Empty;
+ language.Text = b.NewValue?.OnlineInfo?.Language?.Name ?? string.Empty;
};
}
@@ -139,7 +142,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
if (string.IsNullOrEmpty(value))
{
- this.FadeOut(transition_duration);
+ Hide();
return;
}
@@ -149,12 +152,6 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
- public Color4 TextColour
- {
- get => textFlow.Colour;
- set => textFlow.Colour = value;
- }
-
public MetadataSection(string title)
{
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
index 4d77e5f93d..cb0639d85d 100644
--- a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Chat.Selection
private Color4 topicColour;
private Color4 hoverColour;
- public IEnumerable FilterTerms => new[] { channel.Name };
+ public IEnumerable FilterTerms => new[] { channel.Name, channel.Topic };
public bool MatchingFilter
{
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index 24ed0cc022..c6d96c5917 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -296,7 +296,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
this.MoveTo(pos, 200, Easing.OutQuint);
}
- protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
+ protected override void PopIn()
+ {
+ instantMove |= !IsPresent;
+ this.FadeIn(200, Easing.OutQuint);
+ }
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 9142492610..520a8852b3 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -44,6 +44,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = "Always show key overlay",
Bindable = config.GetBindable(OsuSetting.KeyOverlay)
},
+ new SettingsEnumDropdown
+ {
+ LabelText = "Score meter type",
+ Bindable = config.GetBindable(OsuSetting.ScoreMeter)
+ },
new SettingsEnumDropdown
{
LabelText = "Score display mode",
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 4a6f261905..a24476418c 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@@ -278,6 +279,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false);
}
+ ///
+ /// Schedules an to this .
+ ///
+ ///
+ /// Only provided temporarily until hitobject pooling is implemented.
+ ///
+ protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
+
private double? lifetimeStart;
public override double LifetimeStart
diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs
index fe099aaee7..e88af67c7c 100644
--- a/osu.Game/Rulesets/Objects/HitWindows.cs
+++ b/osu.Game/Rulesets/Objects/HitWindows.cs
@@ -65,6 +65,19 @@ namespace osu.Game.Rulesets.Objects
return HitResult.None;
}
+ ///
+ /// Retrieves a mapping of s to their half window timing for all allowed s.
+ ///
+ ///
+ public IEnumerable<(HitResult result, double length)> GetAllAvailableHalfWindows()
+ {
+ for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
+ {
+ if (IsHitResultAllowed(result))
+ yield return (result, HalfWindowFor(result));
+ }
+ }
+
///
/// Check whether it is possible to achieve the provided .
///
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index b089840f1e..b63292757d 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -18,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Scoring;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets
{
@@ -44,6 +45,8 @@ namespace osu.Game.Rulesets
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First();
+ public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null;
+
protected Ruleset(RulesetInfo rulesetInfo = null)
{
RulesetInfo = rulesetInfo ?? createRulesetInfo();
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index e47df6b473..3b7e457990 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Scoring
///
/// Whether all s have been processed.
///
- protected virtual bool HasCompleted => false;
+ public virtual bool HasCompleted => false;
///
/// Whether this ScoreProcessor has already triggered the failed state.
@@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Scoring
private const double combo_portion = 0.7;
private const double max_score = 1000000;
- protected sealed override bool HasCompleted => JudgedHits == MaxHits;
+ public sealed override bool HasCompleted => JudgedHits == MaxHits;
protected int MaxHits { get; private set; }
protected int JudgedHits { get; private set; }
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 021bd515b5..0ee9196fb8 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -215,10 +215,6 @@ namespace osu.Game.Rulesets.UI
continueResume();
}
- public ResumeOverlay ResumeOverlay { get; private set; }
-
- protected virtual ResumeOverlay CreateResumeOverlay() => null;
-
///
/// Creates and adds the visual representation of a to this .
///
@@ -389,6 +385,13 @@ namespace osu.Game.Rulesets.UI
///
public abstract GameplayCursorContainer Cursor { get; }
+ ///
+ /// An optional overlay used when resuming gameplay from a paused state.
+ ///
+ public ResumeOverlay ResumeOverlay { get; protected set; }
+
+ protected virtual ResumeOverlay CreateResumeOverlay() => null;
+
///
/// Sets a replay to be used, overriding local input.
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 1df8c8218f..bd1f496dfa 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@@ -50,8 +51,13 @@ namespace osu.Game.Rulesets.UI.Scrolling
public override bool Remove(DrawableHitObject hitObject)
{
var result = base.Remove(hitObject);
+
if (result)
+ {
initialStateCache.Invalidate();
+ hitObjectInitialStateCache.Remove(hitObject);
+ }
+
return result;
}
@@ -86,13 +92,34 @@ namespace osu.Game.Rulesets.UI.Scrolling
scrollingInfo.Algorithm.Reset();
foreach (var obj in Objects)
+ {
+ computeLifetimeStartRecursive(obj);
computeInitialStateRecursive(obj);
+ }
+
initialStateCache.Validate();
}
}
- private void computeInitialStateRecursive(DrawableHitObject hitObject)
+ private void computeLifetimeStartRecursive(DrawableHitObject hitObject)
{
+ hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value);
+
+ foreach (var obj in hitObject.NestedHitObjects)
+ computeLifetimeStartRecursive(obj);
+ }
+
+ private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
+
+ // Cant use AddOnce() since the delegate is re-constructed every invocation
+ private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
+ {
+ if (!hitObjectInitialStateCache.TryGetValue(hitObject, out var cached))
+ cached = hitObjectInitialStateCache[hitObject] = new Cached();
+
+ if (cached.IsValid)
+ return;
+
double endTime = hitObject.HitObject.StartTime;
if (hitObject.HitObject is IHasEndTime e)
@@ -113,7 +140,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
}
- hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value);
hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
foreach (var obj in hitObject.NestedHitObjects)
@@ -123,7 +149,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime);
}
- }
+
+ cached.Validate();
+ });
protected override void UpdateAfterChildrenLife()
{
diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index ea3b68e3bd..2aeb1ef04b 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -4,6 +4,9 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Colour;
+using osu.Game.Graphics;
+using osuTK.Graphics;
namespace osu.Game.Screens.Edit
{
@@ -35,5 +38,41 @@ namespace osu.Game.Screens.Edit
protected override int DefaultMinValue => VALID_DIVISORS.First();
protected override int DefaultMaxValue => VALID_DIVISORS.Last();
protected override int DefaultPrecision => 1;
+
+ ///
+ /// Retrieves the appropriate colour for a beat divisor.
+ ///
+ /// The beat divisor.
+ /// The set of colours.
+ /// The applicable colour from for .
+ public static ColourInfo GetColourFor(int beatDivisor, OsuColour colours)
+ {
+ switch (beatDivisor)
+ {
+ case 2:
+ return colours.BlueLight;
+
+ case 4:
+ return colours.Blue;
+
+ case 8:
+ return colours.BlueDarker;
+
+ case 16:
+ return colours.PurpleDark;
+
+ case 3:
+ return colours.YellowLight;
+
+ case 6:
+ return colours.Yellow;
+
+ case 12:
+ return colours.YellowDarker;
+
+ default:
+ return Color4.White;
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index 0d16d8474b..4d89e43ee5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -188,6 +188,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
private Marker marker;
+ [Resolved]
+ private OsuColour colours { get; set; }
+
private readonly BindableBeatDivisor beatDivisor;
private readonly int[] availableDivisors;
@@ -204,11 +207,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
foreach (var t in availableDivisors)
{
- AddInternal(new Tick(t)
+ AddInternal(new Tick
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
+ Colour = BindableBeatDivisor.GetColourFor(t, colours),
X = getMappedPosition(t)
});
}
@@ -284,11 +288,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private class Tick : CompositeDrawable
{
- private readonly int divisor;
-
- public Tick(int divisor)
+ public Tick()
{
- this.divisor = divisor;
Size = new Vector2(2.5f, 10);
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
@@ -296,42 +297,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
CornerRadius = 0.5f;
Masking = true;
}
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Colour = getColourForDivisor(divisor, colours);
- }
-
- private ColourInfo getColourForDivisor(int divisor, OsuColour colours)
- {
- switch (divisor)
- {
- case 2:
- return colours.BlueLight;
-
- case 4:
- return colours.Blue;
-
- case 8:
- return colours.BlueDarker;
-
- case 16:
- return colours.PurpleDark;
-
- case 3:
- return colours.YellowLight;
-
- case 6:
- return colours.Yellow;
-
- case 12:
- return colours.YellowDarker;
-
- default:
- return Color4.White;
- }
- }
}
private class Marker : CompositeDrawable
diff --git a/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs b/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs
index 45fca493a2..299059407c 100644
--- a/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit
public Storyboard Storyboard => workingBeatmap.Storyboard;
- public Skin Skin => workingBeatmap.Skin;
+ public ISkin Skin => workingBeatmap.Skin;
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods) => playableBeatmap;
}
diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs
index 4fa1a81123..c069f82134 100644
--- a/osu.Game/Screens/Menu/IntroCircles.cs
+++ b/osu.Game/Screens/Menu/IntroCircles.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Menu
Scheduler.AddDelayed(delegate
{
- // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu.
+ // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu.
if (menuMusic.Value)
{
track.Restart();
diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs
index 9d0a5cd05b..6984959e9c 100644
--- a/osu.Game/Screens/Menu/LogoVisualisation.cs
+++ b/osu.Game/Screens/Menu/LogoVisualisation.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.Menu
private const float visualiser_rounds = 5;
///
- /// How much should each bar go down each milisecond (based on a full bar).
+ /// How much should each bar go down each millisecond (based on a full bar).
///
private const float decay_per_milisecond = 0.0024f;
@@ -161,7 +161,7 @@ namespace osu.Game.Screens.Menu
private IShader shader;
private Texture texture;
- //Asuming the logo is a circle, we don't need a second dimension.
+ //Assuming the logo is a circle, we don't need a second dimension.
private float size;
private Color4 colour;
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 0c5bf12bdb..534400e720 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -229,7 +229,7 @@ namespace osu.Game.Screens.Menu
}
///
- /// Schedule a new extenral animation. Handled queueing and finishing previous animations in a sane way.
+ /// Schedule a new external animation. Handled queueing and finishing previous animations in a sane way.
///
/// The animation to be performed
/// If true, the new animation is delayed until all previous transforms finish. If false, existing transformed are cleared.
diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
new file mode 100644
index 0000000000..cdfa0e993b
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
@@ -0,0 +1,100 @@
+// 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.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class HitErrorDisplay : Container
+ {
+ private const int fade_duration = 200;
+ private const int margin = 10;
+
+ private readonly Bindable type = new Bindable();
+
+ private readonly HitWindows hitWindows;
+
+ private readonly ScoreProcessor processor;
+
+ public HitErrorDisplay(ScoreProcessor processor, HitWindows hitWindows)
+ {
+ this.processor = processor;
+ this.hitWindows = hitWindows;
+
+ RelativeSizeAxes = Axes.Both;
+
+ processor.NewJudgement += onNewJudgement;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.ScoreMeter, type);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ type.BindValueChanged(typeChanged, true);
+ }
+
+ private void onNewJudgement(JudgementResult result)
+ {
+ foreach (var c in Children)
+ c.OnNewJudgement(result);
+ }
+
+ private void typeChanged(ValueChangedEvent type)
+ {
+ Children.ForEach(c => c.FadeOut(fade_duration, Easing.OutQuint));
+
+ if (hitWindows == null)
+ return;
+
+ switch (type.NewValue)
+ {
+ case ScoreMeterType.HitErrorBoth:
+ createBar(false);
+ createBar(true);
+ break;
+
+ case ScoreMeterType.HitErrorLeft:
+ createBar(false);
+ break;
+
+ case ScoreMeterType.HitErrorRight:
+ createBar(true);
+ break;
+ }
+ }
+
+ private void createBar(bool rightAligned)
+ {
+ var display = new BarHitErrorMeter(hitWindows, rightAligned)
+ {
+ Margin = new MarginPadding(margin),
+ Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
+ Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
+ Alpha = 0,
+ };
+
+ Add(display);
+ display.FadeInFromZero(fade_duration, Easing.OutQuint);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ processor.NewJudgement -= onNewJudgement;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
new file mode 100644
index 0000000000..594dd64e52
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -0,0 +1,284 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play.HUD.HitErrorMeters
+{
+ public class BarHitErrorMeter : HitErrorMeter
+ {
+ private readonly Anchor alignment;
+
+ private const int arrow_move_duration = 400;
+
+ private const int judgement_line_width = 6;
+
+ private const int bar_height = 200;
+
+ private const int bar_width = 2;
+
+ private const int spacing = 2;
+
+ private const float chevron_size = 8;
+
+ private SpriteIcon arrow;
+
+ private Container colourBarsEarly;
+ private Container colourBarsLate;
+
+ private Container judgementsContainer;
+
+ private double maxHitWindow;
+
+ public BarHitErrorMeter(HitWindows hitWindows, bool rightAligned = false)
+ : base(hitWindows)
+ {
+ alignment = rightAligned ? Anchor.x0 : Anchor.x2;
+
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChild = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ Height = bar_height,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(spacing, 0),
+ Margin = new MarginPadding(2),
+ Children = new Drawable[]
+ {
+ judgementsContainer = new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = Anchor.y1 | alignment,
+ Width = judgement_line_width,
+ RelativeSizeAxes = Axes.Y,
+ },
+ colourBars = new Container
+ {
+ Width = bar_width,
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.y1 | alignment,
+ Origin = Anchor.y1 | alignment,
+ Children = new Drawable[]
+ {
+ colourBarsEarly = new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = alignment,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.5f,
+ Scale = new Vector2(1, -1),
+ },
+ colourBarsLate = new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = alignment,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.5f,
+ },
+ new SpriteIcon
+ {
+ Y = -10,
+ Size = new Vector2(10),
+ Icon = FontAwesome.Solid.ShippingFast,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ },
+ new SpriteIcon
+ {
+ Y = 10,
+ Size = new Vector2(10),
+ Icon = FontAwesome.Solid.Bicycle,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ }
+ }
+ },
+ new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = Anchor.y1 | alignment,
+ Width = chevron_size,
+ RelativeSizeAxes = Axes.Y,
+ Child = arrow = new SpriteIcon
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = 0.5f,
+ Icon = alignment == Anchor.x2 ? FontAwesome.Solid.ChevronRight : FontAwesome.Solid.ChevronLeft,
+ Size = new Vector2(chevron_size),
+ }
+ },
+ }
+ };
+
+ createColourBars(colours);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ colourBars.Height = 0;
+ colourBars.ResizeHeightTo(1, 800, Easing.OutQuint);
+
+ arrow.Alpha = 0;
+ arrow.Delay(200).FadeInFromZero(600);
+ }
+
+ private void createColourBars(OsuColour colours)
+ {
+ var windows = HitWindows.GetAllAvailableHalfWindows().ToArray();
+
+ maxHitWindow = windows.First().length;
+
+ for (var i = 0; i < windows.Length; i++)
+ {
+ var (result, length) = windows[i];
+
+ colourBarsEarly.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0));
+ colourBarsLate.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0));
+ }
+
+ // a little nub to mark the centre point.
+ var centre = createColourBar(windows.Last().result, 0.01f);
+ centre.Anchor = centre.Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2);
+ centre.Width = 2.5f;
+ colourBars.Add(centre);
+
+ Color4 getColour(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Meh:
+ return colours.Yellow;
+
+ case HitResult.Ok:
+ return colours.Green;
+
+ case HitResult.Good:
+ return colours.GreenLight;
+
+ case HitResult.Great:
+ return colours.Blue;
+
+ default:
+ return colours.BlueLight;
+ }
+ }
+
+ Drawable createColourBar(HitResult result, float height, bool first = false)
+ {
+ var colour = getColour(result);
+
+ if (first)
+ {
+ // the first bar needs gradient rendering.
+ const float gradient_start = 0.8f;
+
+ return new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = getColour(result),
+ Height = height * gradient_start
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(colour, colour.Opacity(0)),
+ Y = gradient_start,
+ Height = height * (1 - gradient_start)
+ },
+ }
+ };
+ }
+
+ return new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colour,
+ Height = height
+ };
+ }
+ }
+
+ private double floatingAverage;
+ private Container colourBars;
+
+ public override void OnNewJudgement(JudgementResult judgement)
+ {
+ if (!judgement.IsHit)
+ return;
+
+ judgementsContainer.Add(new JudgementLine
+ {
+ Y = getRelativeJudgementPosition(judgement.TimeOffset),
+ Anchor = alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2,
+ Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2),
+ });
+
+ arrow.MoveToY(
+ getRelativeJudgementPosition(floatingAverage = floatingAverage * 0.9 + judgement.TimeOffset * 0.1)
+ , arrow_move_duration, Easing.Out);
+ }
+
+ private float getRelativeJudgementPosition(double value) => (float)((value / maxHitWindow) + 1) / 2;
+
+ private class JudgementLine : CompositeDrawable
+ {
+ private const int judgement_fade_duration = 10000;
+
+ public JudgementLine()
+ {
+ RelativeSizeAxes = Axes.X;
+ RelativePositionAxes = Axes.Y;
+ Height = 3;
+
+ InternalChild = new CircularContainer
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.Both,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Width = 0;
+
+ this.ResizeWidthTo(1, 200, Easing.OutElasticHalf);
+ this.FadeTo(0.8f, 150).Then().FadeOut(judgement_fade_duration, Easing.OutQuint).Expire();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
new file mode 100644
index 0000000000..da1d9fff0d
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
@@ -0,0 +1,21 @@
+// 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.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Play.HUD.HitErrorMeters
+{
+ public abstract class HitErrorMeter : CompositeDrawable
+ {
+ protected readonly HitWindows HitWindows;
+
+ protected HitErrorMeter(HitWindows hitWindows)
+ {
+ HitWindows = hitWindows;
+ }
+
+ public abstract void OnNewJudgement(JudgementResult judgement);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 43b9491750..8e642ea552 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
public readonly HealthDisplay HealthDisplay;
public readonly SongProgress Progress;
public readonly ModDisplay ModDisplay;
+ public readonly HitErrorDisplay HitErrorDisplay;
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
@@ -84,6 +86,7 @@ namespace osu.Game.Screens.Play
HealthDisplay = CreateHealthDisplay(),
Progress = CreateProgress(),
ModDisplay = CreateModsContainer(),
+ HitErrorDisplay = CreateHitErrorDisplayOverlay(),
}
},
PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
@@ -256,6 +259,8 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20, Right = 10 },
};
+ protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset.Objects.FirstOrDefault()?.HitWindows);
+
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
protected virtual void BindProcessor(ScoreProcessor processor)
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index e7398be176..3f1603eabe 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -60,7 +60,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private ScoreManager scoreManager { get; set; }
- private RulesetInfo ruleset;
+ private RulesetInfo rulesetInfo;
+
+ private Ruleset ruleset;
private IAPIProvider api;
@@ -121,21 +123,53 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = new GameplayClockContainer(working, Mods.Value, DrawableRuleset.GameplayStartTime);
- GameplayClockContainer.Children = new[]
+ addUnderlayComponents(GameplayClockContainer);
+ addGameplayComponents(GameplayClockContainer, working);
+ addOverlayComponents(GameplayClockContainer, working);
+
+ DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true);
+
+ // bind clock into components that require it
+ DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
+
+ // Bind ScoreProcessor to ourselves
+ ScoreProcessor.AllJudged += onCompletion;
+ ScoreProcessor.Failed += onFail;
+
+ foreach (var mod in Mods.Value.OfType())
+ mod.ApplyToScoreProcessor(ScoreProcessor);
+ }
+
+ private void addUnderlayComponents(Container target)
+ {
+ target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both });
+ }
+
+ private void addGameplayComponents(Container target, WorkingBeatmap working)
+ {
+ var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin);
+
+ // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
+ // full access to all skin sources.
+ var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider));
+
+ // load the skinning hierarchy first.
+ // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
+ target.Add(new ScalingContainer(ScalingMode.Gameplay)
+ .WithChild(beatmapSkinProvider
+ .WithChild(target = rulesetSkinProvider)));
+
+ target.AddRange(new Drawable[]
+ {
+ DrawableRuleset,
+ new ComboEffects(ScoreProcessor)
+ });
+ }
+
+ private void addOverlayComponents(Container target, WorkingBeatmap working)
+ {
+ target.AddRange(new[]
{
- DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both },
- new ScalingContainer(ScalingMode.Gameplay)
- {
- Child = new LocalSkinOverrideContainer(working.Skin)
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- DrawableRuleset,
- new ComboEffects(ScoreProcessor)
- }
- }
- },
breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Anchor = Anchor.Centre,
@@ -144,6 +178,7 @@ namespace osu.Game.Screens.Play
},
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
+ DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, Mods.Value)
{
HoldToQuit =
@@ -194,19 +229,7 @@ namespace osu.Game.Screens.Play
},
},
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }
- };
-
- DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true);
-
- // bind clock into components that require it
- DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
-
- // Bind ScoreProcessor to ourselves
- ScoreProcessor.AllJudged += onCompletion;
- ScoreProcessor.Failed += onFail;
-
- foreach (var mod in Mods.Value.OfType())
- mod.ApplyToScoreProcessor(ScoreProcessor);
+ });
}
private WorkingBeatmap loadBeatmap()
@@ -222,20 +245,20 @@ namespace osu.Game.Screens.Play
if (beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded");
- ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
- var rulesetInstance = ruleset.CreateInstance();
+ rulesetInfo = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
+ ruleset = rulesetInfo.CreateInstance();
try
{
- DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(working, Mods.Value);
+ DrawableRuleset = ruleset.CreateDrawableRulesetWith(working, Mods.Value);
}
catch (BeatmapInvalidForRulesetException)
{
// we may fail to create a DrawableRuleset if the beatmap cannot be loaded with the user's preferred ruleset
// let's try again forcing the beatmap's ruleset.
- ruleset = beatmap.BeatmapInfo.Ruleset;
- rulesetInstance = ruleset.CreateInstance();
- DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(Beatmap.Value, Mods.Value);
+ rulesetInfo = beatmap.BeatmapInfo.Ruleset;
+ ruleset = rulesetInfo.CreateInstance();
+ DrawableRuleset = ruleset.CreateDrawableRulesetWith(Beatmap.Value, Mods.Value);
}
if (!DrawableRuleset.Objects.Any())
@@ -313,7 +336,7 @@ namespace osu.Game.Screens.Play
var score = DrawableRuleset.ReplayScore?.ScoreInfo ?? new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
- Ruleset = ruleset,
+ Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
User = api.LocalUser.Value,
};
diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
new file mode 100644
index 0000000000..345df35b12
--- /dev/null
+++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
@@ -0,0 +1,39 @@
+// 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.Bindables;
+using osu.Game.Audio;
+using osu.Game.Configuration;
+
+namespace osu.Game.Skinning
+{
+ ///
+ /// A container which overrides existing skin options with beatmap-local values.
+ ///
+ public class BeatmapSkinProvidingContainer : SkinProvidingContainer
+ {
+ private readonly Bindable beatmapSkins = new Bindable();
+ private readonly Bindable beatmapHitsounds = new Bindable();
+
+ protected override bool AllowConfigurationLookup => beatmapSkins.Value;
+ protected override bool AllowDrawableLookup(string componentName) => beatmapSkins.Value;
+ protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value;
+ protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value;
+
+ public BeatmapSkinProvidingContainer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
+ config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds);
+
+ beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
+ beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
+ }
+ }
+}
diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs
index 6072bb64ed..ec957566cb 100644
--- a/osu.Game/Skinning/DefaultSkin.cs
+++ b/osu.Game/Skinning/DefaultSkin.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Skinning
public DefaultSkin()
: base(SkinInfo.Default)
{
- Configuration = new SkinConfiguration();
+ Configuration = new DefaultSkinConfiguration();
}
public override Drawable GetDrawableComponent(string componentName) => null;
diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs
new file mode 100644
index 0000000000..722b35f102
--- /dev/null
+++ b/osu.Game/Skinning/DefaultSkinConfiguration.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK.Graphics;
+
+namespace osu.Game.Skinning
+{
+ ///
+ /// A skin configuration pre-populated with sane defaults.
+ ///
+ public class DefaultSkinConfiguration : SkinConfiguration
+ {
+ public DefaultSkinConfiguration()
+ {
+ HitCircleFont = "default";
+
+ ComboColours.AddRange(new[]
+ {
+ new Color4(17, 136, 170, 255),
+ new Color4(102, 136, 0, 255),
+ new Color4(204, 102, 0, 255),
+ new Color4(121, 9, 13, 255)
+ });
+
+ CursorExpand = true;
+ }
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 0cc5e9c9b6..56b8aa19c2 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -1,30 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
-using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Animations;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
-using osu.Framework.Text;
using osu.Game.Audio;
-using osu.Game.Database;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Rulesets.UI;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Skinning
@@ -35,13 +19,6 @@ namespace osu.Game.Skinning
protected IResourceStore Samples;
- ///
- /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
- /// Their hittable area is 128px, but the actual circle portion is 118px.
- /// We must account for some gameplay elements such as slider bodies, where this padding is not present.
- ///
- private const float legacy_circle_radius = 64 - 5;
-
public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager)
: this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini")
{
@@ -49,8 +26,6 @@ namespace osu.Game.Skinning
if (!Configuration.CustomColours.ContainsKey("SliderBall")) Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
}
- private readonly bool hasHitCircle;
-
protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename)
: base(skin)
{
@@ -59,18 +34,10 @@ namespace osu.Game.Skinning
using (StreamReader reader = new StreamReader(stream))
Configuration = new LegacySkinDecoder().Decode(reader);
else
- Configuration = new SkinConfiguration();
+ Configuration = new DefaultSkinConfiguration();
Samples = audioManager.GetSampleStore(storage);
Textures = new TextureStore(new TextureLoaderStore(storage));
-
- using (var testStream = storage.GetStream("hitcircle@2x") ?? storage.GetStream("hitcircle"))
- hasHitCircle |= testStream != null;
-
- if (hasHitCircle)
- {
- Configuration.SliderPathRadius = legacy_circle_radius;
- }
}
protected override void Dispose(bool isDisposing)
@@ -80,8 +47,6 @@ namespace osu.Game.Skinning
Samples?.Dispose();
}
- private const double default_frame_time = 1000 / 60d;
-
public override Drawable GetDrawableComponent(string componentName)
{
bool animatable = false;
@@ -89,40 +54,6 @@ namespace osu.Game.Skinning
switch (componentName)
{
- case "Play/osu/cursor":
- if (GetTexture("cursor") != null)
- return new LegacyCursor();
-
- return null;
-
- case "Play/osu/sliderball":
- var sliderBallContent = getAnimation("sliderb", true, true, "");
-
- if (sliderBallContent != null)
- {
- var size = sliderBallContent.Size;
-
- sliderBallContent.RelativeSizeAxes = Axes.Both;
- sliderBallContent.Size = Vector2.One;
-
- return new LegacySliderBall(sliderBallContent)
- {
- Size = size
- };
- }
-
- return null;
-
- case "Play/osu/hitcircle":
- if (hasHitCircle)
- return new LegacyMainCirclePiece();
-
- return null;
-
- case "Play/osu/sliderfollowcircle":
- animatable = true;
- break;
-
case "Play/Miss":
componentName = "hit0";
animatable = true;
@@ -146,19 +77,9 @@ namespace osu.Game.Skinning
animatable = true;
looping = false;
break;
-
- case "Play/osu/number-text":
- return !hasFont(Configuration.HitCircleFont)
- ? null
- : new LegacySpriteText(this, Configuration.HitCircleFont)
- {
- Scale = new Vector2(0.96f),
- // Spacing value was reverse-engineered from the ratio of the rendered sprite size in the visual inspector vs the actual texture size
- Spacing = new Vector2(-Configuration.HitCircleOverlap * 0.89f, 0)
- };
}
- return getAnimation(componentName, animatable, looping);
+ return this.GetAnimation(componentName, animatable, looping);
}
public override Texture GetTexture(string componentName)
@@ -197,298 +118,10 @@ namespace osu.Game.Skinning
return null;
}
- private bool hasFont(string fontName) => GetTexture($"{fontName}-0") != null;
-
private string getFallbackName(string componentName)
{
string lastPiece = componentName.Split('/').Last();
return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece;
}
-
- private Drawable getAnimation(string componentName, bool animatable, bool looping, string animationSeparator = "-")
- {
- Texture texture;
-
- Texture getFrameTexture(int frame) => GetTexture($"{componentName}{animationSeparator}{frame}");
-
- TextureAnimation animation = null;
-
- if (animatable)
- {
- for (int i = 0;; i++)
- {
- if ((texture = getFrameTexture(i)) == null)
- break;
-
- if (animation == null)
- animation = new TextureAnimation
- {
- DefaultFrameLength = default_frame_time,
- Repeat = looping
- };
-
- animation.AddFrame(texture);
- }
- }
-
- if (animation != null)
- return animation;
-
- if ((texture = GetTexture(componentName)) != null)
- return new Sprite { Texture = texture };
-
- return null;
- }
-
- protected class LegacySkinResourceStore : IResourceStore
- where T : INamedFileInfo
- {
- private readonly IHasFiles source;
- private readonly IResourceStore underlyingStore;
-
- private string getPathForFile(string filename)
- {
- if (source.Files == null)
- return null;
-
- bool hasExtension = filename.Contains('.');
-
- var file = source.Files.Find(f =>
- string.Equals(hasExtension ? f.Filename : Path.ChangeExtension(f.Filename, null), filename, StringComparison.InvariantCultureIgnoreCase));
- return file?.FileInfo.StoragePath;
- }
-
- public LegacySkinResourceStore(IHasFiles source, IResourceStore underlyingStore)
- {
- this.source = source;
- this.underlyingStore = underlyingStore;
- }
-
- public Stream GetStream(string name)
- {
- string path = getPathForFile(name);
- return path == null ? null : underlyingStore.GetStream(path);
- }
-
- public IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename);
-
- byte[] IResourceStore.Get(string name) => GetAsync(name).Result;
-
- public Task GetAsync(string name)
- {
- string path = getPathForFile(name);
- return path == null ? Task.FromResult(null) : underlyingStore.GetAsync(path);
- }
-
- #region IDisposable Support
-
- private bool isDisposed;
-
- protected virtual void Dispose(bool disposing)
- {
- if (!isDisposed)
- {
- isDisposed = true;
- }
- }
-
- ~LegacySkinResourceStore()
- {
- Dispose(false);
- }
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- #endregion
- }
-
- private class LegacySpriteText : OsuSpriteText
- {
- private readonly LegacyGlyphStore glyphStore;
-
- public LegacySpriteText(ISkin skin, string font)
- {
- Shadow = false;
- UseFullGlyphHeight = false;
-
- Font = new FontUsage(font, OsuFont.DEFAULT_FONT_SIZE);
- glyphStore = new LegacyGlyphStore(skin);
- }
-
- protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
-
- private class LegacyGlyphStore : ITexturedGlyphLookupStore
- {
- private readonly ISkin skin;
-
- public LegacyGlyphStore(ISkin skin)
- {
- this.skin = skin;
- }
-
- public ITexturedCharacterGlyph Get(string fontName, char character)
- {
- var texture = skin.GetTexture($"{fontName}-{character}");
-
- if (texture != null)
- // Approximate value that brings character sizing roughly in-line with stable
- texture.ScaleAdjust *= 18;
-
- if (texture == null)
- return null;
-
- return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, null), texture, 1f / texture.ScaleAdjust);
- }
-
- public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
- }
- }
-
- public class LegacyCursor : CompositeDrawable
- {
- public LegacyCursor()
- {
- Size = new Vector2(50);
-
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- }
-
- [BackgroundDependencyLoader]
- private void load(ISkinSource skin)
- {
- InternalChildren = new Drawable[]
- {
- new NonPlayfieldSprite
- {
- Texture = skin.GetTexture("cursormiddle"),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new NonPlayfieldSprite
- {
- Texture = skin.GetTexture("cursor"),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- };
- }
- }
-
- public class LegacySliderBall : CompositeDrawable
- {
- private readonly Drawable animationContent;
-
- public LegacySliderBall(Drawable animationContent)
- {
- this.animationContent = animationContent;
- }
-
- [BackgroundDependencyLoader]
- private void load(ISkinSource skin, DrawableHitObject drawableObject)
- {
- animationContent.Colour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White;
-
- InternalChildren = new[]
- {
- new Sprite
- {
- Texture = skin.GetTexture("sliderb-nd"),
- Colour = new Color4(5, 5, 5, 255),
- },
- animationContent,
- new Sprite
- {
- Texture = skin.GetTexture("sliderb-spec"),
- Blending = BlendingParameters.Additive,
- },
- };
- }
- }
-
- public class LegacyMainCirclePiece : CompositeDrawable
- {
- public LegacyMainCirclePiece()
- {
- Size = new Vector2(128);
- }
-
- private readonly IBindable state = new Bindable();
-
- private readonly Bindable accentColour = new Bindable();
-
- [BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
- {
- Sprite hitCircleSprite;
-
- InternalChildren = new Drawable[]
- {
- hitCircleSprite = new Sprite
- {
- Texture = skin.GetTexture("hitcircle"),
- Colour = drawableObject.AccentColour.Value,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
- {
- Font = OsuFont.Numeric.With(size: 40),
- UseFullGlyphHeight = false,
- }, confineMode: ConfineMode.NoScaling)
- {
- Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
- },
- new Sprite
- {
- Texture = skin.GetTexture("hitcircleoverlay"),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- };
-
- state.BindTo(drawableObject.State);
- state.BindValueChanged(updateState, true);
-
- accentColour.BindTo(drawableObject.AccentColour);
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
- }
-
- private void updateState(ValueChangedEvent state)
- {
- const double legacy_fade_duration = 240;
-
- switch (state.NewValue)
- {
- case ArmedState.Hit:
- this.FadeOut(legacy_fade_duration, Easing.Out);
- this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- break;
- }
- }
- }
-
- ///
- /// A sprite which is displayed within the playfield, but historically was not considered part of the playfield.
- /// Performs scale adjustment to undo the scale applied by (osu! ruleset specifically).
- ///
- private class NonPlayfieldSprite : Sprite
- {
- public override Texture Texture
- {
- get => base.Texture;
- set
- {
- if (value != null)
- // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
- value.ScaleAdjust *= 1.6f;
- base.Texture = value;
- }
- }
- }
}
}
diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs
index ecb112955c..0160755eed 100644
--- a/osu.Game/Skinning/LegacySkinDecoder.cs
+++ b/osu.Game/Skinning/LegacySkinDecoder.cs
@@ -5,14 +5,14 @@ using osu.Game.Beatmaps.Formats;
namespace osu.Game.Skinning
{
- public class LegacySkinDecoder : LegacyDecoder
+ public class LegacySkinDecoder : LegacyDecoder
{
public LegacySkinDecoder()
: base(1)
{
}
- protected override void ParseLine(SkinConfiguration skin, Section section, string line)
+ protected override void ParseLine(DefaultSkinConfiguration skin, Section section, string line)
{
line = StripComments(line);
diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
new file mode 100644
index 0000000000..c5582af836
--- /dev/null
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -0,0 +1,53 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Skinning
+{
+ public static class LegacySkinExtensions
+ {
+ public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, string animationSeparator = "-")
+ {
+ const double default_frame_time = 1000 / 60d;
+
+ Texture texture;
+
+ Texture getFrameTexture(int frame) => source.GetTexture($"{componentName}{animationSeparator}{frame}");
+
+ TextureAnimation animation = null;
+
+ if (animatable)
+ {
+ for (int i = 0;; i++)
+ {
+ if ((texture = getFrameTexture(i)) == null)
+ break;
+
+ if (animation == null)
+ animation = new TextureAnimation
+ {
+ DefaultFrameLength = default_frame_time,
+ Repeat = looping
+ };
+
+ animation.AddFrame(texture);
+ }
+ }
+
+ if (animation != null)
+ return animation;
+
+ if ((texture = source.GetTexture(componentName)) != null)
+ return new Sprite
+ {
+ Texture = texture
+ };
+
+ return null;
+ }
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs
new file mode 100644
index 0000000000..72f3b9ed78
--- /dev/null
+++ b/osu.Game/Skinning/LegacySkinResourceStore.cs
@@ -0,0 +1,79 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework.IO.Stores;
+using osu.Game.Database;
+
+namespace osu.Game.Skinning
+{
+ public class LegacySkinResourceStore : IResourceStore
+ where T : INamedFileInfo
+ {
+ private readonly IHasFiles source;
+ private readonly IResourceStore underlyingStore;
+
+ private string getPathForFile(string filename)
+ {
+ if (source.Files == null)
+ return null;
+
+ bool hasExtension = filename.Contains('.');
+
+ var file = source.Files.Find(f =>
+ string.Equals(hasExtension ? f.Filename : Path.ChangeExtension(f.Filename, null), filename, StringComparison.InvariantCultureIgnoreCase));
+ return file?.FileInfo.StoragePath;
+ }
+
+ public LegacySkinResourceStore(IHasFiles source, IResourceStore underlyingStore)
+ {
+ this.source = source;
+ this.underlyingStore = underlyingStore;
+ }
+
+ public Stream GetStream(string name)
+ {
+ string path = getPathForFile(name);
+ return path == null ? null : underlyingStore.GetStream(path);
+ }
+
+ public IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename);
+
+ byte[] IResourceStore.Get(string name) => GetAsync(name).Result;
+
+ public Task GetAsync(string name)
+ {
+ string path = getPathForFile(name);
+ return path == null ? Task.FromResult(null) : underlyingStore.GetAsync(path);
+ }
+
+ #region IDisposable Support
+
+ private bool isDisposed;
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!isDisposed)
+ {
+ isDisposed = true;
+ }
+ }
+
+ ~LegacySkinResourceStore()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs
new file mode 100644
index 0000000000..dbcec019d6
--- /dev/null
+++ b/osu.Game/Skinning/LegacySpriteText.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Text;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Skinning
+{
+ public class LegacySpriteText : OsuSpriteText
+ {
+ private readonly LegacyGlyphStore glyphStore;
+
+ public LegacySpriteText(ISkin skin, string font)
+ {
+ Shadow = false;
+ UseFullGlyphHeight = false;
+
+ Font = new FontUsage(font, OsuFont.DEFAULT_FONT_SIZE);
+ glyphStore = new LegacyGlyphStore(skin);
+ }
+
+ protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
+
+ private class LegacyGlyphStore : ITexturedGlyphLookupStore
+ {
+ private readonly ISkin skin;
+
+ public LegacyGlyphStore(ISkin skin)
+ {
+ this.skin = skin;
+ }
+
+ public ITexturedCharacterGlyph Get(string fontName, char character)
+ {
+ var texture = skin.GetTexture($"{fontName}-{character}");
+
+ if (texture != null)
+ // Approximate value that brings character sizing roughly in-line with stable
+ texture.ScaleAdjust *= 18;
+
+ if (texture == null)
+ return null;
+
+ return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, null), texture, 1f / texture.ScaleAdjust);
+ }
+
+ public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
+ }
+ }
+}
diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs
index 93b599f9f6..d585c58ef1 100644
--- a/osu.Game/Skinning/SkinConfiguration.cs
+++ b/osu.Game/Skinning/SkinConfiguration.cs
@@ -7,21 +7,18 @@ using osuTK.Graphics;
namespace osu.Game.Skinning
{
+ ///
+ /// An empty skin configuration.
+ ///
public class SkinConfiguration : IHasComboColours, IHasCustomColours
{
public readonly SkinInfo SkinInfo = new SkinInfo();
- public List ComboColours { get; set; } = new List
- {
- new Color4(17, 136, 170, 255),
- new Color4(102, 136, 0, 255),
- new Color4(204, 102, 0, 255),
- new Color4(121, 9, 13, 255)
- };
+ public List ComboColours { get; set; } = new List();
public Dictionary CustomColours { get; set; } = new Dictionary();
- public string HitCircleFont { get; set; } = "default";
+ public string HitCircleFont { get; set; }
public int HitCircleOverlap { get; set; }
@@ -29,6 +26,6 @@ namespace osu.Game.Skinning
public float? SliderPathRadius { get; set; }
- public bool? CursorExpand { get; set; } = true;
+ public bool? CursorExpand { get; set; }
}
}
diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
similarity index 60%
rename from osu.Game/Skinning/LocalSkinOverrideContainer.cs
rename to osu.Game/Skinning/SkinProvidingContainer.cs
index fc36d1c8da..45b8baa0bb 100644
--- a/osu.Game/Skinning/LocalSkinOverrideContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -4,38 +4,43 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
-using osu.Game.Configuration;
namespace osu.Game.Skinning
{
///
- /// A container which overrides existing skin options with beatmap-local values.
+ /// A container which adds a local to the hierarchy.
///
- public class LocalSkinOverrideContainer : Container, ISkinSource
+ public class SkinProvidingContainer : Container, ISkinSource
{
public event Action SourceChanged;
- private readonly Bindable beatmapSkins = new Bindable();
- private readonly Bindable beatmapHitsounds = new Bindable();
-
private readonly ISkin skin;
private ISkinSource fallbackSource;
- public LocalSkinOverrideContainer(ISkin skin)
+ protected virtual bool AllowDrawableLookup(string componentName) => true;
+
+ protected virtual bool AllowTextureLookup(string componentName) => true;
+
+ protected virtual bool AllowSampleLookup(ISampleInfo componentName) => true;
+
+ protected virtual bool AllowConfigurationLookup => true;
+
+ public SkinProvidingContainer(ISkin skin)
{
this.skin = skin;
+
+ RelativeSizeAxes = Axes.Both;
}
public Drawable GetDrawableComponent(string componentName)
{
Drawable sourceDrawable;
- if (beatmapSkins.Value && (sourceDrawable = skin?.GetDrawableComponent(componentName)) != null)
+ if (AllowDrawableLookup(componentName) && (sourceDrawable = skin?.GetDrawableComponent(componentName)) != null)
return sourceDrawable;
return fallbackSource?.GetDrawableComponent(componentName);
@@ -44,7 +49,7 @@ namespace osu.Game.Skinning
public Texture GetTexture(string componentName)
{
Texture sourceTexture;
- if (beatmapSkins.Value && (sourceTexture = skin?.GetTexture(componentName)) != null)
+ if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName)) != null)
return sourceTexture;
return fallbackSource.GetTexture(componentName);
@@ -53,7 +58,7 @@ namespace osu.Game.Skinning
public SampleChannel GetSample(ISampleInfo sampleInfo)
{
SampleChannel sourceChannel;
- if (beatmapHitsounds.Value && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
+ if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
return sourceChannel;
return fallbackSource?.GetSample(sampleInfo);
@@ -62,14 +67,13 @@ namespace osu.Game.Skinning
public TValue GetValue(Func query) where TConfiguration : SkinConfiguration
{
TValue val;
- if ((skin as Skin)?.Configuration is TConfiguration conf)
- if (beatmapSkins.Value && (val = query.Invoke(conf)) != null)
- return val;
+ if (AllowConfigurationLookup && skin != null && (val = skin.GetValue(query)) != null)
+ return val;
return fallbackSource == null ? default : fallbackSource.GetValue(query);
}
- private void onSourceChanged() => SourceChanged?.Invoke();
+ protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -77,18 +81,10 @@ namespace osu.Game.Skinning
fallbackSource = dependencies.Get();
if (fallbackSource != null)
- fallbackSource.SourceChanged += onSourceChanged;
+ fallbackSource.SourceChanged += TriggerSourceChanged;
dependencies.CacheAs(this);
- var config = dependencies.Get();
-
- config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
- config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds);
-
- beatmapSkins.BindValueChanged(_ => onSourceChanged());
- beatmapHitsounds.BindValueChanged(_ => onSourceChanged());
-
return dependencies;
}
@@ -100,7 +96,7 @@ namespace osu.Game.Skinning
base.Dispose(isDisposing);
if (fallbackSource != null)
- fallbackSource.SourceChanged -= onSourceChanged;
+ fallbackSource.SourceChanged -= TriggerSourceChanged;
}
}
}
diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index 0c635a3d2f..07f802944b 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Skinning
///
/// The displayed component.
///
- protected Drawable Drawable { get; private set; }
+ public Drawable Drawable { get; private set; }
private readonly string componentName;
diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs
index 03e17a819c..ccd996098c 100644
--- a/osu.Game/Tests/Visual/PlayerTestScene.cs
+++ b/osu.Game/Tests/Visual/PlayerTestScene.cs
@@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets;
@@ -22,12 +23,13 @@ namespace osu.Game.Tests.Visual
this.ruleset = ruleset;
}
+ protected OsuConfigManager LocalConfig;
+
[BackgroundDependencyLoader]
private void load()
{
- OsuConfigManager manager;
- Dependencies.Cache(manager = new OsuConfigManager(LocalStorage));
- manager.GetBindable(OsuSetting.DimLevel).Value = 1.0;
+ Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage));
+ LocalConfig.GetBindable(OsuSetting.DimLevel).Value = 1.0;
}
[SetUpSteps]
@@ -39,6 +41,8 @@ namespace osu.Game.Tests.Visual
protected virtual bool AllowFail => false;
+ protected virtual bool Autoplay => false;
+
private void loadPlayer()
{
var beatmap = CreateBeatmap(ruleset.RulesetInfo);
@@ -48,6 +52,13 @@ namespace osu.Game.Tests.Visual
if (!AllowFail)
Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
+ if (Autoplay)
+ {
+ var mod = ruleset.GetAutoplayMod();
+ if (mod != null)
+ Mods.Value = Mods.Value.Concat(mod.Yield()).ToArray();
+ }
+
Player = CreatePlayer(ruleset);
LoadScreen(Player);
}
diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs
index b93a1466e0..31f6edadec 100644
--- a/osu.Game/Tests/Visual/TestPlayer.cs
+++ b/osu.Game/Tests/Visual/TestPlayer.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.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
@@ -9,6 +10,8 @@ namespace osu.Game.Tests.Visual
{
protected override bool PauseOnFocusLost => false;
+ public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
+
public TestPlayer(bool allowPause = true, bool showResults = true)
: base(allowPause, showResults)
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 8e6ce2d1ba..330018d5cb 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 47cc6ec97a..9f8d82ad1e 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -118,8 +118,8 @@
-
-
+
+