diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 5d11c574b1..a4f54bfe82 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + // todo: add support for HT DT const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs similarity index 97% rename from osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 0fe4a3c669..ece523e84c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs similarity index 95% rename from osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 149f6582ab..176fbba921 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs similarity index 96% rename from osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs index 3b9c03b86a..d3afbc63eb 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { [TestFixture] public class TestSceneEditor : EditorTestScene diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs similarity index 93% rename from osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs index b4332264b9..87c74a12cf 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs similarity index 97% rename from osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs index 90394f3d1b..24f4c6858e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs similarity index 98% rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 639be0bc11..654b752001 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -20,7 +20,7 @@ using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneManiaBeatSnapGrid : EditorClockTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs similarity index 99% rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 1a3fa29d4a..c9551ee79e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -23,7 +23,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneManiaHitObjectComposer : EditorClockTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs similarity index 97% rename from osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs index 2d97e61aa5..36c34a8fb9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs @@ -18,7 +18,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs similarity index 96% rename from osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs index 1514bdf0bd..0e47a12a8e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 483327d5b3..3ebbe5af8e 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var actions = new List(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs similarity index 94% rename from osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index 4c6abc45f7..7bccec6c97 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs similarity index 98% rename from osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs index 0ecce42e88..66cd405195 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 0d0be2953b..1232369a0b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -19,7 +19,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs similarity index 97% rename from osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 21fa283b6d..738a21b17e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestScenePathControlPointVisualiser : OsuTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index fe9973f4d8..49d7d9249c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -14,7 +14,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index d5be538d94..f6e1be693b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -16,7 +16,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs similarity index 94% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index d74d072857..fa6c660b01 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs similarity index 96% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs index 011463ab14..4248f68a60 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index a8719e0aa8..746ff4ac19 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. /// - private const double editor_hit_object_fade_out_extension = 500; + private const double editor_hit_object_fade_out_extension = 700; public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) @@ -32,20 +33,37 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState(DrawableHitObject hitObject, ArmedState state) { - switch (state) + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) { - case ArmedState.Miss: - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - if (existing == null) - return; + default: + // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + return; - hitObject.RemoveTransform(existing); + case DrawableSlider _: + // no specifics to sliders but let them fade slower below. + break; - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); break; } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); } protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0d0a139a8a..912a705d16 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -9,7 +9,9 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -17,6 +19,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -39,11 +42,11 @@ namespace osu.Game.Rulesets.Osu.Edit new SpinnerCompositionTool() }; - private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" }; + private readonly Bindable distanceSnapToggle = new Bindable(); - protected override IEnumerable> Toggles => base.Toggles.Concat(new[] + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] { - distanceSnapToggle + new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) }); private BindableList selectedHitObjects; @@ -156,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Edit distanceSnapGridCache.Invalidate(); distanceSnapGrid = null; - if (!distanceSnapToggle.Value) + if (distanceSnapToggle.Value != TernaryState.True) return; switch (BlueprintContainer.CurrentTool) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 76b2631894..9b350278f3 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Osu.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + buttonIndex = 0; AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs new file mode 100644 index 0000000000..d1c4a1c56d --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public abstract class DrawableTaikoRulesetTestScene : OsuTestScene + { + protected DrawableTaikoRuleset DrawableRuleset { get; private set; } + protected Container PlayfieldContainer { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(0, new TimingControlPoint()); + + WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap + { + HitObjects = new List { new Hit { Type = HitType.Centre } }, + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Sample Beatmap", + AuthorString = @"peppy", + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + ControlPointInfo = controlPointInfo + }); + + Add(PlayfieldContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 768, + Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } + }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 1db07b3244..4eeb4a1475 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -2,26 +2,36 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Tests { - internal class DrawableTestHit : DrawableTaikoHitObject + public class DrawableTestHit : DrawableHit { - private readonly HitResult type; + public readonly HitResult Type; public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) : base(hit) { - this.type = type; + Type = type; + + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + + protected override void UpdateInitialTransforms() + { + // base implementation in DrawableHitObject forces alpha to 1. + // suppress locally to allow hiding the visuals wherever necessary. } [BackgroundDependencyLoader] private void load() { - Result.Type = type; + Result.Type = Type; } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs index 7cb984b254..829bcf34a1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs @@ -2,17 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Tests { - public class DrawableTestStrongHit : DrawableHit + public class DrawableTestStrongHit : DrawableTestHit { - private readonly HitResult type; private readonly bool hitBoth; public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true) @@ -20,12 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { IsStrong = true, StartTime = startTime, - }) + }, type) { - // in order to create nested strong hit - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - this.type = type; this.hitBoth = hitBoth; } @@ -33,10 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { base.LoadAsyncComplete(); - Result.Type = type; - var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single(); - nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss; + nestedStrongHit.Result.Type = hitBoth ? Type : HitResult.Miss; } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs similarity index 88% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs index 411fe08bcf..e3c1613bd9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Editor { [TestFixture] public class TestSceneEditor : EditorTestScene diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs similarity index 97% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 34d5fdf857..626537053a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Editor { public class TestSceneTaikoHitObjectComposer : EditorClockTestScene { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 47d8a5c012..36289bda93 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning foreach (var playfield in playfields) { var hit = new DrawableTestHit(new Hit(), judgementResult.Type); - Add(hit); + playfield.Add(hit); playfield.OnNewResult(hit, judgementResult); } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 48969e0f5a..45c94a8a86 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Tests.Skinning @@ -29,15 +28,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); } - private Drawable getContentFor(DrawableTaikoHitObject hit) + private Drawable getContentFor(DrawableTestHit hit) { return new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - hit, - new HitExplosion(hit) + // the hit needs to be added to hierarchy in order for nested objects to be created correctly. + // setting zero alpha is supposed to prevent the test from looking broken. + hit.With(h => h.Alpha = 0), + new HitExplosion(hit, hit.Type) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -46,9 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }; } - private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); + private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); - private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth) - => new DrawableTestStrongHit(Time.Current, type, hitBoth); + private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs new file mode 100644 index 0000000000..7492a76a67 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -0,0 +1,48 @@ +// 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.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneFlyingHits : DrawableTaikoRulesetTestScene + { + [TestCase(HitType.Centre)] + [TestCase(HitType.Rim)] + public void TestFlyingHits(HitType hitType) + { + DrawableFlyingHit flyingHit = null; + + AddStep("add flying hit", () => + { + addFlyingHit(hitType); + + // flying hits all land in one common scrolling container (and stay there for rewind purposes), + // so we need to manually get the latest one. + flyingHit = this.ChildrenOfType() + .OrderByDescending(h => h.HitObject.StartTime) + .FirstOrDefault(); + }); + + AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType); + } + + private void addFlyingHit(HitType hitType) + { + var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + + DrawableDrumRollTick h; + DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Perfect }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 99d1b72ea4..b6cfe368f7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -2,11 +2,9 @@ // 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.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -18,13 +16,12 @@ using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneHits : OsuTestScene + public class TestSceneHits : DrawableTaikoRulesetTestScene { private const double default_duration = 3000; private const float scroll_time = 1000; @@ -32,8 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override double TimePerAction => default_duration * 2; private readonly Random rng = new Random(1337); - private DrawableTaikoRuleset drawableRuleset; - private Container playfieldContainer; [BackgroundDependencyLoader] private void load() @@ -64,35 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Height test 4", () => changePlayfieldSize(4)); AddStep("Height test 5", () => changePlayfieldSize(5)); AddStep("Reset height", () => changePlayfieldSize(6)); - - var controlPointInfo = new ControlPointInfo(); - controlPointInfo.Add(0, new TimingControlPoint()); - - WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap - { - HitObjects = new List { new Hit { Type = HitType.Centre } }, - BeatmapInfo = new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - Metadata = new BeatmapMetadata - { - Artist = @"Unknown", - Title = @"Sample Beatmap", - AuthorString = @"peppy", - }, - Ruleset = new TaikoRuleset().RulesetInfo - }, - ControlPointInfo = controlPointInfo - }); - - Add(playfieldContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 768, - Children = new[] { drawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } - }); } private void changePlayfieldSize(int step) @@ -128,11 +94,11 @@ namespace osu.Game.Rulesets.Taiko.Tests switch (step) { default: - playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); break; case 6: - playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); break; } } @@ -149,9 +115,9 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - Add(h); + DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); } private void addStrongHitJudgement(bool kiai) @@ -166,37 +132,37 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - Add(h); + DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); } private void addMissJudgement() { DrawableTestHit h; - Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); + DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) { - BarLine bl = new BarLine { StartTime = drawableRuleset.Playfield.Time.Current + delay }; + BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay }; - drawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); + DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); } private void addSwell(double duration = default_duration) { var swell = new Swell { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, Duration = duration, }; swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableSwell(swell)); + DrawableRuleset.Playfield.Add(new DrawableSwell(swell)); } private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false) @@ -206,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var d = new DrumRoll { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong, Duration = duration, TickRate = 8, @@ -217,33 +183,33 @@ namespace osu.Game.Rulesets.Taiko.Tests d.ApplyDefaults(cpi, new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); + DrawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); } private void addCentreHit(bool strong) { Hit h = new Hit { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableHit(h)); + DrawableRuleset.Playfield.Add(new DrawableHit(h)); } private void addRimHit(bool strong) { Hit h = new Hit { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableHit(h)); + DrawableRuleset.Playfield.Add(new DrawableHit(h)); } private class TestStrongNestedHit : DrawableStrongNestedHit diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index a3ecf7ed95..d5dd758e10 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -57,7 +57,13 @@ namespace osu.Game.Rulesets.Taiko.Edit ChangeHandler.BeginChange(); foreach (var h in hits) - h.IsStrong = state; + { + if (h.IsStrong != state) + { + h.IsStrong = state; + EditorBeatmap.UpdateHitObject(h); + } + } ChangeHandler.EndChange(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 460e760629..3253c1ce5a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -27,5 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.LoadComplete(); ApplyResult(r => r.Type = r.Judgement.MaxResult); } + + protected override void LoadSamples() + { + // block base call - flying hits are not supposed to play samples + // the base call could overwrite the type of this hit + } } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 273f4e4105..db2e5948f5 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + bool hitButton = true; Frames.Add(new TaikoReplayFrame(-100000)); diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 9943a58e3e..7b8ab89233 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -15,8 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI { internal class DefaultHitExplosion : CircularContainer { - [Resolved] - private DrawableHitObject judgedObject { get; set; } + private readonly DrawableHitObject judgedObject; + private readonly HitResult result; + + public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result) + { + this.judgedObject = judgedObject; + this.result = result; + } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -31,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI Alpha = 0.15f; Masking = true; - if (judgedObject.Result.Type == HitResult.Miss) + if (result == HitResult.Miss) return; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index e3eabbf88f..efd1b25046 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -25,15 +25,18 @@ namespace osu.Game.Rulesets.Taiko.UI [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; + private readonly HitResult result; + private SkinnableDrawable skinnable; public override double LifetimeStart => skinnable.Drawable.LifetimeStart; public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; - public HitExplosion(DrawableHitObject judgedObject) + public HitExplosion(DrawableHitObject judgedObject, HitResult result) { JudgedObject = judgedObject; + this.result = result; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -47,14 +50,12 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion()); + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result)); } private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) { - var resultType = judgedObject.Result?.Type ?? HitResult.Great; - - switch (resultType) + switch (result) { case HitResult.Miss: return TaikoSkinComponents.TaikoExplosionMiss; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7976d5bc6d..7b3fbb1faf 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -206,8 +207,7 @@ namespace osu.Game.Rulesets.Taiko.UI }); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; - - addExplosion(judgedObject, type); + addExplosion(judgedObject, result.Type, type); break; } } @@ -219,9 +219,9 @@ namespace osu.Game.Rulesets.Taiko.UI /// As legacy skins have different explosions for singular and double strong hits, /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame. /// - private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() => + private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() => { - hitExplosionContainer.Add(new HitExplosion(drawableObject)); + hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); }); diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index feda1ae0e9..ff2c9fb1a9 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Editing @@ -13,11 +15,12 @@ namespace osu.Game.Tests.Editing [Test] public void TestSaveRestoreState() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False); + addArbitraryChange(beatmap); handler.SaveState(); Assert.That(handler.CanUndo.Value, Is.True); @@ -29,15 +32,48 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanRedo.Value, Is.True); } + [Test] + public void TestSaveSameStateDoesNotSave() + { + var (handler, beatmap) = createChangeHandler(); + + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.False); + + addArbitraryChange(beatmap); + handler.SaveState(); + + Assert.That(handler.CanUndo.Value, Is.True); + Assert.That(handler.CanRedo.Value, Is.False); + + string hash = handler.CurrentStateHash; + + // save a save without making any changes + handler.SaveState(); + + Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + + handler.RestoreState(-1); + + Assert.That(hash, Is.Not.EqualTo(handler.CurrentStateHash)); + + // we should only be able to restore once even though we saved twice. + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.True); + } + [Test] public void TestMaxStatesSaved() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -53,12 +89,15 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesExceeded() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) + { + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -70,5 +109,17 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); } + + private (EditorChangeHandler, EditorBeatmap) createChangeHandler() + { + var beatmap = new EditorBeatmap(new Beatmap()); + + return (new EditorChangeHandler(beatmap), beatmap); + } + + private void addArbitraryChange(EditorBeatmap beatmap) + { + beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); + } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 0bb2c4b60c..efeb5eeba2 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -1,15 +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 System.Threading; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; @@ -175,6 +178,24 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(0); } + [Test] + public void TestSingleLongObjectDoesNotDrain() + { + var beatmap = new Beatmap + { + HitObjects = { new JudgeableLongHitObject() } + }; + + beatmap.HitObjects[0].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + createProcessor(beatmap); + setTime(0); + assertHealthEqualTo(1); + + setTime(5000); + assertHealthEqualTo(1); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap @@ -235,5 +256,23 @@ namespace osu.Game.Tests.Gameplay } } } + + private class JudgeableLongHitObject : JudgeableHitObject, IHasDuration + { + public double EndTime => StartTime + Duration; + public double Duration { get; set; } = 5000; + + public JudgeableLongHitObject() + : base(false) + { + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + AddNested(new JudgeableHitObject()); + } + } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs new file mode 100644 index 0000000000..720cf51f2c --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -0,0 +1,69 @@ +// 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.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit.Setup; +using osu.Game.Tests.Resources; +using SharpCompress.Archives; +using SharpCompress.Archives.Zip; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorBeatmapCreation : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; + + public override void SetUpSteps() + { + AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); + + base.SetUpSteps(); + + // if we save a beatmap with a hash collision, things fall over. + // probably needs a more solid resolution in the future but this will do for now. + AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); + } + + [Test] + public void TestCreateNewBeatmap() + { + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); + } + + [Test] + public void TestAddAudioTrack() + { + AddAssert("switch track to real track", () => + { + var setup = Editor.ChildrenOfType().First(); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")); + + File.Delete(temp); + Directory.Delete(extractedFolder, true); + + return success; + }); + + AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 3ee0f4e720..ce04b940e7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -12,6 +12,14 @@ namespace osu.Game.Tests.Visual.Gameplay { protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddUntilStep("gameplay has started", + () => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime); + } + [Test] public void TestGameplayOverlayActivation() { @@ -21,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivationPaused() { - AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("pause gameplay", () => Player.Pause()); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index f6b0107bd2..8b1f5a366a 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -17,6 +17,11 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; + /// + /// All valid sample addition constants. + /// + public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH }; + /// /// The bank to load the sample from. /// diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e9f41f6bff..b48ab6112e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -260,7 +260,7 @@ namespace osu.Game.Beatmaps fileInfo.Filename = beatmapInfo.Path; stream.Seek(0, SeekOrigin.Begin); - UpdateFile(setInfo, fileInfo, stream); + ReplaceFile(setInfo, fileInfo, stream); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 76bc4f7755..bbe2604216 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -401,12 +401,27 @@ namespace osu.Game.Database } /// - /// Update an existing file, or create a new entry if not already part of the 's files. + /// Replace an existing file with a new version. /// /// The item to operate on. - /// The file model to be updated or added. + /// The existing file to be replaced. /// The new file contents. - public void UpdateFile(TModel model, TFileModel file, Stream contents) + /// An optional filename for the new file. Will use the previous filename if not specified. + public void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null) + { + using (ContextFactory.GetForWrite()) + { + DeleteFile(model, file); + AddFile(model, contents, filename ?? file.Filename); + } + } + + /// + /// Delete new file. + /// + /// The item to operate on. + /// The existing file to be deleted. + public void DeleteFile(TModel model, TFileModel file) { using (var usage = ContextFactory.GetForWrite()) { @@ -415,15 +430,28 @@ namespace osu.Game.Database { Files.Dereference(file.FileInfo); - // Remove the file model. + // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked + // Definitely can be removed once we rework the database backend. usage.Context.Set().Remove(file); } - // Add the new file info and containing file model. model.Files.Remove(file); + } + } + + /// + /// Add a new file. + /// + /// The item to operate on. + /// The new file contents. + /// The filename for the new file. + public void AddFile(TModel model, Stream contents, string filename) + { + using (ContextFactory.GetForWrite()) + { model.Files.Add(new TFileModel { - Filename = file.Filename, + Filename = filename, FileInfo = Files.Add(contents) }); diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index d504a11b22..ed5c73bee6 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -112,6 +112,9 @@ namespace osu.Game.Graphics.Containers CornerRadius = 5; + // needs to be set initially for the ResizeTo to respect minimum size + Size = new Vector2(SCROLL_BAR_HEIGHT); + const float margin = 3; Margin = new MarginPadding diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 902c23c3c6..4aeda74be8 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -44,13 +44,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 Component.BorderColour = colours.Blue; } - protected override OsuTextBox CreateComponent() => new OsuTextBox + protected virtual OsuTextBox CreateTextBox() => new OsuTextBox { CommitOnFocusLost = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, CornerRadius = CORNER_RADIUS, - }.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText)); + }; + + protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => + { + t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText); + }); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b568e4d02b..0764f34697 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -81,6 +81,11 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } + /// + /// Forcefully reload the current 's track from disk. + /// + public void ReloadCurrentTrack() => changeTrack(); + /// /// Change the position of a in the current playlist. /// diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 89e7866707..1070b8cbd2 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -45,15 +45,21 @@ namespace osu.Game.Rulesets.Edit base.LoadComplete(); beatmap.HitObjectAdded += addHitObject; + beatmap.HitObjectUpdated += updateReplay; beatmap.HitObjectRemoved += removeHitObject; } + private void updateReplay(HitObject obj = null) => + drawableRuleset.RegenerateAutoplay(); + private void addHitObject(HitObject hitObject) { var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.PostProcess(); + + updateReplay(); } private void removeHitObject(HitObject hitObject) @@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.PostProcess(); + + drawableRuleset.RegenerateAutoplay(); } public override bool PropagatePositionalInputSubTree => false; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index abd8374d98..6e377ff207 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -14,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -24,6 +22,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Edit private RadioButtonCollection toolboxCollection; - private ToolboxGroup togglesCollection; + private FillFlowContainer togglesCollection; protected HitObjectComposer(Ruleset ruleset) { @@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Edit try { - drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) + drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() })) { Clock = EditorClock, ProcessCustomClock = false @@ -121,14 +120,19 @@ namespace osu.Game.Rulesets.Edit Spacing = new Vector2(10), Children = new Drawable[] { - new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, - togglesCollection = new ToolboxGroup("toggles") + new ToolboxGroup("toolbox (1-9)") { - ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox + Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } + }, + new ToolboxGroup("toggles (Q~P)") + { + Child = togglesCollection = new FillFlowContainer { - Bindable = b, - LabelText = b?.Description ?? "unknown" - }) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + }, } } }, @@ -139,6 +143,9 @@ namespace osu.Game.Rulesets.Edit .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .ToList(); + TernaryStates = CreateTernaryButtons().ToArray(); + togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); + setSelectTool(); EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; @@ -167,10 +174,14 @@ namespace osu.Game.Rulesets.Edit protected abstract IReadOnlyList CompositionTools { get; } /// - /// A collection of toggles which will be displayed to the user. - /// The display name will be decided by . + /// A collection of states which will be displayed to the user in the toolbox. /// - protected virtual IEnumerable> Toggles => BlueprintContainer.Toggles; + public TernaryButton[] TernaryStates { get; private set; } + + /// + /// Create all ternary states required to be displayed to the user. + /// + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.TernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -215,9 +226,9 @@ namespace osu.Game.Rulesets.Edit { var item = togglesCollection.ElementAtOrDefault(rightIndex); - if (item is SettingsCheckbox checkbox) + if (item is DrawableTernaryButton button) { - checkbox.Bindable.Value = !checkbox.Bindable.Value; + button.Button.Toggle(); return true; } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 91793e3f70..cae41e22f4 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Scoring private double computeDrainRate() { - if (healthIncreases.Count == 0) + if (healthIncreases.Count <= 1) return 0; int adjustment = 1; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index fbb9acfe90..50e9a93e22 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -151,8 +151,11 @@ namespace osu.Game.Rulesets.UI public virtual PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PlayfieldAdjustmentContainer(); + [Resolved] + private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] - private void load(OsuConfigManager config, CancellationToken? cancellationToken) + private void load(CancellationToken? cancellationToken) { InternalChildren = new Drawable[] { @@ -178,11 +181,18 @@ namespace osu.Game.Rulesets.UI .WithChild(ResumeOverlay))); } - applyRulesetMods(Mods, config); + RegenerateAutoplay(); loadObjects(cancellationToken); } + public void RegenerateAutoplay() + { + // for now this is applying mods which aren't just autoplay. + // we'll need to reconsider this flow in the future. + applyRulesetMods(Mods, config); + } + /// /// Creates and adds drawable representations of hit objects to the play field. /// diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index d574991fa0..a4af92749f 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -123,39 +123,63 @@ namespace osu.Game.Rulesets.UI try { - if (!FrameStablePlayback) - return; - - if (firstConsumption) + if (FrameStablePlayback) { - // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. - // Instead we perform an initial seek to the proposed time. + if (firstConsumption) + { + // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. + // Instead we perform an initial seek to the proposed time. - // process frame (in addition to finally clause) to clear out ElapsedTime - manualClock.CurrentTime = newProposedTime; - framedClock.ProcessFrame(); + // process frame (in addition to finally clause) to clear out ElapsedTime + manualClock.CurrentTime = newProposedTime; + framedClock.ProcessFrame(); - firstConsumption = false; - } - else if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); - else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) - { - newProposedTime = newProposedTime > manualClock.CurrentTime - ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) - : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); + firstConsumption = false; + } + else if (manualClock.CurrentTime < gameplayStartTime) + manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); + else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) + { + newProposedTime = newProposedTime > manualClock.CurrentTime + ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) + : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); + } } if (isAttached) { - double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime); + double? newTime; - if (newTime == null) + if (FrameStablePlayback) { - // we shouldn't execute for this time value. probably waiting on more replay data. - validState = false; - requireMoreUpdateLoops = true; - return; + // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. + if ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) == null) + { + // setting invalid state here ensures that gameplay will not continue (ie. our child + // hierarchy won't be updated). + validState = false; + + // potentially loop to catch-up playback. + requireMoreUpdateLoops = true; + + return; + } + } + else + { + // when stability is disabled, we don't really care about accuracy. + // looping over the replay will allow it to catch up and feed out the required values + // for the current time. + while ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) != newProposedTime) + { + if (newTime == null) + { + // special case for when the replay actually can't arrive at the required time. + // protects from potential endless loop. + validState = false; + return; + } + } } newProposedTime = newTime.Value; diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index cb5078a479..08091fc3f7 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Components private const float contents_padding = 15; protected readonly IBindable Beatmap = new Bindable(); - protected Track Track => Beatmap.Value.Track; + + protected readonly IBindable Track = new Bindable(); private readonly Drawable background; private readonly Container content; @@ -42,9 +43,11 @@ namespace osu.Game.Screens.Edit.Components } [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours) + private void load(IBindable beatmap, OsuColour colours, EditorClock clock) { Beatmap.BindTo(beatmap); + Track.BindTo(clock.Track); + background.Colour = colours.Gray1; } } diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 59b3d1c565..9739f2876a 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -62,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components } }; - Track?.AddAdjustment(AdjustableProperty.Tempo, tempo); + Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true); } protected override void Dispose(bool isDisposing) { - Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs new file mode 100644 index 0000000000..c72fff5c91 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -0,0 +1,112 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Components.TernaryButtons +{ + internal class DrawableTernaryButton : TriangleButton + { + private Color4 defaultBackgroundColour; + private Color4 defaultBubbleColour; + private Color4 selectedBackgroundColour; + private Color4 selectedBubbleColour; + + private Drawable icon; + + public readonly TernaryButton Button; + + public DrawableTernaryButton(TernaryButton button) + { + Button = button; + + Text = button.Description; + + RelativeSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + defaultBackgroundColour = colours.Gray3; + defaultBubbleColour = defaultBackgroundColour.Darken(0.5f); + selectedBackgroundColour = colours.BlueDark; + selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f); + + Triangles.Alpha = 0; + + Content.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 2, + Offset = new Vector2(0, 1), + Colour = Color4.Black.Opacity(0.5f) + }; + + Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b => + { + b.Blending = BlendingParameters.Additive; + b.Anchor = Anchor.CentreLeft; + b.Origin = Anchor.CentreLeft; + b.Size = new Vector2(20); + b.X = 10; + })); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Button.Bindable.BindValueChanged(selected => updateSelectionState(), true); + + Action = onAction; + } + + private void onAction() + { + Button.Toggle(); + } + + private void updateSelectionState() + { + if (!IsLoaded) + return; + + switch (Button.Bindable.Value) + { + case TernaryState.Indeterminate: + icon.Colour = selectedBubbleColour.Darken(0.5f); + BackgroundColour = selectedBackgroundColour.Darken(0.5f); + break; + + case TernaryState.False: + icon.Colour = defaultBubbleColour; + BackgroundColour = defaultBackgroundColour; + break; + + case TernaryState.True: + icon.Colour = selectedBubbleColour; + BackgroundColour = selectedBackgroundColour; + break; + } + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40f + }; + } +} diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs new file mode 100644 index 0000000000..7f64695bde --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.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; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Components.TernaryButtons +{ + public class TernaryButton + { + public readonly Bindable Bindable; + + public readonly string Description; + + /// + /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. + /// + public readonly Func CreateIcon; + + public TernaryButton(Bindable bindable, string description, Func createIcon = null) + { + Bindable = bindable; + Description = description; + CreateIcon = createIcon; + } + + public void Toggle() + { + switch (Bindable.Value) + { + case TernaryState.False: + case TernaryState.Indeterminate: + Bindable.Value = TernaryState.True; + break; + + case TernaryState.True: + Bindable.Value = TernaryState.False; + break; + } + } + } +} diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 4a7c3f26bc..5b8f7c747b 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osuTK; using osu.Framework.Graphics; @@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { protected readonly IBindable Beatmap = new Bindable(); + protected readonly IBindable Track = new Bindable(); + private readonly Container content; protected override Container Content => content; @@ -35,12 +38,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts updateRelativeChildSize(); LoadBeatmap(b.NewValue); }; + + Track.ValueChanged += _ => updateRelativeChildSize(); } [BackgroundDependencyLoader] - private void load(IBindable beatmap) + private void load(IBindable beatmap, EditorClock clock) { Beatmap.BindTo(beatmap); + Track.BindTo(clock.Track); } private void updateRelativeChildSize() diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8908520cd7..970e16d1c3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -201,6 +201,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (isDraggingBlueprint) { + // handle positional change etc. + foreach (var obj in selectedHitObjects) + Beatmap.UpdateHitObject(obj); + changeHandler?.EndChange(); isDraggingBlueprint = false; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 6f66c1bd6f..9b3314e2ad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -3,16 +3,21 @@ using System.Collections.Generic; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input; +using osu.Game.Audio; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -48,6 +53,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + TernaryStates = CreateTernaryButtons().ToArray(); + AddInternal(placementBlueprintContainer); } @@ -57,36 +64,92 @@ namespace osu.Game.Screens.Edit.Compose.Components inputManager = GetContainingInputManager(); - Beatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateTogglesFromSelection(); + // updates to selected are handled for us by SelectionHandler. + NewCombo.BindTo(SelectionHandler.SelectionNewComboState); - // the updated object may be in the selection - Beatmap.HitObjectUpdated += _ => updateTogglesFromSelection(); + // we are responsible for current placement blueprint updated based on state changes. + NewCombo.ValueChanged += _ => updatePlacementNewCombo(); - NewCombo.ValueChanged += combo => + // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity) + foreach (var kvp in SelectionHandler.SelectionSampleStates) { - if (Beatmap.SelectedHitObjects.Count > 0) - { - SelectionHandler.SetNewCombo(combo.NewValue); - } - else if (currentPlacement != null) - { - // update placement object from toggle - if (currentPlacement.HitObject is IHasComboInformation c) - c.NewCombo = combo.NewValue; - } - }; + kvp.Value.BindValueChanged(_ => updatePlacementSamples()); + } } - private void updateTogglesFromSelection() => - NewCombo.Value = Beatmap.SelectedHitObjects.OfType().All(c => c.NewCombo); + private void updatePlacementNewCombo() + { + if (currentPlacement == null) return; - public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; + if (currentPlacement.HitObject is IHasComboInformation c) + c.NewCombo = NewCombo.Value == TernaryState.True; + } - public virtual IEnumerable> Toggles => new[] + private void updatePlacementSamples() + { + if (currentPlacement == null) return; + + foreach (var kvp in SelectionHandler.SelectionSampleStates) + sampleChanged(kvp.Key, kvp.Value.Value); + } + + private void sampleChanged(string sampleName, TernaryState state) + { + if (currentPlacement == null) return; + + var samples = currentPlacement.HitObject.Samples; + + var existingSample = samples.FirstOrDefault(s => s.Name == sampleName); + + switch (state) + { + case TernaryState.False: + if (existingSample != null) + samples.Remove(existingSample); + break; + + case TernaryState.True: + if (existingSample == null) + samples.Add(new HitSampleInfo { Name = sampleName }); + break; + } + } + + public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; + + /// + /// A collection of states which will be displayed to the user in the toolbox. + /// + public TernaryButton[] TernaryStates { get; private set; } + + /// + /// Create all ternary states required to be displayed to the user. + /// + protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - NewCombo - }; + yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); + + foreach (var kvp in SelectionHandler.SelectionSampleStates) + yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key)); + } + + private Drawable getIconForSample(string sampleName) + { + switch (sampleName) + { + case HitSampleInfo.HIT_CLAP: + return new SpriteIcon { Icon = FontAwesome.Solid.Hands }; + + case HitSampleInfo.HIT_WHISTLE: + return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn }; + + case HitSampleInfo.HIT_FINISH: + return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan }; + } + + return null; + } #region Placement @@ -149,10 +212,17 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { + // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. + blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); + placementBlueprintContainer.Child = currentPlacement = blueprint; // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame updatePlacementPosition(); + + updatePlacementSamples(); + + updatePlacementNewCombo(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index a316f34ad0..a0220cf987 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -288,8 +288,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var comboInfo = h as IHasComboInformation; - if (comboInfo == null) - continue; + if (comboInfo == null || comboInfo.NewCombo == state) continue; comboInfo.NewCombo = state; EditorBeatmap?.UpdateHitObject(h); @@ -316,19 +315,22 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection State - private readonly Bindable selectionNewComboState = new Bindable(); + /// + /// The state of "new combo" for all selected hitobjects. + /// + public readonly Bindable SelectionNewComboState = new Bindable(); - private readonly Dictionary> selectionSampleStates = new Dictionary>(); + /// + /// The state of each sample type for all selected hitobjects. Keys match with constant specifications. + /// + public readonly Dictionary> SelectionSampleStates = new Dictionary>(); /// /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) /// private void createStateBindables() { - // hit samples - var sampleTypes = new[] { HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH }; - - foreach (var sampleName in sampleTypes) + foreach (var sampleName in HitSampleInfo.AllAdditions) { var bindable = new Bindable { @@ -349,11 +351,11 @@ namespace osu.Game.Screens.Edit.Compose.Components } }; - selectionSampleStates[sampleName] = bindable; + SelectionSampleStates[sampleName] = bindable; } // new combo - selectionNewComboState.ValueChanged += state => + SelectionNewComboState.ValueChanged += state => { switch (state.NewValue) { @@ -377,9 +379,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void UpdateTernaryStates() { - selectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); + SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); - foreach (var (sampleName, bindable) in selectionSampleStates) + foreach (var (sampleName, bindable) in SelectionSampleStates) { bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); } @@ -413,7 +415,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) { - items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = selectionNewComboState } }); + items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } }); } if (selectedBlueprints.Count == 1) @@ -423,7 +425,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { new OsuMenuItem("Sound") { - Items = selectionSampleStates.Select(kvp => + Items = SelectionSampleStates.Select(kvp => new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() }, new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c9d57785f9..fd090e0959 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -43,6 +43,7 @@ using osuTK.Input; namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] + [Cached] public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider { public override float BackgroundParallaxAmount => 0.1f; @@ -91,6 +92,9 @@ namespace osu.Game.Screens.Edit [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private MusicController music { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host) { @@ -98,9 +102,9 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; - clock.ChangeSource(sourceClock); + + UpdateClockSource(); dependencies.CacheAs(clock); AddInternal(clock); @@ -271,6 +275,15 @@ namespace osu.Game.Screens.Edit bottomBackground.Colour = colours.Gray2; } + /// + /// If the beatmap's track has changed, this method must be called to keep the editor in a valid state. + /// + public void UpdateClockSource() + { + var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); + clock.ChangeSource(sourceClock); + } + protected void Save() { // apply any set-level metadata changes. diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index aa0f89912a..617c436ee0 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -89,23 +90,27 @@ namespace osu.Game.Screens.Edit if (isRestoring) return; - if (currentState < savedStates.Count - 1) - savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); - - if (savedStates.Count > MAX_SAVED_STATES) - savedStates.RemoveAt(0); - using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); - savedStates.Add(stream.ToArray()); + var newState = stream.ToArray(); + + // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. + if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; + + if (currentState < savedStates.Count - 1) + savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + + if (savedStates.Count > MAX_SAVED_STATES) + savedStates.RemoveAt(0); + + savedStates.Add(newState); + + currentState = savedStates.Count - 1; + updateBindables(); } - - currentState = savedStates.Count - 1; - - updateBindables(); } /// diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d4d0feb813..ec203df064 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -3,6 +3,8 @@ using System; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; @@ -17,7 +19,11 @@ namespace osu.Game.Screens.Edit /// public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public readonly double TrackLength; + public IBindable Track => track; + + private readonly Bindable track = new Bindable(); + + public double TrackLength => track.Value?.Length ?? 60000; public ControlPointInfo ControlPointInfo; @@ -35,7 +41,6 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor; ControlPointInfo = controlPointInfo; - TrackLength = trackLength; underlyingClock = new DecoupleableInterpolatingFramedClock(); } @@ -190,7 +195,11 @@ namespace osu.Game.Screens.Edit public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; - public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source); + public void ChangeSource(IClock source) + { + track.Value = source as Track; + underlyingClock.ChangeSource(source); + } public IClock Source => underlyingClock.Source; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 9dcdcb25f5..f6eb92e1ec 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,17 +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 System; +using System.IO; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Setup @@ -23,6 +30,16 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox titleTextBox; private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; + private LabelledTextBox audioTrackTextBox; + + [Resolved] + private MusicController music { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved(canBeNull: true)] + private Editor editor { get; set; } public SetupScreen() : base(EditorScreenMode.SongSetup) @@ -32,6 +49,12 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load(OsuColour colours) { + Container audioTrackFileChooserContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + Child = new Container { RelativeSizeAxes = Axes.Both, @@ -75,6 +98,18 @@ namespace osu.Game.Screens.Edit.Setup }, }, new OsuSpriteText + { + Text = "Resources" + }, + audioTrackTextBox = new FileChooserLabelledTextBox + { + Label = "Audio Track", + Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" }, + Target = audioTrackFileChooserContainer, + TabbableContentContainer = this + }, + audioTrackFileChooserContainer, + new OsuSpriteText { Text = "Beatmap metadata" }, @@ -109,10 +144,47 @@ namespace osu.Game.Screens.Edit.Setup } }; + audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); + foreach (var item in flow.OfType()) item.OnCommit += onCommit; } + public bool ChangeAudioTrack(string path) + { + var info = new FileInfo(path); + + if (!info.Exists) + return false; + + var set = Beatmap.Value.BeatmapSetInfo; + + // remove the previous audio track for now. + // in the future we probably want to check if this is being used elsewhere (other difficulties?) + var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); + + using (var stream = info.OpenRead()) + { + if (oldFile != null) + beatmaps.ReplaceFile(set, oldFile, stream, info.Name); + else + beatmaps.AddFile(set, stream, info.Name); + } + + Beatmap.Value.Metadata.AudioFile = info.Name; + + music.ReloadCurrentTrack(); + + editor?.UpdateClockSource(); + return true; + } + + private void audioTrackChanged(ValueChangedEvent filePath) + { + if (!ChangeAudioTrack(filePath.NewValue)) + audioTrackTextBox.Current.Value = filePath.OldValue; + } + private void onCommit(TextBox sender, bool newText) { if (!newText) return; @@ -125,4 +197,60 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; } } + + internal class FileChooserLabelledTextBox : LabelledTextBox + { + public Container Target; + + private readonly IBindable currentFile = new Bindable(); + + public FileChooserLabelledTextBox() + { + currentFile.BindValueChanged(onFileSelected); + } + + private void onFileSelected(ValueChangedEvent file) + { + if (file.NewValue == null) + return; + + Target.Clear(); + Current.Value = file.NewValue.FullName; + } + + protected override OsuTextBox CreateTextBox() => + new FileChooserOsuTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + CornerRadius = CORNER_RADIUS, + OnFocused = DisplayFileChooser + }; + + public void DisplayFileChooser() + { + Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" }) + { + RelativeSizeAxes = Axes.X, + Height = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + CurrentFile = { BindTarget = currentFile } + }; + } + + internal class FileChooserOsuTextBox : OsuTextBox + { + public Action OnFocused; + + protected override void OnFocus(FocusEvent e) + { + OnFocused?.Invoke(); + base.OnFocus(e); + + GetContainingInputManager().TriggerFocusContention(this); + } + } + } } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 8f76f247cf..a9ee8e2668 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -26,13 +26,15 @@ namespace osu.Game.Tests.Visual Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); } + protected virtual bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + public override void SetUpSteps() { base.SetUpSteps(); AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); - AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true - && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for editor to load", () => EditorComponentsReady); AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType().Single()); AddStep("get clock", () => EditorClock = Editor.ChildrenOfType().Single()); } diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 29ca385275..64f3d41acb 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -909,6 +909,7 @@ private void load() True True True + True True True True