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;