diff --git a/osu.Android.props b/osu.Android.props
index 6cbb4b2e68..d701aaf199 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png
new file mode 100644
index 0000000000..8304617d8c
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png
new file mode 100644
index 0000000000..c3b85eb873
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png
new file mode 100644
index 0000000000..7f65eb7ca7
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png
new file mode 100644
index 0000000000..82bec3babe
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png
new file mode 100644
index 0000000000..5e38c75a9d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png
new file mode 100644
index 0000000000..a562d9f2ac
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png
new file mode 100644
index 0000000000..b4cf81f26e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png
new file mode 100644
index 0000000000..a23f5379b2
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png
new file mode 100644
index 0000000000..430b18509d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png
new file mode 100644
index 0000000000..add1202c31
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png
new file mode 100644
index 0000000000..508cc85e4a
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png
new file mode 100644
index 0000000000..84f74e1ec9
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png
new file mode 100644
index 0000000000..49625c6623
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png
new file mode 100644
index 0000000000..623b24612f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png
new file mode 100644
index 0000000000..a33286dc8f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png
new file mode 100644
index 0000000000..d8250b0c63
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png
new file mode 100644
index 0000000000..75d3cbd3bd
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png
new file mode 100644
index 0000000000..cfe2021df4
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png
new file mode 100644
index 0000000000..ba9492c7f8
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png
new file mode 100644
index 0000000000..a7b6b81570
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index b4f123598b..e055f08dc2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() =>
{
area.AttemptCatch(fruit);
- area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
+ area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
drawable.Expire();
});
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
new file mode 100644
index 0000000000..e79792e04a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneComboCounter : CatchSkinnableTestScene
+ {
+ private ScoreProcessor scoreProcessor;
+
+ private Color4 judgedObjectColour = Color4.White;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ scoreProcessor = new ScoreProcessor();
+
+ SetContents(() => new CatchComboDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(2.5f),
+ });
+ });
+
+ [Test]
+ public void TestCatchComboCounter()
+ {
+ AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20);
+ AddStep("perform miss", () => performJudgement(HitResult.Miss));
+
+ AddStep("randomize judged object colour", () =>
+ {
+ judgedObjectColour = new Color4(
+ RNG.NextSingle(1f),
+ RNG.NextSingle(1f),
+ RNG.NextSingle(1f),
+ 1f
+ );
+ });
+ }
+
+ private void performJudgement(HitResult type, Judgement judgement = null)
+ {
+ var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
+
+ var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
+ scoreProcessor.ApplyResult(result);
+
+ foreach (var counter in CreatedDrawables.Cast())
+ counter.OnNewResult(judgedObject, result);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index 80390705fe..23d8428fec 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch
Droplet,
CatcherIdle,
CatcherFail,
- CatcherKiai
+ CatcherKiai,
+ CatchComboCounter
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index ea2f031d65..47224bd195 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning
{
@@ -52,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
+
+ case CatchSkinComponents.CatchComboCounter:
+ var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
+
+ // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
+ if (this.HasFont(comboFont))
+ return new LegacyComboCounter(Source);
+
+ break;
}
return null;
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
new file mode 100644
index 0000000000..c8abc9e832
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
@@ -0,0 +1,103 @@
+// 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.Containers;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ ///
+ /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
+ ///
+ public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
+ {
+ private readonly LegacyRollingCounter counter;
+
+ private readonly LegacyRollingCounter explosion;
+
+ public LegacyComboCounter(ISkin skin)
+ {
+ var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
+ var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f;
+
+ AutoSizeAxes = Axes.Both;
+
+ Alpha = 0f;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Scale = new Vector2(0.8f);
+
+ InternalChildren = new Drawable[]
+ {
+ explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
+ {
+ Alpha = 0.65f,
+ Blending = BlendingParameters.Additive,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(1.5f),
+ },
+ counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+ }
+
+ private int lastDisplayedCombo;
+
+ public void UpdateCombo(int combo, Color4? hitObjectColour = null)
+ {
+ if (combo == lastDisplayedCombo)
+ return;
+
+ // There may still be existing transforms to the counter (including value change after 250ms),
+ // finish them immediately before new transforms.
+ counter.SetCountWithoutRolling(lastDisplayedCombo);
+
+ lastDisplayedCombo = combo;
+
+ if (Time.Elapsed < 0)
+ {
+ // needs more work to make rewind somehow look good.
+ // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
+ Hide();
+ return;
+ }
+
+ // Combo fell to zero, roll down and fade out the counter.
+ if (combo == 0)
+ {
+ counter.Current.Value = 0;
+ explosion.Current.Value = 0;
+
+ this.FadeOut(400, Easing.Out);
+ }
+ else
+ {
+ this.FadeInFromZero().Then().Delay(1000).FadeOut(300);
+
+ counter.ScaleTo(1.5f)
+ .ScaleTo(0.8f, 250, Easing.Out)
+ .OnComplete(c => c.SetCountWithoutRolling(combo));
+
+ counter.Delay(250)
+ .ScaleTo(1f)
+ .ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30);
+
+ explosion.Colour = hitObjectColour ?? Color4.White;
+
+ explosion.SetCountWithoutRolling(combo);
+ explosion.ScaleTo(1.5f)
+ .ScaleTo(1.9f, 400, Easing.Out)
+ .FadeOutFromOne(400);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
new file mode 100644
index 0000000000..58a3140bb5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
@@ -0,0 +1,62 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// Represents a component that displays a skinned and handles combo judgement results for updating it accordingly.
+ ///
+ public class CatchComboDisplay : SkinnableDrawable
+ {
+ private int currentCombo;
+
+ [CanBeNull]
+ public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
+
+ public CatchComboDisplay()
+ : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
+ {
+ }
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+ ComboCounter?.UpdateCombo(currentCombo);
+ }
+
+ public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
+ {
+ if (!result.Judgement.AffectsCombo || !result.HasResult)
+ return;
+
+ if (result.Type == HitResult.Miss)
+ {
+ updateCombo(0, null);
+ return;
+ }
+
+ updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
+ }
+
+ public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
+ {
+ if (!result.Judgement.AffectsCombo || !result.HasResult)
+ return;
+
+ updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
+ }
+
+ private void updateCombo(int newCombo, Color4? hitObjectColour)
+ {
+ currentCombo = newCombo;
+ ComboCounter?.UpdateCombo(newCombo, hitObjectColour);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 154e1576db..735d7fc300 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
explodingFruitContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer,
- CatcherArea
+ CatcherArea,
};
}
@@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Catch.UI
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
+ h.OnRevertResult += onRevertResult;
base.Add(h);
@@ -70,6 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
- => CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
+ => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
+
+ private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
+ => CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 03ebf01b9b..d3e63b0333 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI
public Func> CreateDrawableRepresentation;
public readonly Catcher MovableCatcher;
+ private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget
{
@@ -34,10 +35,22 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
- Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
+ Children = new Drawable[]
+ {
+ comboDisplay = new CatchComboDisplay
+ {
+ RelativeSizeAxes = Axes.None,
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Bottom = 350f },
+ X = CatchPlayfield.CENTER_X
+ },
+ MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
+ };
}
- public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
+ public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
{
if (result.Judgement is IgnoreJudgement)
return;
@@ -86,8 +99,13 @@ namespace osu.Game.Rulesets.Catch.UI
else
MovableCatcher.Drop();
}
+
+ comboDisplay.OnNewResult(fruit, result);
}
+ public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
+ => comboDisplay.OnRevertResult(fruit, result);
+
public void OnReleased(CatchAction action)
{
}
@@ -105,6 +123,8 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
+
+ comboDisplay.X = MovableCatcher.X;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs
new file mode 100644
index 0000000000..cfb6879067
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs
@@ -0,0 +1,24 @@
+// 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 osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// An interface providing a set of methods to update the combo counter.
+ ///
+ public interface ICatchComboCounter : IDrawable
+ {
+ ///
+ /// Updates the counter to animate a transition from the old combo value it had to the current provided one.
+ ///
+ ///
+ /// This is called regardless of whether the clock is rewinding.
+ ///
+ /// The new combo value.
+ /// The colour of the object if hit, null on miss.
+ void UpdateCombo(int combo, Color4? hitObjectColour = null);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
index 65c8720031..2347d8a34c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
@@ -34,11 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
Alpha = 0.5f,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
- ring = new RingPiece
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre
- }
+ ring = new RingPiece()
};
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index f87bd53ec3..e1cbfa93f6 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -46,14 +46,24 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapToggle
};
+ private BindableList selectedHitObjects;
+
+ private Bindable placementObject;
+
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
- EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
- EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
+ selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
+ selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
+
+ placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
+ placementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
+
+ // we may be entering the screen with a selection already active
+ updateDistanceSnapGrid();
}
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index 4d73e711bb..11571ea761 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -46,7 +46,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void addConnection(FollowPointConnection connection)
{
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
- int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
+ int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) =>
+ {
+ int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
+
+ if (comp != 0)
+ return comp;
+
+ // we always want to insert the new item after equal ones.
+ // this is important for beatmaps with multiple hitobjects at the same point in time.
+ // if we use standard comparison insert order, there will be a churn of connections getting re-updated to
+ // the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal).
+ // this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245
+ return -1;
+ }));
if (index < connections.Count - 1)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
index 82e4383143..bcf64b81a6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
@@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class RingPiece : Container
+ public class RingPiece : CircularContainer
{
public RingPiece()
{
@@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- InternalChild = new CircularContainer
+ Masking = true;
+ BorderThickness = 10;
+ BorderColour = Color4.White;
+
+ Child = new Box
{
- Masking = true,
- BorderThickness = 10,
- BorderColour = Color4.White,
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- AlwaysPresent = true,
- Alpha = 0,
- RelativeSizeAxes = Axes.Both
- }
- }
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 81d1d05b66..851a8d56c9 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
- return !hasFont(font)
+ return !this.HasFont(font)
? null
: new LegacySpriteText(Source, font)
{
@@ -145,7 +145,5 @@ namespace osu.Game.Rulesets.Osu.Skinning
return Source.GetConfig(lookup);
}
-
- private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null;
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs
new file mode 100644
index 0000000000..7cb984b254
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.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 System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class DrawableTestStrongHit : DrawableHit
+ {
+ private readonly HitResult type;
+ private readonly bool hitBoth;
+
+ public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true)
+ : base(new Hit
+ {
+ IsStrong = true,
+ StartTime = startTime,
+ })
+ {
+ // in order to create nested strong hit
+ HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ this.type = type;
+ this.hitBoth = hitBoth;
+ }
+
+ protected override void LoadAsyncComplete()
+ {
+ base.LoadAsyncComplete();
+
+ Result.Type = type;
+
+ var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single();
+ nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss;
+ }
+
+ public override bool OnPressed(TaikoAction action) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index 2b5efec7f9..48969e0f5a 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
@@ -15,24 +14,29 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestNormalHit()
{
- AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
- AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
- AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
+ AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great))));
+ AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good))));
+ AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
}
- private Drawable getContentFor(HitResult type)
+ [Test]
+ public void TestStrongHit([Values(false, true)] bool hitBoth)
{
- DrawableTaikoHitObject hit;
+ AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
+ AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth))));
+ }
+ private Drawable getContentFor(DrawableTaikoHitObject hit)
+ {
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- hit = createHit(type),
+ hit,
new HitExplosion(hit)
{
Anchor = Anchor.Centre,
@@ -43,5 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}
private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
+
+ private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth)
+ => new DrawableTestStrongHit(Time.Current, type, hitBoth);
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index 44452d70c1..99d1b72ea4 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -174,7 +174,9 @@ namespace osu.Game.Rulesets.Taiko.Tests
private void addMissJudgement()
{
- ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
+ DrawableTestHit h;
+ Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
+ ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
}
private void addBarLine(bool major, double delay = scroll_time)
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index eebf6980fe..40565048c2 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Rim", action: state =>
{
+ ChangeHandler.BeginChange();
+
foreach (var h in hits)
{
switch (state)
@@ -35,6 +37,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
break;
}
}
+
+ ChangeHandler.EndChange();
})
{
State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) }
@@ -47,6 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Strong", action: state =>
{
+ ChangeHandler.BeginChange();
+
foreach (var h in hits)
{
switch (state)
@@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
EditorBeatmap?.UpdateHitObject(h);
}
+
+ ChangeHandler.EndChange();
})
{
State = { Value = getTernaryState(hits, h => h.IsStrong) }
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index f032c5f485..c222ccb51f 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -75,7 +75,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
case TaikoSkinComponents.TaikoExplosionGood:
+ case TaikoSkinComponents.TaikoExplosionGoodStrong:
case TaikoSkinComponents.TaikoExplosionGreat:
+ case TaikoSkinComponents.TaikoExplosionGreatStrong:
case TaikoSkinComponents.TaikoExplosionMiss:
var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
@@ -107,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.TaikoExplosionGood:
return "taiko-hit100";
+ case TaikoSkinComponents.TaikoExplosionGoodStrong:
+ return "taiko-hit100k";
+
case TaikoSkinComponents.TaikoExplosionGreat:
return "taiko-hit300";
+
+ case TaikoSkinComponents.TaikoExplosionGreatStrong:
+ return "taiko-hit300k";
}
throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index ac4fb51661..0d785adb4a 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Taiko
BarLine,
TaikoExplosionMiss,
TaikoExplosionGood,
+ TaikoExplosionGoodStrong,
TaikoExplosionGreat,
+ TaikoExplosionGreatStrong,
Scroller,
Mascot,
}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index f0585b9c50..e3eabbf88f 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -9,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
@@ -45,24 +47,41 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion());
+ Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion());
}
- private TaikoSkinComponents getComponentName(HitResult resultType)
+ private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject)
{
+ var resultType = judgedObject.Result?.Type ?? HitResult.Great;
+
switch (resultType)
{
case HitResult.Miss:
return TaikoSkinComponents.TaikoExplosionMiss;
case HitResult.Good:
- return TaikoSkinComponents.TaikoExplosionGood;
+ return useStrongExplosion(judgedObject)
+ ? TaikoSkinComponents.TaikoExplosionGoodStrong
+ : TaikoSkinComponents.TaikoExplosionGood;
case HitResult.Great:
- return TaikoSkinComponents.TaikoExplosionGreat;
+ return useStrongExplosion(judgedObject)
+ ? TaikoSkinComponents.TaikoExplosionGreatStrong
+ : TaikoSkinComponents.TaikoExplosionGreat;
}
- throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type");
+ throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type");
+ }
+
+ private bool useStrongExplosion(DrawableHitObject judgedObject)
+ {
+ if (!(judgedObject.HitObject is Hit))
+ return false;
+
+ if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit))
+ return false;
+
+ return judgedObject.Result.Type == nestedHit.Result.Type;
}
///
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index dabdfe6f44..7976d5bc6d 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -205,9 +205,6 @@ namespace osu.Game.Rulesets.Taiko.UI
X = result.IsHit ? judgedObject.Position.X : 0,
});
- if (!result.IsHit)
- break;
-
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
addExplosion(judgedObject, type);
@@ -218,12 +215,16 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
- private void addExplosion(DrawableHitObject drawableObject, HitType type)
+ ///
+ /// As legacy skins have different explosions for singular and double strong hits,
+ /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame.
+ ///
+ private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() =>
{
hitExplosionContainer.Add(new HitExplosion(drawableObject));
if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
- }
+ });
private class ProxyContainer : LifetimeManagementContainer
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index 2d5e4b911e..58cc324233 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Gameplay
private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation
{
- public bool NewCombo { get; } = false;
+ public bool NewCombo { get; set; } = false;
public int ComboOffset { get; } = 0;
public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
new file mode 100644
index 0000000000..89e3b48aa3
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -0,0 +1,134 @@
+// 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.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.UI;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Rulesets
+{
+ public class TestSceneDrawableRulesetDependencies : OsuTestScene
+ {
+ [Test]
+ public void TestDisposalDoesNotDisposeParentStores()
+ {
+ DrawableWithDependencies drawable = null;
+ TestTextureStore textureStore = null;
+ TestSampleStore sampleStore = null;
+
+ AddStep("add dependencies", () =>
+ {
+ Child = drawable = new DrawableWithDependencies();
+ textureStore = drawable.ParentTextureStore;
+ sampleStore = drawable.ParentSampleStore;
+ });
+
+ AddStep("clear children", Clear);
+ AddUntilStep("wait for disposal", () => drawable.IsDisposed);
+
+ AddStep("GC", () =>
+ {
+ drawable = null;
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ });
+
+ AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed);
+ AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed);
+ }
+
+ private class DrawableWithDependencies : CompositeDrawable
+ {
+ public TestTextureStore ParentTextureStore { get; private set; }
+ public TestSampleStore ParentSampleStore { get; private set; }
+
+ public DrawableWithDependencies()
+ {
+ InternalChild = new Box { RelativeSizeAxes = Axes.Both };
+ }
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ dependencies.CacheAs(ParentTextureStore = new TestTextureStore());
+ dependencies.CacheAs(ParentSampleStore = new TestSampleStore());
+
+ return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
+ }
+
+ public new bool IsDisposed { get; private set; }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ IsDisposed = true;
+ }
+ }
+
+ private class TestTextureStore : TextureStore
+ {
+ public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null;
+
+ public bool IsDisposed { get; private set; }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ IsDisposed = true;
+ }
+ }
+
+ private class TestSampleStore : ISampleStore
+ {
+ public bool IsDisposed { get; private set; }
+
+ public void Dispose()
+ {
+ IsDisposed = true;
+ }
+
+ public SampleChannel Get(string name) => null;
+
+ public Task GetAsync(string name) => null;
+
+ public Stream GetStream(string name) => null;
+
+ public IEnumerable GetAvailableResources() => throw new NotImplementedException();
+
+ public BindableNumber Volume => throw new NotImplementedException();
+ public BindableNumber Balance => throw new NotImplementedException();
+ public BindableNumber Frequency => throw new NotImplementedException();
+ public BindableNumber Tempo => throw new NotImplementedException();
+
+ public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException();
+
+ public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException();
+
+ public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException();
+
+ public IBindable AggregateVolume => throw new NotImplementedException();
+ public IBindable AggregateBalance => throw new NotImplementedException();
+ public IBindable AggregateFrequency => throw new NotImplementedException();
+ public IBindable AggregateTempo => throw new NotImplementedException();
+
+ public int PlaybackConcurrency { get; set; }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index e0a1f947ec..ed75d83151 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -1,12 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio;
@@ -20,6 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
+ private TestSkinSourceContainer skinSource;
private SkinnableSound skinnableSound;
[SetUp]
@@ -29,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new Drawable[]
{
- new Container
+ skinSource = new TestSkinSourceContainer
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
@@ -101,5 +107,55 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing);
}
+
+ [Test]
+ public void TestSkinChangeDoesntPlayOnPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample", () =>
+ {
+ skinnableSound.Play();
+ sample = skinnableSound.ChildrenOfType().Single();
+ });
+
+ AddAssert("sample playing", () => sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+
+ AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
+
+ AddAssert("retrieve and ensure current sample is different", () =>
+ {
+ DrawableSample oldSample = sample;
+ sample = skinnableSound.ChildrenOfType().Single();
+ return sample != oldSample;
+ });
+
+ AddAssert("new sample stopped", () => !sample.Playing);
+ AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+
+ AddWaitStep("wait a bit", 5);
+ AddAssert("new sample not played", () => !sample.Playing);
+ }
+
+ [Cached(typeof(ISkinSource))]
+ private class TestSkinSourceContainer : Container, ISkinSource
+ {
+ [Resolved]
+ private ISkinSource source { get; set; }
+
+ public event Action SourceChanged;
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
+ public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup);
+
+ public void TriggerSourceChanged()
+ {
+ SourceChanged?.Invoke();
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
index 08130e60db..c2a18330c9 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online
public void TestMultipleLoads()
{
var comments = exampleComments;
- int topLevelCommentCount = exampleComments.Comments.Count(comment => comment.IsTopLevel);
+ int topLevelCommentCount = exampleComments.Comments.Count;
AddStep("hide container", () => commentsContainer.Hide());
setUpCommentsResponse(comments);
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 03cb5fa3db..ff96a999ec 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value);
+ AddAssert("download button is disabled", () => !screen.ChildrenOfType().Last().Enabled.Value);
AddStep("click contracted panel", () =>
{
@@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking
InputManager.Click(MouseButton.Left);
});
- AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value);
+ AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value);
}
private class TestResultsContainer : Container
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 45b07581ec..41be4cfcc3 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings
handler = game;
}
- public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings);
+ public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings);
public IEnumerable GlobalKeyBindings => new[]
{
@@ -50,6 +50,14 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
};
+ public IEnumerable EditorKeyBindings => new[]
+ {
+ new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode),
+ new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
+ new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
+ new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
+ };
+
public IEnumerable InGameKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
@@ -68,7 +76,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
- new KeyBinding(InputKey.F4, GlobalAction.ToggleMute),
+ new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev),
new KeyBinding(InputKey.F1, GlobalAction.MusicPrev),
@@ -139,7 +147,7 @@ namespace osu.Game.Input.Bindings
[Description("Quick exit (Hold)")]
QuickExit,
- // Game-wide beatmap msi ccotolle keybindings
+ // Game-wide beatmap music controller keybindings
[Description("Next track")]
MusicNext,
@@ -166,5 +174,18 @@ namespace osu.Game.Input.Bindings
[Description("Pause")]
PauseGameplay,
+
+ // Editor
+ [Description("Setup Mode")]
+ EditorSetupMode,
+
+ [Description("Compose Mode")]
+ EditorComposeMode,
+
+ [Description("Design Mode")]
+ EditorDesignMode,
+
+ [Description("Timing Mode")]
+ EditorTimingMode,
}
}
diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
index 5b44c486a3..9a27c55c53 100644
--- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Overlays.KeyBinding
Add(new DefaultBindingsSubsection(manager));
Add(new AudioControlKeyBindingsSubsection(manager));
Add(new InGameKeyBindingsSubsection(manager));
+ Add(new EditorKeyBindingsSubsection(manager));
}
private class DefaultBindingsSubsection : KeyBindingsSubsection
@@ -56,5 +57,16 @@ namespace osu.Game.Overlays.KeyBinding
Defaults = manager.AudioControlKeyBindings;
}
}
+
+ private class EditorKeyBindingsSubsection : KeyBindingsSubsection
+ {
+ protected override string Header => "Editor";
+
+ public EditorKeyBindingsSubsection(GlobalActionContainer manager)
+ : base(null)
+ {
+ Defaults = manager.EditorKeyBindings;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index e42a359d2e..b81e0ce159 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Edit
private RadioButtonCollection toolboxCollection;
+ private ToolboxGroup togglesCollection;
+
protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
@@ -78,7 +80,7 @@ namespace osu.Game.Rulesets.Edit
}
catch (Exception e)
{
- Logger.Error(e, "Could not load beatmap sucessfully!");
+ Logger.Error(e, "Could not load beatmap successfully!");
return;
}
@@ -115,7 +117,7 @@ namespace osu.Game.Rulesets.Edit
Children = new Drawable[]
{
new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } },
- new ToolboxGroup("toggles")
+ togglesCollection = new ToolboxGroup("toggles")
{
ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox
{
@@ -190,9 +192,9 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
- if (e.Key >= Key.Number1 && e.Key <= Key.Number9)
+ if (checkLeftToggleFromKey(e.Key, out var leftIndex))
{
- var item = toolboxCollection.Items.ElementAtOrDefault(e.Key - Key.Number1);
+ var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex);
if (item != null)
{
@@ -201,9 +203,84 @@ namespace osu.Game.Rulesets.Edit
}
}
+ if (checkRightToggleFromKey(e.Key, out var rightIndex))
+ {
+ var item = togglesCollection.Children[rightIndex];
+
+ if (item is SettingsCheckbox checkbox)
+ {
+ checkbox.Bindable.Value = !checkbox.Bindable.Value;
+ return true;
+ }
+ }
+
return base.OnKeyDown(e);
}
+ private bool checkLeftToggleFromKey(Key key, out int index)
+ {
+ if (key < Key.Number1 || key > Key.Number9)
+ {
+ index = -1;
+ return false;
+ }
+
+ index = key - Key.Number1;
+ return true;
+ }
+
+ private bool checkRightToggleFromKey(Key key, out int index)
+ {
+ switch (key)
+ {
+ case Key.Q:
+ index = 0;
+ break;
+
+ case Key.W:
+ index = 1;
+ break;
+
+ case Key.E:
+ index = 2;
+ break;
+
+ case Key.R:
+ index = 3;
+ break;
+
+ case Key.T:
+ index = 4;
+ break;
+
+ case Key.Y:
+ index = 5;
+ break;
+
+ case Key.U:
+ index = 6;
+ break;
+
+ case Key.I:
+ index = 7;
+ break;
+
+ case Key.O:
+ index = 8;
+ break;
+
+ case Key.P:
+ index = 9;
+ break;
+
+ default:
+ index = -1;
+ break;
+ }
+
+ return index >= 0;
+ }
+
private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
{
if (EditorBeatmap.SelectedHitObjects.Any())
diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs
index 4e3de04278..211c077d4f 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs
@@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Objects.Types
///
int ComboIndex { get; set; }
+ ///
+ /// Whether the HitObject starts a new combo.
+ ///
+ new bool NewCombo { get; set; }
+
Bindable LastInComboBindable { get; }
///
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 83a1077d70..a9b2a15b35 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Rulesets.Configuration;
@@ -46,12 +47,11 @@ namespace osu.Game.Rulesets.UI
if (resources != null)
{
TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures")));
- TextureStore.AddStore(parent.Get());
- Cache(TextureStore);
+ CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get()));
SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples"));
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
- CacheAs(new FallbackSampleStore(SampleStore, parent.Get()));
+ CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get()));
}
RulesetConfigManager = parent.Get().GetConfigFor(ruleset);
@@ -82,69 +82,92 @@ namespace osu.Game.Rulesets.UI
isDisposed = true;
SampleStore?.Dispose();
+ TextureStore?.Dispose();
RulesetConfigManager = null;
}
#endregion
- }
- ///
- /// A sample store which adds a fallback source.
- ///
- ///
- /// This is a temporary implementation to workaround ISampleStore limitations.
- ///
- public class FallbackSampleStore : ISampleStore
- {
- private readonly ISampleStore primary;
- private readonly ISampleStore secondary;
-
- public FallbackSampleStore(ISampleStore primary, ISampleStore secondary)
+ ///
+ /// A sample store which adds a fallback source and prevents disposal of the fallback source.
+ ///
+ private class FallbackSampleStore : ISampleStore
{
- this.primary = primary;
- this.secondary = secondary;
+ private readonly ISampleStore primary;
+ private readonly ISampleStore fallback;
+
+ public FallbackSampleStore(ISampleStore primary, ISampleStore fallback)
+ {
+ this.primary = primary;
+ this.fallback = fallback;
+ }
+
+ public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name);
+
+ public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name);
+
+ public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name);
+
+ public IEnumerable GetAvailableResources() => throw new NotSupportedException();
+
+ public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException();
+
+ public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException();
+
+ public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
+
+ public BindableNumber Volume => throw new NotSupportedException();
+
+ public BindableNumber Balance => throw new NotSupportedException();
+
+ public BindableNumber Frequency => throw new NotSupportedException();
+
+ public BindableNumber Tempo => throw new NotSupportedException();
+
+ public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException();
+
+ public IBindable AggregateVolume => throw new NotSupportedException();
+
+ public IBindable AggregateBalance => throw new NotSupportedException();
+
+ public IBindable AggregateFrequency => throw new NotSupportedException();
+
+ public IBindable AggregateTempo => throw new NotSupportedException();
+
+ public int PlaybackConcurrency
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ public void Dispose()
+ {
+ primary?.Dispose();
+ }
}
- public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name);
-
- public Task GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name);
-
- public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name);
-
- public IEnumerable GetAvailableResources() => throw new NotSupportedException();
-
- public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException();
-
- public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException();
-
- public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
-
- public BindableNumber Volume => throw new NotSupportedException();
-
- public BindableNumber Balance => throw new NotSupportedException();
-
- public BindableNumber Frequency => throw new NotSupportedException();
-
- public BindableNumber Tempo => throw new NotSupportedException();
-
- public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException();
-
- public IBindable AggregateVolume => throw new NotSupportedException();
-
- public IBindable AggregateBalance => throw new NotSupportedException();
-
- public IBindable AggregateFrequency => throw new NotSupportedException();
-
- public IBindable AggregateTempo => throw new NotSupportedException();
-
- public int PlaybackConcurrency
+ ///
+ /// A texture store which adds a fallback source and prevents disposal of the fallback source.
+ ///
+ private class FallbackTextureStore : TextureStore
{
- get => throw new NotSupportedException();
- set => throw new NotSupportedException();
- }
+ private readonly TextureStore primary;
+ private readonly TextureStore fallback;
- public void Dispose()
- {
+ public FallbackTextureStore(TextureStore primary, TextureStore fallback)
+ {
+ this.primary = primary;
+ this.fallback = fallback;
+ }
+
+ public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
+ => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT);
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ primary?.Dispose();
+ }
}
}
}
diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs
index f4f66f1272..9a0217a1eb 100644
--- a/osu.Game/Rulesets/UI/HitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs
@@ -43,10 +43,20 @@ namespace osu.Game.Rulesets.UI
return true;
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ unbindStartTimeMap();
+ }
+
public virtual void Clear(bool disposeChildren = true)
{
ClearInternal(disposeChildren);
+ unbindStartTimeMap();
+ }
+ private void unbindStartTimeMap()
+ {
foreach (var kvp in startTimeMap)
kvp.Value.bindable.UnbindAll();
startTimeMap.Clear();
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index c52183f3f2..d92ba210db 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -37,7 +37,21 @@ namespace osu.Game.Rulesets.UI
///
/// All the s contained in this and all .
///
- public IEnumerable AllHitObjects => HitObjectContainer?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty();
+ public IEnumerable AllHitObjects
+ {
+ get
+ {
+ if (HitObjectContainer == null)
+ return Enumerable.Empty();
+
+ var enumerable = HitObjectContainer.Objects;
+
+ if (nestedPlayfields.IsValueCreated)
+ enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
+
+ return enumerable;
+ }
+ }
///
/// All s nested inside this .
diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs
index c1f54d7938..c68eeeb4f9 100644
--- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs
+++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs
@@ -22,10 +22,12 @@ namespace osu.Game.Screens.Edit.Components
{
trackTimer = new OsuSpriteText
{
- Origin = Anchor.BottomLeft,
- RelativePositionAxes = Axes.Y,
- Font = OsuFont.GetFont(size: 22, fixedWidth: true),
- Y = 0.5f,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ // intentionally fudged centre to avoid movement of the number portion when
+ // going negative.
+ X = -35,
+ Font = OsuFont.GetFont(size: 25, fixedWidth: true),
}
};
}
@@ -34,7 +36,8 @@ namespace osu.Game.Screens.Edit.Components
{
base.Update();
- trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff");
+ var timespan = TimeSpan.FromMilliseconds(editorClock.CurrentTime);
+ trackTimer.Text = $"{(timespan < TimeSpan.Zero ? "-" : string.Empty)}{timespan:mm\\:ss\\:fff}";
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index f95bf350b6..6e2c8bd01c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -20,6 +20,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@@ -44,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)]
- private IEditorChangeHandler changeHandler { get; set; }
+ protected IEditorChangeHandler ChangeHandler { get; private set; }
public SelectionHandler()
{
@@ -193,12 +194,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void deleteSelected()
{
- changeHandler?.BeginChange();
+ ChangeHandler?.BeginChange();
foreach (var h in selectedBlueprints.ToList())
EditorBeatmap?.Remove(h.HitObject);
- changeHandler?.EndChange();
+ ChangeHandler?.EndChange();
}
#endregion
@@ -254,7 +255,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// The name of the hit sample.
public void AddHitSample(string sampleName)
{
- changeHandler?.BeginChange();
+ ChangeHandler?.BeginChange();
foreach (var h in SelectedHitObjects)
{
@@ -265,7 +266,30 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
- changeHandler?.EndChange();
+ ChangeHandler?.EndChange();
+ }
+
+ ///
+ /// Set the new combo state of all selected s.
+ ///
+ /// Whether to set or unset.
+ /// Throws if any selected object doesn't implement
+ public void SetNewCombo(bool state)
+ {
+ ChangeHandler?.BeginChange();
+
+ foreach (var h in SelectedHitObjects)
+ {
+ var comboInfo = h as IHasComboInformation;
+
+ if (comboInfo == null)
+ throw new InvalidOperationException($"Tried to change combo state of a {h.GetType()}, which doesn't implement {nameof(IHasComboInformation)}");
+
+ comboInfo.NewCombo = state;
+ EditorBeatmap?.UpdateHitObject(h);
+ }
+
+ ChangeHandler?.EndChange();
}
///
@@ -274,12 +298,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// The name of the hit sample.
public void RemoveHitSample(string sampleName)
{
- changeHandler?.BeginChange();
+ ChangeHandler?.BeginChange();
foreach (var h in SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
- changeHandler?.EndChange();
+ ChangeHandler?.EndChange();
}
#endregion
@@ -297,6 +321,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
+ if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
+ items.Add(createNewComboMenuItem());
+
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
@@ -326,6 +353,41 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual IEnumerable
public class BeatmapSkinProvidingContainer : SkinProvidingContainer
{
- private readonly Bindable beatmapSkins = new Bindable();
- private readonly Bindable beatmapHitsounds = new Bindable();
+ private Bindable beatmapSkins;
+ private Bindable beatmapHitsounds;
- protected override bool AllowConfigurationLookup => beatmapSkins.Value;
- protected override bool AllowDrawableLookup(ISkinComponent component) => beatmapSkins.Value;
- protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value;
- protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value;
+ protected override bool AllowConfigurationLookup
+ {
+ get
+ {
+ if (beatmapSkins == null)
+ throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
+
+ return beatmapSkins.Value;
+ }
+ }
+
+ protected override bool AllowDrawableLookup(ISkinComponent component)
+ {
+ if (beatmapSkins == null)
+ throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
+
+ return beatmapSkins.Value;
+ }
+
+ protected override bool AllowTextureLookup(string componentName)
+ {
+ if (beatmapSkins == null)
+ throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
+
+ return beatmapSkins.Value;
+ }
+
+ protected override bool AllowSampleLookup(ISampleInfo componentName)
+ {
+ if (beatmapSkins == null)
+ throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
+
+ return beatmapHitsounds.Value;
+ }
public BeatmapSkinProvidingContainer(ISkin skin)
: base(skin)
{
}
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
- config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds);
+ var config = parent.Get();
+ beatmapSkins = config.GetBindable(OsuSetting.BeatmapSkins);
+ beatmapHitsounds = config.GetBindable(OsuSetting.BeatmapHitsounds);
+
+ return base.CreateChildDependencies(parent);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
}
diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs
new file mode 100644
index 0000000000..8aa9d4e9af
--- /dev/null
+++ b/osu.Game/Skinning/LegacyRollingCounter.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
+namespace osu.Game.Skinning
+{
+ ///
+ /// An integer that uses number sprites from a legacy skin.
+ ///
+ public class LegacyRollingCounter : RollingCounter
+ {
+ private readonly ISkin skin;
+
+ private readonly string fontName;
+ private readonly float fontOverlap;
+
+ protected override bool IsRollingProportional => true;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The from which to get counter number sprites.
+ /// The name of the legacy font to use.
+ ///
+ /// The numeric overlap of number sprites to use.
+ /// A positive number will bring the number sprites closer together, while a negative number
+ /// will split them apart more.
+ ///
+ public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap)
+ {
+ this.skin = skin;
+ this.fontName = fontName;
+ this.fontOverlap = fontOverlap;
+ }
+
+ protected override double GetProportionalDuration(int currentValue, int newValue)
+ {
+ return Math.Abs(newValue - currentValue) * 75.0;
+ }
+
+ protected sealed override OsuSpriteText CreateSpriteText() =>
+ new LegacySpriteText(skin, fontName)
+ {
+ Spacing = new Vector2(-fontOverlap, 0f)
+ };
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs
index 41b7aea34b..1d5412d93f 100644
--- a/osu.Game/Skinning/LegacySkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacySkinConfiguration.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Skinning
public enum LegacySetting
{
Version,
+ ComboPrefix,
+ ComboOverlap,
AnimationFramerate,
LayeredHitSounds,
}
diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
index bb46dc8b9f..0ee02a2442 100644
--- a/osu.Game/Skinning/LegacySkinExtensions.cs
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -62,6 +62,9 @@ namespace osu.Game.Skinning
}
}
+ public static bool HasFont(this ISkin source, string fontPrefix)
+ => source.GetTexture($"{fontPrefix}-0") != null;
+
public class SkinnableTextureAnimation : TextureAnimation
{
[Resolved(canBeNull: true)]
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index cf629f231f..ba14049b41 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -114,6 +114,8 @@ namespace osu.Game.Skinning
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
+ bool wasPlaying = IsPlaying;
+
var channels = hitSamples.Select(s =>
{
var ch = skin.GetSample(s);
@@ -138,8 +140,9 @@ namespace osu.Game.Skinning
samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
- if (requestedPlaying)
- Play();
+ // Start playback internally for the new samples if the previous ones were playing beforehand.
+ if (wasPlaying)
+ play();
}
#region Re-expose AudioContainer
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index 81c13112d0..a856789d96 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -160,6 +160,11 @@ namespace osu.Game.Tests.Visual
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
+ var lookup = base.GetTexture(componentName, wrapModeS, wrapModeT);
+
+ if (lookup != null)
+ return lookup;
+
// extrapolate frames to test longer animations
if (extrapolateAnimations)
{
@@ -169,7 +174,7 @@ namespace osu.Game.Tests.Visual
return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT);
}
- return base.GetTexture(componentName, wrapModeS, wrapModeT);
+ return null;
}
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 8d23a32c3c..71826e161c 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index d00b174195..90aa903318 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+