diff --git a/osu.Android.props b/osu.Android.props index d59e41ba6d..cd57d7478e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs new file mode 100644 index 0000000000..158c8edba5 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs @@ -0,0 +1,66 @@ +// 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.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public class CatchEditorTestSceneContainer : Container + { + [Cached(typeof(Playfield))] + public readonly ScrollingPlayfield Playfield; + + protected override Container Content { get; } + + public CatchEditorTestSceneContainer() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Width = CatchPlayfield.WIDTH; + Height = 1000; + Padding = new MarginPadding + { + Bottom = 100 + }; + + InternalChildren = new Drawable[] + { + new ScrollingTestContainer(ScrollingDirection.Down) + { + TimeRange = 1000, + RelativeSizeAxes = Axes.Both, + Child = Playfield = new TestCatchPlayfield + { + RelativeSizeAxes = Axes.Both + } + }, + new PlayfieldBorder + { + PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full }, + Clock = new FramedClock(new StopwatchClock(true)) + }, + Content = new Container + { + RelativeSizeAxes = Axes.Both + } + }; + } + + private class TestCatchPlayfield : CatchEditorPlayfield + { + public TestCatchPlayfield() + : base(new BeatmapDifficulty { CircleSize = 0 }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs new file mode 100644 index 0000000000..1d30ae34cd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene + { + protected const double TIME_SNAP = 100; + + protected DrawableCatchHitObject LastObject; + + protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer; + + protected override Container Content => contentContainer; + + private readonly CatchEditorTestSceneContainer contentContainer; + + protected CatchPlacementBlueprintTestScene() + { + base.Content.Add(contentContainer = new CatchEditorTestSceneContainer()); + + contentContainer.Playfield.Clock = new FramedClock(new ManualClock()); + } + + [SetUp] + public void Setup() => Schedule(() => + { + HitObjectContainer.Clear(); + ResetPlacement(); + LastObject = null; + }); + + protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () => + { + float y = HitObjectContainer.PositionAtTime(time); + Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight)); + InputManager.MoveMouseTo(pos); + }); + + protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () => + { + InputManager.Click(button); + }); + + protected IEnumerable FruitOutlines => Content.ChildrenOfType(); + + // Unused because AddHitObject is overriden + protected override Container CreateHitObjectContainer() => new Container(); + + protected override void AddHitObject(DrawableHitObject hitObject) + { + LastObject = (DrawableCatchHitObject)hitObject; + contentContainer.Playfield.HitObjectContainer.Add(hitObject); + } + + protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) + { + var result = base.SnapForBlueprint(blueprint); + result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; + return result; + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs new file mode 100644 index 0000000000..dcdc32145b --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.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.Framework.Graphics.Containers; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene + { + protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer; + + protected override Container Content => contentContainer; + + private readonly CatchEditorTestSceneContainer contentContainer; + + protected CatchSelectionBlueprintTestScene() + { + base.Content.Add(contentContainer = new CatchEditorTestSceneContainer()); + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs new file mode 100644 index 0000000000..e3811b7669 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Edit.Blueprints; +using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene + { + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject); + + protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint(); + + protected override void AddHitObject(DrawableHitObject hitObject) + { + // Create nested bananas (but positions are not randomized because beatmap processing is not done). + hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty); + + base.AddHitObject(hitObject); + } + + [Test] + public void TestBasicPlacement() + { + const double start_time = 100; + const double end_time = 500; + + AddMoveStep(start_time, 0); + AddClickStep(MouseButton.Left); + AddMoveStep(end_time, 0); + AddClickStep(MouseButton.Right); + AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower); + AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time)); + AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time)); + } + + [Test] + public void TestReversePlacement() + { + const double start_time = 100; + const double end_time = 500; + + AddMoveStep(end_time, 0); + AddClickStep(MouseButton.Left); + AddMoveStep(start_time, 0); + AddClickStep(MouseButton.Right); + AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time)); + AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time)); + } + + [Test] + public void TestFinishWithZeroDuration() + { + AddMoveStep(100, 0); + AddClickStep(MouseButton.Left); + AddClickStep(MouseButton.Right); + AddAssert("banana shower is not placed", () => LastObject == null); + AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting); + } + + [Test] + public void TestOpacity() + { + AddMoveStep(100, 0); + AddClickStep(MouseButton.Left); + AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha)); + AddMoveStep(200, 0); + AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1)); + AddMoveStep(100, 0); + AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha)); + } + + private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType().Single(); + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs new file mode 100644 index 0000000000..4b1c45ae2f --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Edit.Blueprints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene + { + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject); + + protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint(); + + [Test] + public void TestFruitPlacementPosition() + { + const double time = 300; + const float x = CatchPlayfield.CENTER_X; + + AddMoveStep(time, x); + AddClickStep(MouseButton.Left); + + AddAssert("outline position is correct", () => + { + var outline = FruitOutlines.Single(); + return Precision.AlmostEquals(outline.X, x) && + Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time)); + }); + + AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time)); + AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x)); + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs new file mode 100644 index 0000000000..1b96175020 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.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.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Edit.Blueprints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene + { + public TestSceneJuiceStreamSelectionBlueprint() + { + var hitObject = new JuiceStream + { + OriginalX = 100, + StartTime = 100, + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(200, 100), + new Vector2(0, 200), + }), + }; + var controlPoint = new ControlPointInfo(); + controlPoint.Add(0, new TimingControlPoint + { + BeatLength = 100 + }); + hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 }); + AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject)); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index 271fbde5c3..449401c0bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -31,10 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache(); [Test] - public void TestLocal([Values("Beatmap", "Some long title and stuff")] - string title, - [Values("Trial", "Some1's very hardest difficulty")] - string version) + public void TestLocal( + [Values("Beatmap", "Some long title and stuff")] + string title, + [Values("Trial", "Some1's very hardest difficulty")] + string version) { showMetadataForBeatmap(() => CreateWorkingBeatmap(new Beatmap { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index bf494d4362..e0d76b3e4a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -157,6 +157,23 @@ namespace osu.Game.Tests.Visual.UserInterface checkBindableAtValue("Circle Size", 3); } + [Test] + public void TestResetToDefaults() + { + setBeatmapWithDifficultyParameters(5); + + setSliderValue("Circle Size", 3); + setExtendedLimits(true); + + checkSliderAtValue("Circle Size", 3); + checkBindableAtValue("Circle Size", 3); + + AddStep("reset mod settings", () => modDifficultyAdjust.ResetSettingsToDefaults()); + + checkSliderAtValue("Circle Size", 5); + checkBindableAtValue("Circle Size", null); + } + private void resetToDefault(string name) { AddStep($"Reset {name} to default", () => diff --git a/osu.Game/Rulesets/Edit/HitObjectSelectionBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectSelectionBlueprint.cs index 56434b1d82..77dc55c6ef 100644 --- a/osu.Game/Rulesets/Edit/HitObjectSelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectSelectionBlueprint.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Edit /// protected virtual bool AlwaysShowWhenSelected => false; - protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected); + protected override bool ShouldBeAlive => (DrawableObject?.IsAlive == true && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected); protected HitObjectSelectionBlueprint(HitObject hitObject) : base(hitObject) diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index 1f76a3e366..664b88eef4 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mods } public DifficultyBindable() + : this(null) + { + } + + public DifficultyBindable(float? defaultValue = null) + : base(defaultValue) { ExtendedLimits.BindValueChanged(_ => updateMaxValue()); } @@ -93,15 +99,35 @@ namespace osu.Game.Rulesets.Mods CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue; } - public new DifficultyBindable GetBoundCopy() => new DifficultyBindable + public override void BindTo(Bindable them) { - BindTarget = this, - CurrentNumber = { BindTarget = CurrentNumber }, - ExtendedLimits = { BindTarget = ExtendedLimits }, - ReadCurrentFromDifficulty = ReadCurrentFromDifficulty, - // the following is only safe as long as these values are effectively constants. - MaxValue = maxValue, - ExtendedMaxValue = extendedMaxValue - }; + if (!(them is DifficultyBindable otherDifficultyBindable)) + throw new InvalidOperationException($"Cannot bind to a non-{nameof(DifficultyBindable)}."); + + ReadCurrentFromDifficulty = otherDifficultyBindable.ReadCurrentFromDifficulty; + + // the following max value copies are only safe as long as these values are effectively constants. + MaxValue = otherDifficultyBindable.maxValue; + ExtendedMaxValue = otherDifficultyBindable.extendedMaxValue; + + ExtendedLimits.BindTarget = otherDifficultyBindable.ExtendedLimits; + + // the actual values need to be copied after the max value constraints. + CurrentNumber.BindTarget = otherDifficultyBindable.CurrentNumber; + base.BindTo(them); + } + + public override void UnbindFrom(IUnbindable them) + { + if (!(them is DifficultyBindable otherDifficultyBindable)) + throw new InvalidOperationException($"Cannot unbind from a non-{nameof(DifficultyBindable)}."); + + base.UnbindFrom(them); + + CurrentNumber.UnbindFrom(otherDifficultyBindable.CurrentNumber); + ExtendedLimits.UnbindFrom(otherDifficultyBindable.ExtendedLimits); + } + + public new DifficultyBindable GetBoundCopy() => new DifficultyBindable { BindTarget = this }; } } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 2dc77fa72a..42cf826bd4 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -17,11 +17,11 @@ namespace osu.Game.Tests.Visual public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler { protected readonly Container HitObjectContainer; - private PlacementBlueprint currentBlueprint; + protected PlacementBlueprint CurrentBlueprint { get; private set; } protected PlacementBlueprintTestScene() { - Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); + base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); } [BackgroundDependencyLoader] @@ -63,9 +63,9 @@ namespace osu.Game.Tests.Visual protected void ResetPlacement() { - if (currentBlueprint != null) - Remove(currentBlueprint); - Add(currentBlueprint = CreateBlueprint()); + if (CurrentBlueprint != null) + Remove(CurrentBlueprint); + Add(CurrentBlueprint = CreateBlueprint()); } public void Delete(HitObject hitObject) @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - currentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(currentBlueprint)); + CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); } protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index dc12a4999d..c3fb3bfc17 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual }); } - protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, DrawableHitObject drawableObject) + protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, [CanBeNull] DrawableHitObject drawableObject = null) { Add(blueprint.With(d => { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e79901c4bb..eb7a0141c7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 60b358b274..2e5fab758d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +