diff --git a/.idea/.idea.osu.Desktop/.idea/dataSources.xml b/.idea/.idea.osu.Desktop/.idea/dataSources.xml
deleted file mode 100644
index 10f8c1c84d..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/dataSources.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- sqlite.xerial
- true
- org.sqlite.JDBC
- jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db
-
-
-
-
-
-
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
new file mode 100644
index 0000000000..161c685043
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
@@ -0,0 +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 NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneEditor : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index fac5d03833..3a5322ce82 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public const int RNG_SEED = 1337;
+ public bool HardRockOffsets { get; set; }
+
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
- public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
+ public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
- bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
case Fruit fruit:
- if (shouldApplyHardRockOffset)
+ if (HardRockOffsets)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 23ce444560..76863acc78 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
+using osu.Game.Rulesets.Edit;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@@ -175,12 +177,14 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
+
+ public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
new file mode 100644
index 0000000000..31075db7d1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.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 osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class BananaShowerCompositionTool : HitObjectCompositionTool
+ {
+ public BananaShowerCompositionTool()
+ : base(nameof(BananaShower))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
new file mode 100644
index 0000000000..6dea8b0712
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
@@ -0,0 +1,73 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly TimeSpanOutline outline;
+
+ public BananaShowerPlacementBlueprint()
+ {
+ InternalChild = outline = new TimeSpanOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ if (e.Button != MouseButton.Left) break;
+
+ BeginPlacement(true);
+ return true;
+
+ case PlacementState.Active:
+ if (e.Button != MouseButton.Right) break;
+
+ // If the duration is negative, swap the start and the end time to make the duration positive.
+ if (HitObject.Duration < 0)
+ {
+ HitObject.StartTime = HitObject.EndTime;
+ HitObject.Duration = -HitObject.Duration;
+ }
+
+ EndPlacement(HitObject.Duration > 0);
+ return true;
+ }
+
+ return base.OnMouseDown(e);
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ if (!(result.Time is double time)) return;
+
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ HitObject.StartTime = time;
+ break;
+
+ case PlacementState.Active:
+ HitObject.EndTime = time;
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
new file mode 100644
index 0000000000..9132b1a9e8
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public BananaShowerSelectionBlueprint(BananaShower hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
new file mode 100644
index 0000000000..69054e2c81
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
@@ -0,0 +1,27 @@
+// 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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class CatchPlacementBlueprint : PlacementBlueprint
+ where THitObject : CatchHitObject, new()
+ {
+ protected new THitObject HitObject => (THitObject)base.HitObject;
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public CatchPlacementBlueprint()
+ : base(new THitObject())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
new file mode 100644
index 0000000000..298f9474b0
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
@@ -0,0 +1,38 @@
+// 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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint
+ where THitObject : CatchHitObject
+ {
+ public override Vector2 ScreenSpaceSelectionPoint
+ {
+ get
+ {
+ float x = HitObject.OriginalX;
+ float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
+ }
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ protected CatchSelectionBlueprint(THitObject hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
new file mode 100644
index 0000000000..8769acc382
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
@@ -0,0 +1,38 @@
+// 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.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class FruitOutline : CompositeDrawable
+ {
+ public FruitOutline()
+ {
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.Centre;
+ Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
+ InternalChild = new BorderPiece();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ Colour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
+ {
+ X = hitObject.EffectiveX;
+ Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ Scale = new Vector2(hitObject.Scale);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
new file mode 100644
index 0000000000..65dfce0493
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class TimeSpanOutline : CompositeDrawable
+ {
+ private const float border_width = 4;
+
+ private const float opacity_when_empty = 0.5f;
+
+ private bool isEmpty = true;
+
+ public TimeSpanOutline()
+ {
+ Anchor = Origin = Anchor.BottomLeft;
+ RelativeSizeAxes = Axes.X;
+
+ Masking = true;
+ BorderThickness = border_width;
+ Alpha = opacity_when_empty;
+
+ // A box is needed to make the border visible.
+ InternalChild = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Transparent
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ BorderColour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
+ {
+ float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
+
+ Y = Math.Max(startY, endY);
+ float height = Math.Abs(startY - endY);
+
+ bool wasEmpty = isEmpty;
+ isEmpty = height == 0;
+ if (wasEmpty != isEmpty)
+ this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
+
+ Height = Math.Max(height, border_width);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
new file mode 100644
index 0000000000..0f28cf6786
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitPlacementBlueprint()
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ BeginPlacement();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
+
+ EndPlacement(true);
+ return true;
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
new file mode 100644
index 0000000000..9665aac2fb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
@@ -0,0 +1,27 @@
+// 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.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitSelectionBlueprint : CatchSelectionBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitSelectionBlueprint(Fruit hitObject)
+ : base(hitObject)
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (IsSelected)
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
new file mode 100644
index 0000000000..d6b8c35a09
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
+
+ private float minNestedX;
+ private float maxNestedX;
+
+ public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
+ : base(hitObject)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ HitObject.DefaultsApplied += onDefaultsApplied;
+ computeObjectBounds();
+ }
+
+ private void onDefaultsApplied(HitObject _) => computeObjectBounds();
+
+ private void computeObjectBounds()
+ {
+ minNestedX = HitObject.NestedHitObjects.OfType().Min(nested => nested.OriginalX) - HitObject.OriginalX;
+ maxNestedX = HitObject.NestedHitObjects.OfType().Max(nested => nested.OriginalX) - HitObject.OriginalX;
+ }
+
+ private RectangleF getBoundingBox()
+ {
+ float left = HitObject.OriginalX + minNestedX;
+ float right = HitObject.OriginalX + maxNestedX;
+ float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
+ float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
+ return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ HitObject.DefaultsApplied -= onDefaultsApplied;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
new file mode 100644
index 0000000000..7f2782a474
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -0,0 +1,38 @@
+// 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.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchBlueprintContainer : ComposeBlueprintContainer
+ {
+ public CatchBlueprintContainer(CatchHitObjectComposer composer)
+ : base(composer)
+ {
+ }
+
+ protected override SelectionHandler CreateSelectionHandler() => new CatchSelectionHandler();
+
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ return new FruitSelectionBlueprint(fruit);
+
+ case JuiceStream juiceStream:
+ return new JuiceStreamSelectionBlueprint(juiceStream);
+
+ case BananaShower bananaShower:
+ return new BananaShowerSelectionBlueprint(bananaShower);
+ }
+
+ return base.CreateHitObjectBlueprintFor(hitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
new file mode 100644
index 0000000000..d383eb9ba6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
@@ -0,0 +1,27 @@
+// 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.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchEditorPlayfield : CatchPlayfield
+ {
+ // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
+ public CatchEditorPlayfield(BeatmapDifficulty difficulty)
+ : base(difficulty)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // TODO: honor "hit animation" setting?
+ CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
+
+ // TODO: disable hit lighting as well
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
new file mode 100644
index 0000000000..d9712bc8e9
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.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 System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchHitObjectComposer : HitObjectComposer
+ {
+ public CatchHitObjectComposer(CatchRuleset ruleset)
+ : base(ruleset)
+ {
+ }
+
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
+
+ protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
+ {
+ new FruitCompositionTool(),
+ new BananaShowerCompositionTool()
+ };
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+ // TODO: implement position snap
+ result.ScreenSpacePosition.X = screenSpacePosition.X;
+ return result;
+ }
+
+ protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
new file mode 100644
index 0000000000..c1a491d1ce
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -0,0 +1,46 @@
+// 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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchSelectionHandler : EditorSelectionHandler
+ {
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ {
+ var blueprint = moveEvent.Blueprint;
+ Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
+ Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
+ float deltaX = targetPosition.X - originalPosition.X;
+
+ EditorBeatmap.PerformOnSelection(h =>
+ {
+ if (!(h is CatchHitObject hitObject)) return;
+
+ if (hitObject is BananaShower) return;
+
+ // TODO: confine in bounds
+ hitObject.OriginalXBindable.Value += deltaX;
+
+ // Move the nested hit objects to give an instant result before nested objects are recreated.
+ foreach (var nested in hitObject.NestedHitObjects.OfType())
+ nested.OriginalXBindable.Value += deltaX;
+ });
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
new file mode 100644
index 0000000000..0344709d45
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.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 System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class DrawableCatchEditorRuleset : DrawableCatchRuleset
+ {
+ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
new file mode 100644
index 0000000000..f776fe39c1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.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 osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class FruitCompositionTool : HitObjectCompositionTool
+ {
+ public FruitCompositionTool()
+ : base(nameof(Fruit))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 5f1736450a..bd7a1df2e4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -5,11 +5,12 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModDifficultyAdjust : ModDifficultyAdjust
+ public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
@@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
+ public BindableBool HardRockOffsets { get; } = new BindableBool();
+
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
@@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
+ string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
- approachRate
+ approachRate,
+ spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
@@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
}
+
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = HardRockOffsets.Value;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index 0dde6aa06e..68b6ce96a3 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
+ public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{
public override double ScoreMultiplier => 1.12;
- public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index b23011f1a3..287ed1b4c7 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
///
private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
- public CatchLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public CatchLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
}
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (targetComponent.Target)
{
case SkinnableTarget.MainHUDComponents:
- var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
+ var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
if (providesComboCounter && components != null)
{
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
case CatchSkinComponents.Catcher:
- var version = Source.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
+ var version = GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
if (version < 2.3m)
{
@@ -83,13 +83,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
- return new LegacyCatchComboCounter(Source);
+ return new LegacyCatchComboCounter(Skin);
return null;
}
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup)
{
case CatchSkinColour colour:
- var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour));
+ var result = (Bindable)base.GetConfig(new SkinCustomColourLookup(colour));
if (result == null)
return null;
@@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return (IBindable)result;
}
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index fbb9b3c466..fe736766d9 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 962a13ebea..814a737034 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ HitResult.Miss, "mania-hit0" }
};
- private Lazy isLegacySkin;
+ private readonly Lazy isLegacySkin;
///
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
///
- private Lazy hasKeyTexture;
+ private readonly Lazy hasKeyTexture;
- public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
- : base(source)
+ public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
{
this.beatmap = (ManiaBeatmap)beatmap;
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- isLegacySkin = new Lazy(() => FindProvider(s => s.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
- hasKeyTexture = new Lazy(() => FindProvider(s => s.GetAnimation(
- s.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
- ?? "mania-key1", true, true) != null) != null);
+ isLegacySkin = new Lazy(() => GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null);
+ hasKeyTexture = new Lazy(() =>
+ {
+ var keyImage = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
+ return this.GetAnimation(keyImage, true, true) != null;
+ });
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -125,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break;
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
private Drawable getResult(HitResult result)
@@ -146,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
return new SampleVirtual();
- return Source.GetSample(sampleInfo);
+ return base.GetSample(sampleInfo);
}
public override IBindable GetConfig(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
- return Source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
+ return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index 46274e779b..211b0e8145 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -112,7 +113,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public IBindable GetConfig(TLookup lookup) => null;
- public ISkin FindProvider(Func lookupFunction) => null;
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
+
+ public IEnumerable AllSources => new[] { this };
public event Action SourceChanged
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index fd523fffcb..662cbaee68 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -164,9 +165,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public ISample GetSample(ISampleInfo sampleInfo) => null;
- public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
public IBindable GetConfig(TLookup lookup) => null;
- public ISkin FindProvider(Func lookupFunction) => null;
+
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
+
+ public IEnumerable AllSources => new[] { this };
public event Action SourceChanged;
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 1b9bcd19fd..5f37b0d040 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin);
public int LegacyID => 0;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 3267b48ebf..41b0a88f11 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class OsuLegacySkinTransformer : LegacySkinTransformer
{
- private Lazy hasHitCircle;
+ private readonly Lazy hasHitCircle;
///
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
@@ -20,16 +20,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
///
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
- public OsuLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public OsuLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- hasHitCircle = new Lazy(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null);
+ hasHitCircle = new Lazy(() => GetTexture("hitcircle") != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -49,16 +43,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return followCircle;
case OsuSkinComponents.SliderBall:
- // specular and nd layers must come from the same source as the ball texure.
- var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null);
-
- var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: "");
+ var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
// todo: slider ball has a custom frame delay based on velocity
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null)
- return new LegacySliderBall(sliderBallContent, ballProvider);
+ return new LegacySliderBall(sliderBallContent, this);
return null;
@@ -87,18 +78,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case OsuSkinComponents.Cursor:
- var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null);
-
- if (cursorProvider != null)
- return new LegacyCursor(cursorProvider);
+ if (GetTexture("cursor") != null)
+ return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
- var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null);
-
- if (trailProvider != null)
- return new LegacyCursorTrail(trailProvider);
+ if (GetTexture("cursortrail") != null)
+ return new LegacyCursorTrail(this);
return null;
@@ -113,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
case OsuSkinComponents.SpinnerBody:
- bool hasBackground = Source.GetTexture("spinner-background") != null;
+ bool hasBackground = GetTexture("spinner-background") != null;
- if (Source.GetTexture("spinner-top") != null && !hasBackground)
+ if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
@@ -124,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
@@ -132,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (lookup)
{
case OsuSkinColour colour:
- return Source.GetConfig(new SkinCustomColourLookup(colour));
+ return base.GetConfig(new SkinCustomColourLookup(colour));
case OsuSkinConfiguration osuLookup:
switch (osuLookup)
@@ -146,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
- return Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
- Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
+ return base.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
+ base.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
}
break;
}
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 7ce0f6b93b..a3ecbbc436 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Scoring;
@@ -15,18 +14,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class TaikoLegacySkinTransformer : LegacySkinTransformer
{
- private Lazy hasExplosion;
+ private readonly Lazy hasExplosion;
- public TaikoLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public TaikoLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- hasExplosion = new Lazy(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
+ hasExplosion = new Lazy(() => GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -56,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit:
-
if (GetTexture("taikohitcircle") != null)
return new LegacyHit(taikoComponent.Component);
@@ -91,7 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null;
case TaikoSkinComponents.TaikoExplosionMiss:
-
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
if (missSprite != null)
return new LegacyHitExplosion(missSprite);
@@ -100,7 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.TaikoExplosionOk:
case TaikoSkinComponents.TaikoExplosionGreat:
-
var hitName = getHitName(taikoComponent.Component);
var hitSprite = this.GetAnimation(hitName, true, false);
@@ -132,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
private string getHitName(TaikoSkinComponents component)
@@ -155,13 +145,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override ISample GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is HitSampleInfo hitSampleInfo)
- return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
+ return base.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
return base.GetSample(sampleInfo);
}
- public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup);
-
private class LegacyTaikoSampleInfo : HitSampleInfo
{
public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 5854d4770c..ab5fcf6336 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin);
public const string SHORT_NAME = "taiko";
diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs
index 9f27289d7e..4c126f0a3b 100644
--- a/osu.Game.Tests/Mods/ModUtilsTest.cs
+++ b/osu.Game.Tests/Mods/ModUtilsTest.cs
@@ -14,6 +14,14 @@ namespace osu.Game.Tests.Mods
[TestFixture]
public class ModUtilsTest
{
+ [Test]
+ public void TestModIsNotCompatibleWithItself()
+ {
+ var mod = new Mock();
+ Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object, mod.Object }, out var invalid), Is.False);
+ Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
+ }
+
[Test]
public void TestModIsCompatibleByItself()
{
@@ -147,7 +155,7 @@ namespace osu.Game.Tests.Mods
// multi mod.
new object[]
{
- new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() },
+ new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
new[] { typeof(MultiMod) }
},
// valid pair.
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 07162c3cd1..b6ae91844a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -55,7 +55,12 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestExitWithoutSave()
{
- AddStep("exit without save", () => Editor.Exit());
+ AddStep("exit without save", () =>
+ {
+ Editor.Exit();
+ DialogOverlay.CurrentDialog.PerformOkAction();
+ });
+
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index cc53e50884..13e84e335d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -116,12 +116,12 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestOsuRuleset : OsuRuleset
{
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(skin);
private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer
{
- public TestOsuLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public TestOsuLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 96418f6d28..f29fbbf52b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
@@ -167,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void Disable()
{
allow = false;
- TriggerSourceChanged();
+ OnSourceChanged();
}
public SwitchableSkinProvidingContainer(ISkin skin)
@@ -330,6 +331,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public ISkin FindProvider(Func lookupFunction) => throw new NotImplementedException();
+ public IEnumerable AllSources => throw new NotImplementedException();
+
public event Action SourceChanged
{
add { }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index 55ee01e0d5..ccf13e1e8f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -146,7 +147,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup);
- public ISkin FindProvider(Func lookupFunction) => source?.FindProvider(lookupFunction);
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction);
+ public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty());
public void TriggerSourceChanged()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index c5a6723508..599dfb082b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -6,15 +6,20 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
@@ -159,6 +164,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
}
+ [Test]
+ public void TestLeaveNavigation()
+ {
+ loadMultiplayer();
+
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ AllowedMods = { new OsuModHidden() }
+ }
+ }
+ });
+
+ AddStep("open mod overlay", () => this.ChildrenOfType().ElementAt(2).Click());
+
+ AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
+
+ AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+
+ AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
+
+ testLeave("lounge tab item", () => this.ChildrenOfType.BreadcrumbTabItem>().First().Click());
+
+ testLeave("back button", () => multiplayerScreen.OnBackButton());
+
+ // mimics home button and OS window close
+ testLeave("forced exit", () => multiplayerScreen.Exit());
+
+ void testLeave(string actionName, Action action)
+ {
+ AddStep($"leave via {actionName}", action);
+
+ AddAssert("dialog overlay is visible", () => DialogOverlay.State.Value == Visibility.Visible);
+
+ AddStep("close dialog overlay", () => InputManager.Key(Key.Escape));
+ }
+ }
+
private void createRoom(Func room)
{
AddStep("open room", () =>
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 712a3dd33a..662d24cc83 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -133,6 +133,9 @@ namespace osu.Game.Beatmaps
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
+ foreach (var mod in mods.OfType())
+ mod.ApplyToBeatmapProcessor(processor);
+
processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs
index 40eb4cad15..a3a86df678 100644
--- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs
+++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs
@@ -20,7 +20,8 @@ namespace osu.Game.Graphics.Containers.Markdown
public override MarkdownTextFlowContainer CreateTextFlow() => new HeadingTextFlowContainer
{
- Weight = GetFontWeightByLevel(level),
+ FontSize = GetFontSizeByLevel(level),
+ FontWeight = GetFontWeightByLevel(level),
};
protected override float GetFontSizeByLevel(int level)
@@ -28,27 +29,25 @@ namespace osu.Game.Graphics.Containers.Markdown
// Reference for this font size
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/bem/osu-md.less#L9
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/variables.less#L161
- const float base_font_size = 14;
-
switch (level)
{
case 1:
- return 30 / base_font_size;
+ return 30;
case 2:
- return 26 / base_font_size;
+ return 26;
case 3:
- return 20 / base_font_size;
+ return 20;
case 4:
- return 18 / base_font_size;
+ return 18;
case 5:
- return 16 / base_font_size;
+ return 16;
default:
- return 1;
+ return 14;
}
}
@@ -67,9 +66,11 @@ namespace osu.Game.Graphics.Containers.Markdown
private class HeadingTextFlowContainer : OsuMarkdownTextFlowContainer
{
- public FontWeight Weight { get; set; }
+ public float FontSize;
+ public FontWeight FontWeight;
- protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight));
+ protected override SpriteText CreateSpriteText()
+ => base.CreateSpriteText().With(t => t.Font = t.Font.With(size: FontSize, weight: FontWeight));
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs
index e85525b2f8..d7bd7d7e01 100644
--- a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs
+++ b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs
@@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
namespace osu.Game.Graphics.UserInterface
@@ -19,8 +20,13 @@ namespace osu.Game.Graphics.UserInterface
if (stack.CurrentScreen != null)
onPushed(null, stack.CurrentScreen);
+ }
- Current.ValueChanged += current => current.NewValue.MakeCurrent();
+ protected override void SelectTab(TabItem tab)
+ {
+ // override base method to prevent current item from being changed on click.
+ // depend on screen push/exit to change current item instead.
+ tab.Value.MakeCurrent();
}
private void onPushed(IScreen lastScreen, IScreen newScreen)
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index f81eaa08a5..7cc34114bf 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -261,7 +261,7 @@ namespace osu.Game
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
- // this should likely be moved to ArchiveModelManager when another case appers where it is necessary
+ // this should likely be moved to ArchiveModelManager when another case appears where it is necessary
// to have inter-dependent model managers. this could be obtained with an IHasForeign interface to
// allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete.
List getBeatmapScores(BeatmapSetInfo set)
diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs
index 7f8559e7de..cd1391a3d8 100644
--- a/osu.Game/Overlays/OverlayStreamItem.cs
+++ b/osu.Game/Overlays/OverlayStreamItem.cs
@@ -39,9 +39,9 @@ namespace osu.Game.Overlays
protected OverlayStreamItem(T value)
: base(value)
{
- Height = 60;
- Width = 100;
- Padding = new MarginPadding(5);
+ Height = 50;
+ Width = 90;
+ Margin = new MarginPadding(5);
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
new file mode 100644
index 0000000000..e23a5d8d99
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
@@ -0,0 +1,18 @@
+// 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.Beatmaps;
+
+namespace osu.Game.Rulesets.Mods
+{
+ ///
+ /// Interface for a that applies changes to a .
+ ///
+ public interface IApplicableToBeatmapProcessor : IApplicableMod
+ {
+ ///
+ /// Applies this to a .
+ ///
+ void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor);
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs
new file mode 100644
index 0000000000..001a9d214c
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IHasSeed.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public interface IHasSeed
+ {
+ Bindable Seed { get; }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs
index e0c3008ae8..61297c162d 100644
--- a/osu.Game/Rulesets/Mods/ModRandom.cs
+++ b/osu.Game/Rulesets/Mods/ModRandom.cs
@@ -2,17 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics;
-using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModRandom : Mod
+ public abstract class ModRandom : Mod, IHasSeed
{
public override string Name => "Random";
public override string Acronym => "RD";
@@ -20,88 +16,11 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.Dice;
public override double ScoreMultiplier => 1;
- [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))]
+ [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))]
public Bindable Seed { get; } = new Bindable
{
Default = null,
Value = null
};
-
- private class ModRandomSettingsControl : SettingsItem
- {
- protected override Drawable CreateControl() => new SeedControl
- {
- RelativeSizeAxes = Axes.X,
- Margin = new MarginPadding { Top = 5 }
- };
-
- private sealed class SeedControl : CompositeDrawable, IHasCurrentValue
- {
- private readonly BindableWithCurrent current = new BindableWithCurrent();
-
- public Bindable Current
- {
- get => current;
- set
- {
- current.Current = value;
- seedNumberBox.Text = value.Value.ToString();
- }
- }
-
- private readonly SettingsNumberBox.NumberBox seedNumberBox;
-
- public SeedControl()
- {
- AutoSizeAxes = Axes.Y;
-
- InternalChildren = new[]
- {
- new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.Absolute, 2),
- new Dimension(GridSizeMode.Relative, 0.25f)
- },
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.AutoSize)
- },
- Content = new[]
- {
- new Drawable[]
- {
- seedNumberBox = new SettingsNumberBox.NumberBox
- {
- RelativeSizeAxes = Axes.X,
- CommitOnFocusLost = true
- }
- }
- }
- }
- };
-
- seedNumberBox.Current.BindValueChanged(e =>
- {
- int? value = null;
-
- if (int.TryParse(e.NewValue, out var intVal))
- value = intVal;
-
- current.Value = value;
- });
- }
-
- protected override void Update()
- {
- if (current.Value == null)
- seedNumberBox.Text = current.Current.Value.ToString();
- }
- }
- }
}
}
diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs
new file mode 100644
index 0000000000..5c57717d93
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs
@@ -0,0 +1,92 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Rulesets.Mods
+{
+ ///
+ /// A settings control for use by mods which have a customisable seed value.
+ ///
+ public class SeedSettingsControl : SettingsItem
+ {
+ protected override Drawable CreateControl() => new SeedControl
+ {
+ RelativeSizeAxes = Axes.X,
+ Margin = new MarginPadding { Top = 5 }
+ };
+
+ private sealed class SeedControl : CompositeDrawable, IHasCurrentValue
+ {
+ private readonly BindableWithCurrent current = new BindableWithCurrent();
+
+ public Bindable Current
+ {
+ get => current;
+ set
+ {
+ current.Current = value;
+ seedNumberBox.Text = value.Value.ToString();
+ }
+ }
+
+ private readonly OsuNumberBox seedNumberBox;
+
+ public SeedControl()
+ {
+ AutoSizeAxes = Axes.Y;
+
+ InternalChildren = new[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 2),
+ new Dimension(GridSizeMode.Relative, 0.25f)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ seedNumberBox = new OsuNumberBox
+ {
+ RelativeSizeAxes = Axes.X,
+ CommitOnFocusLost = true
+ }
+ }
+ }
+ }
+ };
+
+ seedNumberBox.Current.BindValueChanged(e =>
+ {
+ int? value = null;
+
+ if (int.TryParse(e.NewValue, out var intVal))
+ value = intVal;
+
+ current.Value = value;
+ });
+ }
+
+ protected override void Update()
+ {
+ if (current.Value == null)
+ seedNumberBox.Text = current.Current.Value.ToString();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index 7bdf84ace4..9fdaca88fd 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Rulesets
[CanBeNull]
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault();
- public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null;
+ public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;
protected Ruleset()
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 8a4d381535..185f029d14 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -94,6 +95,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Creates a for a specific item.
///
/// The item to create the overlay for.
+ [CanBeNull]
protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null;
protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect);
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 3e97e15cca..79b38861ee 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using Humanizer;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -256,9 +257,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (drawable == null)
return null;
- return CreateHitObjectBlueprintFor(item).With(b => b.DrawableObject = drawable);
+ return CreateHitObjectBlueprintFor(item)?.With(b => b.DrawableObject = drawable);
}
+ [CanBeNull]
public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null;
private void hitObjectAdded(HitObject obj)
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 6f04f36b83..a642768574 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
- placementBlueprint = CreateBlueprintFor(obj.NewValue);
+ placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull();
placementBlueprint.Colour = Color4.MediumPurple;
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 61056aeced..b56f9bee14 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -73,15 +73,7 @@ namespace osu.Game.Screens.Edit.Compose
{
Debug.Assert(ruleset != null);
- var beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmap.Value.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, EditorBeatmap.PlayableBeatmap));
-
- // 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.
- return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
+ return new RulesetSkinProvidingContainer(ruleset, EditorBeatmap.PlayableBeatmap, beatmap.Value.Skin).WithChild(content);
}
#region Input Handling
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index f9b3549f3c..4b8c4422ec 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -305,18 +305,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
+ return base.OnBackButton();
+ }
+
+ public override bool OnExiting(IScreen next)
+ {
+ if (client.Room == null)
+ {
+ // room has not been created yet; exit immediately.
+ return base.OnExiting(next);
+ }
+
if (!exitConfirmed && dialogOverlay != null)
{
- dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
+ if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
+ confirmDialog.PerformOkAction();
+ else
{
- exitConfirmed = true;
- this.Exit();
- }));
+ dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
+ {
+ exitConfirmed = true;
+ this.Exit();
+ }));
+ }
return true;
}
- return base.OnBackButton();
+ return base.OnExiting(next);
}
private ModSettingChangeTracker modSettingChangeTracker;
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index e418d36d40..ceee002c6e 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -240,13 +240,15 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(IScreen next)
{
+ if (screenStack.CurrentScreen?.OnExiting(next) == true)
+ return true;
+
RoomManager.PartRoom();
waves.Hide();
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
- screenStack.CurrentScreen?.OnExiting(next);
base.OnExiting(next);
return false;
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index cadcc474b2..58f60d14cf 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -228,29 +228,23 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(GameplayBeatmap);
- var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.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(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
+ var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin);
// 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.
- GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider));
+ GameplayClockContainer.Add(rulesetSkinProvider);
rulesetSkinProvider.AddRange(new[]
{
- // underlay and gameplay should have access the to skinning sources.
+ // underlay and gameplay should have access to the skinning sources.
createUnderlayComponents(),
createGameplayComponents(Beatmap.Value, playableBeatmap)
});
- // also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
- // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
- var hudRulesetContainer = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
-
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
- GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value)));
+ // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
+ // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
+ rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value));
if (!DrawableRuleset.AllowGameplayOverlays)
{
diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
index 57c08a903f..f12f44e347 100644
--- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
@@ -83,9 +83,9 @@ namespace osu.Game.Skinning
[BackgroundDependencyLoader]
private void load()
{
- beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
- beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
- beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
+ beatmapSkins.BindValueChanged(_ => OnSourceChanged());
+ beatmapColours.BindValueChanged(_ => OnSourceChanged());
+ beatmapHitsounds.BindValueChanged(_ => OnSourceChanged());
}
}
}
diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs
index c7ebe91d64..ba3e2bf6ad 100644
--- a/osu.Game/Skinning/ISkinSource.cs
+++ b/osu.Game/Skinning/ISkinSource.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using JetBrains.Annotations;
namespace osu.Game.Skinning
@@ -20,5 +21,10 @@ namespace osu.Game.Skinning
/// The skin to be used for subsequent lookups, or null if none is available.
[CanBeNull]
ISkin FindProvider(Func lookupFunction);
+
+ ///
+ /// Retrieve all sources available for lookup, with highest priority source first.
+ ///
+ IEnumerable AllSources { get; }
}
}
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index 613b0218f2..92b7a04dee 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using JetBrains.Annotations;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -16,45 +17,38 @@ namespace osu.Game.Skinning
///
/// Transformer used to handle support of legacy features for individual rulesets.
///
- public abstract class LegacySkinTransformer : ISkinSource
+ public abstract class LegacySkinTransformer : ISkin
{
///
- /// Source of the which is being transformed.
+ /// The which is being transformed.
///
- protected ISkinSource Source { get; }
+ [NotNull]
+ protected ISkin Skin { get; }
- protected LegacySkinTransformer(ISkinSource source)
+ protected LegacySkinTransformer([NotNull] ISkin skin)
{
- Source = source;
+ Skin = skin ?? throw new ArgumentNullException(nameof(skin));
}
- public abstract Drawable GetDrawableComponent(ISkinComponent component);
+ public virtual Drawable GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component);
public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
- => Source.GetTexture(componentName, wrapModeS, wrapModeT);
+ => Skin.GetTexture(componentName, wrapModeS, wrapModeT);
public virtual ISample GetSample(ISampleInfo sampleInfo)
{
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
- return Source.GetSample(sampleInfo);
+ return Skin.GetSample(sampleInfo);
var playLayeredHitSounds = GetConfig(LegacySetting.LayeredHitSounds);
if (legacySample.IsLayered && playLayeredHitSounds?.Value == false)
return new SampleVirtual();
- return Source.GetSample(sampleInfo);
+ return Skin.GetSample(sampleInfo);
}
- public abstract IBindable GetConfig(TLookup lookup);
-
- public ISkin FindProvider(Func lookupFunction) => Source.FindProvider(lookupFunction);
-
- public event Action SourceChanged
- {
- add => Source.SourceChanged += value;
- remove => Source.SourceChanged -= value;
- }
+ public virtual IBindable GetConfig(TLookup lookup) => Skin.GetConfig(lookup);
}
}
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
new file mode 100644
index 0000000000..c48aeca99a
--- /dev/null
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.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 JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Skinning
+{
+ ///
+ /// A type of specialized for and other gameplay-related components.
+ /// Providing access to parent skin sources and the beatmap skin each surrounded with the ruleset legacy skin transformer.
+ ///
+ public class RulesetSkinProvidingContainer : SkinProvidingContainer
+ {
+ protected readonly Ruleset Ruleset;
+ protected readonly IBeatmap Beatmap;
+
+ ///
+ /// This container already re-exposes all parent sources in a ruleset-usable form.
+ /// Therefore disallow falling back to any parent any further.
+ ///
+ protected override bool AllowFallingBackToParent => false;
+
+ protected override Container Content { get; }
+
+ public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin)
+ {
+ Ruleset = ruleset;
+ Beatmap = beatmap;
+
+ InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin is LegacySkin ? GetLegacyRulesetTransformedSkin(beatmapSkin) : beatmapSkin)
+ {
+ Child = Content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ };
+ }
+
+ [Resolved]
+ private ISkinSource skinSource { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ UpdateSkins();
+ skinSource.SourceChanged += OnSourceChanged;
+ }
+
+ protected override void OnSourceChanged()
+ {
+ UpdateSkins();
+ base.OnSourceChanged();
+ }
+
+ protected virtual void UpdateSkins()
+ {
+ SkinSources.Clear();
+
+ foreach (var skin in skinSource.AllSources)
+ {
+ switch (skin)
+ {
+ case LegacySkin legacySkin:
+ SkinSources.Add(GetLegacyRulesetTransformedSkin(legacySkin));
+ break;
+
+ default:
+ SkinSources.Add(skin);
+ break;
+ }
+ }
+ }
+
+ protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
+ {
+ if (legacySkin == null)
+ return null;
+
+ var rulesetTransformed = Ruleset.CreateLegacySkinProvider(legacySkin, Beatmap);
+ if (rulesetTransformed != null)
+ return rulesetTransformed;
+
+ return legacySkin;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (skinSource != null)
+ skinSource.SourceChanged -= OnSourceChanged;
+ }
+ }
+}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 9e274227a2..4cde4cd2b8 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -30,6 +30,13 @@ using osu.Game.IO.Archives;
namespace osu.Game.Skinning
{
+ ///
+ /// Handles the storage and retrieval of s.
+ ///
+ ///
+ /// This is also exposed and cached as to allow for any component to potentially have skinning support.
+ /// For gameplay components, see which adds extra legacy and toggle logic that may affect the lookup process.
+ ///
[ExcludeFromDynamicCompile]
public class SkinManager : ArchiveModelManager, ISkinSource, IStorageResourceProvider
{
@@ -48,9 +55,15 @@ namespace osu.Game.Skinning
protected override string ImportFromStablePath => "Skins";
- private readonly Skin defaultLegacySkin;
+ ///
+ /// The default skin.
+ ///
+ public Skin DefaultSkin { get; }
- private readonly Skin defaultSkin;
+ ///
+ /// The default legacy skin.
+ ///
+ public Skin DefaultLegacySkin { get; }
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), host)
@@ -59,12 +72,12 @@ namespace osu.Game.Skinning
this.host = host;
this.resources = resources;
- defaultLegacySkin = new DefaultLegacySkin(this);
- defaultSkin = new DefaultSkin(this);
+ DefaultLegacySkin = new DefaultLegacySkin(this);
+ DefaultSkin = new DefaultSkin(this);
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
- CurrentSkin.Value = defaultSkin;
+ CurrentSkin.Value = DefaultSkin;
CurrentSkin.ValueChanged += skin =>
{
if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value)
@@ -83,8 +96,8 @@ namespace osu.Game.Skinning
public List GetAllUsableSkins()
{
var userSkins = GetAllUserSkins();
- userSkins.Insert(0, SkinInfo.Default);
- userSkins.Insert(1, DefaultLegacySkin.Info);
+ userSkins.Insert(0, DefaultSkin.SkinInfo);
+ userSkins.Insert(1, DefaultLegacySkin.SkinInfo);
return userSkins;
}
@@ -223,32 +236,39 @@ namespace osu.Game.Skinning
public ISkin FindProvider(Func lookupFunction)
{
- if (lookupFunction(CurrentSkin.Value))
- return CurrentSkin.Value;
-
- if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin))
- return defaultLegacySkin;
-
- if (lookupFunction(defaultSkin))
- return defaultSkin;
+ foreach (var source in AllSources)
+ {
+ if (lookupFunction(source))
+ return source;
+ }
return null;
}
+ public IEnumerable AllSources
+ {
+ get
+ {
+ yield return CurrentSkin.Value;
+
+ if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
+ yield return DefaultLegacySkin;
+
+ if (CurrentSkin.Value != DefaultSkin)
+ yield return DefaultSkin;
+ }
+ }
+
private T lookupWithFallback(Func lookupFunction)
where T : class
{
- if (lookupFunction(CurrentSkin.Value) is T skinSourced)
- return skinSourced;
+ foreach (var source in AllSources)
+ {
+ if (lookupFunction(source) is T skinSourced)
+ return skinSourced;
+ }
- // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements.
- // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow
- // for beatmap skin visibility).
- if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin) is T legacySourced)
- return legacySourced;
-
- // Finally fall back to the (non-legacy) default.
- return lookupFunction(defaultSkin);
+ return null;
}
#region IResourceStorageProvider
diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
index b1929aac6f..c83c299723 100644
--- a/osu.Game/Skinning/SkinProvidingContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -2,6 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -21,13 +24,24 @@ namespace osu.Game.Skinning
{
public event Action SourceChanged;
- [CanBeNull]
- private readonly ISkin skin;
+ ///
+ /// Skins which should be exposed by this container, in order of lookup precedence.
+ ///
+ protected readonly BindableList SkinSources = new BindableList();
+
+ ///
+ /// A dictionary mapping each from the
+ /// to one that performs the "allow lookup" checks before proceeding with a lookup.
+ ///
+ private readonly Dictionary disableableSkinSources = new Dictionary();
[CanBeNull]
private ISkinSource fallbackSource;
- private readonly NoFallbackProxy noFallbackLookupProxy;
+ ///
+ /// Whether falling back to parent s is allowed in this container.
+ ///
+ protected virtual bool AllowFallingBackToParent => true;
protected virtual bool AllowDrawableLookup(ISkinComponent component) => true;
@@ -39,123 +53,159 @@ namespace osu.Game.Skinning
protected virtual bool AllowColourLookup => true;
- public SkinProvidingContainer(ISkin skin)
+ ///
+ /// Constructs a new initialised with a single skin source.
+ ///
+ public SkinProvidingContainer([CanBeNull] ISkin skin)
+ : this()
{
- this.skin = skin;
+ if (skin != null)
+ SkinSources.Add(skin);
+ }
+ ///
+ /// Constructs a new with no sources.
+ /// Implementations can add or change sources through the list.
+ ///
+ protected SkinProvidingContainer()
+ {
RelativeSizeAxes = Axes.Both;
- noFallbackLookupProxy = new NoFallbackProxy(this);
+ SkinSources.BindCollectionChanged(((_, args) =>
+ {
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ foreach (var skin in args.NewItems.Cast())
+ {
+ disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
- if (skin is ISkinSource source)
- source.SourceChanged += TriggerSourceChanged;
+ if (skin is ISkinSource source)
+ source.SourceChanged += OnSourceChanged;
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var skin in args.OldItems.Cast())
+ {
+ disableableSkinSources.Remove(skin);
+
+ if (skin is ISkinSource source)
+ source.SourceChanged -= OnSourceChanged;
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ foreach (var skin in args.OldItems.Cast())
+ {
+ disableableSkinSources.Remove(skin);
+
+ if (skin is ISkinSource source)
+ source.SourceChanged -= OnSourceChanged;
+ }
+
+ foreach (var skin in args.NewItems.Cast())
+ {
+ disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
+
+ if (skin is ISkinSource source)
+ source.SourceChanged += OnSourceChanged;
+ }
+
+ break;
+ }
+ }), true);
}
public ISkin FindProvider(Func lookupFunction)
{
- if (skin is ISkinSource source)
+ foreach (var skin in SkinSources)
{
- if (source.FindProvider(lookupFunction) is ISkin found)
- return found;
- }
- else if (skin != null)
- {
- // a proxy must be used here to correctly pass through the "Allow" checks without implicitly falling back to the fallbackSource.
- if (lookupFunction(noFallbackLookupProxy))
+ if (lookupFunction(disableableSkinSources[skin]))
return skin;
}
return fallbackSource?.FindProvider(lookupFunction);
}
- public Drawable GetDrawableComponent(ISkinComponent component)
- => GetDrawableComponent(component, true);
-
- public Drawable GetDrawableComponent(ISkinComponent component, bool fallback)
+ public IEnumerable AllSources
{
- Drawable sourceDrawable;
- if (AllowDrawableLookup(component) && (sourceDrawable = skin?.GetDrawableComponent(component)) != null)
- return sourceDrawable;
+ get
+ {
+ foreach (var skin in SkinSources)
+ yield return skin;
- if (!fallback)
- return null;
+ if (fallbackSource != null)
+ {
+ foreach (var skin in fallbackSource.AllSources)
+ yield return skin;
+ }
+ }
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component)
+ {
+ foreach (var skin in SkinSources)
+ {
+ Drawable sourceDrawable;
+ if ((sourceDrawable = disableableSkinSources[skin]?.GetDrawableComponent(component)) != null)
+ return sourceDrawable;
+ }
return fallbackSource?.GetDrawableComponent(component);
}
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
- => GetTexture(componentName, wrapModeS, wrapModeT, true);
-
- public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool fallback)
{
- Texture sourceTexture;
- if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
- return sourceTexture;
-
- if (!fallback)
- return null;
+ foreach (var skin in SkinSources)
+ {
+ Texture sourceTexture;
+ if ((sourceTexture = disableableSkinSources[skin]?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
+ return sourceTexture;
+ }
return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
}
public ISample GetSample(ISampleInfo sampleInfo)
- => GetSample(sampleInfo, true);
-
- public ISample GetSample(ISampleInfo sampleInfo, bool fallback)
{
- ISample sourceChannel;
- if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
- return sourceChannel;
-
- if (!fallback)
- return null;
+ foreach (var skin in SkinSources)
+ {
+ ISample sourceSample;
+ if ((sourceSample = disableableSkinSources[skin]?.GetSample(sampleInfo)) != null)
+ return sourceSample;
+ }
return fallbackSource?.GetSample(sampleInfo);
}
public IBindable GetConfig(TLookup lookup)
- => GetConfig(lookup, true);
-
- public IBindable GetConfig(TLookup lookup, bool fallback)
{
- if (skin != null)
+ foreach (var skin in SkinSources)
{
- if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup)
- return lookupWithFallback(lookup, AllowColourLookup, fallback);
-
- return lookupWithFallback(lookup, AllowConfigurationLookup, fallback);
- }
-
- if (!fallback)
- return null;
-
- return fallbackSource?.GetConfig(lookup);
- }
-
- private IBindable lookupWithFallback(TLookup lookup, bool canUseSkinLookup, bool canUseFallback)
- {
- if (canUseSkinLookup)
- {
- var bindable = skin?.GetConfig(lookup);
- if (bindable != null)
+ IBindable bindable;
+ if ((bindable = disableableSkinSources[skin]?.GetConfig(lookup)) != null)
return bindable;
}
- if (!canUseFallback)
- return null;
-
return fallbackSource?.GetConfig(lookup);
}
- protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke();
+ protected virtual void OnSourceChanged() => SourceChanged?.Invoke();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- fallbackSource = dependencies.Get();
- if (fallbackSource != null)
- fallbackSource.SourceChanged += TriggerSourceChanged;
+ if (AllowFallingBackToParent)
+ {
+ fallbackSource = dependencies.Get();
+ if (fallbackSource != null)
+ fallbackSource.SourceChanged += OnSourceChanged;
+ }
dependencies.CacheAs(this);
@@ -170,41 +220,67 @@ namespace osu.Game.Skinning
base.Dispose(isDisposing);
if (fallbackSource != null)
- fallbackSource.SourceChanged -= TriggerSourceChanged;
+ fallbackSource.SourceChanged -= OnSourceChanged;
- if (skin is ISkinSource source)
- source.SourceChanged -= TriggerSourceChanged;
+ foreach (var source in SkinSources.OfType())
+ source.SourceChanged -= OnSourceChanged;
}
- private class NoFallbackProxy : ISkinSource
+ private class DisableableSkinSource : ISkin
{
+ private readonly ISkin skin;
private readonly SkinProvidingContainer provider;
- public NoFallbackProxy(SkinProvidingContainer provider)
+ public DisableableSkinSource(ISkin skin, SkinProvidingContainer provider)
{
+ this.skin = skin;
this.provider = provider;
}
public Drawable GetDrawableComponent(ISkinComponent component)
- => provider.GetDrawableComponent(component, false);
-
- public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
- => provider.GetTexture(componentName, wrapModeS, wrapModeT, false);
-
- public ISample GetSample(ISampleInfo sampleInfo)
- => provider.GetSample(sampleInfo, false);
-
- public IBindable GetConfig(TLookup lookup)
- => provider.GetConfig(lookup, false);
-
- public event Action SourceChanged
{
- add => provider.SourceChanged += value;
- remove => provider.SourceChanged -= value;
+ if (provider.AllowDrawableLookup(component))
+ return skin.GetDrawableComponent(component);
+
+ return null;
}
- public ISkin FindProvider(Func lookupFunction) =>
- provider.FindProvider(lookupFunction);
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
+ {
+ if (provider.AllowTextureLookup(componentName))
+ return skin.GetTexture(componentName, wrapModeS, wrapModeT);
+
+ return null;
+ }
+
+ public ISample GetSample(ISampleInfo sampleInfo)
+ {
+ if (provider.AllowSampleLookup(sampleInfo))
+ return skin.GetSample(sampleInfo);
+
+ return null;
+ }
+
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case GlobalSkinColours _:
+ case SkinCustomColourLookup _:
+ if (provider.AllowColourLookup)
+ return skin.GetConfig(lookup);
+
+ break;
+
+ default:
+ if (provider.AllowConfigurationLookup)
+ return skin.GetConfig(lookup);
+
+ break;
+ }
+
+ return null;
+ }
}
}
}
diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs
index 2540b6d7da..e0c2965fa0 100644
--- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs
+++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs
@@ -159,7 +159,9 @@ namespace osu.Game.Tests.Beatmaps
remove { }
}
- public ISkin FindProvider(Func lookupFunction) => null;
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
+
+ public IEnumerable AllSources => new[] { this };
}
}
}
diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs
index 33cc00e748..b30be05ac4 100644
--- a/osu.Game/Tests/Visual/ScreenTestScene.cs
+++ b/osu.Game/Tests/Visual/ScreenTestScene.cs
@@ -1,9 +1,11 @@
// 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.Testing;
+using osu.Game.Overlays;
using osu.Game.Screens;
namespace osu.Game.Tests.Visual
@@ -19,12 +21,16 @@ namespace osu.Game.Tests.Visual
protected override Container Content => content;
+ [Cached]
+ protected DialogOverlay DialogOverlay { get; private set; }
+
protected ScreenTestScene()
{
base.Content.AddRange(new Drawable[]
{
Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
- content = new Container { RelativeSizeAxes = Axes.Both }
+ content = new Container { RelativeSizeAxes = Axes.Both },
+ DialogOverlay = new DialogOverlay()
});
}
diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs
index 98766cb844..7485950f47 100644
--- a/osu.Game/Utils/ModUtils.cs
+++ b/osu.Game/Utils/ModUtils.cs
@@ -51,14 +51,31 @@ namespace osu.Game.Utils
/// Whether all s in the combination are compatible with each-other.
public static bool CheckCompatibleSet(IEnumerable combination, [NotNullWhen(false)] out List? invalidMods)
{
- combination = FlattenMods(combination).ToArray();
+ var mods = FlattenMods(combination).ToArray();
invalidMods = null;
- foreach (var mod in combination)
+ // ensure there are no duplicate mod definitions.
+ for (int i = 0; i < mods.Length; i++)
+ {
+ var candidate = mods[i];
+
+ for (int j = i + 1; j < mods.Length; j++)
+ {
+ var m = mods[j];
+
+ if (candidate.Equals(m))
+ {
+ invalidMods ??= new List();
+ invalidMods.Add(m);
+ }
+ }
+ }
+
+ foreach (var mod in mods)
{
foreach (var type in mod.IncompatibleMods)
{
- foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
+ foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m)))
{
if (invalid == mod)
continue;