diff --git a/osu.Android.props b/osu.Android.props index d701aaf199..d7817cf4cf 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 9839d16030..db73bb7e7f 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -9,7 +9,7 @@ using osu.Framework.Android; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { protected override Framework.Game CreateGame() => new OsuGameAndroid(); diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index c1b7214d72..3e06e78dba 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); // We only care about testing misses, hits are tested via JuiceStream - [TestCase(false)] + [TestCase(true)] public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index e79792e04a..c7b322c8a0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestCatchComboCounter() { - AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); + AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20); AddStep("perform miss", () => performJudgement(HitResult.Miss)); AddStep("randomize judged object colour", () => diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 1a9a79f6ff..1f27de3352 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new CatchPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); public int LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 33c807333f..6a3a16ed33 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty private int tinyTicksMissed; private int misses; - public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) - : base(ruleset, beatmap, score, attributes) + public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { } @@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { mods = Score.Mods; - fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect); + fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great); ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit); tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit); tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss); diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs index a7449ba4e1..b919102215 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs @@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchBananaJudgement : CatchJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 1100; - } - } - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return DEFAULT_MAX_HEALTH_INCREASE * 0.75; - } - } + public override HitResult MaxResult => HitResult.LargeBonus; public override bool ShouldExplodeFor(JudgementResult result) => true; } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs index e87ecba749..8fd7b93e4c 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs @@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchDropletJudgement : CatchJudgement { - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 30; - } - } + public override HitResult MaxResult => HitResult.LargeTickHit; } } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index 2149ed9712..ccafe0abc4 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchJudgement : Judgement { - public override HitResult MaxResult => HitResult.Perfect; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 300; - } - } + public override HitResult MaxResult => HitResult.Great; /// /// Whether fruit on the platter should explode or drop. diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs index d607b49ea4..d957d4171b 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs @@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchTinyDropletJudgement : CatchJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 10; - } - } - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 0.02; - } - } + public override HitResult MaxResult => HitResult.SmallTickHit; } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 2fe017dc62..d03a764bda 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Catch.UI; using osuTK; using osuTK.Graphics; @@ -86,7 +85,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (CheckPosition == null) return; if (timeOffset >= 0 && Result != null) - ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); + ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateStateTransforms(ArmedState state) 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.Catch/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs index ff793a372e..0a444d923e 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring { switch (result) { - case HitResult.Perfect: + case HitResult.Great: case HitResult.Miss: return true; } diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 4c7bc4ab73..2cc05826b4 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring { public class CatchScoreProcessor : ScoreProcessor { - public override HitWindows CreateHitWindows() => new CatchHitWindows(); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index 58a3140bb5..75feb21298 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -33,10 +33,10 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result) { - if (!result.Judgement.AffectsCombo || !result.HasResult) + if (!result.Type.AffectsCombo() || !result.HasResult) return; - if (result.Type == HitResult.Miss) + if (!result.IsHit) { updateCombo(0, null); return; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result) { - if (!result.Judgement.AffectsCombo || !result.HasResult) + if (!result.Type.AffectsCombo() || !result.HasResult) return; updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index d3e63b0333..5e794a76aa 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osuTK; @@ -52,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) { - if (result.Judgement is IgnoreJudgement) + if (!result.Type.IsScorable()) return; void runAfterLoaded(Action action) 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.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 95e86de884..9c4c2b3d5b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneHoldNote : ManiaHitObjectTestScene { - public TestSceneHoldNote() + [Test] + public void TestHoldNote() { AddToggleStep("toggle hitting", v => { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 95072cf4f8..5cb1519196 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.Perfect); + assertNoteJudgement(HitResult.IgnoreHit); } /// @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Perfect); } @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Meh); } @@ -280,10 +280,10 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type == HitResult.Miss)); + .All(j => !j.Type.IsHit())); AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type == HitResult.Perfect)); + .All(j => j.Type.IsHit())); } private void assertHeadJudgement(HitResult result) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index dd5fd93710..6b8f5d5d9d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -28,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class TestSceneNotes : OsuTestScene { - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestVariousNotes() { - Child = new FillFlowContainer + DrawableNote note1 = null; + DrawableNote note2 = null; + DrawableHoldNote holdNote1 = null; + DrawableHoldNote holdNote2 = null; + + AddStep("create notes", () => { - Clock = new FramedClock(new ManualClock()), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new[] + Child = new FillFlowContainer { - createNoteDisplay(ScrollingDirection.Down, 1, out var note1), - createNoteDisplay(ScrollingDirection.Up, 2, out var note2), - createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), - createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), - } - }; + Clock = new FramedClock(new ManualClock()), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new[] + { + createNoteDisplay(ScrollingDirection.Down, 1, out note1), + createNoteDisplay(ScrollingDirection.Up, 2, out note2), + createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1), + createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2), + } + }; + }); AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 5a32509b8d..00bec18a45 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -29,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private int countMeh; private int countMiss; - public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes) - : base(ruleset, beatmap, score, attributes) + public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { } diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs index 28e5d2cc1b..ee6cbbc828 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -7,18 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements { public class HoldNoteTickJudgement : ManiaJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0; - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 0.01; - } - } + public override HitResult MaxResult => HitResult.LargeTickHit; } } diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 53967ffa05..d28b7bdf58 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -8,27 +8,33 @@ namespace osu.Game.Rulesets.Mania.Judgements { public class ManiaJudgement : Judgement { - protected override int NumericResultFor(HitResult result) + protected override double HealthIncreaseFor(HitResult result) { switch (result) { - default: - return 0; + case HitResult.LargeTickHit: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.LargeTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; case HitResult.Meh: - return 50; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.Ok: - return 100; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; case HitResult.Good: - return 200; + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; case HitResult.Great: - return 300; + return DEFAULT_MAX_HEALTH_INCREASE * 0.8; case HitResult.Perfect: - return 350; + return DEFAULT_MAX_HEALTH_INCREASE; + + default: + return base.HealthIncreaseFor(result); } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 93390c1f0a..ecb09ebe85 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new ManiaPerformanceCalculator(this, beatmap, score, attributes); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); public const string SHORT_NAME = "mania"; diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index b470405df2..de77af8306 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania new SettingsEnumDropdown { LabelText = "Scrolling direction", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollDirection) + Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection) }, new SettingsSlider { LabelText = "Scroll speed", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime), + Current = config.GetBindable(ManiaRulesetSetting.ScrollTime), KeyboardStep = 5 }, }; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 2ebcc5451a..f6d539c91b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = HitResult.Perfect); + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } - if (Tail.Result.Type == HitResult.Miss) + if (Tail.Judged && !Tail.IsHit) HasBroken = true; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 31e43d3ee2..c780c0836e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 9b0322a6cd..f265419aa0 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNoteTick : DrawableManiaHitObject { + public override bool DisplayResult => false; + /// /// References the time at which the user started holding the hold note. /// @@ -73,9 +74,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables var startTime = HoldStartTime?.Invoke(); if (startTime == null || startTime > HitObject.StartTime) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); else - ApplyResult(r => r.Type = HitResult.Perfect); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 08c41b0d75..27960b3f3a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); } public abstract class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 973dc06e05..b3402d13e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } 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.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 4b2f643333..71cc0bdf1f 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double DefaultAccuracyPortion => 0.95; protected override double DefaultComboPortion => 0.05; - - public override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 439e6f7df2..3724269f4d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -130,6 +130,9 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable getResult(HitResult result) { + if (!hitresult_mapping.ContainsKey(result)) + return null; + string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value ?? default_hitresult_skin_filenames[result]; diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9aabcc6699..c28a1c13d8 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI if (result.IsHit) hitPolicy.HandleHit(judgedObject); - if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) + if (!result.IsHit || !DisplayJudgements.Value) return; HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); 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/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs new file mode 100644 index 0000000000..1ca94df26b --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneObjectObjectSnap : TestSceneOsuEditor + { + private OsuPlayfield playfield; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false); + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); + } + + [TestCase(true)] + [TestCase(false)] + public void TestHitCircleSnapsToOtherHitCircle(bool distanceSnapEnabled) + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + if (!distanceSnapEnabled) + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); + + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); + + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); + + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddAssert("both objects at same location", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var second = (OsuHitObject)objects.Last(); + + return first.Position == second.Position; + }); + } + + [Test] + public void TestHitCircleSnapsToSliderEnd() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); + + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); + + AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0))); + + AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2)); + + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0))); + + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddAssert("circle is at slider's end", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (Slider)objects.First(); + var second = (OsuHitObject)objects.Last(); + + return Precision.AlmostEquals(first.EndPosition, second.Position); + }); + } + } +} 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/TestSceneEditor.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs similarity index 76% rename from osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs index 9239034a53..e1ca3ddd61 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs @@ -4,10 +4,10 @@ using NUnit.Framework; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { [TestFixture] - public class TestSceneEditor : EditorTestScene + public class TestSceneOsuEditor : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); } 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.Tests/Resources/metrics-skin/sliderendcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png new file mode 100644 index 0000000000..c6c3771593 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png new file mode 100644 index 0000000000..232560a1d4 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 37df0d6e37..596bc06c68 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneHitCircle() + [Test] + public void TestVariousHitCircles() { AddStep("Miss Big Single", () => SetContents(() => testSingle(2))); AddStep("Miss Medium Single", () => SetContents(() => testSingle(5))); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index f3221ffe32..39deba2f57 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests { HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } }, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit }); } @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Autoplay = false, Beatmap = beatmap, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit }); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 854626d362..32a36ab317 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -209,9 +209,9 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss); - addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } /// @@ -252,9 +252,9 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great); - addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } /// @@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); } private void addJudgementAssert(OsuHitObject hitObject, HitResult result) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 6a689a1f80..c9e112f76d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneSlider() + [Test] + public void TestVariousSliders() { AddStep("Big Single", () => SetContents(() => testSimpleBig())); AddStep("Medium Single", () => SetContents(() => testSimpleMedium())); @@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, + StartTime = Time.Current + time_offset, Position = new Vector2(239, 176), Path = new SliderPath(PathType.PerfectCurve, new[] { @@ -185,22 +186,26 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5); - private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5); + private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5); private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15); - private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); + private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15); - private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) + private const double time_offset = 1500; + + private const float max_length = 200; + + private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-(distance / 2), 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(0, -(distance / 2)), Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, - new Vector2(distance, 0), + new Vector2(0, distance), }, distance), RepeatCount = repeats, StackHeight = stackHeight @@ -213,14 +218,14 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-200, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 2, 0), Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, - new Vector2(200, 200), - new Vector2(400, 0) - }, 600), + new Vector2(max_length / 2, max_length / 2), + new Vector2(max_length, 0) + }, max_length * 1.5f), RepeatCount = repeats, }; @@ -233,16 +238,16 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-200, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 2, 0), Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(150, 75), - new Vector2(200, 0), - new Vector2(300, -200), - new Vector2(400, 0), - new Vector2(430, 0) + new Vector2(max_length * 0.375f, max_length * 0.18f), + new Vector2(max_length / 2, 0), + new Vector2(max_length * 0.75f, -max_length / 2), + new Vector2(max_length * 0.95f, 0), + new Vector2(max_length, 0) }), RepeatCount = repeats, }; @@ -256,15 +261,15 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-200, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 2, 0), Path = new SliderPath(PathType.Bezier, new[] { Vector2.Zero, - new Vector2(150, 75), - new Vector2(200, 100), - new Vector2(300, -200), - new Vector2(430, 0) + new Vector2(max_length * 0.375f, max_length * 0.18f), + new Vector2(max_length / 2, max_length / 4), + new Vector2(max_length * 0.75f, -max_length / 2), + new Vector2(max_length, 0) }), RepeatCount = repeats, }; @@ -278,16 +283,16 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, + StartTime = Time.Current + time_offset, Position = new Vector2(0, 0), Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(-200, 0), + new Vector2(-max_length / 2, 0), new Vector2(0, 0), - new Vector2(0, -200), - new Vector2(-200, -200), - new Vector2(0, -200) + new Vector2(0, -max_length / 2), + new Vector2(-max_length / 2, -max_length / 2), + new Vector2(0, -max_length / 2) }), RepeatCount = repeats, }; @@ -305,14 +310,14 @@ namespace osu.Game.Rulesets.Osu.Tests var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-100, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 4, 0), Path = new SliderPath(PathType.Catmull, new[] { Vector2.Zero, - new Vector2(50, -50), - new Vector2(150, 50), - new Vector2(200, 0) + new Vector2(max_length * 0.125f, max_length * 0.125f), + new Vector2(max_length * 0.375f, max_length * 0.125f), + new Vector2(max_length / 2, 0) }), RepeatCount = repeats, NodeSamples = repeatSamples diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index b543b6fa94..0164fb8bf4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking retained", assertGreatJudge); + AddAssert("Tracking retained", assertMaxJudge); } /// @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertGreatJudge); + AddAssert("Tracking retained", assertMaxJudge); } /// @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertGreatJudge); + AddAssert("Tracking retained", assertMaxJudge); } /// @@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, }); - AddAssert("Tracking kept", assertGreatJudge); + AddAssert("Tracking kept", assertMaxJudge); } /// @@ -312,13 +312,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great); + private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; + private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; - private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great; + private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit; - private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss; + private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; private ScoreAccessibleReplayPlayer currentPlayer; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index f7909071ea..53bf1ea566 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Timing; @@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK; + return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); @@ -194,13 +193,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(0); - AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); - // autoplay replay frames use track time; - // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. - // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, - // as real time is what we care about for spinners - // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate). - transformReplay(replay => applyRateAdjustment(replay, rate)); + AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate); addSeekStep(1000); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index a9879013f8..fff033357d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SpeedStrain; public double ApproachRate; public double OverallDifficulty; + public int HitCircleCount; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index b0d261a1cc..6027635b75 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); + int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); + return new OsuDifficultyAttributes { StarRating = starRating, @@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, MaxCombo = maxCombo, + HitCircleCount = hitCirclesCount, Skills = skills }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index e3faa55d9e..063cde8747 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,11 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -19,26 +17,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes; - private readonly int countHitCircles; - private readonly int beatmapMaxCombo; - private Mod[] mods; private double accuracy; private int scoreMaxCombo; private int countGreat; - private int countGood; + private int countOk; private int countMeh; private int countMiss; - public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes) - : base(ruleset, beatmap, score, attributes) + public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { - countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); - - beatmapMaxCombo = Beatmap.HitObjects.Count; - // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above) - beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); } public override double Calculate(Dictionary categoryRatings = null) @@ -47,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty accuracy = Score.Accuracy; scoreMaxCombo = Score.MaxCombo; countGreat = Score.Statistics.GetOrDefault(HitResult.Great); - countGood = Score.Statistics.GetOrDefault(HitResult.Good); + countOk = Score.Statistics.GetOrDefault(HitResult.Ok); countMeh = Score.Statistics.GetOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetOrDefault(HitResult.Miss); @@ -81,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty categoryRatings.Add("Accuracy", accuracyValue); categoryRatings.Add("OD", Attributes.OverallDifficulty); categoryRatings.Add("AR", Attributes.ApproachRate); - categoryRatings.Add("Max Combo", beatmapMaxCombo); + categoryRatings.Add("Max Combo", Attributes.MaxCombo); } return totalValue; @@ -106,8 +96,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= Math.Pow(0.97, countMiss); // Combo scaling - if (beatmapMaxCombo > 0) - aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0); + if (Attributes.MaxCombo > 0) + aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); double approachRateFactor = 1.0; @@ -154,8 +144,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= Math.Pow(0.97, countMiss); // Combo scaling - if (beatmapMaxCombo > 0) - speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0); + if (Attributes.MaxCombo > 0) + speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); double approachRateFactor = 1.0; if (Attributes.ApproachRate > 10.33) @@ -178,10 +168,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window double betterAccuracyPercentage; - int amountHitObjectsWithAccuracy = countHitCircles; + int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; @@ -204,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty return accuracyValue; } - private int totalHits => countGreat + countGood + countMeh + countMiss; - private int totalSuccessfulHits => countGreat + countGood + countMeh; + private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countOk + countMeh; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 78f4c4d992..9349ef7a18 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -15,6 +15,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { private readonly ManualSliderBody body; + /// + /// Offset in absolute (local) coordinates from the start of the curve. + /// + public Vector2 PathStartLocation => body.PathOffset; + public SliderBodyPiece() { InternalChild = body = new ManualSliderBody diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 6633136673..94862eb205 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), }; - public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre; + public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); 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 e1cbfa93f6..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,12 +42,12 @@ 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 => new[] + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] { - distanceSnapToggle - }; + new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) + }); private BindableList selectedHitObjects; @@ -94,6 +97,10 @@ namespace osu.Game.Rulesets.Osu.Edit public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { + if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + return snapResult; + + // will be null if distance snap is disabled or not feasible for the current time value. if (distanceSnapGrid == null) return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); @@ -102,13 +109,57 @@ namespace osu.Game.Rulesets.Osu.Edit return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); } + private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) + { + // check other on-screen objects for snapping/stacking + var blueprints = BlueprintContainer.SelectionBlueprints.AliveChildren; + + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + + float snapRadius = + playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X - + playfield.GamefieldToScreenSpace(Vector2.Zero).X; + + foreach (var b in blueprints) + { + if (b.IsSelected) + continue; + + var hitObject = (OsuHitObject)b.HitObject; + + Vector2? snap = checkSnap(hitObject.Position); + if (snap == null && hitObject.Position != hitObject.EndPosition) + snap = checkSnap(hitObject.EndPosition); + + if (snap != null) + { + // only return distance portion, since time is not really valid + snapResult = new SnapResult(snap.Value, null, playfield); + return true; + } + + Vector2? checkSnap(Vector2 checkPos) + { + Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos); + + if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius) + return checkScreenPos; + + return null; + } + } + + snapResult = null; + return false; + } + private void updateDistanceSnapGrid() { distanceSnapGridContainer.Clear(); distanceSnapGridCache.Invalidate(); distanceSnapGrid = null; - if (!distanceSnapToggle.Value) + if (distanceSnapToggle.Value != TernaryState.True) return; switch (BlueprintContainer.CurrentTool) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 9418565907..7ae0730e39 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -1,7 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -10,40 +16,219 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuSelectionHandler : SelectionHandler { - public override bool HandleMovement(MoveSelectionEvent moveEvent) + protected override void OnSelectionChanged() { - Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); - Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + base.OnSelectionChanged(); - // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var h in SelectedHitObjects.OfType()) + bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider); + + SelectionBox.CanRotate = canOperate; + SelectionBox.CanScaleX = canOperate; + SelectionBox.CanScaleY = canOperate; + } + + protected override void OnOperationEnded() + { + base.OnOperationEnded(); + referenceOrigin = null; + } + + public override bool HandleMovement(MoveSelectionEvent moveEvent) => + moveSelection(moveEvent.InstantDelta); + + /// + /// During a transform, the initial origin is stored so it can be used throughout the operation. + /// + private Vector2? referenceOrigin; + + public override bool HandleFlip(Direction direction) + { + var hitObjects = selectedMovableObjects; + + var selectedObjectsQuad = getSurroundingQuad(hitObjects); + var centre = selectedObjectsQuad.Centre; + + foreach (var h in hitObjects) { - if (h is Spinner) + var pos = h.Position; + + switch (direction) { - // Spinners don't support position adjustments - continue; + case Direction.Horizontal: + pos.X = centre.X - (pos.X - centre.X); + break; + + case Direction.Vertical: + pos.Y = centre.Y - (pos.Y - centre.Y); + break; } - // Stacking is not considered - minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); - maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); - } + h.Position = pos; - if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight) - return false; - - foreach (var h in SelectedHitObjects.OfType()) - { - if (h is Spinner) + if (h is Slider slider) { - // Spinners don't support position adjustments - continue; + foreach (var point in slider.Path.ControlPoints) + { + point.Position.Value = new Vector2( + (direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X, + (direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y + ); + } } - - h.Position += moveEvent.InstantDelta; } return true; } + + public override bool HandleScale(Vector2 scale, Anchor reference) + { + adjustScaleFromAnchor(ref scale, reference); + + var hitObjects = selectedMovableObjects; + + // for the time being, allow resizing of slider paths only if the slider is + // the only hit object selected. with a group selection, it's likely the user + // is not looking to change the duration of the slider but expand the whole pattern. + if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) + { + Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height); + + foreach (var point in slider.Path.ControlPoints) + point.Position.Value *= pathRelativeDeltaScale; + } + else + { + // move the selection before scaling if dragging from top or left anchors. + if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; + if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; + + Quad quad = getSurroundingQuad(hitObjects); + + foreach (var h in hitObjects) + { + h.Position = new Vector2( + quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X), + quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y) + ); + } + } + + return true; + } + + private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) + { + // cancel out scale in axes we don't care about (based on which drag handle was used). + if ((reference & Anchor.x1) > 0) scale.X = 0; + if ((reference & Anchor.y1) > 0) scale.Y = 0; + + // reverse the scale direction if dragging from top or left. + if ((reference & Anchor.x0) > 0) scale.X = -scale.X; + if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; + } + + public override bool HandleRotation(float delta) + { + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); + + referenceOrigin ??= quad.Centre; + + foreach (var h in hitObjects) + { + h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); + + if (h is IHasPath path) + { + foreach (var point in path.Path.ControlPoints) + point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); + } + } + + // this isn't always the case but let's be lenient for now. + return true; + } + + private bool moveSelection(Vector2 delta) + { + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); + + if (quad.TopLeft.X + delta.X < 0 || + quad.TopLeft.Y + delta.Y < 0 || + quad.BottomRight.X + delta.X > DrawWidth || + quad.BottomRight.Y + delta.Y > DrawHeight) + return false; + + foreach (var h in hitObjects) + h.Position += delta; + + return true; + } + + /// + /// Returns a gamefield-space quad surrounding the provided hit objects. + /// + /// The hit objects to calculate a quad for. + private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => + getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition })); + + /// + /// Returns a gamefield-space quad surrounding the provided points. + /// + /// The points to calculate a quad for. + private Quad getSurroundingQuad(IEnumerable points) + { + if (!SelectedHitObjects.Any()) + return new Quad(); + + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); + Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + + // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted + foreach (var p in points) + { + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); + } + + Vector2 size = maxPosition - minPosition; + + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + } + + /// + /// All osu! hitobjects which can be moved/rotated/scaled. + /// + private OsuHitObject[] selectedMovableObjects => SelectedHitObjects + .OfType() + .Where(h => !(h is Spinner)) + .ToArray(); + + /// + /// Rotate a point around an arbitrary origin. + /// + /// The point. + /// The centre origin to rotate around. + /// The angle to rotate (in degrees). + private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) + { + angle = -angle; + + point.X -= origin.X; + point.Y -= origin.Y; + + Vector2 ret; + ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); + ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); + + ret.X += origin.X; + ret.Y += origin.Y; + + return ret; + } } } diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs index e528f65dca..1999785efe 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs @@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Osu.Judgements { public class OsuIgnoreJudgement : OsuJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 0; - - protected override double HealthIncreaseFor(HitResult result) => 0; + public override HitResult MaxResult => HitResult.IgnoreHit; } } diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index bf30fbc351..1a88e2a8b2 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -9,23 +9,5 @@ namespace osu.Game.Rulesets.Osu.Judgements public class OsuJudgement : Judgement { public override HitResult MaxResult => HitResult.Great; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Meh: - return 50; - - case HitResult.Good: - return 100; - - case HitResult.Great: - return 300; - } - } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 08fd13915d..80e40af717 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods base.ApplyToDrawableHitObjects(drawables); } + private double lastSliderHeadFadeOutStartTime; + private double lastSliderHeadFadeOutDuration; + protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) { if (!(drawable is DrawableOsuHitObject d)) @@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { + case DrawableSliderTail sliderTail: + // use stored values from head circle to achieve same fade sequence. + fadeOutDuration = lastSliderHeadFadeOutDuration; + fadeOutStartTime = lastSliderHeadFadeOutStartTime; + + using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + sliderTail.FadeOut(fadeOutDuration); + + break; + + case DrawableSliderRepeat sliderRepeat: + // use stored values from head circle to achieve same fade sequence. + fadeOutDuration = lastSliderHeadFadeOutDuration; + fadeOutStartTime = lastSliderHeadFadeOutStartTime; + + using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + // only apply to circle piece – reverse arrow is not affected by hidden. + sliderRepeat.CirclePiece.FadeOut(fadeOutDuration); + + break; + case DrawableHitCircle circle: + + if (circle is DrawableSliderHead) + { + lastSliderHeadFadeOutDuration = fadeOutDuration; + lastSliderHeadFadeOutStartTime = fadeOutStartTime; + } + // we don't want to see the approach circle using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) circle.ApproachCircle.Hide(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index a438dc8be4..b5ac26c824 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var circleResult = (OsuHitCircleJudgementResult)r; // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (result != HitResult.Miss) + if (result.IsHit()) { var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 2946331bc6..45c664ba3b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 012d9f8878..49535e7fff 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -8,7 +8,6 @@ using osu.Game.Configuration; using osuTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK.Graphics; @@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject != null) { lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); + lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true); } else { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 07f40f763b..b00d12983d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -52,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), + tailContainer = new Container { RelativeSizeAxes = Axes.Both }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) @@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }; } @@ -87,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } - private SkinnableSound slidingSample; + private PausableSkinnableSound slidingSample; protected override void LoadSamples() { @@ -103,19 +102,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "sliderslide"; - AddInternal(slidingSample = new SkinnableSound(clone) + AddInternal(slidingSample = new PausableSkinnableSound(clone) { Looping = true }); } } + public override void StopAllSamples() + { + base.StopAllSamples(); + slidingSample?.Stop(); + } + private void updateSlidingSample(ValueChangedEvent tracking) { - // note that samples will not start playing if exiting a seek operation in the middle of a slider. - // may be something we want to address at a later point, but not so easy to make happen right now - // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). - if (tracking.NewValue && ShouldPlaySamples) + if (tracking.NewValue) slidingSample?.Play(); else slidingSample?.Stop(); @@ -247,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // rather than doing it this way, we should probably attach the sample to the tail circle. // this can only be done after we stop using LegacyLastTick. - if (TailCircle.Result.Type != HitResult.Miss) + if (TailCircle.IsHit) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index d79ecb7b4e..2a88f11f69 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -6,10 +6,11 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Drawable scaleContainer; + public readonly Drawable CirclePiece; + public override bool DisplayResult => false; public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) @@ -35,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - InternalChild = scaleContainer = new ReverseArrowPiece(); + InternalChild = scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + // no default for this; only visible in legacy skins. + CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()), + arrow = new ReverseArrowPiece(), + } + }; } private readonly IBindable scaleBindable = new BindableFloat(); @@ -50,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (sliderRepeat.StartTime <= Time.Current) - ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() @@ -86,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private bool hasRotation; + private readonly ReverseArrowPiece arrow; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) { // When the repeat is hit, the arrow should fade out on spot rather than following the slider @@ -115,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); - while (Math.Abs(aimRotation - Rotation) > 180) - aimRotation += aimRotation < Rotation ? 360 : -360; + while (Math.Abs(aimRotation - arrow.Rotation) > 180) + aimRotation += aimRotation < arrow.Rotation ? 360 : -360; if (!hasRotation) { - Rotation = aimRotation; + arrow.Rotation = aimRotation; hasRotation = true; } else { // If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly). - Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint); + arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 29a4929c1b..f5bcecccdf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -1,16 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking + public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking { - private readonly Slider slider; + private readonly SliderTailCircle tailCircle; /// /// The judgement text is provided by the . @@ -19,36 +23,82 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - private readonly IBindable positionBindable = new Bindable(); - private readonly IBindable pathVersion = new Bindable(); + private readonly IBindable scaleBindable = new BindableFloat(); - public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) - : base(hitCircle) + private readonly SkinnableDrawable circlePiece; + + private readonly Container scaleContainer; + + public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle) + : base(tailCircle) { - this.slider = slider; - + this.tailCircle = tailCircle; Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - AlwaysPresent = true; + InternalChildren = new Drawable[] + { + scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + // no default for this; only visible in legacy skins. + circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()) + } + }, + }; + } - positionBindable.BindTo(hitCircle.PositionBindable); - pathVersion.BindTo(slider.Path.Version); + [BackgroundDependencyLoader] + private void load() + { + scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + scaleBindable.BindTo(HitObject.ScaleBindable); + } - positionBindable.BindValueChanged(_ => updatePosition()); - pathVersion.BindValueChanged(_ => updatePosition(), true); + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); - // TODO: This has no drawable content. Support for skins should be added. + circlePiece.FadeInFromZero(HitObject.TimeFadeIn); + } + + protected override void UpdateStateTransforms(ArmedState state) + { + base.UpdateStateTransforms(state); + + Debug.Assert(HitObject.HitWindows != null); + + switch (state) + { + case ArmedState.Idle: + this.Delay(HitObject.TimePreempt).FadeOut(500); + + Expire(true); + break; + + case ArmedState.Miss: + this.FadeOut(100); + break; + + case ArmedState.Hit: + // todo: temporary / arbitrary + this.Delay(800).FadeOut(); + break; + } } protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered && timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } - private void updatePosition() => Position = HitObject.Position - slider.Position; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) => + Position = tailCircle.RepeatIndex % 2 == 0 ? end : start; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 66eb60aa28..9b68b446a4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -10,7 +10,6 @@ using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Skinning; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -64,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a57bb466c7..936bfaeb86 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables isSpinning.BindValueChanged(updateSpinningSample); } - private SkinnableSound spinningSample; + private PausableSkinnableSound spinningSample; private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "spinnerspin"; - AddInternal(spinningSample = new SkinnableSound(clone) + AddInternal(spinningSample = new PausableSkinnableSound(clone) { Volume = { Value = 0 }, Looping = true, @@ -113,10 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void updateSpinningSample(ValueChangedEvent tracking) { - // note that samples will not start playing if exiting a seek operation in the middle of a spinner. - // may be something we want to address at a later point, but not so easy to make happen right now - // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). - if (tracking.NewValue && ShouldPlaySamples) + if (tracking.NewValue) { spinningSample?.Play(); spinningSample?.VolumeTo(1, 200); @@ -127,6 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + public override void StopAllSamples() + { + base.StopAllSamples(); + spinningSample?.Stop(); + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -209,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; // Trigger a miss result for remaining ticks to avoid infinite gameplay. - foreach (var tick in ticks.Where(t => !t.IsHit)) + foreach (var tick in ticks.Where(t => !t.Result.HasResult)) tick.TriggerResult(false); ApplyResult(r => @@ -217,11 +220,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (Progress >= 1) r.Type = HitResult.Great; else if (Progress > .9) - r.Type = HitResult.Good; + r.Type = HitResult.Ok; else if (Progress > .75) r.Type = HitResult.Meh; else if (Time.Current >= Spinner.EndTime) - r.Type = HitResult.Miss; + r.Type = r.Judgement.MinResult; }); } @@ -265,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables while (wholeSpins != spins) { - var tick = ticks.FirstOrDefault(t => !t.IsHit); + var tick = ticks.FirstOrDefault(t => !t.Result.HasResult); // tick may be null if we've hit the spin limit. if (tick != null) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index c390b673be..e9cede1398 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject @@ -18,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Apply a judgement result. /// /// Whether this tick was reached. - internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss); + internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index aab01f45d4..e95cdc7ee3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(TextureStore textures, DrawableHitObject drawableHitObject) { InternalChildren = new Drawable[] { @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, - new TrianglesPiece + new TrianglesPiece((int)drawableHitObject.HitObject.StartTime) { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 1476fe6010..e855317544 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private Spinner spinner; + private const float initial_scale = 1.3f; private const float idle_alpha = 0.2f; private const float tracking_alpha = 0.4f; @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // we are slightly bigger than our parent, to clip the top and bottom of the circle // this should probably be revisited when scaled spinners are a thing. - Scale = new Vector2(1.3f); + Scale = new Vector2(initial_scale); Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -93,7 +93,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces base.LoadComplete(); drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); - drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } protected override void Update() @@ -116,50 +118,66 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime)); } - const float initial_scale = 0.2f; - float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; + const float initial_fill_scale = 0.2f; + float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress; fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; } - private void updateStateTransforms(ValueChangedEvent state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - centre.ScaleTo(0); - mainContainer.ScaleTo(0); + if (!(drawableHitObject is DrawableSpinner)) + return; - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) { - // constant ambient rotation to give the spinner "spinning" character. - this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); - - centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); - mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + this.ScaleTo(initial_scale); + this.RotateTo(0); using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { - centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); - mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + // constant ambient rotation to give the spinner "spinning" character. + this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + } + + using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true)) + { + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out); + this.RotateTo(mainContainer.Rotation + 180, 320); + break; + + case ArmedState.Miss: + this.ScaleTo(initial_scale * 0.8f, 320, Easing.In); + break; + } + } + } + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + centre.ScaleTo(0); + mainContainer.ScaleTo(0); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); + mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); + mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + } } } // transforms we have from completing the spinner will be rolled back, so reapply immediately. - updateComplete(state.NewValue == ArmedState.Hit, 0); - - using (BeginDelayedSequence(spinner.Duration, true)) - { - switch (state.NewValue) - { - case ArmedState.Hit: - this.ScaleTo(Scale * 1.2f, 320, Easing.Out); - this.RotateTo(mainContainer.Rotation + 180, 320); - break; - - case ArmedState.Miss: - this.ScaleTo(Scale * 0.8f, 320, Easing.In); - break; - } - } + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + updateComplete(state == ArmedState.Hit, 0); } private void updateComplete(bool complete, double duration) @@ -185,5 +203,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return true; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index bcf64b81a6..619fea73bc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre; Masking = true; - BorderThickness = 10; + BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other. BorderColour = Color4.White; Child = new Box diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs index b499d7a92b..f483bb1b26 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// public class SpinnerBonusDisplay : CompositeDrawable { + private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult; + private readonly OsuSpriteText bonusCounter; public SpinnerBonusDisplay() @@ -36,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; displayedCount = count; - bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}"; + bonusCounter.Text = $"{score_per_tick * count}"; bonusCounter.FadeOutFromOne(1500); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index f1a782cbb5..05ed38d241 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -77,6 +79,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private bool rotationTransferred; + [Resolved(canBeNull: true)] + private GameplayClock gameplayClock { get; set; } + protected override void Update() { base.Update(); @@ -126,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate); + RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs index 0e29a1dcd8..6cdb0d3df3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs @@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override bool CreateNewTriangles => false; protected override float SpawnRatio => 0.5f; - public TrianglesPiece() + public TrianglesPiece(int? seed = null) + : base(seed) { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 51f6a44a87..917382eccf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects // if this is to change, we should revisit this. AddNested(TailCircle = new SliderTailCircle(this) { + RepeatIndex = e.SpanIndex, StartTime = e.Time, Position = EndPosition, StackHeight = StackHeight @@ -183,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new SliderRepeat + AddNested(new SliderRepeat(this) { RepeatIndex = e.SpanIndex, - SpanDuration = SpanDuration, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs deleted file mode 100644 index 151902a752..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Osu.Objects -{ - public class SliderCircle : HitCircle - { - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs new file mode 100644 index 0000000000..a6aed2c00e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects +{ + /// + /// A hit circle which is at the end of a slider path (either repeat or final tail). + /// + public abstract class SliderEndCircle : HitCircle + { + private readonly Slider slider; + + protected SliderEndCircle(Slider slider) + { + this.slider = slider; + } + + public int RepeatIndex { get; set; } + + public double SpanDuration => slider.SpanDuration; + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + if (RepeatIndex > 0) + { + // Repeat points after the first span should appear behind the still-visible one. + TimeFadeIn = 0; + + // The next end circle should appear exactly after the previous circle (on the same end) is hit. + TimePreempt = SpanDuration * 2; + } + else + { + // taken from osu-stable + const float first_end_circle_preempt_adjust = 2 / 3f; + + // The first end circle should fade in with the slider. + TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust; + } + } + + protected override HitWindows CreateHitWindows() => HitWindows.Empty; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index ac6c6905e4..cca86361c2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -1,40 +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 osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class SliderRepeat : OsuHitObject + public class SliderRepeat : SliderEndCircle { - public int RepeatIndex { get; set; } - public double SpanDuration { get; set; } - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + public SliderRepeat(Slider slider) + : base(slider) { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - // Out preempt should be one span early to give the user ample warning. - TimePreempt += SpanDuration; - - // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders - // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time. - if (RepeatIndex > 0) - TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); } - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderRepeatJudgement(); public class SliderRepeatJudgement : OsuJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; + public override HitResult MaxResult => HitResult.LargeTickHit; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 1e54b576f1..f9450062f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -13,25 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects /// Note that this should not be used for timing correctness. /// See usage in for more information. /// - public class SliderTailCircle : SliderCircle + public class SliderTailCircle : SliderEndCircle { - private readonly IBindable pathVersion = new Bindable(); - public SliderTailCircle(Slider slider) + : base(slider) { - pathVersion.BindTo(slider.Path.Version); - pathVersion.BindValueChanged(_ => Position = slider.EndPosition); } - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderTailJudgement(); public class SliderTailJudgement : OsuJudgement { - protected override int NumericResultFor(HitResult result) => 0; - - public override bool AffectsCombo => false; + public override HitResult MaxResult => HitResult.SmallTickHit; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 22f3f559db..a427ee1955 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderTickJudgement : OsuJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; + public override HitResult MaxResult => HitResult.LargeTickHit; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 0b1232b8db..235dc8710a 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerBonusTick : SpinnerTick { - public new const int SCORE_PER_TICK = 50; - public SpinnerBonusTick() { Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); @@ -20,9 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0; - - protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; + public override HitResult MaxResult => HitResult.LargeBonus; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index f54e7a9a15..d715b9a428 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -9,19 +9,13 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerTick : OsuHitObject { - public const int SCORE_PER_TICK = 10; - public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; public class OsuSpinnerTickJudgement : OsuJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0; - - protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; + public override HitResult MaxResult => HitResult.SmallBonus; } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 452491361a..cc2eebdd36 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new OsuPerformanceCalculator(this, beatmap, score, attributes); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 5468764692..2883f0c187 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu ReverseArrow, HitCircleText, SliderHeadHitCircle, + SliderTailHitCircle, SliderFollowCircle, SliderBall, SliderBody, diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 76b2631894..954a217473 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))); @@ -134,13 +137,13 @@ namespace osu.Game.Rulesets.Osu.Replays if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } - else if (h.StartTime - hitWindows.WindowFor(HitResult.Good) > endTime + hitWindows.WindowFor(HitResult.Good) + 50) + else if (h.StartTime - hitWindows.WindowFor(HitResult.Ok) > endTime + hitWindows.WindowFor(HitResult.Ok) + 50) { if (!(prev is Spinner) && h.StartTime - endTime < 1000) - AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Good), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Ok), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); if (!(h is Spinner)) - AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Good), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Ok), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index 6f2998006f..dafe63a6d1 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Scoring private static readonly DifficultyRange[] osu_ranges = { new DifficultyRange(HitResult.Great, 80, 50, 20), - new DifficultyRange(HitResult.Good, 140, 100, 60), + new DifficultyRange(HitResult.Ok, 140, 100, 60), new DifficultyRange(HitResult.Meh, 200, 150, 100), new DifficultyRange(HitResult.Miss, 400, 400, 400), }; @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Scoring switch (result) { case HitResult.Great: - case HitResult.Good: + case HitResult.Ok: case HitResult.Meh: case HitResult.Miss: return true; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 86ec76e373..44118227d9 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -25,7 +25,5 @@ namespace osu.Game.Rulesets.Osu.Scoring return new OsuJudgementResult(hitObject, judgement); } } - - public override HitWindows CreateHitWindows() => new OsuHitWindows(); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs index 1885c76fcc..e6cd7bc59d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private bool disjointTrail; private double lastTrailTime; + private IBindable cursorSize; public LegacyCursorTrail() { @@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, OsuConfigManager config) { Texture = skin.GetTexture("cursortrail"); disjointTrail = skin.GetTexture("cursormiddle") == null; @@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. Texture.ScaleAdjust *= 1.6f; } + + cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); } protected override double FadeDuration => disjointTrail ? 150 : 500; protected override bool InterpolateMovements => !disjointTrail; + protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); + protected override bool OnMouseMove(MouseMoveEvent e) { if (!disjointTrail) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index d15a0a3203..382d6e53cc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning public class LegacyMainCirclePiece : CompositeDrawable { private readonly string priorityLookup; + private readonly bool hasNumber; - public LegacyMainCirclePiece(string priorityLookup = null) + public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true) { this.priorityLookup = priorityLookup; + this.hasNumber = hasNumber; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } @@ -47,6 +49,23 @@ namespace osu.Game.Rulesets.Osu.Skinning { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; + bool allowFallback = false; + + // attempt lookup using priority specification + Texture baseTexture = getTextureWithFallback(string.Empty); + + // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup. + if (baseTexture == null) + { + allowFallback = true; + baseTexture = getTextureWithFallback(string.Empty); + } + + // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. + // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. + // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin). + Texture overlayTexture = getTextureWithFallback("overlay"); + InternalChildren = new Drawable[] { circleSprites = new Container @@ -58,19 +77,23 @@ namespace osu.Game.Rulesets.Osu.Skinning { hitCircleSprite = new Sprite { - Texture = getTextureWithFallback(string.Empty), + Texture = baseTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, hitCircleOverlay = new Sprite { - Texture = getTextureWithFallback("overlay"), + Texture = overlayTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, } } }, - hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText + }; + + if (hasNumber) + { + AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, @@ -78,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - }; + }); + } bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; @@ -95,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture tex = null; if (!string.IsNullOrEmpty(priorityLookup)) + { tex = skin.GetTexture($"{priorityLookup}{name}"); + if (!allowFallback) + return tex; + } + return tex ?? skin.GetTexture($"hitcircle{name}"); } } @@ -107,7 +135,8 @@ namespace osu.Game.Rulesets.Osu.Skinning state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); - indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + if (hasNumber) + indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } private void updateState(ValueChangedEvent state) @@ -120,16 +149,19 @@ namespace osu.Game.Rulesets.Osu.Skinning circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - - if (legacyVersion >= 2.0m) - // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. - hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); - else + if (hasNumber) { - // old skins scale and fade it normally along other pieces. - hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); - hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; + + if (legacyVersion >= 2.0m) + // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. + hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); + else + { + // old skins scale and fade it normally along other pieces. + hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + } } break; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 739c87e037..56b5571ce1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -71,20 +70,30 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - this.FadeOut(); - drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } - private void updateStateTransforms(ValueChangedEvent state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { + if (!(drawableHitObject is DrawableSpinner)) + return; + var spinner = (Spinner)drawableSpinner.HitObject; + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) this.FadeInFromZero(spinner.TimeFadeIn / 2); - fixedMiddle.FadeColour(Color4.White); - using (BeginAbsoluteSequence(spinner.StartTime, true)) - fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + fixedMiddle.FadeColour(Color4.White); + + using (BeginDelayedSequence(spinner.TimePreempt, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + } } protected override void Update() @@ -95,5 +104,13 @@ namespace osu.Game.Rulesets.Osu.Skinning Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index e157842fd1..7b0d7acbbc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -25,12 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite metreSprite; private Container metre; + private bool spinnerBlink; + private const float sprite_scale = 1 / 1.6f; private const float final_metre_height = 692 * sprite_scale; [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { + spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; + drawableSpinner = (DrawableSpinner)drawableObject; RelativeSizeAxes = Axes.Both; @@ -85,14 +88,20 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - this.FadeOut(); - drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } - private void updateStateTransforms(ValueChangedEvent state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { + if (!(drawableHitObject is DrawableSpinner)) + return; + var spinner = drawableSpinner.HitObject; + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) this.FadeInFromZero(spinner.TimeFadeIn / 2); } @@ -117,15 +126,26 @@ namespace osu.Game.Rulesets.Osu.Skinning private float getMetreHeight(float progress) { - progress = Math.Min(99, progress * 100); + progress *= 100; + + // the spinner should still blink at 100% progress. + if (spinnerBlink) + progress = Math.Min(99, progress); int barCount = (int)progress / 10; - // todo: add SpinnerNoBlink support - if (RNG.NextBool(((int)progress % 10) / 10f)) + if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f)) barCount++; return (float)barCount / total_bars * final_metre_height; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 851a8d56c9..78bc26eff7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.SliderTailHitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece("sliderendcircle", false); + + return null; + case OsuSkinComponents.SliderHeadHitCircle: if (hasHitCircle.Value) return new LegacyMainCirclePiece("sliderstartcircle"); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index e034e14eb0..63c9b53278 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorRotate, HitCircleOverlayAboveNumber, HitCircleOverlayAboveNumer, // Some old skins will have this typo - SpinnerFrequencyModulate + SpinnerFrequencyModulate, + SpinnerNoBlink } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 9bcb3abc63..0b30c28b8d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// protected virtual bool InterpolateMovements => true; + protected virtual float IntervalMultiplier => 1.0f; + private Vector2? lastPosition; private readonly InputResampler resampler = new InputResampler(); @@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f; + float interval = partSize.X / 2.5f * IntervalMultiplier; for (float d = interval; d < distance; d += interval) { diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 88adf72551..3870f303b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Osu.UI new SettingsCheckbox { LabelText = "Snaking in sliders", - Bindable = config.GetBindable(OsuRulesetSetting.SnakingInSliders) + Current = config.GetBindable(OsuRulesetSetting.SnakingInSliders) }, new SettingsCheckbox { LabelText = "Snaking out sliders", - Bindable = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) + Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) }, new SettingsCheckbox { LabelText = "Cursor trail", - Bindable = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) + Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, }; } 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..99e103da3b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); - assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); } [Test] @@ -102,8 +102,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Kiai); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); } @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Idle); } [TestCase(true)] @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Clear); } [TestCase(true, TaikoMascotAnimationState.Kiai)] @@ -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..fecb5d4a74 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 @@ -18,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public void TestNormalHit() { AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great)))); - AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good)))); + AddStep("Ok", () => SetContents(() => getContentFor(createHit(HitResult.Ok)))); AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss)))); } @@ -26,18 +25,20 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public void TestStrongHit([Values(false, true)] bool hitBoth) { AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth)))); - AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); + AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, 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/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 16ef5b968d..114038b81c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning })); AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value = - new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); + new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss })); AddToggleStep("toggle playback direction", reversed => this.reversed = reversed); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs new file mode 100644 index 0000000000..63854e7ead --- /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.Great }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 99d1b72ea4..e4c0766844 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -2,11 +2,8 @@ // 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 +15,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,11 +28,9 @@ 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() + [Test] + public void TestVariousHits() { AddStep("Hit", () => addHitJudgement(false)); AddStep("Strong hit", () => addStrongHitJudgement(false)); @@ -64,35 +58,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,18 +93,18 @@ 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; } } private void addHitJudgement(bool kiai) { - HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; + HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; var cpi = new ControlPointInfo(); cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); @@ -147,16 +112,16 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); - var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 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) { - HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; + HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; var cpi = new ControlPointInfo(); cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); @@ -164,39 +129,39 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); - var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 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 +171,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 +182,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/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index 7c39c040b1..fcf7c529f5 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -42,9 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Audio } } - private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) + private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) { - var drawable = new SkinnableSound(hitSampleInfo) + var drawable = new PausableSkinnableSound(hitSampleInfo) { LifetimeStart = lifetimeStart, LifetimeEnd = lifetimeEnd @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio public class DrumSample { - public SkinnableSound Centre; - public SkinnableSound Rim; + public PausableSkinnableSound Centre; + public PausableSkinnableSound Rim; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 26840d72f1..2d9b95ae88 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -20,12 +19,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private Mod[] mods; private int countGreat; - private int countGood; + private int countOk; private int countMeh; private int countMiss; - public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) - : base(ruleset, beatmap, score, attributes) + public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { } @@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { mods = Score.Mods; countGreat = Score.Statistics.GetOrDefault(HitResult.Great); - countGood = Score.Statistics.GetOrDefault(HitResult.Good); + countOk = Score.Statistics.GetOrDefault(HitResult.Ok); countMeh = Score.Statistics.GetOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetOrDefault(HitResult.Miss); @@ -102,6 +101,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); } - private int totalHits => countGreat + countGood + countMeh + countMiss; + private int totalHits => countGreat + countOk + countMeh + countMiss; } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 40565048c2..d5dd758e10 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -14,75 +15,86 @@ namespace osu.Game.Rulesets.Taiko.Edit { public class TaikoSelectionHandler : SelectionHandler { + private readonly Bindable selectionRimState = new Bindable(); + private readonly Bindable selectionStrongState = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + selectionStrongState.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + SetStrongState(false); + break; + + case TernaryState.True: + SetStrongState(true); + break; + } + }; + + selectionRimState.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + SetRimState(false); + break; + + case TernaryState.True: + SetRimState(true); + break; + } + }; + } + + public void SetStrongState(bool state) + { + var hits = SelectedHitObjects.OfType(); + + ChangeHandler.BeginChange(); + + foreach (var h in hits) + { + if (h.IsStrong != state) + { + h.IsStrong = state; + EditorBeatmap.UpdateHitObject(h); + } + } + + ChangeHandler.EndChange(); + } + + public void SetRimState(bool state) + { + var hits = SelectedHitObjects.OfType(); + + ChangeHandler.BeginChange(); + + foreach (var h in hits) + h.Type = state ? HitType.Rim : HitType.Centre; + + ChangeHandler.EndChange(); + } + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) { if (selection.All(s => s.HitObject is Hit)) - { - var hits = selection.Select(s => s.HitObject).OfType(); - - yield return new TernaryStateMenuItem("Rim", action: state => - { - ChangeHandler.BeginChange(); - - foreach (var h in hits) - { - switch (state) - { - case TernaryState.True: - h.Type = HitType.Rim; - break; - - case TernaryState.False: - h.Type = HitType.Centre; - break; - } - } - - ChangeHandler.EndChange(); - }) - { - State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) } - }; - } + yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } }; if (selection.All(s => s.HitObject is TaikoHitObject)) - { - var hits = selection.Select(s => s.HitObject).OfType(); - - yield return new TernaryStateMenuItem("Strong", action: state => - { - ChangeHandler.BeginChange(); - - foreach (var h in hits) - { - switch (state) - { - case TernaryState.True: - h.IsStrong = true; - break; - - case TernaryState.False: - h.IsStrong = false; - break; - } - - EditorBeatmap?.UpdateHitObject(h); - } - - ChangeHandler.EndChange(); - }) - { - State = { Value = getTernaryState(hits, h => h.IsStrong) } - }; - } + yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } }; } - private TernaryState getTernaryState(IEnumerable selection, Func func) + protected override void UpdateTernaryStates() { - if (selection.Any(func)) - return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; + base.UpdateTernaryStates(); - return TernaryState.False; + selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.Type == HitType.Rim); + selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.IsStrong); } } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index a617028f1c..647ad7853d 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -7,25 +7,13 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollTickJudgement : TaikoJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - case HitResult.Great: - return 200; - - default: - return 0; - } - } + public override HitResult MaxResult => HitResult.SmallTickHit; protected override double HealthIncreaseFor(HitResult result) { switch (result) { - case HitResult.Great: + case HitResult.SmallTickHit: return 0.15; default: diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs index eb5f443365..e272c1a4ef 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs @@ -10,21 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public override HitResult MaxResult => HitResult.Great; - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - case HitResult.Good: - return 100; - - case HitResult.Great: - return 300; - - default: - return 0; - } - } - protected override double HealthIncreaseFor(HitResult result) { switch (result) @@ -32,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements case HitResult.Miss: return -1.0; - case HitResult.Good: + case HitResult.Ok: return 1.1; case HitResult.Great: diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index e045ea324f..06495ad9f4 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs @@ -7,9 +7,9 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoStrongJudgement : TaikoJudgement { + public override HitResult MaxResult => HitResult.SmallBonus; + // MainObject already changes the HP protected override double HealthIncreaseFor(HitResult result) => 0; - - public override bool AffectsCombo => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 2c1c2d2bc1..8f268dc1c7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!(obj is DrawableDrumRollTick)) return; - if (result.Type > HitResult.Miss) + if (result.IsHit) rollingHits++; else rollingHits--; @@ -129,10 +129,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (countHit >= HitObject.RequiredGoodHits) { - ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); } else - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); } protected override void UpdateStateTransforms(ArmedState state) @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!MainObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 62405cf047..9d7dcc7218 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -34,14 +33,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (timeOffset > HitObject.HitWindow) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - ApplyResult(r => r.Type = HitResult.Great); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } protected override void UpdateStateTransforms(ArmedState state) @@ -74,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!MainObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; 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/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 92ae7e0fd3..bb42240f25 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -36,35 +36,64 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool pressHandledThisFrame; - private Bindable type; + private readonly Bindable type; public DrawableHit(Hit hit) : base(hit) { + type = HitObject.TypeBindable.GetBoundCopy(); FillMode = FillMode.Fit; + + updateActionsFromType(); } [BackgroundDependencyLoader] private void load() { - type = HitObject.TypeBindable.GetBoundCopy(); type.BindValueChanged(_ => { - updateType(); + updateActionsFromType(); + + // will overwrite samples, should only be called on change. + updateSamplesFromTypeChange(); + RecreatePieces(); }); - - updateType(); } - private void updateType() + private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); + + protected override void LoadSamples() + { + base.LoadSamples(); + + type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre; + } + + private void updateSamplesFromTypeChange() + { + var rimSamples = getRimSamples(); + + bool isRimType = HitObject.Type == HitType.Rim; + + if (isRimType != rimSamples.Any()) + { + if (isRimType) + HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); + else + { + foreach (var sample in rimSamples) + HitObject.Samples.Remove(sample); + } + } + } + + private void updateActionsFromType() { HitActions = HitObject.Type == HitType.Centre ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } : new[] { TaikoAction.LeftRim, TaikoAction.RightRim }; - - RecreatePieces(); } protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre @@ -114,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } @@ -123,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; if (!validActionPressed) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); else ApplyResult(r => r.Type = result); } @@ -228,19 +257,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!MainObject.Result.IsHit) { - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } if (!userTriggered) { if (timeOffset - MainObject.Result.TimeOffset > second_hit_window) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset - MainObject.Result.TimeOffset) <= second_hit_window) - ApplyResult(r => r.Type = MainObject.Result.Type); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(TaikoAction action) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 7294587b10..8ee4a5db71 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - nextTick?.TriggerResult(HitResult.Great); + nextTick?.TriggerResult(true); var numHits = ticks.Count(r => r.IsHit); @@ -208,12 +208,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables continue; } - tick.TriggerResult(HitResult.Miss); + tick.TriggerResult(false); } - var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss; - - ApplyResult(r => r.Type = hitResult); + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 1685576f0d..6202583494 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -19,10 +18,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateInitialTransforms() => this.FadeOut(); - public void TriggerResult(HitResult type) + public void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = type); + ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 929cf8a937..9cd23383c4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -1,19 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; -using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Objects.Drawables; -using osuTK; -using System.Linq; -using osu.Game.Audio; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; +using osu.Framework.Input.Bindings; +using osu.Game.Audio; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - private Bindable isStrong; + private readonly Bindable isStrong; private readonly Container strongHitContainer; @@ -128,6 +128,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables : base(hitObject) { HitObject = hitObject; + isStrong = HitObject.IsStrongBindable.GetBoundCopy(); Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; @@ -140,8 +141,40 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables [BackgroundDependencyLoader] private void load() { - isStrong = HitObject.IsStrongBindable.GetBoundCopy(); - isStrong.BindValueChanged(_ => RecreatePieces(), true); + isStrong.BindValueChanged(_ => + { + // will overwrite samples, should only be called on change. + updateSamplesFromStrong(); + + RecreatePieces(); + }); + + RecreatePieces(); + } + + private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); + + protected override void LoadSamples() + { + base.LoadSamples(); + + isStrong.Value = getStrongSamples().Any(); + } + + private void updateSamplesFromStrong() + { + var strongSamples = getStrongSamples(); + + if (isStrong.Value != strongSamples.Any()) + { + if (isStrong.Value) + HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); + else + { + foreach (var sample in strongSamples) + HitObject.Samples.Remove(sample); + } + } } protected virtual void RecreatePieces() 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/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index dd3c2289ea..f7a1d130eb 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring private double hpMultiplier; /// - /// HP multiplier for a . + /// HP multiplier for a that does not satisfy . /// private double hpMissMultiplier; @@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring } protected override double GetHealthIncreaseFor(JudgementResult result) - => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); + => base.GetHealthIncreaseFor(result) * (result.IsHit ? hpMultiplier : hpMissMultiplier); } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index 9d273392ff..cf806c0c97 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring private static readonly DifficultyRange[] taiko_ranges = { new DifficultyRange(HitResult.Great, 50, 35, 20), - new DifficultyRange(HitResult.Good, 120, 80, 50), + new DifficultyRange(HitResult.Ok, 120, 80, 50), new DifficultyRange(HitResult.Miss, 135, 95, 70), }; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring switch (result) { case HitResult.Great: - case HitResult.Good: + case HitResult.Ok: case HitResult.Miss: return true; } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index e29ea87d25..1829ea2513 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Taiko.Scoring protected override double DefaultAccuracyPortion => 0.75; protected override double DefaultComboPortion => 0.25; - - public override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index b5ec2e8def..19493271be 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -1,21 +1,64 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyHitExplosion : CompositeDrawable { - public LegacyHitExplosion(Drawable sprite) - { - InternalChild = sprite; + private readonly Drawable sprite; + private readonly Drawable strongSprite; + private DrawableStrongNestedHit nestedStrongHit; + private bool switchedToStrongSprite; + + /// + /// Creates a new legacy hit explosion. + /// + /// + /// Contrary to stable's, this implementation doesn't require a frame-perfect hit + /// for the strong sprite to be displayed. + /// + /// The normal legacy explosion sprite. + /// The strong legacy explosion sprite. + public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null) + { + this.sprite = sprite; + this.strongSprite = strongSprite; + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject judgedObject) + { Anchor = Anchor.Centre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; + + AddInternal(sprite.With(s => + { + s.Anchor = Anchor.Centre; + s.Origin = Anchor.Centre; + })); + + if (strongSprite != null) + { + AddInternal(strongSprite.With(s => + { + s.Alpha = 0; + s.Anchor = Anchor.Centre; + s.Origin = Anchor.Centre; + })); + } + + if (judgedObject is DrawableHit hit) + nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit; } protected override void LoadComplete() @@ -33,5 +76,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning Expire(true); } + + protected override void Update() + { + base.Update(); + + if (shouldSwitchToStrongSprite() && !switchedToStrongSprite) + { + sprite.FadeOut(50, Easing.OutQuint); + strongSprite.FadeIn(50, Easing.OutQuint); + switchedToStrongSprite = true; + } + } + + private bool shouldSwitchToStrongSprite() + { + if (nestedStrongHit == null || strongSprite == null) + return false; + + return nestedStrongHit.IsHit; + } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 03813e0a99..e029040ef3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning var r = result.NewValue; // always ignore hitobjects that don't affect combo (drumroll ticks etc.) - if (r?.Judgement.AffectsCombo == false) + if (r?.Type.AffectsCombo() == false) return; - passing = r == null || r.Type > HitResult.Miss; + passing = r == null || r.IsHit; foreach (var sprite in InternalChildren.OfType()) sprite.Passing = passing; diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index c222ccb51f..a804ea5f82 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -7,6 +7,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; @@ -14,13 +15,29 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class TaikoLegacySkinTransformer : LegacySkinTransformer { + private Lazy hasExplosion; + public TaikoLegacySkinTransformer(ISkinSource source) : base(source) { + Source.SourceChanged += sourceChanged; + sourceChanged(); + } + + private void sourceChanged() + { + hasExplosion = new Lazy(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null); } public override Drawable GetDrawableComponent(ISkinComponent component) { + if (component is GameplaySkinComponent) + { + // if a taiko skin is providing explosion sprites, hide the judgements completely + if (hasExplosion.Value) + return Drawable.Empty(); + } + if (!(component is TaikoSkinComponent taikoComponent)) return null; @@ -74,15 +91,26 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; - case TaikoSkinComponents.TaikoExplosionGood: - case TaikoSkinComponents.TaikoExplosionGoodStrong: - case TaikoSkinComponents.TaikoExplosionGreat: - case TaikoSkinComponents.TaikoExplosionGreatStrong: case TaikoSkinComponents.TaikoExplosionMiss: - var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); - if (sprite != null) - return new LegacyHitExplosion(sprite); + var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); + if (missSprite != null) + return new LegacyHitExplosion(missSprite); + + return null; + + case TaikoSkinComponents.TaikoExplosionOk: + case TaikoSkinComponents.TaikoExplosionGreat: + + var hitName = getHitName(taikoComponent.Component); + var hitSprite = this.GetAnimation(hitName, true, false); + + if (hitSprite != null) + { + var strongHitSprite = this.GetAnimation($"{hitName}k", true, false); + + return new LegacyHitExplosion(hitSprite, strongHitSprite); + } return null; @@ -106,20 +134,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionMiss: return "taiko-hit0"; - case TaikoSkinComponents.TaikoExplosionGood: + case TaikoSkinComponents.TaikoExplosionOk: return "taiko-hit100"; - case TaikoSkinComponents.TaikoExplosionGoodStrong: - return "taiko-hit100k"; - case TaikoSkinComponents.TaikoExplosionGreat: return "taiko-hit300"; - - case TaikoSkinComponents.TaikoExplosionGreatStrong: - return "taiko-hit300k"; } - throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type"); + throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}"); } public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 093084805d..642eb0ddcc 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new TaikoPerformanceCalculator(this, beatmap, score, attributes); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); public int LegacyID => 1; diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 0d785adb4a..132d8f8868 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -16,10 +16,8 @@ namespace osu.Game.Rulesets.Taiko PlayfieldBackgroundRight, BarLine, TaikoExplosionMiss, - TaikoExplosionGood, - TaikoExplosionGoodStrong, + TaikoExplosionOk, TaikoExplosionGreat, - TaikoExplosionGreatStrong, Scroller, Mascot, } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 9943a58e3e..3bd20e4bb4 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.IsHit()) return; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index f91bbb14e8..cbfc5a8628 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (Result.Type) { - case HitResult.Good: + case HitResult.Ok: JudgementBody.Colour = colours.GreenLight; break; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index b937beae3c..6a16f311bf 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Screens.Play; @@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI lastObjectHit = true; } - if (!result.Judgement.AffectsCombo) + if (!result.Type.AffectsCombo()) return; lastObjectHit = result.IsHit; @@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.UI } private bool triggerComboClear(JudgementResult judgementResult) - => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Judgement.AffectsCombo && judgementResult.IsHit; + => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Type.AffectsCombo() && judgementResult.IsHit; private bool triggerSwellClear(JudgementResult judgementResult) => judgementResult.Judgement is TaikoSwellJudgement && judgementResult.IsHit; diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index e3eabbf88f..247c0dde73 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -10,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI @@ -25,15 +23,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,41 +48,24 @@ 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(result)), _ => new DefaultHitExplosion(JudgedObject, result)); } - private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) + private static TaikoSkinComponents getComponentName(HitResult result) { - var resultType = judgedObject.Result?.Type ?? HitResult.Great; - - switch (resultType) + switch (result) { case HitResult.Miss: return TaikoSkinComponents.TaikoExplosionMiss; - case HitResult.Good: - return useStrongExplosion(judgedObject) - ? TaikoSkinComponents.TaikoExplosionGoodStrong - : TaikoSkinComponents.TaikoExplosionGood; + case HitResult.Ok: + return TaikoSkinComponents.TaikoExplosionOk; case HitResult.Great: - return useStrongExplosion(judgedObject) - ? TaikoSkinComponents.TaikoExplosionGreatStrong - : TaikoSkinComponents.TaikoExplosionGreat; + return TaikoSkinComponents.TaikoExplosionGreat; } - throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type"); - } - - private bool useStrongExplosion(DrawableHitObject judgedObject) - { - if (!(judgedObject.HitObject is Hit)) - return false; - - if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit)) - return false; - - return judgedObject.Result.Type == nestedHit.Result.Type; + throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}"); } /// diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 5966b24b34..1ca1be1bdf 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI target = centreHit; back = centre; - if (gameplayClock?.IsSeeking != true) - drumSample.Centre?.Play(); + drumSample.Centre?.Play(); } else if (action == RimAction) { target = rimHit; back = rim; - if (gameplayClock?.IsSeeking != true) - drumSample.Rim?.Play(); + drumSample.Rim?.Play(); } if (target != null) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7976d5bc6d..120cf264c3 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; } } @@ -215,16 +215,12 @@ namespace osu.Game.Rulesets.Taiko.UI private void addDrumRollHit(DrawableDrumRollTick drawableTick) => drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick)); - /// - /// 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) { - hitExplosionContainer.Add(new HitExplosion(drawableObject)); + hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); - }); + } private class ProxyContainer : LifetimeManagementContainer { diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index feda1ae0e9..b7a41ffd1c 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 @@ -10,16 +12,27 @@ namespace osu.Game.Tests.Editing [TestFixture] public class EditorChangeHandlerTest { + private int stateChangedFired; + + [SetUp] + public void SetUp() + { + stateChangedFired = 0; + } + [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(stateChangedFired, Is.EqualTo(1)); + Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.False); @@ -27,17 +40,57 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.True); + + Assert.That(stateChangedFired, Is.EqualTo(2)); + } + + [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); + Assert.That(stateChangedFired, Is.EqualTo(1)); + + string hash = handler.CurrentStateHash; + + // save a save without making any changes + handler.SaveState(); + + Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + Assert.That(stateChangedFired, Is.EqualTo(1)); + + 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); + Assert.That(stateChangedFired, Is.EqualTo(2)); } [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++) + { + Assert.That(stateChangedFired, Is.EqualTo(i)); + + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -53,12 +106,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 +126,20 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); } + + private (EditorChangeHandler, EditorBeatmap) createChangeHandler() + { + var beatmap = new EditorBeatmap(new Beatmap()); + + var changeHandler = new EditorChangeHandler(beatmap); + + changeHandler.OnStateChange += () => stateChangedFired++; + return (changeHandler, beatmap); + } + + private void addArbitraryChange(EditorBeatmap beatmap) + { + beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); + } } } diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index b491157627..afaaafdd26 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 1000 } + new HitCircle { StartTime = 1000, NewCombo = true } } }; @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 3000 }, }); @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, (OsuHitObject)current.HitObjects[1], (OsuHitObject)current.HitObjects[2], } @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new OsuHitObject[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new Slider { StartTime = 2000, @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, (OsuHitObject)current.HitObjects[0], new HitCircle { StartTime = 1500 }, (OsuHitObject)current.HitObjects[1], @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1500 }, new HitCircle { StartTime = 2000 }, @@ -226,6 +226,9 @@ namespace osu.Game.Tests.Editing new HitCircle { StartTime = 3500 }, }); + var patchedFirst = (HitCircle)current.HitObjects[1]; + patchedFirst.NewCombo = true; + var patch = new OsuBeatmap { HitObjects = @@ -244,7 +247,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1500 }, new HitCircle { StartTime = 2000 }, @@ -277,7 +280,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1500 }, new HitCircle { StartTime = 2000 }, @@ -291,7 +294,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 750 }, + new HitCircle { StartTime = 750, NewCombo = true }, (OsuHitObject)current.HitObjects[1], (OsuHitObject)current.HitObjects[4], (OsuHitObject)current.HitObjects[5], @@ -309,20 +312,20 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500, Position = new Vector2(50) }, - new HitCircle { StartTime = 500, Position = new Vector2(100) }, - new HitCircle { StartTime = 500, Position = new Vector2(150) }, - new HitCircle { StartTime = 500, Position = new Vector2(200) }, + new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true }, }); var patch = new OsuBeatmap { HitObjects = { - new HitCircle { StartTime = 500, Position = new Vector2(150) }, - new HitCircle { StartTime = 500, Position = new Vector2(100) }, - new HitCircle { StartTime = 500, Position = new Vector2(50) }, - new HitCircle { StartTime = 500, Position = new Vector2(200) }, + new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true }, } }; diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 460ad1b898..7264083338 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; @@ -167,7 +170,7 @@ namespace osu.Game.Tests.Gameplay beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 }); for (double time = 0; time < 5000; time += 100) - beatmap.HitObjects.Add(new JudgeableHitObject(false) { StartTime = time }); + beatmap.HitObjects.Add(new JudgeableHitObject(HitResult.LargeBonus) { StartTime = time }); beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 5000 }); createProcessor(beatmap); @@ -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 @@ -215,25 +236,43 @@ namespace osu.Game.Tests.Gameplay private class JudgeableHitObject : HitObject { - private readonly bool affectsCombo; + private readonly HitResult maxResult; - public JudgeableHitObject(bool affectsCombo = true) + public JudgeableHitObject(HitResult maxResult = HitResult.Perfect) { - this.affectsCombo = affectsCombo; + this.maxResult = maxResult; } - public override Judgement CreateJudgement() => new TestJudgement(affectsCombo); + public override Judgement CreateJudgement() => new TestJudgement(maxResult); protected override HitWindows CreateHitWindows() => new HitWindows(); private class TestJudgement : Judgement { - public override bool AffectsCombo { get; } + public override HitResult MaxResult { get; } - public TestJudgement(bool affectsCombo) + public TestJudgement(HitResult maxResult) { - AffectsCombo = affectsCombo; + MaxResult = maxResult; } } } + + private class JudgeableLongHitObject : JudgeableHitObject, IHasDuration + { + public double EndTime => StartTime + Duration; + public double Duration { get; set; } = 5000; + + public JudgeableLongHitObject() + : base(HitResult.LargeBonus) + { + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + AddNested(new JudgeableHitObject()); + } + } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index c9ab4fa489..432e3df95e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -17,13 +17,13 @@ namespace osu.Game.Tests.Gameplay [Test] public void TestNoScoreIncreaseFromMiss() { - var beatmap = new Beatmap { HitObjects = { new TestHitObject() } }; + var beatmap = new Beatmap { HitObjects = { new HitObject() } }; var scoreProcessor = new ScoreProcessor(); scoreProcessor.ApplyBeatmap(beatmap); // Apply a miss judgement - scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss }); + scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement()) { Type = HitResult.Miss }); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0)); } @@ -31,37 +31,25 @@ namespace osu.Game.Tests.Gameplay [Test] public void TestOnlyBonusScore() { - var beatmap = new Beatmap { HitObjects = { new TestBonusHitObject() } }; + var beatmap = new Beatmap { HitObjects = { new HitObject() } }; var scoreProcessor = new ScoreProcessor(); scoreProcessor.ApplyBeatmap(beatmap); // Apply a judgement - scoreProcessor.ApplyResult(new JudgementResult(new TestBonusHitObject(), new TestBonusJudgement()) { Type = HitResult.Perfect }); + scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus }); - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(100)); - } - - private class TestHitObject : HitObject - { - public override Judgement CreateJudgement() => new TestJudgement(); + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE)); } private class TestJudgement : Judgement { - protected override int NumericResultFor(HitResult result) => 100; - } + public override HitResult MaxResult { get; } - private class TestBonusHitObject : HitObject - { - public override Judgement CreateJudgement() => new TestBonusJudgement(); - } - - private class TestBonusJudgement : Judgement - { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 100; + public TestJudgement(HitResult maxResult = HitResult.Perfect) + { + MaxResult = maxResult; + } } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index a690eb3b59..d46769a7c0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Audio; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Audio; @@ -106,9 +108,14 @@ namespace osu.Game.Tests.Gameplay Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); SelectedMods.Value = new[] { testedMod }; - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)); + var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) + Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0) + { + Child = beatmapSkinSourceContainer + }); + + beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { Clock = gameplayContainer.GameplayClock }); @@ -116,7 +123,7 @@ namespace osu.Game.Tests.Gameplay AddStep("start", () => gameplayContainer.Start()); - AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); + AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType().First().AggregateFrequency.Value == expectedRate); } private class TestSkin : LegacySkin @@ -168,8 +175,6 @@ namespace osu.Game.Tests.Gameplay : base(sampleInfo) { } - - public new SampleChannel Channel => base.Channel; } } } diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 830e4bc603..90a487c0ac 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -139,6 +139,22 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.Groups.Count, Is.EqualTo(0)); } + [Test] + public void TestRemoveGroupAlsoRemovedControlPoints() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + + group.Add(new SampleControlPoint()); + + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1)); + + cpi.RemoveGroup(group); + + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(0)); + } + [Test] public void TestAddControlPointToGroup() { diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs new file mode 100644 index 0000000000..3fd7c364b7 --- /dev/null +++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs @@ -0,0 +1,39 @@ +// 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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Timing; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class GameplayClockTest + { + [TestCase(0)] + [TestCase(1)] + public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate) + { + var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); + var gameplayClock = new TestGameplayClock(framedClock); + + gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble()); + + Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0)); + } + + private class TestGameplayClock : GameplayClock + { + public List> MutableNonGameplayAdjustments { get; } = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public TestGameplayClock(IFrameBasedClock underlyingClock) + : base(underlyingClock) + { + } + } + } +} diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 64d1024efb..ace57aad1d 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -35,10 +35,10 @@ namespace osu.Game.Tests.Rulesets.Scoring } [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] - [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] [TestCase(ScoringMode.Classic, HitResult.Meh, 50)] - [TestCase(ScoringMode.Classic, HitResult.Good, 100)] + [TestCase(ScoringMode.Classic, HitResult.Ok, 100)] [TestCase(ScoringMode.Classic, HitResult.Great, 300)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs new file mode 100644 index 0000000000..33e3c7cb8c --- /dev/null +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -0,0 +1,136 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Rulesets +{ + [HeadlessTest] + public class TestSceneDrawableRulesetDependencies : OsuTestScene + { + [Test] + public void TestDisposalDoesNotDisposeParentStores() + { + DrawableWithDependencies drawable = null; + TestTextureStore textureStore = null; + TestSampleStore sampleStore = null; + + AddStep("add dependencies", () => + { + Child = drawable = new DrawableWithDependencies(); + textureStore = drawable.ParentTextureStore; + sampleStore = drawable.ParentSampleStore; + }); + + AddStep("clear children", Clear); + AddUntilStep("wait for disposal", () => drawable.IsDisposed); + + AddStep("GC", () => + { + drawable = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + }); + + AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed); + AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed); + } + + private class DrawableWithDependencies : CompositeDrawable + { + public TestTextureStore ParentTextureStore { get; private set; } + public TestSampleStore ParentSampleStore { get; private set; } + + public DrawableWithDependencies() + { + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(ParentTextureStore = new TestTextureStore()); + dependencies.CacheAs(ParentSampleStore = new TestSampleStore()); + + return new DrawableRulesetDependencies(new OsuRuleset(), dependencies); + } + + public new bool IsDisposed { get; private set; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + IsDisposed = true; + } + } + + private class TestTextureStore : TextureStore + { + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + IsDisposed = true; + } + } + + private class TestSampleStore : ISampleStore + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + + public SampleChannel Get(string name) => null; + + public Task GetAsync(string name) => null; + + public Stream GetStream(string name) => null; + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public BindableNumber Volume => throw new NotImplementedException(); + public BindableNumber Balance => throw new NotImplementedException(); + public BindableNumber Frequency => throw new NotImplementedException(); + public BindableNumber Tempo => throw new NotImplementedException(); + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + + public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException(); + + public IBindable AggregateVolume => throw new NotImplementedException(); + public IBindable AggregateBalance => throw new NotImplementedException(); + public IBindable AggregateFrequency => throw new NotImplementedException(); + public IBindable AggregateTempo => throw new NotImplementedException(); + + public int PlaybackConcurrency { get; set; } + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs new file mode 100644 index 0000000000..da98a7a024 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneComposeSelectBox : OsuTestScene + { + private Container selectionArea; + + public TestSceneComposeSelectBox() + { + SelectionBox selectionBox = null; + + AddStep("create box", () => + Child = selectionArea = new Container + { + Size = new Vector2(400), + Position = -new Vector2(150), + Anchor = Anchor.Centre, + Children = new Drawable[] + { + selectionBox = new SelectionBox + { + CanRotate = true, + CanScaleX = true, + CanScaleY = true, + + OnRotation = handleRotation, + OnScale = handleScale + } + } + }); + + AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state); + AddToggleStep("toggle x", state => selectionBox.CanScaleX = state); + AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); + } + + private void handleScale(Vector2 amount, Anchor reference) + { + if ((reference & Anchor.y1) == 0) + { + int directionY = (reference & Anchor.y0) > 0 ? -1 : 1; + if (directionY < 0) + selectionArea.Y += amount.Y; + selectionArea.Height += directionY * amount.Y; + } + + if ((reference & Anchor.x1) == 0) + { + int directionX = (reference & Anchor.x0) > 0 ? -1 : 1; + if (directionX < 0) + selectionArea.X += amount.X; + selectionArea.Width += directionX * amount.X; + } + } + + private void handleRotation(float angle) + { + // kinda silly and wrong, but just showing that the drag handles work. + selectionArea.Rotation += angle; + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs new file mode 100644 index 0000000000..7584c74c71 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -0,0 +1,83 @@ +// 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.Allocation; +using osu.Framework.Screens; +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; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + 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); + AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false); + } + + [Test] + public void TestExitWithoutSave() + { + AddStep("exit without save", () => Editor.Exit()); + AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); + AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true); + } + + [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.ChildrenOfType().First().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/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs new file mode 100644 index 0000000000..f182023c0e --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Audio; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorSamplePlayback : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + [Test] + public void TestSlidingSampleStopsOnSeek() + { + DrawableSlider slider = null; + DrawableSample[] loopingSamples = null; + DrawableSample[] onceOffSamples = null; + + AddStep("get first slider", () => + { + slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); + onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); + loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); + }); + + AddStep("start playback", () => EditorClock.Start()); + + AddUntilStep("wait for slider sliding then seek", () => + { + if (!slider.Tracking.Value) + return false; + + if (!loopingSamples.Any(s => s.Playing)) + return false; + + EditorClock.Seek(20000); + return true; + }); + + AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing)); + AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing)); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 09f5ac2224..b82e776164 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; @@ -17,16 +17,26 @@ namespace osu.Game.Tests.Visual.Editing [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; + protected override bool ScrollUsingMouseWheel => false; + public TestSceneTimingScreen() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); } [BackgroundDependencyLoader] private void load() { Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + Beatmap.Disabled = true; + Child = new TimingScreen(); } + + protected override void Dispose(bool isDisposing) + { + Beatmap.Disabled = false; + base.Dispose(isDisposing); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs new file mode 100644 index 0000000000..5bb3851264 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -0,0 +1,68 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Audio; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneGameplaySamplePlayback : PlayerTestScene + { + [Test] + public void TestAllSamplesStopDuringSeek() + { + DrawableSlider slider = null; + DrawableSample[] samples = null; + ISamplePlaybackDisabler gameplayClock = null; + + AddStep("get variables", () => + { + gameplayClock = Player.ChildrenOfType().First().GameplayClock; + slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); + samples = slider.ChildrenOfType().ToArray(); + }); + + AddUntilStep("wait for slider sliding then seek", () => + { + if (!slider.Tracking.Value) + return false; + + if (!samples.Any(s => s.Playing)) + return false; + + Player.ChildrenOfType().First().Seek(40000); + return true; + }); + + AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + + // because we are in frame stable context, it's quite likely that not all samples are "played" at this point. + // the important thing is that at least one started, and that sample has since stopped. + AddAssert("all looping samples stopped immediately", () => allStopped(allLoopingSounds)); + AddUntilStep("all samples stopped eventually", () => allStopped(allSounds)); + + AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + + AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value); + AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying)); + } + + private IEnumerable allSounds => Player.ChildrenOfType(); + private IEnumerable allLoopingSounds => allSounds.Where(sound => sound.Looping); + + private bool allStopped(IEnumerable sounds) => sounds.All(sound => !sound.IsPlaying); + + protected override bool Autoplay => true; + + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 253b8d9c55..377f305d63 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new[] { new OsuSpriteText { Text = $@"Great: {hitWindows?.WindowFor(HitResult.Great)}" }, - new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Good)}" }, + new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Ok)}" }, new OsuSpriteText { Text = $@"Meh: {hitWindows?.WindowFor(HitResult.Meh)}" }, } }); 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.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 420bf29429..ac0e8eb0d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestQuickRetryFromFailedGameplay() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddStep("quick retry", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke()); + AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); } @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestQuickExitFromFailedGameplay() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke()); + AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); } @@ -183,7 +183,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromGameplay() { - AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke()); + AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index ed75d83151..8f2011e5dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSkinnableSound : OsuTestScene { - [Cached] + [Cached(typeof(ISamplePlaybackDisabler))] private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private TestSkinSourceContainer skinSource; - private SkinnableSound skinnableSound; + private PausableSkinnableSound skinnableSound; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Clock = gameplayClock, RelativeSizeAxes = Axes.Both, - Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) + Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) }, }; }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 03cb5fa3db..ff96a999ec 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); - AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value); + AddAssert("download button is disabled", () => !screen.ChildrenOfType().Last().Enabled.Value); AddStep("click contracted panel", () => { @@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking InputManager.Click(MouseButton.Left); }); - AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value); + AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value); } private class TestResultsContainer : Container diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index 0cd0f13b5f..082d85603e 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Platform; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Tests.Visual.Settings @@ -11,7 +10,7 @@ namespace osu.Game.Tests.Visual.Settings public class TestSceneDirectorySelector : OsuTestScene { [BackgroundDependencyLoader] - private void load(GameHost host) + private void load() { Add(new DirectorySelector { RelativeSizeAxes = Axes.Both }); } diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs new file mode 100644 index 0000000000..311e4c3362 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneFileSelector : OsuTestScene + { + [Test] + public void TestAllFiles() + { + AddStep("create", () => Child = new FileSelector { RelativeSizeAxes = Axes.Both }); + } + + [Test] + public void TestJpgFilesOnly() + { + AddStep("create", () => Child = new FileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both }); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs new file mode 100644 index 0000000000..393420e700 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneLabelledSliderBar : OsuTestScene + { + [TestCase(false)] + [TestCase(true)] + public void TestSliderBar(bool hasDescription) => createSliderBar(hasDescription); + + private void createSliderBar(bool hasDescription = false) + { + AddStep("create component", () => + { + LabelledSliderBar component; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Child = component = new LabelledSliderBar + { + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + } + } + }; + + component.Label = "a sample component"; + component.Description = hasDescription ? "this text describes the component" : string.Empty; + }); + } + } +} diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index aee5241e35..5782301a65 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -10,34 +10,34 @@ namespace osu.Game.Tournament.Components { public class DateTextBox : SettingsTextBox { - public new Bindable Bindable + public new Bindable Current { - get => bindable; + get => current; set { - bindable = value.GetBoundCopy(); - bindable.BindValueChanged(dto => - base.Bindable.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); + current = value.GetBoundCopy(); + current.BindValueChanged(dto => + base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); } } // hold a reference to the provided bindable so we don't have to in every settings section. - private Bindable bindable = new Bindable(); + private Bindable current = new Bindable(); public DateTextBox() { - base.Bindable = new Bindable(); + base.Current = new Bindable(); - ((OsuTextBox)Control).OnCommit = (sender, newText) => + ((OsuTextBox)Control).OnCommit += (sender, newText) => { try { - bindable.Value = DateTimeOffset.Parse(sender.Text); + current.Value = DateTimeOffset.Parse(sender.Text); } catch { // reset textbox content to its last valid state on a parse failure. - bindable.TriggerChange(); + current.TriggerChange(); } }; } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 8b8078e119..069ddfa4db 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -63,25 +63,25 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Name", Width = 0.33f, - Bindable = Model.Name + Current = Model.Name }, new SettingsTextBox { LabelText = "Description", Width = 0.33f, - Bindable = Model.Description + Current = Model.Description }, new DateTextBox { LabelText = "Start Time", Width = 0.33f, - Bindable = Model.StartDate + Current = Model.StartDate }, new SettingsSlider { LabelText = "Best of", Width = 0.33f, - Bindable = Model.BestOf + Current = Model.BestOf }, new SettingsButton { @@ -186,14 +186,14 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "Beatmap ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmapId, + Current = beatmapId, }, new SettingsTextBox { LabelText = "Mods", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = mods, + Current = mods, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 0973a7dc75..7bd8d3f6a0 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -74,13 +74,13 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Mod", Width = 0.33f, - Bindable = Model.Mod + Current = Model.Mod }, new SettingsSlider { LabelText = "Seed", Width = 0.33f, - Bindable = Model.Seed + Current = Model.Seed }, new SettingsButton { @@ -187,21 +187,21 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "Beatmap ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmapId, + Current = beatmapId, }, new SettingsSlider { LabelText = "Seed", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmap.Seed + Current = beatmap.Seed }, new SettingsTextBox { LabelText = "Score", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = score, + Current = score, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index dbfcfe4225..7196f47bd6 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -102,31 +102,31 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Name", Width = 0.2f, - Bindable = Model.FullName + Current = Model.FullName }, new SettingsTextBox { LabelText = "Acronym", Width = 0.2f, - Bindable = Model.Acronym + Current = Model.Acronym }, new SettingsTextBox { LabelText = "Flag", Width = 0.2f, - Bindable = Model.FlagName + Current = Model.FlagName }, new SettingsTextBox { LabelText = "Seed", Width = 0.2f, - Bindable = Model.Seed + Current = Model.Seed }, new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, - Bindable = Model.LastYearPlacing + Current = Model.LastYearPlacing }, new SettingsButton { @@ -247,7 +247,7 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "User ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = userId, + Current = userId, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index e4e3842369..e4ec45c00e 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -113,13 +113,13 @@ namespace osu.Game.Tournament.Screens.Gameplay new SettingsSlider { LabelText = "Chroma width", - Bindable = LadderInfo.ChromaKeyWidth, + Current = LadderInfo.ChromaKeyWidth, KeyboardStep = 1, }, new SettingsSlider { LabelText = "Players per team", - Bindable = LadderInfo.PlayersPerTeam, + Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index b60eb814e5..cf4466a2e3 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -51,15 +51,15 @@ namespace osu.Game.Tournament.Screens.Ladder.Components editorInfo.Selected.ValueChanged += selection => { - roundDropdown.Bindable = selection.NewValue?.Round; + roundDropdown.Current = selection.NewValue?.Round; losersCheckbox.Current = selection.NewValue?.Losers; - dateTimeBox.Bindable = selection.NewValue?.Date; + dateTimeBox.Current = selection.NewValue?.Date; - team1Dropdown.Bindable = selection.NewValue?.Team1; - team2Dropdown.Bindable = selection.NewValue?.Team2; + team1Dropdown.Current = selection.NewValue?.Team1; + team2Dropdown.Current = selection.NewValue?.Team2; }; - roundDropdown.Bindable.ValueChanged += round => + roundDropdown.Current.ValueChanged += round => { if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) { @@ -88,7 +88,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { public SettingsRoundDropdown(BindableList rounds) { - Bindable = new Bindable(); + Current = new Bindable(); foreach (var r in rounds.Prepend(new TournamentRound())) add(r); diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index b4d56f60c7..717b43f704 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -129,7 +129,7 @@ namespace osu.Game.Tournament.Screens protected virtual void ChangePath() { - var target = directorySelector.CurrentDirectory.Value.FullName; + var target = directorySelector.CurrentPath.Value.FullName; var fileBasedIpc = ipc as FileBasedIPC; Logger.Log($"Changing Stable CE location to {target}"); diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index eed3cac9f0..b343608e69 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new SettingsTeamDropdown(LadderInfo.Teams) { LabelText = "Show specific team", - Bindable = currentTeam, + Current = currentTeam, } } } 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/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index ee85908aa2..bdabe148a7 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -115,6 +115,25 @@ namespace osu.Game.Beatmaps return computeDifficulty(key, beatmapInfo, rulesetInfo); } + /// + /// Retrieves the that describes a star rating. + /// + /// + /// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties + /// + /// The star rating. + /// The that best describes . + public static DifficultyRating GetDifficultyRating(double starRating) + { + if (starRating < 2.0) return DifficultyRating.Easy; + if (starRating < 2.7) return DifficultyRating.Normal; + if (starRating < 4.0) return DifficultyRating.Hard; + if (starRating < 5.3) return DifficultyRating.Insane; + if (starRating < 6.5) return DifficultyRating.Expert; + + return DifficultyRating.ExpertPlus; + } + private CancellationTokenSource trackedUpdateCancellationSource; private readonly List linkedCancellationSources = new List(); @@ -310,5 +329,7 @@ namespace osu.Game.Beatmaps Attributes = attributes; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } + + public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars); } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index c5be5810e9..acab525821 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -135,21 +135,7 @@ namespace osu.Game.Beatmaps public List Scores { get; set; } [JsonIgnore] - public DifficultyRating DifficultyRating - { - get - { - var rating = StarDifficulty; - - if (rating < 2.0) return DifficultyRating.Easy; - if (rating < 2.7) return DifficultyRating.Normal; - if (rating < 4.0) return DifficultyRating.Hard; - if (rating < 5.3) return DifficultyRating.Insane; - if (rating < 6.5) return DifficultyRating.Expert; - - return DifficultyRating.ExpertPlus; - } - } + public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(StarDifficulty); public string[] SearchableTerms => new[] { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e9f41f6bff..4c75069f08 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps /// public readonly WorkingBeatmap DefaultBeatmap; - public override string[] HandledExtensions => new[] { ".osz" }; + public override IEnumerable HandledExtensions => new[] { ".osz" }; protected override string[] HashableFileTypes => new[] { ".osu" }; @@ -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/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index 250cc49ad4..b7b5adc52e 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -22,8 +22,18 @@ namespace osu.Game.Beatmaps { IHasComboInformation lastObj = null; + bool isFirst = true; + foreach (var obj in Beatmap.HitObjects.OfType()) { + if (isFirst) + { + obj.NewCombo = true; + + // first hitobject should always be marked as a new combo for sanity. + isFirst = false; + } + if (obj.NewCombo) { obj.IndexInCurrentCombo = 0; diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index a1822a1163..c6649f6af1 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); + public virtual Color4 GetRepresentingColour(OsuColour colours) => colours.Yellow; + /// /// Determines whether this results in a meaningful change when placed alongside another. /// diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e7788b75f3..22314f28c7 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -158,6 +158,9 @@ namespace osu.Game.Beatmaps.ControlPoints public void RemoveGroup(ControlPointGroup group) { + foreach (var item in group.ControlPoints.ToArray()) + group.Remove(item); + group.ItemAdded -= groupItemAdded; group.ItemRemoved -= groupItemRemoved; diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 1d38790f87..283bf76572 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -23,6 +25,8 @@ namespace osu.Game.Beatmaps.ControlPoints MaxValue = 10 }; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark; + /// /// The speed multiplier at this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 9e8e3978be..ea28fca170 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; + /// /// Whether the first bar line of this control point is ignored. /// diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index c052c04ea0..f57ecfb9e3 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -3,6 +3,8 @@ using osu.Framework.Bindables; using osu.Game.Audio; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -16,6 +18,8 @@ namespace osu.Game.Beatmaps.ControlPoints SampleVolumeBindable = { Disabled = true } }; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink; + /// /// The default sample bank at this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 9345299c3a..d9378bca4a 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -3,6 +3,8 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints /// private const double default_beat_length = 60000.0 / 60.0; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark; + public static readonly TimingControlPoint DEFAULT = new TimingControlPoint { BeatLengthBindable = diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 8a0d981e49..45327d4514 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Threading; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -21,9 +26,6 @@ namespace osu.Game.Beatmaps.Drawables { public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip { - private readonly BeatmapInfo beatmap; - private readonly RulesetInfo ruleset; - private readonly Container iconContainer; /// @@ -35,23 +37,49 @@ namespace osu.Game.Beatmaps.Drawables set => iconContainer.Size = value; } - public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null, bool shouldShowTooltip = true) + [NotNull] + private readonly BeatmapInfo beatmap; + + [CanBeNull] + private readonly RulesetInfo ruleset; + + [CanBeNull] + private readonly IReadOnlyList mods; + + private readonly bool shouldShowTooltip; + private readonly IBindable difficultyBindable = new Bindable(); + + private Drawable background; + + /// + /// Creates a new with a given and combination. + /// + /// The beatmap to show the difficulty of. + /// The ruleset to show the difficulty with. + /// The mods to show the difficulty with. + /// Whether to display a tooltip when hovered. + public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) + : this(beatmap, shouldShowTooltip) + { + this.ruleset = ruleset ?? beatmap.Ruleset; + this.mods = mods ?? Array.Empty(); + } + + /// + /// Creates a new that follows the currently-selected ruleset and mods. + /// + /// The beatmap to show the difficulty of. + /// Whether to display a tooltip when hovered. + public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true) { this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); - - this.ruleset = ruleset ?? beatmap.Ruleset; - if (shouldShowTooltip) - TooltipContent = beatmap; + this.shouldShowTooltip = shouldShowTooltip; AutoSizeAxes = Axes.Both; InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; } - public ITooltip GetCustomTooltip() => new DifficultyIconTooltip(); - - public object TooltipContent { get; } - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -70,10 +98,10 @@ namespace osu.Game.Beatmaps.Drawables Type = EdgeEffectType.Shadow, Radius = 5, }, - Child = new Box + Child = background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForDifficultyRating(beatmap.DifficultyRating), + Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer @@ -82,16 +110,73 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } - } + Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + }, + new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0), }; + + difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating)); + } + + public ITooltip GetCustomTooltip() => new DifficultyIconTooltip(); + + public object TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; + + private class DifficultyRetriever : Component + { + public readonly Bindable StarDifficulty = new Bindable(); + + private readonly BeatmapInfo beatmap; + private readonly RulesetInfo ruleset; + private readonly IReadOnlyList mods; + + private CancellationTokenSource difficultyCancellation; + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + + public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) + { + this.beatmap = beatmap; + this.ruleset = ruleset; + this.mods = mods; + } + + private IBindable localStarDifficulty; + + [BackgroundDependencyLoader] + private void load() + { + difficultyCancellation = new CancellationTokenSource(); + localStarDifficulty = ruleset != null + ? difficultyManager.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) + : difficultyManager.GetBindableDifficulty(beatmap, difficultyCancellation.Token); + localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + difficultyCancellation?.Cancel(); + } + } + + private class DifficultyIconTooltipContent + { + public readonly BeatmapInfo Beatmap; + public readonly IBindable Difficulty; + + public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable difficulty) + { + Beatmap = beatmap; + Difficulty = difficulty; + } } private class DifficultyIconTooltip : VisibilityContainer, ITooltip { private readonly OsuSpriteText difficultyName, starRating; private readonly Box background; - private readonly FillFlowContainer difficultyFlow; public DifficultyIconTooltip() @@ -159,14 +244,22 @@ namespace osu.Game.Beatmaps.Drawables background.Colour = colours.Gray3; } + private readonly IBindable starDifficulty = new Bindable(); + public bool SetContent(object content) { - if (!(content is BeatmapInfo beatmap)) + if (!(content is DifficultyIconTooltipContent iconContent)) return false; - difficultyName.Text = beatmap.Version; - starRating.Text = $"{beatmap.StarDifficulty:0.##}"; - difficultyFlow.Colour = colours.ForDifficultyRating(beatmap.DifficultyRating, true); + difficultyName.Text = iconContent.Beatmap.Version; + + starDifficulty.UnbindAll(); + starDifficulty.BindTo(iconContent.Difficulty); + starDifficulty.BindValueChanged(difficulty => + { + starRating.Text = $"{difficulty.NewValue.Stars:0.##}"; + difficultyFlow.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating, true); + }, true); return true; } diff --git a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs index fbad113caa..fcee4c2f1a 100644 --- a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs @@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.Drawables public class GroupedDifficultyIcon : DifficultyIcon { public GroupedDifficultyIcon(List beatmaps, RulesetInfo ruleset, Color4 counterColour) - : base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, false) + : base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, null, false) { AddInternal(new OsuSpriteText { diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fe487cb1d0..50069be4b2 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -57,7 +57,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber, + Current = bNumber, KeyboardStep = 0.1f, }; @@ -67,7 +67,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber, + Current = bNumber, KeyboardStep = 0.1f, }; @@ -77,7 +77,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber + Current = bNumber }; break; @@ -86,7 +86,7 @@ namespace osu.Game.Configuration yield return new SettingsCheckbox { LabelText = attr.Label, - Bindable = bBool + Current = bBool }; break; @@ -95,7 +95,7 @@ namespace osu.Game.Configuration yield return new SettingsTextBox { LabelText = attr.Label, - Bindable = bString + Current = bString }; break; @@ -105,7 +105,7 @@ namespace osu.Game.Configuration var dropdown = (Drawable)Activator.CreateInstance(dropdownType); dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); - dropdownType.GetProperty(nameof(SettingsDropdown.Bindable))?.SetValue(dropdown, bindable); + dropdownType.GetProperty(nameof(SettingsDropdown.Current))?.SetValue(dropdown, bindable); yield return dropdown; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 76bc4f7755..3292936f5f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -70,7 +70,7 @@ namespace osu.Game.Database private readonly Bindable> itemRemoved = new Bindable>(); - public virtual string[] HandledExtensions => new[] { ".zip" }; + public virtual IEnumerable HandledExtensions => new[] { ".zip" }; public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop; @@ -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/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index b9f882468d..e4d92d957c 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Threading.Tasks; namespace osu.Game.Database @@ -19,6 +20,6 @@ namespace osu.Game.Database /// /// An array of accepted file extensions (in the standard format of ".abc"). /// - string[] HandledExtensions { get; } + IEnumerable HandledExtensions { get; } } } diff --git a/osu.Game/Extensions/EditorDisplayExtensions.cs b/osu.Game/Extensions/EditorDisplayExtensions.cs new file mode 100644 index 0000000000..f749b88b46 --- /dev/null +++ b/osu.Game/Extensions/EditorDisplayExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Extensions +{ + public static class EditorDisplayExtensions + { + /// + /// Get an editor formatted string (mm:ss:mss) + /// + /// A time value in milliseconds. + /// An editor formatted display string. + public static string ToEditorFormattedString(this double milliseconds) => + ToEditorFormattedString(TimeSpan.FromMilliseconds(milliseconds)); + + /// + /// Get an editor formatted string (mm:ss:mss) + /// + /// A time value. + /// An editor formatted display string. + public static string ToEditorFormattedString(this TimeSpan timeSpan) => + $"{(timeSpan < TimeSpan.Zero ? "-" : string.Empty)}{timeSpan:mm\\:ss\\:fff}"; + } +} diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 27027202ce..5b0fa44444 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds /// public float Velocity = 1; + private readonly Random stableRandom; + + private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); + private readonly SortedList parts = new SortedList(Comparer.Default); private IShader shader; private readonly Texture texture; - public Triangles() + /// + /// Construct a new triangle visualisation. + /// + /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time. + public Triangles(int? seed = null) { + if (seed != null) + stableRandom = new Random(seed.Value); + texture = Texture.WhitePixel; } @@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds { TriangleParticle particle = CreateTriangle(); - particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1); - particle.ColourShade = RNG.NextSingle(); + particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1); + particle.ColourShade = nextRandom(); particle.Colour = CreateTriangleShade(particle.ColourShade); return particle; @@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds const float std_dev = 0.16f; const float mean = 0.5f; - float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats - float u2 = 1 - RNG.NextSingle(); + float u1 = 1 - nextRandom(); //uniform(0,1] random floats + float u2 = 1 - nextRandom(); float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1) var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2) 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/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index ae34281bfb..a1cd074619 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -28,11 +28,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 private GameHost host { get; set; } [Cached] - public readonly Bindable CurrentDirectory = new Bindable(); + public readonly Bindable CurrentPath = new Bindable(); public DirectorySelector(string initialPath = null) { - CurrentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + CurrentPath.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } [BackgroundDependencyLoader] @@ -74,7 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }; - CurrentDirectory.BindValueChanged(updateDisplay, true); + CurrentPath.BindValueChanged(updateDisplay, true); } private void updateDisplay(ValueChangedEvent directory) @@ -92,22 +92,27 @@ namespace osu.Game.Graphics.UserInterfaceV2 } else { - directoryFlow.Add(new ParentDirectoryPiece(CurrentDirectory.Value.Parent)); + directoryFlow.Add(new ParentDirectoryPiece(CurrentPath.Value.Parent)); - foreach (var dir in CurrentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) - { - if ((dir.Attributes & FileAttributes.Hidden) == 0) - directoryFlow.Add(new DirectoryPiece(dir)); - } + directoryFlow.AddRange(GetEntriesForPath(CurrentPath.Value)); } } catch (Exception) { - CurrentDirectory.Value = directory.OldValue; + CurrentPath.Value = directory.OldValue; this.FlashColour(Color4.Red, 300); } } + protected virtual IEnumerable GetEntriesForPath(DirectoryInfo path) + { + foreach (var dir in path.GetDirectories().OrderBy(d => d.Name)) + { + if ((dir.Attributes & FileAttributes.Hidden) == 0) + yield return new DirectoryPiece(dir); + } + } + private class CurrentDirectoryDisplay : CompositeDrawable { [Resolved] @@ -126,7 +131,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Spacing = new Vector2(5), - Height = DirectoryPiece.HEIGHT, + Height = DisplayPiece.HEIGHT, Direction = FillDirection.Horizontal, }, }; @@ -150,7 +155,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 flow.ChildrenEnumerable = new Drawable[] { - new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DirectoryPiece.HEIGHT), }, + new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DisplayPiece.HEIGHT), }, new ComputerPiece(), }.Concat(pathPieces); } @@ -198,24 +203,44 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - private class DirectoryPiece : CompositeDrawable + protected class DirectoryPiece : DisplayPiece { - public const float HEIGHT = 20; - - protected const float FONT_SIZE = 16; - protected readonly DirectoryInfo Directory; - private readonly string displayName; - - protected FillFlowContainer Flow; - [Resolved] private Bindable currentDirectory { get; set; } public DirectoryPiece(DirectoryInfo directory, string displayName = null) + : base(displayName) { Directory = directory; + } + + protected override bool OnClick(ClickEvent e) + { + currentDirectory.Value = Directory; + return true; + } + + protected override string FallbackName => Directory.Name; + + protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) + ? FontAwesome.Solid.Database + : FontAwesome.Regular.Folder; + } + + protected abstract class DisplayPiece : CompositeDrawable + { + public const float HEIGHT = 20; + + protected const float FONT_SIZE = 16; + + private readonly string displayName; + + protected FillFlowContainer Flow; + + protected DisplayPiece(string displayName = null) + { this.displayName = displayName; } @@ -259,20 +284,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = displayName ?? Directory.Name, + Text = displayName ?? FallbackName, Font = OsuFont.Default.With(size: FONT_SIZE) }); } - protected override bool OnClick(ClickEvent e) - { - currentDirectory.Value = Directory; - return true; - } + protected abstract string FallbackName { get; } - protected virtual IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) - ? FontAwesome.Solid.Database - : FontAwesome.Regular.Folder; + protected abstract IconUsage? Icon { get; } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs new file mode 100644 index 0000000000..e10b8f7033 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class FileSelector : DirectorySelector + { + private readonly string[] validFileExtensions; + + [Cached] + public readonly Bindable CurrentFile = new Bindable(); + + public FileSelector(string initialPath = null, string[] validFileExtensions = null) + : base(initialPath) + { + this.validFileExtensions = validFileExtensions ?? Array.Empty(); + } + + protected override IEnumerable GetEntriesForPath(DirectoryInfo path) + { + foreach (var dir in base.GetEntriesForPath(path)) + yield return dir; + + IEnumerable files = path.GetFiles(); + + if (validFileExtensions.Length > 0) + files = files.Where(f => validFileExtensions.Contains(f.Extension)); + + foreach (var file in files.OrderBy(d => d.Name)) + { + if ((file.Attributes & FileAttributes.Hidden) == 0) + yield return new FilePiece(file); + } + } + + protected class FilePiece : DisplayPiece + { + private readonly FileInfo file; + + [Resolved] + private Bindable currentFile { get; set; } + + public FilePiece(FileInfo file) + { + this.file = file; + } + + protected override bool OnClick(ClickEvent e) + { + currentFile.Value = file; + return true; + } + + protected override string FallbackName => file.Name; + + protected override IconUsage? Icon + { + get + { + switch (file.Extension) + { + case ".ogg": + case ".mp3": + case ".wav": + return FontAwesome.Regular.FileAudio; + + case ".jpg": + case ".jpeg": + case ".png": + return FontAwesome.Regular.FileImage; + + case ".mp4": + case ".avi": + case ".mov": + case ".flv": + return FontAwesome.Regular.FileVideo; + + default: + return FontAwesome.Regular.File; + } + } + } + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs new file mode 100644 index 0000000000..cba94e314b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class LabelledSliderBar : LabelledComponent, TNumber> + where TNumber : struct, IEquatable, IComparable, IConvertible + { + public LabelledSliderBar() + : base(true) + { + } + + protected override SettingsSlider CreateComponent() => new SettingsSlider + { + TransferValueOnCommit = true, + RelativeSizeAxes = Axes.X, + }; + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 290aba3468..4aeda74be8 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -44,12 +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/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index b941cd8973..3d3c07a5ad 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -38,6 +38,7 @@ namespace osu.Game.Online.API.Requests.Responses Rank = Rank, Ruleset = ruleset, Mods = mods, + IsLegacyScore = true }; if (Statistics != null) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index f8810c778f..8b0caddbc6 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -59,12 +59,13 @@ namespace osu.Game.Online.Chat RelativeSizeAxes = Axes.X, Height = textbox_height, PlaceholderText = "type your message", - OnCommit = postMessage, ReleaseFocusOnCommit = false, HoldFocus = true, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, }); + + textbox.OnCommit += postMessage; } Channel.BindValueChanged(channelChanged); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9a4710d576..70d33e7cf7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -237,9 +237,9 @@ namespace osu.Game dependencies.Cache(new SessionStatics()); dependencies.Cache(new OsuColour()); - fileImporters.Add(BeatmapManager); - fileImporters.Add(ScoreManager); - fileImporters.Add(SkinManager); + RegisterImportHandler(BeatmapManager); + RegisterImportHandler(ScoreManager); + RegisterImportHandler(SkinManager); // tracks play so loud our samples can't keep up. // this adds a global reduction of track volume for the time being. @@ -348,6 +348,18 @@ namespace osu.Game private readonly List fileImporters = new List(); + /// + /// Register a global handler for file imports. Most recently registered will have precedence. + /// + /// The handler to register. + public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Insert(0, handler); + + /// + /// Unregister a global handler for file imports. + /// + /// The previously registered handler. + public void UnregisterImportHandler(ICanAcceptFiles handler) => fileImporters.Remove(handler); + public async Task Import(params string[] paths) { var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); @@ -359,7 +371,7 @@ namespace osu.Game } } - public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); + public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 56866765b6..968355c377 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -11,9 +11,11 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Users.Drawables; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -55,6 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores highAccuracyColour = colours.GreenLight; } + /// + /// The statistics that appear in the table, in order of appearance. + /// + private readonly List statisticResultTypes = new List(); + private bool showPerformancePoints; public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn) @@ -65,11 +72,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; showPerformancePoints = showPerformanceColumn; + statisticResultTypes.Clear(); for (int i = 0; i < scores.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); - Columns = createHeaders(scores.FirstOrDefault()); + Columns = createHeaders(scores); Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } @@ -79,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores backgroundFlow.Clear(); } - private TableColumn[] createHeaders(ScoreInfo score) + private TableColumn[] createHeaders(IReadOnlyList scores) { var columns = new List { @@ -92,10 +100,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new TableColumn("max combo", Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120)) }; - foreach (var statistic in score.SortedStatistics.Take(score.SortedStatistics.Count() - 1)) - columns.Add(new TableColumn(statistic.Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); + // All statistics across all scores, unordered. + var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet(); - columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); + foreach (var result in OrderAttributeUtils.GetValuesInOrder()) + { + if (!allScoreStatistics.Contains(result)) + continue; + + columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); + statisticResultTypes.Add(result); + } if (showPerformancePoints) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); @@ -148,13 +163,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } }; - foreach (var kvp in score.SortedStatistics) + var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result); + + foreach (var result in statisticResultTypes) { + if (!availableStatistics.TryGetValue(result, out var stat)) + stat = (result, 0, null); + content.Add(new OsuSpriteText { - Text = $"{kvp.Value}", + Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}", Font = OsuFont.GetFont(size: text_size), - Colour = kvp.Value == 0 ? Color4.Gray : Color4.White + Colour = stat.count == 0 ? Color4.Gray : Color4.White }); } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 3a842d0a43..05789e1fc0 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Text = $@"{value.PP:N0}"; - statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); + statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount)); modsColumn.Mods = value.Mods; if (scoreManager != null) @@ -125,9 +125,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } - private TextColumn createStatisticsColumn(HitResult hitResult, int count) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width) + private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width) { - Text = count.ToString() + Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}" }; private class InfoColumn : CompositeDrawable diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index c53eccf78b..8bc7e21047 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -146,7 +146,6 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Height = 1, PlaceholderText = "type your message", - OnCommit = postMessage, ReleaseFocusOnCommit = false, HoldFocus = true, } @@ -186,6 +185,8 @@ namespace osu.Game.Overlays }, }; + textbox.OnCommit += postMessage; + ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; ChannelSelectionOverlay.State.ValueChanged += state => diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 050e687dfb..b8d04eab4e 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music }, }; - filter.Search.OnCommit = (sender, newText) => + filter.Search.OnCommit += (sender, newText) => { BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); 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/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 3da64e0de4..bed74542c9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio updateItems(); - dropdown.Bindable = audio.AudioDevice; + dropdown.Current = audio.AudioDevice; audio.OnNewDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index a303f93b34..d5de32ed05 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -21,23 +21,23 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsCheckbox { LabelText = "Interface voices", - Bindable = config.GetBindable(OsuSetting.MenuVoice) + Current = config.GetBindable(OsuSetting.MenuVoice) }, new SettingsCheckbox { LabelText = "osu! music theme", - Bindable = config.GetBindable(OsuSetting.MenuMusic) + Current = config.GetBindable(OsuSetting.MenuMusic) }, new SettingsDropdown { LabelText = "Intro sequence", - Bindable = config.GetBindable(OsuSetting.IntroSequence), + Current = config.GetBindable(OsuSetting.IntroSequence), Items = Enum.GetValues(typeof(IntroSequence)).Cast() }, new SettingsDropdown { LabelText = "Background source", - Bindable = config.GetBindable(OsuSetting.MenuBackgroundSource), + Current = config.GetBindable(OsuSetting.MenuBackgroundSource), Items = Enum.GetValues(typeof(BackgroundSource)).Cast() } }; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index aaa4302553..c9a81b955b 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Audio offset", - Bindable = config.GetBindable(OsuSetting.AudioOffset), + Current = config.GetBindable(OsuSetting.AudioOffset), KeyboardStep = 1f }, new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index bda677ecd6..c172a76ab9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -20,28 +20,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Master", - Bindable = audio.Volume, + Current = audio.Volume, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Master (window inactive)", - Bindable = config.GetBindable(OsuSetting.VolumeInactive), + Current = config.GetBindable(OsuSetting.VolumeInactive), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Effect", - Bindable = audio.VolumeSample, + Current = audio.VolumeSample, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Music", - Bindable = audio.VolumeTrack, + Current = audio.VolumeTrack, KeyboardStep = 0.01f, DisplayAsPercentage = true }, diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 9edb18e065..f05b876d8f 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -19,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Debug new SettingsCheckbox { LabelText = "Show log overlay", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox { LabelText = "Bypass front-to-back render pass", - Bindable = config.GetBindable(DebugSetting.BypassFrontToBackPass) + Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 0149e6c3a6..73968761e2 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -21,62 +21,62 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsSlider { LabelText = "Background dim", - Bindable = config.GetBindable(OsuSetting.DimLevel), + Current = config.GetBindable(OsuSetting.DimLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Background blur", - Bindable = config.GetBindable(OsuSetting.BlurLevel), + Current = config.GetBindable(OsuSetting.BlurLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsCheckbox { LabelText = "Lighten playfield during breaks", - Bindable = config.GetBindable(OsuSetting.LightenDuringBreaks) + Current = config.GetBindable(OsuSetting.LightenDuringBreaks) }, new SettingsCheckbox { LabelText = "Show score overlay", - Bindable = config.GetBindable(OsuSetting.ShowInterface) + Current = config.GetBindable(OsuSetting.ShowInterface) }, new SettingsCheckbox { LabelText = "Show difficulty graph on progress bar", - Bindable = config.GetBindable(OsuSetting.ShowProgressGraph) + Current = config.GetBindable(OsuSetting.ShowProgressGraph) }, new SettingsCheckbox { LabelText = "Show health display even when you can't fail", - Bindable = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox { LabelText = "Fade playfield to red when health is low", - Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), + Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), }, new SettingsCheckbox { LabelText = "Always show key overlay", - Bindable = config.GetBindable(OsuSetting.KeyOverlay) + Current = config.GetBindable(OsuSetting.KeyOverlay) }, new SettingsCheckbox { LabelText = "Positional hitsounds", - Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) + Current = config.GetBindable(OsuSetting.PositionalHitSounds) }, new SettingsEnumDropdown { LabelText = "Score meter type", - Bindable = config.GetBindable(OsuSetting.ScoreMeter) + Current = config.GetBindable(OsuSetting.ScoreMeter) }, new SettingsEnumDropdown { LabelText = "Score display mode", - Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + Current = config.GetBindable(OsuSetting.ScoreDisplayMode) } }; @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Add(new SettingsCheckbox { LabelText = "Disable Windows key during gameplay", - Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) + Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) }); } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index 0babb98066..2b2fb9cef7 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Increase visibility of first object when visual impairment mods are enabled", - Bindable = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), + Current = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs index 0c42247993..b26876556e 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs @@ -31,31 +31,31 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Right mouse drag to absolute scroll", - Bindable = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), + Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), }, new SettingsCheckbox { LabelText = "Show converted beatmaps", - Bindable = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), }, new SettingsSlider { LabelText = "Display beatmaps from", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMinimum), + Current = config.GetBindable(OsuSetting.DisplayStarsMinimum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsSlider { LabelText = "up to", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMaximum), + Current = config.GetBindable(OsuSetting.DisplayStarsMaximum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsEnumDropdown { LabelText = "Random selection algorithm", - Bindable = config.GetBindable(OsuSetting.RandomSelectAlgorithm), + Current = config.GetBindable(OsuSetting.RandomSelectAlgorithm), } }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index 236bfbecc3..44e42ecbfe 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.General new SettingsCheckbox { LabelText = "Prefer metadata in original language", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 34e5da4ef4..9e358d0cf5 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -236,17 +236,16 @@ namespace osu.Game.Overlays.Settings.Sections.General PlaceholderText = "password", RelativeSizeAxes = Axes.X, TabbableContentContainer = this, - OnCommit = (sender, newText) => performLogin() }, new SettingsCheckbox { LabelText = "Remember username", - Bindable = config.GetBindable(OsuSetting.SaveUsername), + Current = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { LabelText = "Stay signed in", - Bindable = config.GetBindable(OsuSetting.SavePassword), + Current = config.GetBindable(OsuSetting.SavePassword), }, new Container { @@ -276,6 +275,8 @@ namespace osu.Game.Overlays.Settings.Sections.General } } }; + + password.OnCommit += (sender, newText) => performLogin(); } public override bool AcceptsFocus => true; diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 9c7d0b0be4..a59a6b00b9 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsEnumDropdown { LabelText = "Release stream", - Bindable = config.GetBindable(OsuSetting.ReleaseStream), + Current = config.GetBindable(OsuSetting.ReleaseStream), }); if (updateManager?.CanCheckForUpdate == true) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 3089040f96..30caa45995 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -19,22 +19,22 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsCheckbox { LabelText = "Storyboard / Video", - Bindable = config.GetBindable(OsuSetting.ShowStoryboard) + Current = config.GetBindable(OsuSetting.ShowStoryboard) }, new SettingsCheckbox { LabelText = "Hit Lighting", - Bindable = config.GetBindable(OsuSetting.HitLighting) + Current = config.GetBindable(OsuSetting.HitLighting) }, new SettingsEnumDropdown { LabelText = "Screenshot format", - Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) + Current = config.GetBindable(OsuSetting.ScreenshotFormat) }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", - Bindable = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) + Current = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 4312b319c0..14b8dbfac0 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown = new SettingsDropdown { LabelText = "Screen mode", - Bindable = config.GetBindable(FrameworkSetting.WindowMode), + Current = config.GetBindable(FrameworkSetting.WindowMode), ItemSource = windowModes, }, resolutionSettingsContainer = new Container @@ -74,14 +74,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { LabelText = "UI Scaling", TransferValueOnCommit = true, - Bindable = osuConfig.GetBindable(OsuSetting.UIScale), + Current = osuConfig.GetBindable(OsuSetting.UIScale), KeyboardStep = 0.01f, Keywords = new[] { "scale", "letterbox" }, }, new SettingsEnumDropdown { LabelText = "Screen Scaling", - Bindable = osuConfig.GetBindable(OsuSetting.Scaling), + Current = osuConfig.GetBindable(OsuSetting.Scaling), Keywords = new[] { "scale", "letterbox" }, }, scalingSettings = new FillFlowContainer> @@ -97,28 +97,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider { LabelText = "Horizontal position", - Bindable = scalingPositionX, + Current = scalingPositionX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Vertical position", - Bindable = scalingPositionY, + Current = scalingPositionY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Horizontal scale", - Bindable = scalingSizeX, + Current = scalingSizeX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Vertical scale", - Bindable = scalingSizeY, + Current = scalingSizeY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - scalingSettings.ForEach(s => bindPreviewEvent(s.Bindable)); + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); var resolutions = getResolutions(); @@ -137,10 +137,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Resolution", ShowsDefaultIndicator = false, Items = resolutions, - Bindable = sizeFullscreen + Current = sizeFullscreen }; - windowModeDropdown.Bindable.BindValueChanged(mode => + windowModeDropdown.Current.BindValueChanged(mode => { if (mode.NewValue == WindowMode.Fullscreen) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 69ff9b43e5..8773e6763c 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -23,17 +23,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsEnumDropdown { LabelText = "Frame limiter", - Bindable = config.GetBindable(FrameworkSetting.FrameSync) + Current = config.GetBindable(FrameworkSetting.FrameSync) }, new SettingsEnumDropdown { LabelText = "Threading mode", - Bindable = config.GetBindable(FrameworkSetting.ExecutionMode) + Current = config.GetBindable(FrameworkSetting.ExecutionMode) }, new SettingsCheckbox { LabelText = "Show FPS", - Bindable = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) + Current = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs index a8953ac3a2..38c30ddd64 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs @@ -20,17 +20,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsCheckbox { LabelText = "Rotate cursor when dragging", - Bindable = config.GetBindable(OsuSetting.CursorRotation) + Current = config.GetBindable(OsuSetting.CursorRotation) }, new SettingsCheckbox { LabelText = "Parallax", - Bindable = config.GetBindable(OsuSetting.MenuParallax) + Current = config.GetBindable(OsuSetting.MenuParallax) }, new SettingsSlider { LabelText = "Hold-to-confirm activation time", - Bindable = config.GetBindable(OsuSetting.UIHoldActivationDelay), + Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), KeyboardStep = 50 }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index d27ab63fb7..5227e328ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -35,32 +35,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input new SettingsCheckbox { LabelText = "Raw input", - Bindable = rawInputToggle + Current = rawInputToggle }, new SensitivitySetting { LabelText = "Cursor sensitivity", - Bindable = sensitivityBindable + Current = sensitivityBindable }, new SettingsCheckbox { LabelText = "Map absolute input to window", - Bindable = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) + Current = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) }, new SettingsEnumDropdown { LabelText = "Confine mouse cursor to window", - Bindable = config.GetBindable(FrameworkSetting.ConfineMouseMode), + Current = config.GetBindable(FrameworkSetting.ConfineMouseMode), }, new SettingsCheckbox { LabelText = "Disable mouse wheel during gameplay", - Bindable = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) + Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) }, new SettingsCheckbox { LabelText = "Disable mouse buttons during gameplay", - Bindable = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) + Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 79d842a617..ad540e3691 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private void start() { - var target = directorySelector.CurrentDirectory.Value; + var target = directorySelector.CurrentPath.Value; try { diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 23513eade8..6461bd7b93 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -19,13 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Online new SettingsCheckbox { LabelText = "Warn about opening external links", - Bindable = config.GetBindable(OsuSetting.ExternalLinkWarning) + Current = config.GetBindable(OsuSetting.ExternalLinkWarning) }, new SettingsCheckbox { LabelText = "Prefer downloads without video", Keywords = new[] { "no-video" }, - Bindable = config.GetBindable(OsuSetting.PreferNoVideo) + Current = config.GetBindable(OsuSetting.PreferNoVideo) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 596d3a9801..1ade4befdc 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -47,29 +47,29 @@ namespace osu.Game.Overlays.Settings.Sections new SettingsSlider { LabelText = "Menu cursor size", - Bindable = config.GetBindable(OsuSetting.MenuCursorSize), + Current = config.GetBindable(OsuSetting.MenuCursorSize), KeyboardStep = 0.01f }, new SettingsSlider { LabelText = "Gameplay cursor size", - Bindable = config.GetBindable(OsuSetting.GameplayCursorSize), + Current = config.GetBindable(OsuSetting.GameplayCursorSize), KeyboardStep = 0.01f }, new SettingsCheckbox { LabelText = "Adjust gameplay cursor size based on current beatmap", - Bindable = config.GetBindable(OsuSetting.AutoCursorSize) + Current = config.GetBindable(OsuSetting.AutoCursorSize) }, new SettingsCheckbox { LabelText = "Beatmap skins", - Bindable = config.GetBindable(OsuSetting.BeatmapSkins) + Current = config.GetBindable(OsuSetting.BeatmapSkins) }, new SettingsCheckbox { LabelText = "Beatmap hitsounds", - Bindable = config.GetBindable(OsuSetting.BeatmapHitsounds) + Current = config.GetBindable(OsuSetting.BeatmapHitsounds) }, }; @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections config.BindWith(OsuSetting.Skin, configBindable); - skinDropdown.Bindable = dropdownBindable; + skinDropdown.Current = dropdownBindable; skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); // Todo: This should not be necessary when OsuConfigManager is databased diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index c2dd40d2a6..278479e04f 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -21,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable, ISettingsItem + public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue { protected abstract Drawable CreateControl(); @@ -54,7 +54,14 @@ namespace osu.Game.Overlays.Settings } } - public virtual Bindable Bindable + [Obsolete("Use Current instead")] // Can be removed 20210406 + public Bindable Bindable + { + get => Current; + set => Current = value; + } + + public virtual Bindable Current { get => controlWithCurrent.Current; set => controlWithCurrent.Current = value; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index fc1d232a00..58427f6945 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -1,11 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -16,19 +16,16 @@ namespace osu.Game.Rulesets.Difficulty protected readonly DifficultyAttributes Attributes; protected readonly Ruleset Ruleset; - protected readonly IBeatmap Beatmap; protected readonly ScoreInfo Score; protected double TimeRate { get; private set; } = 1; - protected PerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) + protected PerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) { Ruleset = ruleset; Score = score; - Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); - - Attributes = attributes ?? ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods); + Attributes = attributes ?? throw new ArgumentNullException(nameof(attributes)); ApplyMods(score.Mods); } diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 89e7866707..8ed7885101 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -40,14 +40,29 @@ namespace osu.Game.Rulesets.Edit Playfield.DisplayJudgements.Value = false; } + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + protected override void LoadComplete() { base.LoadComplete(); beatmap.HitObjectAdded += addHitObject; beatmap.HitObjectRemoved += removeHitObject; + + if (changeHandler != null) + { + // for now only regenerate replay on a finalised state change, not HitObjectUpdated. + changeHandler.OnStateChange += updateReplay; + } + else + { + beatmap.HitObjectUpdated += _ => updateReplay(); + } } + private void updateReplay() => drawableRuleset.RegenerateAutoplay(); + private void addHitObject(HitObject hitObject) { var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 28a77a8bdf..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; @@ -31,6 +30,11 @@ using osuTK.Input; namespace osu.Game.Rulesets.Edit { + /// + /// Top level container for editor compose mode. + /// Responsible for providing snapping and generally gluing components together. + /// + /// The base type of supported objects. [Cached(Type = typeof(IPlacementHandler))] public abstract class HitObjectComposer : HitObjectComposer, IPlacementHandler where TObject : HitObject @@ -58,6 +62,8 @@ namespace osu.Game.Rulesets.Edit private RadioButtonCollection toolboxCollection; + private FillFlowContainer togglesCollection; + protected HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; @@ -70,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 @@ -114,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 } }, - 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), + }, } } }, @@ -132,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; @@ -160,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 => Enumerable.Empty(); + 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. @@ -190,9 +208,12 @@ namespace osu.Game.Rulesets.Edit protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Key >= Key.Number1 && e.Key <= Key.Number9) + if (e.ControlPressed || e.AltPressed || e.SuperPressed) + return false; + + if (checkLeftToggleFromKey(e.Key, out var leftIndex)) { - var item = toolboxCollection.Items.ElementAtOrDefault(e.Key - Key.Number1); + var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex); if (item != null) { @@ -201,9 +222,84 @@ namespace osu.Game.Rulesets.Edit } } + if (checkRightToggleFromKey(e.Key, out var rightIndex)) + { + var item = togglesCollection.ElementAtOrDefault(rightIndex); + + if (item is DrawableTernaryButton button) + { + button.Button.Toggle(); + return true; + } + } + return base.OnKeyDown(e); } + private bool checkLeftToggleFromKey(Key key, out int index) + { + if (key < Key.Number1 || key > Key.Number9) + { + index = -1; + return false; + } + + index = key - Key.Number1; + return true; + } + + private bool checkRightToggleFromKey(Key key, out int index) + { + switch (key) + { + case Key.Q: + index = 0; + break; + + case Key.W: + index = 1; + break; + + case Key.E: + index = 2; + break; + + case Key.R: + index = 3; + break; + + case Key.T: + index = 4; + break; + + case Key.Y: + index = 5; + break; + + case Key.U: + index = 6; + break; + + case Key.I: + index = 7; + break; + + case Key.O: + index = 8; + break; + + case Key.P: + index = 9; + break; + + default: + index = -1; + break; + } + + return index >= 0; + } + private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { if (EditorBeatmap.SelectedHitObjects.Any()) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 02d5955ae6..d986b71380 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Edit /// /// The that is being placed. /// - protected readonly HitObject HitObject; + public readonly HitObject HitObject; [Resolved(canBeNull: true)] protected EditorClock EditorClock { get; private set; } diff --git a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs index 1871249c94..d2a434058d 100644 --- a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs @@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Judgements { public class IgnoreJudgement : Judgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 0; - - protected override double HealthIncreaseFor(HitResult result) => 0; + public override HitResult MaxResult => HitResult.IgnoreHit; } } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 9105b920ca..89a3a2b855 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -11,31 +12,69 @@ namespace osu.Game.Rulesets.Judgements /// public class Judgement { + /// + /// The score awarded for a small bonus. + /// + public const int SMALL_BONUS_SCORE = 10; + + /// + /// The score awarded for a large bonus. + /// + public const int LARGE_BONUS_SCORE = 50; + /// /// The default health increase for a maximum judgement, as a proportion of total health. /// By default, each maximum judgement restores 5% of total health. /// protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05; + /// + /// Whether this should affect the current combo. + /// + [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328 + public virtual bool AffectsCombo => true; + + /// + /// Whether this should be counted as base (combo) or bonus score. + /// + [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328 + public virtual bool IsBonus => !AffectsCombo; + /// /// The maximum that can be achieved. /// public virtual HitResult MaxResult => HitResult.Perfect; /// - /// Whether this should affect the current combo. + /// The minimum that can be achieved - the inverse of . /// - public virtual bool AffectsCombo => true; + public HitResult MinResult + { + get + { + switch (MaxResult) + { + case HitResult.SmallBonus: + case HitResult.LargeBonus: + case HitResult.IgnoreHit: + return HitResult.IgnoreMiss; - /// - /// Whether this should be counted as base (combo) or bonus score. - /// - public virtual bool IsBonus => !AffectsCombo; + case HitResult.SmallTickHit: + return HitResult.SmallTickMiss; + + case HitResult.LargeTickHit: + return HitResult.LargeTickMiss; + + default: + return HitResult.Miss; + } + } + } /// /// The numeric score representation for the maximum achievable result. /// - public int MaxNumericResult => NumericResultFor(MaxResult); + public int MaxNumericResult => ToNumericResult(MaxResult); /// /// The health increase for the maximum achievable result. @@ -47,14 +86,15 @@ namespace osu.Game.Rulesets.Judgements /// /// The to find the numeric score representation for. /// The numeric score representation of . - protected virtual int NumericResultFor(HitResult result) => result > HitResult.Miss ? 1 : 0; + [Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be made non-virtual 20210328 + protected virtual int NumericResultFor(HitResult result) => ToNumericResult(result); /// /// Retrieves the numeric score representation of a . /// /// The to find the numeric score representation for. /// The numeric score representation of . - public int NumericResultFor(JudgementResult result) => NumericResultFor(result.Type); + public int NumericResultFor(JudgementResult result) => ToNumericResult(result.Type); /// /// Retrieves the numeric health increase of a . @@ -65,6 +105,21 @@ namespace osu.Game.Rulesets.Judgements { switch (result) { + default: + return 0; + + case HitResult.SmallTickHit: + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; + + case HitResult.SmallTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + + case HitResult.LargeTickHit: + return DEFAULT_MAX_HEALTH_INCREASE; + + case HitResult.LargeTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE; + case HitResult.Miss: return -DEFAULT_MAX_HEALTH_INCREASE; @@ -72,10 +127,10 @@ namespace osu.Game.Rulesets.Judgements return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; case HitResult.Ok: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.01; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.Good: - return DEFAULT_MAX_HEALTH_INCREASE * 0.5; + return DEFAULT_MAX_HEALTH_INCREASE * 0.75; case HitResult.Great: return DEFAULT_MAX_HEALTH_INCREASE; @@ -83,8 +138,11 @@ namespace osu.Game.Rulesets.Judgements case HitResult.Perfect: return DEFAULT_MAX_HEALTH_INCREASE * 1.05; - default: - return 0; + case HitResult.SmallBonus: + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; + + case HitResult.LargeBonus: + return DEFAULT_MAX_HEALTH_INCREASE; } } @@ -95,6 +153,42 @@ namespace osu.Game.Rulesets.Judgements /// The numeric health increase of . public double HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type); - public override string ToString() => $"AffectsCombo:{AffectsCombo} MaxResult:{MaxResult} MaxScore:{MaxNumericResult}"; + public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}"; + + public static int ToNumericResult(HitResult result) + { + switch (result) + { + default: + return 0; + + case HitResult.SmallTickHit: + return 10; + + case HitResult.LargeTickHit: + return 30; + + case HitResult.Meh: + return 50; + + case HitResult.Ok: + return 100; + + case HitResult.Good: + return 200; + + case HitResult.Great: + return 300; + + case HitResult.Perfect: + return 350; + + case HitResult.SmallBonus: + return SMALL_BONUS_SCORE; + + case HitResult.LargeBonus: + return LARGE_BONUS_SCORE; + } + } } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 59a7917e55..3a35fd4433 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Judgements /// /// Whether a successful hit occurred. /// - public bool IsHit => Type > HitResult.Miss; + public bool IsHit => Type.IsHit(); /// /// Creates a new . diff --git a/osu.Game/Rulesets/Mods/IApplicableToSample.cs b/osu.Game/Rulesets/Mods/IApplicableToSample.cs index 559d127cfc..50a6d501b6 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToSample.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToSample.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Audio; namespace osu.Game.Rulesets.Mods { @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToSample : IApplicableMod { - void ApplyToSample(SampleChannel sample); + void ApplyToSample(DrawableSample sample); } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 4004953cd1..282de3a8e1 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mods public class NightcoreBeatContainer : BeatSyncedContainer { - private SkinnableSound hatSample; - private SkinnableSound clapSample; - private SkinnableSound kickSample; - private SkinnableSound finishSample; + private PausableSkinnableSound hatSample; + private PausableSkinnableSound clapSample; + private PausableSkinnableSound kickSample; + private PausableSkinnableSound finishSample; private int? firstBeat; @@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")), - clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")), - kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")), - finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")), + hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")), + clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")), + kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")), + finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")), }; } diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 65f1a972ed..df0fc9c4b6 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "SS or quit."; protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => !(result.Judgement is IgnoreJudgement) - && result.Judgement.AffectsCombo + => result.Type.AffectsAccuracy() && result.Type != result.Judgement.MaxResult; } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index fec21764b0..2150b0fb68 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics.Audio; namespace osu.Game.Rulesets.Mods { @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } - public virtual void ApplyToSample(SampleChannel sample) + public virtual void ApplyToSample(DrawableSample sample) { sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index df10262845..ae71041a64 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mods healthProcessor.FailConditions += FailCondition; } - protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !result.IsHit && result.Judgement.AffectsCombo; + protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + => result.Type.AffectsCombo() + && !result.IsHit; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 20c8d0f3e7..4d43ae73d3 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -6,11 +6,11 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Objects; -using osu.Framework.Audio.Sample; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.TriggerChange(); } - public void ApplyToSample(SampleChannel sample) + public void ApplyToSample(DrawableSample sample) { sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 581617b567..1ef6c8c207 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; @@ -17,7 +18,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Configuration; -using osu.Game.Screens.Play; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); - protected SkinnableSound Samples { get; private set; } + protected PausableSkinnableSound Samples { get; private set; } public virtual IEnumerable GetSamples() => HitObject.Samples; @@ -51,12 +51,12 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool PropagateNonPositionalInputSubTree => HandleUserInput; /// - /// Invoked when a has been applied by this or a nested . + /// Invoked by this or a nested after a has been applied. /// public event Action OnNewResult; /// - /// Invoked when a is being reverted by this or a nested . + /// Invoked by this or a nested prior to a being reverted. /// public event Action OnRevertResult; @@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Objects.Drawables updateState(ArmedState.Idle, true); } + /// + /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. + /// protected virtual void LoadSamples() { if (Samples != null) @@ -176,13 +179,14 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); + Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); AddInternal(Samples); } private void onDefaultsApplied(HitObject hitObject) { apply(hitObject); + updateState(state.Value, true); DefaultsApplied?.Invoke(this); } @@ -232,7 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables #region State / Transform Management /// - /// Bind to apply a custom state which can override the default implementation. + /// Invoked by this or a nested to apply a custom state that can override the default implementation. /// public event Action ApplyCustomUpdateState; @@ -269,7 +273,7 @@ namespace osu.Game.Rulesets.Objects.Drawables // apply any custom state overrides ApplyCustomUpdateState?.Invoke(this, newState); - if (newState == ArmedState.Hit) + if (!force && newState == ArmedState.Hit) PlaySamples(); } @@ -355,9 +359,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { } - [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } - /// /// Calculate the position to be used for sample playback at a specified X position (0..1). /// @@ -370,24 +371,29 @@ namespace osu.Game.Rulesets.Objects.Drawables return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0); } - /// - /// Whether samples should currently be playing. Will be false during seek operations. - /// - protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true; - /// /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// public virtual void PlaySamples() { - if (Samples != null && ShouldPlaySamples) + if (Samples != null) { Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition); Samples.Play(); } } + /// + /// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out. + /// Automatically called when 's lifetime has been exceeded. + /// + public virtual void StopAllSamples() + { + if (Samples?.Looping == true) + Samples.Stop(); + } + protected override void Update() { base.Update(); @@ -456,6 +462,10 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var nested in NestedHitObjects) nested.OnKilled(); + // failsafe to ensure looping samples don't get stuck in a playing state. + // this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped. + StopAllSamples(); + UpdateResult(false); } @@ -466,29 +476,45 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The callback that applies changes to the . protected void ApplyResult(Action application) { + if (Result.HasResult) + throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result."); + application?.Invoke(Result); if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); + // Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements. + // Can be removed 20210328 + if (Result.Judgement.MaxResult == HitResult.IgnoreHit) + { + HitResult originalType = Result.Type; + + if (Result.Type == HitResult.Miss) + Result.Type = HitResult.IgnoreMiss; + else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect) + Result.Type = HitResult.IgnoreHit; + + if (Result.Type != originalType) + { + Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n" + + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2020-03-28 onwards.", level: LogLevel.Important); + } + } + + if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult)) + { + throw new InvalidOperationException( + $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])."); + } + // Ensure that the judgement is given a valid time offset, because this may not get set by the caller var endTime = HitObject.GetEndTime(); Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime); - switch (Result.Type) - { - case HitResult.None: - break; - - case HitResult.Miss: - updateState(ArmedState.Miss); - break; - - default: - updateState(ArmedState.Hit); - break; - } + if (Result.HasResult) + updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); OnNewResult?.Invoke(this, Result); } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 0dfde834ee..826d411822 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Objects /// /// The hit windows for this . /// + [JsonIgnore] public HitWindows HitWindows { get; set; } private readonly List nestedHitObjects = new List(); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index a8f62bca4a..fef36ef16a 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -158,7 +158,28 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); - public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => null; + /// + /// Optionally creates a to generate performance data from the provided score. + /// + /// Difficulty attributes for the beatmap related to the provided score. + /// The score to be processed. + /// A performance calculator instance for the provided score. + [CanBeNull] + public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; + + /// + /// Optionally creates a to generate performance data from the provided score. + /// + /// The beatmap to use as a source for generating . + /// The score to be processed. + /// A performance calculator instance for the provided score. + [CanBeNull] + public PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) + { + var difficultyCalculator = CreateDifficultyCalculator(beatmap); + var difficultyAttributes = difficultyCalculator.Calculate(score.Mods); + return CreatePerformanceCalculator(difficultyAttributes, score); + } public virtual HitObjectComposer CreateHitObjectComposer() => null; diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 130907b242..cae41e22f4 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Scoring { base.ApplyResultInternal(result); - if (!result.Judgement.IsBonus) + if (!result.Type.IsBonus()) healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); } @@ -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/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index b057af2a50..6a3a034fc1 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -2,15 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using System.Diagnostics; +using osu.Game.Utils; namespace osu.Game.Rulesets.Scoring { + [HasOrderedElements] public enum HitResult { /// /// Indicates that the object has not been judged yet. /// [Description(@"")] + [Order(14)] None, /// @@ -21,47 +25,169 @@ namespace osu.Game.Rulesets.Scoring /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). /// [Description(@"Miss")] + [Order(5)] Miss, [Description(@"Meh")] + [Order(4)] Meh, - /// - /// Optional judgement. - /// [Description(@"OK")] + [Order(3)] Ok, [Description(@"Good")] + [Order(2)] Good, [Description(@"Great")] + [Order(1)] Great, - /// - /// Optional judgement. - /// [Description(@"Perfect")] + [Order(0)] Perfect, /// /// Indicates small tick miss. /// + [Order(11)] SmallTickMiss, /// /// Indicates a small tick hit. /// + [Description(@"S Tick")] + [Order(7)] SmallTickHit, /// /// Indicates a large tick miss. /// + [Order(10)] LargeTickMiss, /// /// Indicates a large tick hit. /// - LargeTickHit + [Description(@"L Tick")] + [Order(6)] + LargeTickHit, + + /// + /// Indicates a small bonus. + /// + [Description("S Bonus")] + [Order(9)] + SmallBonus, + + /// + /// Indicates a large bonus. + /// + [Description("L Bonus")] + [Order(8)] + LargeBonus, + + /// + /// Indicates a miss that should be ignored for scoring purposes. + /// + [Order(13)] + IgnoreMiss, + + /// + /// Indicates a hit that should be ignored for scoring purposes. + /// + [Order(12)] + IgnoreHit, + } + + public static class HitResultExtensions + { + /// + /// Whether a increases/decreases the combo, and affects the combo portion of the score. + /// + public static bool AffectsCombo(this HitResult result) + { + switch (result) + { + case HitResult.Miss: + case HitResult.Meh: + case HitResult.Ok: + case HitResult.Good: + case HitResult.Great: + case HitResult.Perfect: + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + return true; + + default: + return false; + } + } + + /// + /// Whether a affects the accuracy portion of the score. + /// + public static bool AffectsAccuracy(this HitResult result) + => IsScorable(result) && !IsBonus(result); + + /// + /// Whether a should be counted as bonus score. + /// + public static bool IsBonus(this HitResult result) + { + switch (result) + { + case HitResult.SmallBonus: + case HitResult.LargeBonus: + return true; + + default: + return false; + } + } + + /// + /// Whether a represents a successful hit. + /// + public static bool IsHit(this HitResult result) + { + switch (result) + { + case HitResult.None: + case HitResult.IgnoreMiss: + case HitResult.Miss: + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + return false; + + default: + return true; + } + } + + /// + /// Whether a is scorable. + /// + public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss; + + /// + /// Whether a is valid within a given range. + /// + /// The to check. + /// The minimum . + /// The maximum . + /// Whether falls between and . + public static bool IsValidHitResult(this HitResult result, HitResult minResult, HitResult maxResult) + { + if (result == HitResult.None) + return false; + + if (result == minResult || result == maxResult) + return true; + + Debug.Assert(minResult <= maxResult); + return result > minResult && result < maxResult; + } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 6fa5a87c8e..7a5b707357 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -71,7 +71,6 @@ namespace osu.Game.Rulesets.Scoring private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; - private double bonusScore; private readonly List hitEvents = new List(); private HitObject lastHitObject; @@ -116,14 +115,15 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - if (result.Judgement.AffectsCombo) + if (!result.Type.IsScorable()) + return; + + if (result.Type.AffectsCombo()) { switch (result.Type) { - case HitResult.None: - break; - case HitResult.Miss: + case HitResult.LargeTickMiss: Combo.Value = 0; break; @@ -133,22 +133,16 @@ namespace osu.Game.Rulesets.Scoring } } - double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result); + double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - if (result.Judgement.IsBonus) + if (!result.Type.IsBonus()) { - if (result.IsHit) - bonusScore += scoreIncrease; - } - else - { - if (result.HasResult) - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; - baseScore += scoreIncrease; rollingMaxBaseScore += result.Judgement.MaxNumericResult; } + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; + hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -171,22 +165,19 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result); + if (!result.Type.IsScorable()) + return; - if (result.Judgement.IsBonus) - { - if (result.IsHit) - bonusScore -= scoreIncrease; - } - else - { - if (result.HasResult) - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; + double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + if (!result.Type.IsBonus()) + { baseScore -= scoreIncrease; rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; + Debug.Assert(hitEvents.Count > 0); lastHitObject = hitEvents[^1].LastHitObject; hitEvents.RemoveAt(hitEvents.Count - 1); @@ -207,7 +198,7 @@ namespace osu.Game.Rulesets.Scoring return GetScore(mode, maxHighestCombo, maxBaseScore > 0 ? baseScore / maxBaseScore : 0, maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0, - bonusScore); + scoreResultCounts); } /// @@ -217,9 +208,9 @@ namespace osu.Game.Rulesets.Scoring /// The maximum combo achievable in the beatmap. /// The accuracy percentage achieved by the player. /// The proportion of achieved by the player. - /// Any bonus score to be added. + /// Any statistics to be factored in. /// The total score. - public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, double bonusScore) + public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, Dictionary statistics) { switch (mode) { @@ -228,14 +219,18 @@ namespace osu.Game.Rulesets.Scoring double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) - return bonusScore + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); + return getBonusScore(statistics) + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); } } + private double getBonusScore(Dictionary statistics) + => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; + private ScoreRank rankFrom(double acc) { if (acc == 1) @@ -282,7 +277,6 @@ namespace osu.Game.Rulesets.Scoring baseScore = 0; rollingMaxBaseScore = 0; - bonusScore = 0; TotalScore.Value = 0; Accuracy.Value = 1; @@ -309,9 +303,7 @@ namespace osu.Game.Rulesets.Scoring score.Rank = Rank.Value; score.Date = DateTimeOffset.Now; - var hitWindows = CreateHitWindows(); - - foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) + foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.IsScorable())) score.Statistics[result] = GetStatistic(result); score.HitEvents = hitEvents; @@ -320,6 +312,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Create a for this processor. /// + [Obsolete("Method is now unused.")] // Can be removed 20210328 public virtual HitWindows CreateHitWindows() => new HitWindows(); } 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/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 83a1077d70..a9b2a15b35 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -10,6 +10,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Rulesets.Configuration; @@ -46,12 +47,11 @@ namespace osu.Game.Rulesets.UI if (resources != null) { TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); - TextureStore.AddStore(parent.Get()); - Cache(TextureStore); + CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - CacheAs(new FallbackSampleStore(SampleStore, parent.Get())); + CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get())); } RulesetConfigManager = parent.Get().GetConfigFor(ruleset); @@ -82,69 +82,92 @@ namespace osu.Game.Rulesets.UI isDisposed = true; SampleStore?.Dispose(); + TextureStore?.Dispose(); RulesetConfigManager = null; } #endregion - } - /// - /// A sample store which adds a fallback source. - /// - /// - /// This is a temporary implementation to workaround ISampleStore limitations. - /// - public class FallbackSampleStore : ISampleStore - { - private readonly ISampleStore primary; - private readonly ISampleStore secondary; - - public FallbackSampleStore(ISampleStore primary, ISampleStore secondary) + /// + /// A sample store which adds a fallback source and prevents disposal of the fallback source. + /// + private class FallbackSampleStore : ISampleStore { - this.primary = primary; - this.secondary = secondary; + private readonly ISampleStore primary; + private readonly ISampleStore fallback; + + public FallbackSampleStore(ISampleStore primary, ISampleStore fallback) + { + this.primary = primary; + this.fallback = fallback; + } + + public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name); + + public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name); + + public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name); + + public IEnumerable GetAvailableResources() => throw new NotSupportedException(); + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + + public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); + + public BindableNumber Volume => throw new NotSupportedException(); + + public BindableNumber Balance => throw new NotSupportedException(); + + public BindableNumber Frequency => throw new NotSupportedException(); + + public BindableNumber Tempo => throw new NotSupportedException(); + + public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException(); + + public IBindable AggregateVolume => throw new NotSupportedException(); + + public IBindable AggregateBalance => throw new NotSupportedException(); + + public IBindable AggregateFrequency => throw new NotSupportedException(); + + public IBindable AggregateTempo => throw new NotSupportedException(); + + public int PlaybackConcurrency + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public void Dispose() + { + primary?.Dispose(); + } } - public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name); - - public Task GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name); - - public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name); - - public IEnumerable GetAvailableResources() => throw new NotSupportedException(); - - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); - - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); - - public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); - - public BindableNumber Volume => throw new NotSupportedException(); - - public BindableNumber Balance => throw new NotSupportedException(); - - public BindableNumber Frequency => throw new NotSupportedException(); - - public BindableNumber Tempo => throw new NotSupportedException(); - - public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException(); - - public IBindable AggregateVolume => throw new NotSupportedException(); - - public IBindable AggregateBalance => throw new NotSupportedException(); - - public IBindable AggregateFrequency => throw new NotSupportedException(); - - public IBindable AggregateTempo => throw new NotSupportedException(); - - public int PlaybackConcurrency + /// + /// A texture store which adds a fallback source and prevents disposal of the fallback source. + /// + private class FallbackTextureStore : TextureStore { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } + private readonly TextureStore primary; + private readonly TextureStore fallback; - public void Dispose() - { + public FallbackTextureStore(TextureStore primary, TextureStore fallback) + { + this.primary = primary; + this.fallback = fallback; + } + + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) + => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + primary?.Dispose(); + } } } } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index d574991fa0..70b3d0c7d4 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; @@ -32,6 +35,7 @@ namespace osu.Game.Rulesets.UI public GameplayClock GameplayClock => stabilityGameplayClock; [Cached(typeof(GameplayClock))] + [Cached(typeof(ISamplePlaybackDisabler))] private readonly StabilityGameplayClock stabilityGameplayClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) @@ -55,13 +59,16 @@ namespace osu.Game.Rulesets.UI private int direction; [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) + private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler) { if (clock != null) { - stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock; + parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock; GameplayClock.IsPaused.BindTo(clock.IsPaused); } + + // this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes). + stabilityGameplayClock.ParentSampleDisabler = sampleDisabler; } protected override void LoadComplete() @@ -123,39 +130,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; @@ -180,25 +211,37 @@ namespace osu.Game.Rulesets.UI private void setClock() { - // in case a parent gameplay clock isn't available, just use the parent clock. - parentGameplayClock ??= Clock; - - Clock = GameplayClock; - ProcessCustomClock = false; + if (parentGameplayClock == null) + { + // in case a parent gameplay clock isn't available, just use the parent clock. + parentGameplayClock ??= Clock; + } + else + { + Clock = GameplayClock; + } } public ReplayInputHandler ReplayInputHandler { get; set; } private class StabilityGameplayClock : GameplayClock { - public IFrameBasedClock ParentGameplayClock; + public GameplayClock ParentGameplayClock; + + public ISamplePlaybackDisabler ParentSampleDisabler; + + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); public StabilityGameplayClock(FramedClock underlyingClock) : base(underlyingClock) { } - public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200; + protected override bool ShouldDisableSamplePlayback => + // handle the case where playback is catching up to real-time. + base.ShouldDisableSamplePlayback + || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true + || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); } } } diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 6f73a284a2..b58f65800d 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -28,37 +28,9 @@ namespace osu.Game.Scoring.Legacy } } - public static int? GetCount300(this ScoreInfo scoreInfo) - { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) - { - case 0: - case 1: - case 3: - return getCount(scoreInfo, HitResult.Great); + public static int? GetCount300(this ScoreInfo scoreInfo) => getCount(scoreInfo, HitResult.Great); - case 2: - return getCount(scoreInfo, HitResult.Perfect); - } - - return null; - } - - public static void SetCount300(this ScoreInfo scoreInfo, int value) - { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) - { - case 0: - case 1: - case 3: - scoreInfo.Statistics[HitResult.Great] = value; - break; - - case 2: - scoreInfo.Statistics[HitResult.Perfect] = value; - break; - } - } + public static void SetCount300(this ScoreInfo scoreInfo, int value) => scoreInfo.Statistics[HitResult.Great] = value; public static int? GetCountKatu(this ScoreInfo scoreInfo) { @@ -94,8 +66,6 @@ namespace osu.Game.Scoring.Legacy { case 0: case 1: - return getCount(scoreInfo, HitResult.Good); - case 3: return getCount(scoreInfo, HitResult.Ok); @@ -112,9 +82,6 @@ namespace osu.Game.Scoring.Legacy { case 0: case 1: - scoreInfo.Statistics[HitResult.Good] = value; - break; - case 3: scoreInfo.Statistics[HitResult.Ok] = value; break; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index efcf1737c9..0206989231 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; @@ -147,8 +148,6 @@ namespace osu.Game.Scoring [JsonProperty("statistics")] public Dictionary Statistics = new Dictionary(); - public IOrderedEnumerable> SortedStatistics => Statistics.OrderByDescending(pair => pair.Key); - [JsonIgnore] [Column("Statistics")] public string StatisticsJson @@ -186,6 +185,76 @@ namespace osu.Game.Scoring [JsonProperty("position")] public int? Position { get; set; } + private bool isLegacyScore; + + /// + /// Whether this represents a legacy (osu!stable) score. + /// + [JsonIgnore] + [NotMapped] + public bool IsLegacyScore + { + get + { + if (isLegacyScore) + return true; + + // The above check will catch legacy online scores that have an appropriate UserString + UserId. + // For non-online scores such as those imported in, a heuristic is used based on the following table: + // + // Mode | UserString | UserId + // --------------- | ---------- | --------- + // stable | | 1 + // lazer | | + // lazer (offline) | Guest | 1 + + return ID > 0 && UserID == 1 && UserString != "Guest"; + } + set => isLegacyScore = value; + } + + public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay() + { + foreach (var key in OrderAttributeUtils.GetValuesInOrder()) + { + if (key.IsBonus()) + continue; + + int value = Statistics.GetOrDefault(key); + + switch (key) + { + case HitResult.SmallTickHit: + { + int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss); + if (total > 0) + yield return (key, value, total); + + break; + } + + case HitResult.LargeTickHit: + { + int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss); + if (total > 0) + yield return (key, value, total); + + break; + } + + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + break; + + default: + if (value > 0 || key == HitResult.Miss) + yield return (key, value, null); + + break; + } + } + } + [Serializable] protected class DeserializedMod : IMod { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 619ca76598..5a6da53839 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -10,6 +10,7 @@ using System.Threading; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -26,7 +27,7 @@ namespace osu.Game.Scoring { public class ScoreManager : DownloadableArchiveModelManager { - public override string[] HandledExtensions => new[] { ".osr" }; + public override IEnumerable HandledExtensions => new[] { ".osr" }; protected override string[] HashableFileTypes => new[] { ".osr" }; @@ -149,23 +150,38 @@ namespace osu.Game.Scoring return; } - int? beatmapMaxCombo = score.Beatmap.MaxCombo; + int beatmapMaxCombo; - if (beatmapMaxCombo == null) + if (score.IsLegacyScore) { - if (score.Beatmap.ID == 0 || difficulties == null) + // This score is guaranteed to be an osu!stable score. + // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. + if (score.Beatmap.MaxCombo == null) { - // We don't have enough information (max combo) to compute the score, so let's use the provided score. - Value = score.TotalScore; + if (score.Beatmap.ID == 0 || difficulties == null) + { + // We don't have enough information (max combo) to compute the score, so use the provided score. + Value = score.TotalScore; + return; + } + + // We can compute the max combo locally after the async beatmap difficulty computation. + difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); + difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); + return; } - // We can compute the max combo locally after the async beatmap difficulty computation. - difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); - difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); + beatmapMaxCombo = score.Beatmap.MaxCombo.Value; } else - updateScore(beatmapMaxCombo.Value); + { + // This score is guaranteed to be an osu!lazer score. + // The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values. + beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum(); + } + + updateScore(beatmapMaxCombo); } private void updateScore(int beatmapMaxCombo) @@ -181,7 +197,7 @@ namespace osu.Game.Scoring scoreProcessor.Mods.Value = score.Mods; - Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, 0)); + Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); } } 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/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index c1f54d7938..0a8c339559 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -3,8 +3,8 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; -using System; using osu.Framework.Allocation; +using osu.Game.Extensions; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components @@ -22,10 +22,12 @@ namespace osu.Game.Screens.Edit.Components { trackTimer = new OsuSpriteText { - Origin = Anchor.BottomLeft, - RelativePositionAxes = Axes.Y, - Font = OsuFont.GetFont(size: 22, fixedWidth: true), - Y = 0.5f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // intentionally fudged centre to avoid movement of the number portion when + // going negative. + X = -35, + Font = OsuFont.GetFont(size: 25, fixedWidth: true), } }; } @@ -33,8 +35,7 @@ namespace osu.Game.Screens.Edit.Components protected override void Update() { base.Update(); - - trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff"); + trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index 102955657e..8c0e31c04c 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -1,70 +1,50 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Specialized; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { /// /// The part of the timeline that displays the control points. /// - public class ControlPointPart : TimelinePart + public class ControlPointPart : TimelinePart { + private IBindableList controlPointGroups; + protected override void LoadBeatmap(WorkingBeatmap beatmap) { base.LoadBeatmap(beatmap); - ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo; - - cpi.TimingPoints.ForEach(addTimingPoint); - - // Consider all non-timing points as the same type - cpi.SamplePoints.Select(c => (ControlPoint)c) - .Concat(cpi.EffectPoints) - .Concat(cpi.DifficultyPoints) - .Distinct() - // Non-timing points should not be added where there are timing points - .Where(c => cpi.TimingPointAt(c.Time).Time != c.Time) - .ForEach(addNonTimingPoint); - } - - private void addTimingPoint(ControlPoint controlPoint) => Add(new TimingPointVisualisation(controlPoint)); - private void addNonTimingPoint(ControlPoint controlPoint) => Add(new NonTimingPointVisualisation(controlPoint)); - - private class TimingPointVisualisation : ControlPointVisualisation - { - public TimingPointVisualisation(ControlPoint controlPoint) - : base(controlPoint) + controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindCollectionChanged((sender, args) => { - } + switch (args.Action) + { + case NotifyCollectionChangedAction.Reset: + Clear(); + break; - [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.YellowDark; - } + case NotifyCollectionChangedAction.Add: + foreach (var group in args.NewItems.OfType()) + Add(new GroupVisualisation(group)); + break; - private class NonTimingPointVisualisation : ControlPointVisualisation - { - public NonTimingPointVisualisation(ControlPoint controlPoint) - : base(controlPoint) - { - } + case NotifyCollectionChangedAction.Remove: + foreach (var group in args.OldItems.OfType()) + { + var matching = Children.SingleOrDefault(gv => gv.Group == group); - [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Green; - } + matching?.Expire(); + } - private abstract class ControlPointVisualisation : PointVisualisation - { - protected ControlPointVisualisation(ControlPoint controlPoint) - : base(controlPoint.Time) - { - } + break; + } + }, true); } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs new file mode 100644 index 0000000000..b9eb53b697 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts +{ + public class GroupVisualisation : PointVisualisation + { + public readonly ControlPointGroup Group; + + private BindableList controlPoints; + + [Resolved] + private OsuColour colours { get; set; } + + public GroupVisualisation(ControlPointGroup group) + : base(group.Time) + { + Group = group; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindCollectionChanged((_, __) => + { + if (controlPoints.Count == 0) + { + Colour = Color4.Transparent; + return; + } + + Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green; + }, true); + } + } +} 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 bf1e18771f..970e16d1c3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -24,15 +24,15 @@ namespace osu.Game.Screens.Edit.Compose.Components { /// /// A container which provides a "blueprint" display of hitobjects. - /// Includes selection and manipulation support via a . + /// Includes selection and manipulation support via a . /// public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler { protected DragBox DragBox { get; private set; } - protected Container SelectionBlueprints { get; private set; } + public Container SelectionBlueprints { get; private set; } - private SelectionHandler selectionHandler; + protected SelectionHandler SelectionHandler { get; private set; } [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private EditorClock editorClock { get; set; } [Resolved] - private EditorBeatmap beatmap { get; set; } + protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); @@ -56,22 +56,22 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - selectionHandler = CreateSelectionHandler(); - selectionHandler.DeselectAll = deselectAll; + SelectionHandler = CreateSelectionHandler(); + SelectionHandler.DeselectAll = deselectAll; AddRangeInternal(new[] { DragBox = CreateDragBox(selectBlueprintsFromDragRectangle), - selectionHandler, + SelectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), - selectionHandler.CreateProxy(), + SelectionHandler.CreateProxy(), DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); - foreach (var obj in beatmap.HitObjects) + foreach (var obj in Beatmap.HitObjects) AddBlueprintFor(obj); - selectedHitObjects.BindTo(beatmap.SelectedHitObjects); + selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.CollectionChanged += (selectedObjects, args) => { switch (args.Action) @@ -94,15 +94,15 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - beatmap.HitObjectAdded += AddBlueprintFor; - beatmap.HitObjectRemoved += removeBlueprintFor; + Beatmap.HitObjectAdded += AddBlueprintFor; + Beatmap.HitObjectRemoved += removeBlueprintFor; } protected virtual Container CreateSelectionBlueprintContainer() => new Container { RelativeSizeAxes = Axes.Both }; /// - /// Creates a which outlines s and handles movement of selections. + /// Creates a which outlines s and handles movement of selections. /// protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler(); @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // store for double-click handling - clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered); + clickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered); // Deselection should only occur if no selected blueprints are hovered // A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // ensure the blueprint which was hovered for the first click is still the hovered blueprint. - if (clickedBlueprint == null || selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) + if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) return false; editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); @@ -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; } @@ -208,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (DragBox.State == Visibility.Visible) { DragBox.Hide(); - selectionHandler.UpdateVisibility(); + SelectionHandler.UpdateVisibility(); } } @@ -217,7 +221,7 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Key) { case Key.Escape: - if (!selectionHandler.SelectedBlueprints.Any()) + if (!SelectionHandler.SelectedBlueprints.Any()) return false; deselectAll(); @@ -271,7 +275,7 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; - if (beatmap.SelectedHitObjects.Contains(hitObject)) + if (Beatmap.SelectedHitObjects.Contains(hitObject)) blueprint.Select(); SelectionBlueprints.Add(blueprint); @@ -298,14 +302,14 @@ namespace osu.Game.Screens.Edit.Compose.Components bool allowDeselection = e.ControlPressed && e.Button == MouseButton.Left; // Todo: This is probably incorrectly disallowing multiple selections on stacked objects - if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) + if (!allowDeselection && SelectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) return; foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (blueprint.IsHovered) { - selectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); + SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); clickSelectionBegan = true; break; } @@ -358,23 +362,23 @@ namespace osu.Game.Screens.Edit.Compose.Components private void selectAll() { SelectionBlueprints.ToList().ForEach(m => m.Select()); - selectionHandler.UpdateVisibility(); + SelectionHandler.UpdateVisibility(); } /// /// Deselects all selected s. /// - private void deselectAll() => selectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); + private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); private void onBlueprintSelected(SelectionBlueprint blueprint) { - selectionHandler.HandleSelected(blueprint); + SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); } private void onBlueprintDeselected(SelectionBlueprint blueprint) { - selectionHandler.HandleDeselected(blueprint); + SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); } @@ -391,16 +395,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void prepareSelectionMovement() { - if (!selectionHandler.SelectedBlueprints.Any()) + if (!SelectionHandler.SelectedBlueprints.Any()) return; // Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement // A special case is added for when a click selection occurred before the drag - if (!clickSelectionBegan && !selectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) + if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) return; // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject - movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); + movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct } @@ -425,14 +429,14 @@ namespace osu.Game.Screens.Edit.Compose.Components var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. - if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) + if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) return true; if (result.Time.HasValue) { // Apply the start time at the newly snapped-to position double offset = result.Time.Value - draggedObject.StartTime; - foreach (HitObject obj in selectionHandler.SelectedHitObjects) + foreach (HitObject obj in SelectionHandler.SelectedHitObjects) obj.StartTime += offset; } @@ -460,10 +464,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Dispose(isDisposing); - if (beatmap != null) + if (Beatmap != null) { - beatmap.HitObjectAdded -= AddBlueprintFor; - beatmap.HitObjectRemoved -= removeBlueprintFor; + Beatmap.HitObjectAdded -= AddBlueprintFor; + Beatmap.HitObjectRemoved -= removeBlueprintFor; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index e1f311f1b8..9b3314e2ad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -3,14 +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 @@ -46,6 +53,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + TernaryStates = CreateTernaryButtons().ToArray(); + AddInternal(placementBlueprintContainer); } @@ -54,6 +63,92 @@ namespace osu.Game.Screens.Edit.Compose.Components base.LoadComplete(); inputManager = GetContainingInputManager(); + + // updates to selected are handled for us by SelectionHandler. + NewCombo.BindTo(SelectionHandler.SelectionNewComboState); + + // we are responsible for current placement blueprint updated based on state changes. + NewCombo.ValueChanged += _ => updatePlacementNewCombo(); + + // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity) + foreach (var kvp in SelectionHandler.SelectionSampleStates) + { + kvp.Value.BindValueChanged(_ => updatePlacementSamples()); + } + } + + private void updatePlacementNewCombo() + { + if (currentPlacement == null) return; + + if (currentPlacement.HitObject is IHasComboInformation c) + c.NewCombo = NewCombo.Value == TernaryState.True; + } + + 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. + 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 @@ -86,7 +181,9 @@ namespace osu.Game.Screens.Edit.Compose.Components removePlacement(); if (currentPlacement != null) + { updatePlacementPosition(); + } } protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) @@ -115,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/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 0615ebfc20..eaee2cd1e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Masking = true, BorderColour = Color4.White, - BorderThickness = SelectionHandler.BORDER_RADIUS, + BorderThickness = SelectionBox.BORDER_RADIUS, Child = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs new file mode 100644 index 0000000000..64191e48e2 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -0,0 +1,216 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class SelectionBox : CompositeDrawable + { + public Action OnRotation; + public Action OnScale; + public Action OnFlip; + + public Action OperationStarted; + public Action OperationEnded; + + private bool canRotate; + + /// + /// Whether rotation support should be enabled. + /// + public bool CanRotate + { + get => canRotate; + set + { + if (canRotate == value) return; + + canRotate = value; + recreate(); + } + } + + private bool canScaleX; + + /// + /// Whether vertical scale support should be enabled. + /// + public bool CanScaleX + { + get => canScaleX; + set + { + if (canScaleX == value) return; + + canScaleX = value; + recreate(); + } + } + + private bool canScaleY; + + /// + /// Whether horizontal scale support should be enabled. + /// + public bool CanScaleY + { + get => canScaleY; + set + { + if (canScaleY == value) return; + + canScaleY = value; + recreate(); + } + } + + private FillFlowContainer buttons; + + public const float BORDER_RADIUS = 3; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + recreate(); + } + + private void recreate() + { + if (LoadState < LoadState.Loading) + return; + + InternalChildren = new Drawable[] + { + new Container + { + Masking = true, + BorderThickness = BORDER_RADIUS, + BorderColour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + + AlwaysPresent = true, + Alpha = 0 + }, + } + }, + buttons = new FillFlowContainer + { + Y = 20, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre + } + }; + + if (CanScaleX) addXScaleComponents(); + if (CanScaleX && CanScaleY) addFullScaleComponents(); + if (CanScaleY) addYScaleComponents(); + if (CanRotate) addRotationComponents(); + } + + private void addRotationComponents() + { + const float separation = 40; + + addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); + addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); + + AddRangeInternal(new Drawable[] + { + new Box + { + Depth = float.MaxValue, + Colour = colours.YellowLight, + Blending = BlendingParameters.Additive, + Alpha = 0.3f, + Size = new Vector2(BORDER_RADIUS, separation), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate") + { + Anchor = Anchor.TopCentre, + Y = -separation, + HandleDrag = e => OnRotation?.Invoke(e.Delta.X), + OperationStarted = operationStarted, + OperationEnded = operationEnded + } + }); + } + + private void addYScaleComponents() + { + addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical)); + + addDragHandle(Anchor.TopCentre); + addDragHandle(Anchor.BottomCentre); + } + + private void addFullScaleComponents() + { + addDragHandle(Anchor.TopLeft); + addDragHandle(Anchor.TopRight); + addDragHandle(Anchor.BottomLeft); + addDragHandle(Anchor.BottomRight); + } + + private void addXScaleComponents() + { + addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal)); + + addDragHandle(Anchor.CentreLeft); + addDragHandle(Anchor.CentreRight); + } + + private void addButton(IconUsage icon, string tooltip, Action action) + { + buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip) + { + OperationStarted = operationStarted, + OperationEnded = operationEnded, + Action = action + }); + } + + private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle + { + Anchor = anchor, + HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), + OperationStarted = operationStarted, + OperationEnded = operationEnded + }); + + private int activeOperations; + + private void operationEnded() + { + if (--activeOperations == 0) + OperationEnded?.Invoke(); + } + + private void operationStarted() + { + if (activeOperations++ == 0) + OperationStarted?.Invoke(); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.cs new file mode 100644 index 0000000000..921b4eb042 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class SelectionBoxDragHandle : Container + { + public Action OperationStarted; + public Action OperationEnded; + + public Action HandleDrag { get; set; } + + private Circle circle; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(10); + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateHoverState(); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateHoverState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + UpdateHoverState(); + } + + protected bool HandlingMouse; + + protected override bool OnMouseDown(MouseDownEvent e) + { + HandlingMouse = true; + UpdateHoverState(); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) + { + OperationStarted?.Invoke(); + return true; + } + + protected override void OnDrag(DragEvent e) + { + HandleDrag?.Invoke(e); + base.OnDrag(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + HandlingMouse = false; + OperationEnded?.Invoke(); + + UpdateHoverState(); + base.OnDragEnd(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + HandlingMouse = false; + UpdateHoverState(); + base.OnMouseUp(e); + } + + protected virtual void UpdateHoverState() + { + circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark); + this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.cs new file mode 100644 index 0000000000..74ae949389 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// A drag "handle" which shares the visual appearance but behaves more like a clickable button. + /// + public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip + { + private SpriteIcon icon; + + private readonly IconUsage iconUsage; + + public Action Action; + + public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip) + { + this.iconUsage = iconUsage; + + TooltipText = tooltip; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load() + { + Size *= 2; + AddInternal(icon = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Icon = iconUsage, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + + protected override bool OnClick(ClickEvent e) + { + OperationStarted?.Invoke(); + Action?.Invoke(); + OperationEnded?.Invoke(); + return true; + } + + protected override void UpdateHoverState() + { + base.UpdateHoverState(); + icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; + } + + public string TooltipText { get; } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6e2c8bd01c..7808d7a5bc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -4,7 +4,9 @@ using System; 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.Cursor; @@ -30,17 +32,19 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { - public const float BORDER_RADIUS = 2; - public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; + public int SelectedCount => selectedBlueprints.Count; + public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject); private Drawable content; private OsuSpriteText selectionDetailsText; + protected SelectionBox SelectionBox { get; private set; } + [Resolved(CanBeNull = true)] protected EditorBeatmap EditorBeatmap { get; private set; } @@ -59,23 +63,13 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { + createStateBindables(); + InternalChild = content = new Container { Children = new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = BORDER_RADIUS, - BorderColour = colours.YellowDark, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 - } - }, + // todo: should maybe be inside the SelectionBox? new Container { Name = "info text", @@ -94,11 +88,39 @@ namespace osu.Game.Screens.Edit.Compose.Components Font = OsuFont.Default.With(size: 11) } } - } + }, + SelectionBox = CreateSelectionBox(), } }; } + public SelectionBox CreateSelectionBox() + => new SelectionBox + { + OperationStarted = OnOperationBegan, + OperationEnded = OnOperationEnded, + + OnRotation = angle => HandleRotation(angle), + OnScale = (amount, anchor) => HandleScale(amount, anchor), + OnFlip = direction => HandleFlip(direction), + }; + + /// + /// Fired when a drag operation ends from the selection box. + /// + protected virtual void OnOperationBegan() + { + ChangeHandler.BeginChange(); + } + + /// + /// Fired when a drag operation begins from the selection box. + /// + protected virtual void OnOperationEnded() + { + ChangeHandler.EndChange(); + } + #region User Input Handling /// @@ -113,7 +135,29 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any s could be moved. /// Returning true will also propagate StartTime changes provided by the closest . /// - public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true; + public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false; + + /// + /// Handles the selected s being rotated. + /// + /// The delta angle to apply to the selection. + /// Whether any s could be moved. + public virtual bool HandleRotation(float angle) => false; + + /// + /// Handles the selected s being scaled. + /// + /// The delta scale to apply, in playfield local coordinates. + /// The point of reference where the scale is originating from. + /// Whether any s could be moved. + public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; + + /// + /// Handled the selected s being flipped. + /// + /// The direction to flip + /// Whether any s could be moved. + public virtual bool HandleFlip(Direction direction) => false; public bool OnPressed(PlatformAction action) { @@ -195,10 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { ChangeHandler?.BeginChange(); - - foreach (var h in selectedBlueprints.ToList()) - EditorBeatmap?.Remove(h.HitObject); - + EditorBeatmap?.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); ChangeHandler?.EndChange(); } @@ -216,11 +257,22 @@ namespace osu.Game.Screens.Edit.Compose.Components selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; if (count > 0) + { Show(); + OnSelectionChanged(); + } else Hide(); } + /// + /// Triggered whenever more than one object is selected, on each change. + /// Should update the selection box's state to match supported operations. + /// + protected virtual void OnSelectionChanged() + { + } + protected override void Update() { base.Update(); @@ -282,8 +334,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var comboInfo = h as IHasComboInformation; - if (comboInfo == null) - throw new InvalidOperationException($"Tried to change combo state of a {h.GetType()}, which doesn't implement {nameof(IHasComboInformation)}"); + if (comboInfo == null || comboInfo.NewCombo == state) continue; comboInfo.NewCombo = state; EditorBeatmap?.UpdateHitObject(h); @@ -308,6 +359,93 @@ namespace osu.Game.Screens.Edit.Compose.Components #endregion + #region Selection State + + /// + /// The state of "new combo" for all selected hitobjects. + /// + public readonly Bindable SelectionNewComboState = new Bindable(); + + /// + /// 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() + { + foreach (var sampleName in HitSampleInfo.AllAdditions) + { + var bindable = new Bindable + { + Description = sampleName.Replace("hit", string.Empty).Titleize() + }; + + bindable.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + RemoveHitSample(sampleName); + break; + + case TernaryState.True: + AddHitSample(sampleName); + break; + } + }; + + SelectionSampleStates[sampleName] = bindable; + } + + // new combo + SelectionNewComboState.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + SetNewCombo(false); + break; + + case TernaryState.True: + SetNewCombo(true); + break; + } + }; + + // bring in updates from selection changes + EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); + EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates(); + } + + /// + /// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated). + /// + protected virtual void UpdateTernaryStates() + { + SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); + + foreach (var (sampleName, bindable) in SelectionSampleStates) + { + bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); + } + } + + /// + /// Given a selection target and a function of truth, retrieve the correct ternary state for display. + /// + protected TernaryState GetStateFromSelection(IEnumerable selection, Func func) + { + if (selection.Any(func)) + return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; + + return TernaryState.False; + } + + #endregion + #region Context Menu public MenuItem[] ContextMenuItems @@ -322,7 +460,9 @@ namespace osu.Game.Screens.Edit.Compose.Components items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints)); if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) - items.Add(createNewComboMenuItem()); + { + items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } }); + } if (selectedBlueprints.Count == 1) items.AddRange(selectedBlueprints[0].ContextMenuItems); @@ -331,12 +471,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { new OsuMenuItem("Sound") { - Items = new[] - { - createHitSampleMenuItem("Whistle", HitSampleInfo.HIT_WHISTLE), - createHitSampleMenuItem("Clap", HitSampleInfo.HIT_CLAP), - createHitSampleMenuItem("Finish", HitSampleInfo.HIT_FINISH) - } + Items = SelectionSampleStates.Select(kvp => + new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() }, new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), }); @@ -353,76 +489,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) => Enumerable.Empty(); - private MenuItem createNewComboMenuItem() - { - return new TernaryStateMenuItem("New combo", MenuItemType.Standard, setNewComboState) - { - State = { Value = getHitSampleState() } - }; - - void setNewComboState(TernaryState state) - { - switch (state) - { - case TernaryState.False: - SetNewCombo(false); - break; - - case TernaryState.True: - SetNewCombo(true); - break; - } - } - - TernaryState getHitSampleState() - { - int countExisting = selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo); - - if (countExisting == 0) - return TernaryState.False; - - if (countExisting < SelectedHitObjects.Count()) - return TernaryState.Indeterminate; - - return TernaryState.True; - } - } - - private MenuItem createHitSampleMenuItem(string name, string sampleName) - { - return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState) - { - State = { Value = getHitSampleState() } - }; - - void setHitSampleState(TernaryState state) - { - switch (state) - { - case TernaryState.False: - RemoveHitSample(sampleName); - break; - - case TernaryState.True: - AddHitSample(sampleName); - break; - } - } - - TernaryState getHitSampleState() - { - int countExisting = SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName)); - - if (countExisting == 0) - return TernaryState.False; - - if (countExisting < SelectedHitObjects.Count()) - return TernaryState.Indeterminate; - - return TernaryState.True; - } - } - #endregion } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs new file mode 100644 index 0000000000..510ba8c094 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -0,0 +1,67 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class DifficultyPointPiece : CompositeDrawable + { + private readonly DifficultyControlPoint difficultyPoint; + + private OsuSpriteText speedMultiplierText; + private readonly BindableNumber speedMultiplier; + + public DifficultyPointPiece(DifficultyControlPoint difficultyPoint) + { + this.difficultyPoint = difficultyPoint; + speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + Color4 colour = difficultyPoint.GetRepresentingColour(colours); + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colour, + Width = 2, + RelativeSizeAxes = Axes.Y, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colour, + RelativeSizeAxes = Axes.Both, + }, + speedMultiplierText = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Colour = Color4.White, + } + } + }, + }; + + speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs new file mode 100644 index 0000000000..0f11fb1126 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -0,0 +1,85 @@ +// 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.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class SamplePointPiece : CompositeDrawable + { + private readonly SampleControlPoint samplePoint; + + private readonly Bindable bank; + private readonly BindableNumber volume; + + private OsuSpriteText text; + private Box volumeBox; + + public SamplePointPiece(SampleControlPoint samplePoint) + { + this.samplePoint = samplePoint; + volume = samplePoint.SampleVolumeBindable.GetBoundCopy(); + bank = samplePoint.SampleBankBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Origin = Anchor.TopLeft; + Anchor = Anchor.TopLeft; + + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + Color4 colour = samplePoint.GetRepresentingColour(colours); + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = 20, + Children = new Drawable[] + { + volumeBox = new Box + { + X = 2, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Colour = ColourInfo.GradientVertical(colour, Color4.Black), + RelativeSizeAxes = Axes.Both, + }, + new Box + { + Colour = colour.Lighten(0.2f), + Width = 2, + RelativeSizeAxes = Axes.Y, + }, + } + }, + text = new OsuSpriteText + { + X = 2, + Y = -5, + Anchor = Anchor.BottomLeft, + Alpha = 0.9f, + Rotation = -90, + Font = OsuFont.Default.With(weight: FontWeight.SemiBold) + } + }; + + volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true); + bank.BindValueChanged(bank => text.Text = bank.NewValue, true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index ed3d328330..be3bca3242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -21,6 +22,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public class Timeline : ZoomableScrollContainer, IPositionSnapProvider { public readonly Bindable WaveformVisible = new Bindable(); + + public readonly Bindable ControlPointsVisible = new Bindable(); + + public readonly Bindable TicksVisible = new Bindable(); + public readonly IBindable Beatmap = new Bindable(); [Resolved] @@ -57,23 +63,41 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private WaveformGraph waveform; + private TimelineTickDisplay ticks; + + private TimelineControlPointDisplay controlPoints; + [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours) { - Add(waveform = new WaveformGraph + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.Blue.Opacity(0.2f), - LowColour = colours.BlueLighter, - MidColour = colours.BlueDark, - HighColour = colours.BlueDarker, - Depth = float.MaxValue + new Container + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Children = new Drawable[] + { + waveform = new WaveformGraph + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Blue.Opacity(0.2f), + LowColour = colours.BlueLighter, + MidColour = colours.BlueDark, + HighColour = colours.BlueDarker, + }, + ticks = new TimelineTickDisplay(), + controlPoints = new TimelineControlPointDisplay(), + } + }, }); // We don't want the centre marker to scroll AddInternal(new CentreMarker { Depth = float.MaxValue }); WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); + ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); + TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); Beatmap.BindTo(beatmap); Beatmap.BindValueChanged(b => diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index b99a053859..0ec48e04c6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -14,9 +14,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineArea : Container { - private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both }; + public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both }; - protected override Container Content => timeline; + protected override Container Content => Timeline; [BackgroundDependencyLoader] private void load() @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline CornerRadius = 5; OsuCheckbox waveformCheckbox; + OsuCheckbox controlPointsCheckbox; + OsuCheckbox ticksCheckbox; InternalChildren = new Drawable[] { @@ -57,12 +59,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Y, Width = 160, - Padding = new MarginPadding { Horizontal = 15 }, + Padding = new MarginPadding { Horizontal = 10 }, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 4), Children = new[] { - waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } + waveformCheckbox = new OsuCheckbox + { + LabelText = "Waveform", + Current = { Value = true }, + }, + controlPointsCheckbox = new OsuCheckbox + { + LabelText = "Control Points", + Current = { Value = true }, + }, + ticksCheckbox = new OsuCheckbox + { + LabelText = "Ticks", + Current = { Value = true }, + } } } } @@ -107,7 +123,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - timeline + Timeline }, }, ColumnDimensions = new[] @@ -119,11 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }; - waveformCheckbox.Current.Value = true; - - timeline.WaveformVisible.BindTo(waveformCheckbox.Current); + Timeline.WaveformVisible.BindTo(waveformCheckbox.Current); + Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current); + Timeline.TicksVisible.BindTo(ticksCheckbox.Current); } - private void changeZoom(float change) => timeline.Zoom += change; + private void changeZoom(float change) => Timeline.Zoom += change; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs new file mode 100644 index 0000000000..3f13e8e5d4 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + /// + /// The part of the timeline that displays the control points. + /// + public class TimelineControlPointDisplay : TimelinePart + { + private IBindableList controlPointGroups; + + public TimelineControlPointDisplay() + { + RelativeSizeAxes = Axes.Both; + } + + protected override void LoadBeatmap(WorkingBeatmap beatmap) + { + base.LoadBeatmap(beatmap); + + controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindCollectionChanged((sender, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Reset: + Clear(); + break; + + case NotifyCollectionChangedAction.Add: + foreach (var group in args.NewItems.OfType()) + Add(new TimelineControlPointGroup(group)); + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var group in args.OldItems.OfType()) + { + var matching = Children.SingleOrDefault(gv => gv.Group == group); + + matching?.Expire(); + } + + break; + } + }, true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs new file mode 100644 index 0000000000..e32616a574 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class TimelineControlPointGroup : CompositeDrawable + { + public readonly ControlPointGroup Group; + + private BindableList controlPoints; + + [Resolved] + private OsuColour colours { get; set; } + + public TimelineControlPointGroup(ControlPointGroup group) + { + Group = group; + + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + X = (float)group.Time; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindCollectionChanged((_, __) => + { + ClearInternal(); + + foreach (var point in controlPoints) + { + switch (point) + { + case DifficultyControlPoint difficultyPoint: + AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 }); + break; + + case TimingControlPoint timingPoint: + AddInternal(new TimingPointPiece(timingPoint)); + break; + + case SampleControlPoint samplePoint: + AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 }); + break; + } + } + }, true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index b95b3842b3..f0757a3dda 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -3,18 +3,23 @@ using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -34,11 +39,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly List shadowComponents = new List(); + private DrawableHitObject drawableHitObject; + + private Bindable comboColour; + + private readonly Container mainComponents; + + private readonly OsuSpriteText comboIndexText; + + private Bindable comboIndex; + private const float thickness = 5; private const float shadow_radius = 5; - private const float circle_size = 16; + private const float circle_size = 24; public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) @@ -54,14 +69,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + AddRangeInternal(new Drawable[] + { + mainComponents = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + comboIndexText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black), + }, + }); + circle = new Circle { Size = new Vector2(circle_size), Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - AlwaysPresent = true, - Colour = Color4.White, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, @@ -77,7 +106,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline DragBar dragBarUnderlay; Container extensionBar; - AddRangeInternal(new Drawable[] + mainComponents.AddRange(new Drawable[] { extensionBar = new Container { @@ -117,18 +146,93 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } else { - AddInternal(circle); + mainComponents.Add(circle); } updateShadows(); } + [BackgroundDependencyLoader(true)] + private void load(HitObjectComposer composer) + { + if (composer != null) + { + // best effort to get the drawable representation for grabbing colour and what not. + drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (HitObject is IHasComboInformation comboInfo) + { + comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); + comboIndex.BindValueChanged(combo => + { + comboIndexText.Text = (combo.NewValue + 1).ToString(); + }, true); + } + + if (drawableHitObject != null) + { + comboColour = drawableHitObject.AccentColour.GetBoundCopy(); + comboColour.BindValueChanged(colour => + { + if (HitObject is IHasDuration) + mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); + else + mainComponents.Colour = drawableHitObject.AccentColour.Value; + + var col = mainComponents.Colour.TopLeft.Linear; + float brightness = col.R + col.G + col.B; + + // decide the combo index colour based on brightness? + comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; + }, true); + } + } + protected override void Update() { base.Update(); // no bindable so we perform this every update - Width = (float)(HitObject.GetEndTime() - HitObject.StartTime); + float duration = (float)(HitObject.GetEndTime() - HitObject.StartTime); + + if (Width != duration) + { + Width = duration; + + // kind of haphazard but yeah, no bindables. + if (HitObject is IHasRepeats repeats) + updateRepeats(repeats); + } + } + + private Container repeatsContainer; + + private void updateRepeats(IHasRepeats repeats) + { + repeatsContainer?.Expire(); + + mainComponents.Add(repeatsContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + for (int i = 0; i < repeats.RepeatCount; i++) + { + repeatsContainer.Add(new Circle + { + Size = new Vector2(circle_size / 2), + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = (float)(i + 1) / (repeats.RepeatCount + 1), + }); + } } protected override bool ShouldBeConsideredForInput(Drawable child) => true; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs new file mode 100644 index 0000000000..ba94916458 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class TimingPointPiece : CompositeDrawable + { + private readonly TimingControlPoint point; + + private readonly BindableNumber beatLength; + private OsuSpriteText bpmText; + + public TimingPointPiece(TimingControlPoint point) + { + this.point = point; + beatLength = point.BeatLengthBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Origin = Anchor.CentreLeft; + Anchor = Anchor.CentreLeft; + + AutoSizeAxes = Axes.Both; + + Color4 colour = point.GetRepresentingColour(colours); + + InternalChildren = new Drawable[] + { + new Box + { + Alpha = 0.9f, + Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)), + RelativeSizeAxes = Axes.Both, + }, + bpmText = new OsuSpriteText + { + Alpha = 0.9f, + Padding = new MarginPadding(3), + Font = OsuFont.Default.With(size: 40) + } + }; + + beatLength.BindValueChanged(beatLength => + { + bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM"; + }, true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 04983ca597..5282b4d998 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -1,8 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Skinning; @@ -13,11 +17,28 @@ namespace osu.Game.Screens.Edit.Compose { private HitObjectComposer composer; - protected override Drawable CreateMainContent() + public ComposeScreen() + : base(EditorScreenMode.Compose) { - var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); + } + + private Ruleset ruleset; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + ruleset = parent.Get>().Value.BeatmapInfo.Ruleset?.CreateInstance(); composer = ruleset?.CreateHitObjectComposer(); + // make the composer available to the timeline and other components in this screen. + dependencies.CacheAs(composer); + + return dependencies; + } + + protected override Drawable CreateMainContent() + { if (ruleset == null || composer == null) return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); diff --git a/osu.Game/Screens/Edit/Design/DesignScreen.cs b/osu.Game/Screens/Edit/Design/DesignScreen.cs index 9f1fcf55b2..f15639733c 100644 --- a/osu.Game/Screens/Edit/Design/DesignScreen.cs +++ b/osu.Game/Screens/Edit/Design/DesignScreen.cs @@ -6,6 +6,7 @@ namespace osu.Game.Screens.Edit.Design public class DesignScreen : EditorScreen { public DesignScreen() + : base(EditorScreenMode.Design) { Child = new ScreenWhiteBox.UnderConstructionMessage("Design mode"); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b7a59bc2e2..3c5cbf30e9 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; @@ -68,7 +69,7 @@ namespace osu.Game.Screens.Edit private string lastSavedHash; private Box bottomBackground; - private Container screenContainer; + private Container screenContainer; private EditorScreen currentScreen; @@ -83,6 +84,8 @@ namespace osu.Game.Screens.Edit private DependencyContainer dependencies; + private bool isNewBeatmap; + protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -91,6 +94,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,18 +104,17 @@ 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); + dependencies.CacheAs(clock); AddInternal(clock); // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - bool isNewBeatmap = false; - if (Beatmap.Value is DummyWorkingBeatmap) { isNewBeatmap = true; @@ -163,7 +168,7 @@ namespace osu.Game.Screens.Edit Name = "Screen container", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 40, Bottom = 60 }, - Child = screenContainer = new Container + Child = screenContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true @@ -271,8 +276,20 @@ 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() { + // no longer new after first user-triggered save. + isNewBeatmap = false; + // apply any set-level metadata changes. beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); @@ -421,10 +438,20 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog)) + if (!exitConfirmed) { - dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); - return true; + // if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save. + if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) + { + confirmExit(); + return true; + } + + if (isNewBeatmap || HasUnsavedChanges) + { + dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); + return true; + } } Background.FadeColour(Color4.White, 500); @@ -442,6 +469,12 @@ namespace osu.Game.Screens.Edit private void confirmExit() { + if (isNewBeatmap) + { + // confirming exit without save means we should delete the new beatmap completely. + beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); + } + exitConfirmed = true; this.Exit(); } @@ -451,8 +484,7 @@ namespace osu.Game.Screens.Edit protected void Cut() { Copy(); - foreach (var h in editorBeatmap.SelectedHitObjects.ToArray()) - editorBeatmap.Remove(h); + editorBeatmap.RemoveRange(editorBeatmap.SelectedHitObjects.ToArray()); } protected void Copy() @@ -512,7 +544,21 @@ namespace osu.Game.Screens.Edit private void onModeChanged(ValueChangedEvent e) { - currentScreen?.Exit(); + var lastScreen = currentScreen; + + lastScreen? + .ScaleTo(0.98f, 200, Easing.OutQuint) + .FadeOut(200, Easing.OutQuint); + + if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null) + { + screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0); + + currentScreen + .ScaleTo(1, 200, Easing.OutQuint) + .FadeIn(200, Easing.OutQuint); + return; + } switch (e.NewValue) { @@ -542,7 +588,7 @@ namespace osu.Game.Screens.Edit private void seek(UIEvent e, int direction) { - double amount = e.ShiftPressed ? 2 : 1; + double amount = e.ShiftPressed ? 4 : 1; if (direction < 1) clock.SeekBackward(!clock.IsRunning, amount); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3248c5b8be..fb75d91d16 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -91,14 +91,19 @@ namespace osu.Game.Screens.Edit private readonly HashSet pendingUpdates = new HashSet(); + private bool isBatchApplying; + /// /// Adds a collection of s to this . /// /// The s to add. public void AddRange(IEnumerable hitObjects) { - foreach (var h in hitObjects) - Add(h); + ApplyBatchChanges(_ => + { + foreach (var h in hitObjects) + Add(h); + }); } /// @@ -126,12 +131,17 @@ namespace osu.Game.Screens.Edit mutableHitObjects.Insert(index, hitObject); - // must be run after any change to hitobject ordering - beatmapProcessor?.PreProcess(); - processHitObject(hitObject); - beatmapProcessor?.PostProcess(); + if (isBatchApplying) + batchPendingInserts.Add(hitObject); + else + { + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); - HitObjectAdded?.Invoke(hitObject); + HitObjectAdded?.Invoke(hitObject); + } } /// @@ -159,6 +169,19 @@ namespace osu.Game.Screens.Edit return true; } + /// + /// Removes a collection of s to this . + /// + /// The s to remove. + public void RemoveRange(IEnumerable hitObjects) + { + ApplyBatchChanges(_ => + { + foreach (var h in hitObjects) + Remove(h); + }); + } + /// /// Finds the index of a in this . /// @@ -180,22 +203,56 @@ namespace osu.Game.Screens.Edit bindable.UnbindAll(); startTimeBindables.Remove(hitObject); - // must be run after any change to hitobject ordering + if (isBatchApplying) + batchPendingDeletes.Add(hitObject); + else + { + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + + HitObjectRemoved?.Invoke(hitObject); + } + } + + private readonly List batchPendingInserts = new List(); + + private readonly List batchPendingDeletes = new List(); + + /// + /// Apply a batch of operations in one go, without performing Pre/Postprocessing each time. + /// + /// The function which will apply the batch changes. + public void ApplyBatchChanges(Action applyFunction) + { + if (isBatchApplying) + throw new InvalidOperationException("Attempting to perform a batch application from within an existing batch"); + + isBatchApplying = true; + + applyFunction(this); + beatmapProcessor?.PreProcess(); - processHitObject(hitObject); + + foreach (var h in batchPendingDeletes) processHitObject(h); + foreach (var h in batchPendingInserts) processHitObject(h); + beatmapProcessor?.PostProcess(); - HitObjectRemoved?.Invoke(hitObject); + foreach (var h in batchPendingDeletes) HitObjectRemoved?.Invoke(h); + foreach (var h in batchPendingInserts) HitObjectAdded?.Invoke(h); + + batchPendingDeletes.Clear(); + batchPendingInserts.Clear(); + + isBatchApplying = false; } /// /// Clears all from this . /// - public void Clear() - { - foreach (var h in HitObjects.ToArray()) - Remove(h); - } + public void Clear() => RemoveRange(HitObjects.ToArray()); protected override void Update() { @@ -258,5 +315,14 @@ namespace osu.Game.Screens.Edit public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor; public int BeatDivisor => beatDivisor?.Value ?? 1; + + /// + /// Update all hit objects with potentially changed difficulty or control point data. + /// + public void UpdateBeatmap() + { + foreach (var h in HitObjects) + pendingUpdates.Add(h); + } } } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index aa0f89912a..b69e9c4c51 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; @@ -20,6 +21,8 @@ namespace osu.Game.Screens.Edit public readonly Bindable CanUndo = new Bindable(); public readonly Bindable CanRedo = new Bindable(); + public event Action OnStateChange; + private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); @@ -78,9 +81,6 @@ namespace osu.Game.Screens.Edit SaveState(); } - /// - /// Saves the current state. - /// public void SaveState() { if (bulkChangesStarted > 0) @@ -89,23 +89,29 @@ 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; + + OnStateChange?.Invoke(); + updateBindables(); } - - currentState = savedStates.Count - 1; - - updateBindables(); } /// @@ -131,6 +137,7 @@ namespace osu.Game.Screens.Edit isRestoring = false; + OnStateChange?.Invoke(); updateBindables(); } diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d4d0feb813..64ed34f5ec 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -3,21 +3,28 @@ 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; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Play; namespace osu.Game.Screens.Edit { /// /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. /// - public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock + public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler { - public readonly double TrackLength; + public IBindable Track => track; + + private readonly Bindable track = new Bindable(); + + public double TrackLength => track.Value?.Length ?? 60000; public ControlPointInfo ControlPointInfo; @@ -25,6 +32,10 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; + + private readonly Bindable samplePlaybackDisabled = new Bindable(); + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) { @@ -35,7 +46,6 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor; ControlPointInfo = controlPointInfo; - TrackLength = trackLength; underlyingClock = new DecoupleableInterpolatingFramedClock(); } @@ -76,7 +86,7 @@ namespace osu.Game.Screens.Edit /// /// Whether to snap to the closest beat after seeking. /// The relative amount (magnitude) which should be seeked. - public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount); + public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount + (IsRunning ? 1.5 : 0)); /// /// Seeks forwards by one beat length. @@ -161,11 +171,14 @@ namespace osu.Game.Screens.Edit public void Stop() { + samplePlaybackDisabled.Value = true; underlyingClock.Stop(); } public bool Seek(double position) { + samplePlaybackDisabled.Value = true; + ClearTransforms(); return underlyingClock.Seek(position); } @@ -190,7 +203,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; @@ -202,8 +219,35 @@ namespace osu.Game.Screens.Edit private const double transform_time = 300; + protected override void Update() + { + base.Update(); + + updateSeekingState(); + } + + private void updateSeekingState() + { + if (samplePlaybackDisabled.Value) + { + if (track.Value?.IsRunning != true) + { + // seeking in the editor can happen while the track isn't running. + // in this case we always want to expose ourselves as seeking (to avoid sample playback). + return; + } + + // we are either running a seek tween or doing an immediate seek. + // in the case of an immediate seek the seeking bool will be set to false after one update. + // this allows for silencing hit sounds and the likes. + samplePlaybackDisabled.Value = Transforms.Any(); + } + } + public void SeekTo(double seekDestination) { + samplePlaybackDisabled.Value = true; + if (IsRunning) Seek(seekDestination); else diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 8b5f0aaa71..4d62a7d3cd 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -23,8 +23,12 @@ namespace osu.Game.Screens.Edit protected override Container Content => content; private readonly Container content; - protected EditorScreen() + public readonly EditorScreenMode Type; + + protected EditorScreen(EditorScreenMode type) { + Type = type; + Anchor = Anchor.Centre; Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; @@ -40,10 +44,5 @@ namespace osu.Game.Screens.Edit .Then() .FadeTo(1f, 250, Easing.OutQuint); } - - public void Exit() - { - Expire(); - } } } diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 66d90809db..b9457f422a 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Edit private Container timelineContainer; + protected EditorScreenWithTimeline(EditorScreenMode type) + : base(type) + { + } + [BackgroundDependencyLoader(true)] private void load([CanBeNull] BindableBeatDivisor beatDivisor) { @@ -107,13 +112,20 @@ namespace osu.Game.Screens.Edit RelativeSizeAxes = Axes.Both, Children = new[] { - new TimelineTickDisplay(), CreateTimelineContent(), } - }, timelineContainer.Add); + }, t => + { + timelineContainer.Add(t); + OnTimelineLoaded(t); + }); }); } + protected virtual void OnTimelineLoaded(TimelineArea timelineArea) + { + } + protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index c1328252d4..0283421b8c 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit @@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit /// public interface IEditorChangeHandler { + /// + /// Fired whenever a state change occurs. + /// + event Action OnStateChange; + /// /// Begins a bulk state change event. should be invoked soon after. /// @@ -29,5 +35,11 @@ namespace osu.Game.Screens.Edit /// This should be invoked as soon as possible after to cause a state change. /// void EndChange(); + + /// + /// Immediately saves the current state. + /// Note that this will be a no-op if there is a change in progress via . + /// + void SaveState(); } } diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 57b7ce6940..fb7d0dd826 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -68,16 +68,19 @@ namespace osu.Game.Screens.Edit toRemove.Sort(); toAdd.Sort(); - // Apply the changes. - for (int i = toRemove.Count - 1; i >= 0; i--) - editorBeatmap.RemoveAt(toRemove[i]); - - if (toAdd.Count > 0) + editorBeatmap.ApplyBatchChanges(eb => { - IBeatmap newBeatmap = readBeatmap(newState); - foreach (var i in toAdd) - editorBeatmap.Insert(i, newBeatmap.HitObjects[i]); - } + // Apply the changes. + for (int i = toRemove.Count - 1; i >= 0; i--) + eb.RemoveAt(toRemove[i]); + + if (toAdd.Count > 0) + { + IBeatmap newBeatmap = readBeatmap(newState); + foreach (var i in toAdd) + eb.Insert(i, newBeatmap.HitObjects[i]); + } + }); } private string readString(byte[] state) => Encoding.UTF8.GetString(state); diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs new file mode 100644 index 0000000000..2d8031c3c8 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class DifficultySection : SetupSection + { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + + private LabelledSliderBar circleSizeSlider; + private LabelledSliderBar healthDrainSlider; + private LabelledSliderBar approachRateSlider; + private LabelledSliderBar overallDifficultySlider; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Difficulty settings" + }, + circleSizeSlider = new LabelledSliderBar + { + Label = "Object Size", + Description = "The size of all hit objects", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 2, + MaxValue = 7, + Precision = 0.1f, + } + }, + healthDrainSlider = new LabelledSliderBar + { + Label = "Health Drain", + Description = "The rate of passive health drain throughout playable time", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + approachRateSlider = new LabelledSliderBar + { + Label = "Approach Rate", + Description = "The speed at which objects are presented to the player", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + overallDifficultySlider = new LabelledSliderBar + { + Label = "Overall Difficulty", + Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + }; + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += onValueChanged; + } + + private void onValueChanged(ValueChangedEvent args) + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + + editorBeatmap.UpdateBeatmap(); + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs new file mode 100644 index 0000000000..b802b3405a --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + /// + /// A labelled textbox which reveals an inline file chooser when clicked. + /// + 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: ResourcesSection.AudioExtensions) + { + 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/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs new file mode 100644 index 0000000000..4ddee2acc6 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class MetadataSection : SetupSection + { + private LabelledTextBox artistTextBox; + private LabelledTextBox titleTextBox; + private LabelledTextBox creatorTextBox; + private LabelledTextBox difficultyTextBox; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap metadata" + }, + artistTextBox = new LabelledTextBox + { + Label = "Artist", + Current = { Value = Beatmap.Value.Metadata.Artist }, + TabbableContentContainer = this + }, + titleTextBox = new LabelledTextBox + { + Label = "Title", + Current = { Value = Beatmap.Value.Metadata.Title }, + TabbableContentContainer = this + }, + creatorTextBox = new LabelledTextBox + { + Label = "Creator", + Current = { Value = Beatmap.Value.Metadata.AuthorString }, + TabbableContentContainer = this + }, + difficultyTextBox = new LabelledTextBox + { + Label = "Difficulty Name", + Current = { Value = Beatmap.Value.BeatmapInfo.Version }, + TabbableContentContainer = this + }, + }; + + foreach (var item in Children.OfType()) + item.OnCommit += onCommit; + } + + private void onCommit(TextBox sender, bool newText) + { + if (!newText) return; + + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; + Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; + Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; + Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs new file mode 100644 index 0000000000..17ecfdd52e --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -0,0 +1,211 @@ +// 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 System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class ResourcesSection : SetupSection, ICanAcceptFiles + { + private LabelledTextBox audioTrackTextBox; + private Container backgroundSpriteContainer; + + public IEnumerable HandledExtensions => ImageExtensions.Concat(AudioExtensions); + + public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" }; + + public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" }; + + [Resolved] + private OsuGameBase game { get; set; } + + [Resolved] + private MusicController music { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved(canBeNull: true)] + private Editor editor { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Container audioTrackFileChooserContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + + Children = new Drawable[] + { + backgroundSpriteContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = 250, + Masking = true, + CornerRadius = 10, + }, + 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, + }; + + updateBackgroundSprite(); + + audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); + } + + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => + { + var firstFile = new FileInfo(paths.First()); + + if (ImageExtensions.Contains(firstFile.Extension)) + { + ChangeBackgroundImage(firstFile.FullName); + } + else if (AudioExtensions.Contains(firstFile.Extension)) + { + audioTrackTextBox.Text = firstFile.FullName; + } + }); + return Task.CompletedTask; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + game.RegisterImportHandler(this); + } + + public bool ChangeBackgroundImage(string path) + { + var info = new FileInfo(path); + + if (!info.Exists) + return false; + + var set = Beatmap.Value.BeatmapSetInfo; + + // remove the previous background 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.BackgroundFile); + + 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.BackgroundFile = info.Name; + updateBackgroundSprite(); + + return true; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + game?.UnregisterImportHandler(this); + } + + 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 updateBackgroundSprite() + { + LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, background => + { + if (background.Texture != null) + backgroundSpriteContainer.Child = background; + else + { + backgroundSpriteContainer.Children = new Drawable[] + { + new Box + { + Colour = Colours.GreySeafoamDarker, + RelativeSizeAxes = Axes.Both, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24)) + { + Text = "Drag image here to set beatmap background!", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + } + }; + } + + background.FadeInFromZero(500); + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index a2c8f19016..1c3cbb7206 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,31 +1,32 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterfaceV2; -using osuTK; +using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Setup { public class SetupScreen : EditorScreen { - private FillFlowContainer flow; - private LabelledTextBox artistTextBox; - private LabelledTextBox titleTextBox; - private LabelledTextBox creatorTextBox; - private LabelledTextBox difficultyTextBox; + [Resolved] + private OsuColour colours { get; set; } + + [Cached] + protected readonly OverlayColourProvider ColourProvider; + + public SetupScreen() + : base(EditorScreenMode.SongSetup) + { + ColourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Child = new Container { @@ -43,81 +44,35 @@ namespace osu.Game.Screens.Edit.Setup Colour = colours.GreySeafoamDark, RelativeSizeAxes = Axes.Both, }, - new OsuScrollContainer + new SectionsContainer { + FixedHeader = new SetupScreenHeader(), RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - Child = flow = new FillFlowContainer + Children = new SetupSection[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - Height = 250, - Masking = true, - CornerRadius = 10, - Child = new BeatmapBackgroundSprite(Beatmap.Value) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - }, - }, - new OsuSpriteText - { - Text = "Beatmap metadata" - }, - artistTextBox = new LabelledTextBox - { - Label = "Artist", - Current = { Value = Beatmap.Value.Metadata.Artist }, - TabbableContentContainer = this - }, - titleTextBox = new LabelledTextBox - { - Label = "Title", - Current = { Value = Beatmap.Value.Metadata.Title }, - TabbableContentContainer = this - }, - creatorTextBox = new LabelledTextBox - { - Label = "Creator", - Current = { Value = Beatmap.Value.Metadata.AuthorString }, - TabbableContentContainer = this - }, - difficultyTextBox = new LabelledTextBox - { - Label = "Difficulty Name", - Current = { Value = Beatmap.Value.BeatmapInfo.Version }, - TabbableContentContainer = this - }, - } - }, + new ResourcesSection(), + new MetadataSection(), + new DifficultySection(), + } }, } } }; - - foreach (var item in flow.OfType()) - item.OnCommit += onCommit; } + } - private void onCommit(TextBox sender, bool newText) + internal class SetupScreenHeader : OverlayHeader + { + protected override OverlayTitle CreateTitle() => new SetupScreenTitle(); + + private class SetupScreenTitle : OverlayTitle { - if (!newText) return; - - // for now, update these on commit rather than making BeatmapMetadata bindables. - // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; - Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; - Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; - Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; + public SetupScreenTitle() + { + Title = "beatmap setup"; + Description = "change general settings of your beatmap"; + IconTexture = "Icons/Hexacons/social"; + } } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs new file mode 100644 index 0000000000..cdf17d355e --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class SetupSection : Container + { + private readonly FillFlowContainer flow; + + [Resolved] + protected OsuColour Colours { get; private set; } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + protected override Container Content => flow; + + public SetupSection() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding(10); + + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Direction = FillDirection.Vertical, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index e1182d9fa4..c40061b97c 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing private IReadOnlyList createSections() => new Drawable[] { + new GroupSection(), new TimingSection(), new DifficultySection(), new SampleSection(), diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index c0c0bcead2..4121e1f7bb 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Timing }, new OsuSpriteText { - Text = $"{group.Time:n0}ms", + Text = group.Time.ToEditorFormattedString(), Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold) }, new ControlGroupAttributes(group), @@ -113,7 +114,14 @@ namespace osu.Game.Screens.Edit.Timing controlPoints = group.ControlPoints.GetBoundCopy(); controlPoints.CollectionChanged += (_, __) => createChildren(); + } + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { createChildren(); } @@ -124,20 +132,22 @@ namespace osu.Game.Screens.Edit.Timing private Drawable createAttribute(ControlPoint controlPoint) { + Color4 colour = controlPoint.GetRepresentingColour(colours); + switch (controlPoint) { case TimingControlPoint timing: - return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}"); + return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}", colour); case DifficultyControlPoint difficulty: - return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x"); + return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour); case EffectControlPoint effect: - return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}"); + return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}", colour); case SampleControlPoint sample: - return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%"); + return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour); } return null; diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index 58a7f97e5f..b55d74e3b4 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -2,27 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { internal class DifficultySection : Section { - private SettingsSlider multiplier; + private SliderWithTextBoxInput multiplierSlider; [BackgroundDependencyLoader] private void load() { Flow.AddRange(new[] { - multiplier = new SettingsSlider + multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier") { - LabelText = "Speed Multiplier", - Bindable = new DifficultyControlPoint().SpeedMultiplierBindable, - RelativeSizeAxes = Axes.X, + Current = new DifficultyControlPoint().SpeedMultiplierBindable } }); } @@ -31,7 +27,8 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - multiplier.Bindable = point.NewValue.SpeedMultiplierBindable; + multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable; + multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 71e7f42713..2f143108a9 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { kiai.Current = point.NewValue.KiaiModeBindable; + kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; + omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs new file mode 100644 index 0000000000..c77d48ef0a --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Screens.Edit.Timing +{ + internal class GroupSection : CompositeDrawable + { + private LabelledTextBox textBox; + + private TriangleButton button; + + [Resolved] + protected Bindable SelectedGroup { get; private set; } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + [Resolved] + private EditorClock clock { get; set; } + + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding(10); + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + textBox = new LabelledTextBox + { + Label = "Time" + }, + button = new TriangleButton + { + Text = "Use current time", + RelativeSizeAxes = Axes.X, + Action = () => changeSelectedGroupTime(clock.CurrentTime) + } + } + }, + }; + + textBox.OnCommit += (sender, isNew) => + { + if (!isNew) + return; + + if (double.TryParse(sender.Text, out var newTime)) + { + changeSelectedGroupTime(newTime); + } + else + { + SelectedGroup.TriggerChange(); + } + }; + + SelectedGroup.BindValueChanged(group => + { + if (group.NewValue == null) + { + textBox.Text = string.Empty; + + textBox.Current.Disabled = true; + button.Enabled.Value = false; + return; + } + + textBox.Current.Disabled = false; + button.Enabled.Value = true; + + textBox.Text = $"{group.NewValue.Time:n0}"; + }, true); + } + + private void changeSelectedGroupTime(in double time) + { + if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time) + return; + + changeHandler?.BeginChange(); + + var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); + + Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); + + foreach (var cp in currentGroupItems) + Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); + + SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time); + + changeHandler?.EndChange(); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/RowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttribute.cs index be8f693683..2757e08026 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttribute.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Timing { @@ -16,11 +17,13 @@ namespace osu.Game.Screens.Edit.Timing { private readonly string header; private readonly Func content; + private readonly Color4 colour; - public RowAttribute(string header, Func content) + public RowAttribute(string header, Func content, Color4 colour) { this.header = header; this.content = content; + this.colour = colour; } [BackgroundDependencyLoader] @@ -40,7 +43,7 @@ namespace osu.Game.Screens.Edit.Timing { new Box { - Colour = colours.Yellow, + Colour = colour, RelativeSizeAxes = Axes.Both, }, new OsuSpriteText @@ -50,7 +53,7 @@ namespace osu.Game.Screens.Edit.Timing Origin = Anchor.Centre, Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12), Text = header, - Colour = colours.Gray3 + Colour = colours.Gray0 }, }; } diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs index 4665c77991..280e19c99a 100644 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs @@ -2,18 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { internal class SampleSection : Section { private LabelledTextBox bank; - private SettingsSlider volume; + private SliderWithTextBoxInput volume; [BackgroundDependencyLoader] private void load() @@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing { Label = "Bank Name", }, - volume = new SettingsSlider + volume = new SliderWithTextBoxInput("Volume") { - Bindable = new SampleControlPoint().SampleVolumeBindable, - LabelText = "Volume", + Current = new SampleControlPoint().SampleVolumeBindable, } }); } @@ -37,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bank.Current = point.NewValue.SampleBankBindable; - volume.Bindable = point.NewValue.SampleVolumeBindable; + bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + + volume.Current = point.NewValue.SampleVolumeBindable; + volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs index 603fb77f31..7a81eeb1a4 100644 --- a/osu.Game/Screens/Edit/Timing/Section.cs +++ b/osu.Game/Screens/Edit/Timing/Section.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] protected Bindable SelectedGroup { get; private set; } + [Resolved(canBeNull: true)] + protected IEditorChangeHandler ChangeHandler { get; private set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs new file mode 100644 index 0000000000..f2f9f76143 --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -0,0 +1,78 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Screens.Edit.Timing +{ + public class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue + where T : struct, IEquatable, IComparable, IConvertible + { + private readonly SettingsSlider slider; + + public SliderWithTextBoxInput(string labelText) + { + LabelledTextBox textbox; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + textbox = new LabelledTextBox + { + Label = labelText, + }, + slider = new SettingsSlider + { + TransferValueOnCommit = true, + RelativeSizeAxes = Axes.X, + } + } + }, + }; + + textbox.OnCommit += (t, isNew) => + { + if (!isNew) return; + + try + { + slider.Current.Parse(t.Text); + } + catch + { + // TriggerChange below will restore the previous text value on failure. + } + + // This is run regardless of parsing success as the parsed number may not actually trigger a change + // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state. + Current.TriggerChange(); + }; + + Current.BindValueChanged(val => + { + textbox.Text = val.NewValue.ToString(); + }, true); + } + + public Bindable Current + { + get => slider.Current; + set => slider.Current = value; + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 8c40c8e721..0796097186 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -24,6 +25,11 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private EditorClock clock { get; set; } + public TimingScreen() + : base(EditorScreenMode.Timing) + { + } + protected override Drawable CreateMainContent() => new GridContainer { RelativeSizeAxes = Axes.Both, @@ -53,6 +59,12 @@ namespace osu.Game.Screens.Edit.Timing }); } + protected override void OnTimelineLoaded(TimelineArea timelineArea) + { + base.OnTimelineLoaded(timelineArea); + timelineArea.Timeline.Zoom = timelineArea.Timeline.MinZoom; + } + public class ControlPointList : CompositeDrawable { private OsuButton deleteButton; @@ -69,6 +81,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -124,11 +139,13 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - controlGroups.CollectionChanged += (sender, args) => createContent(); - createContent(); - } - private void createContent() => table.ControlGroups = controlGroups; + controlGroups.BindCollectionChanged((sender, args) => + { + table.ControlGroups = controlGroups; + changeHandler.SaveState(); + }, true); + } private void delete() { diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 879363ba08..1ae2a86885 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -36,9 +36,14 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - bpmSlider.Bindable = point.NewValue.BeatLengthBindable; + bpmSlider.Current = point.NewValue.BeatLengthBindable; + bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; - timeSignature.Bindable = point.NewValue.TimeSignatureBindable; + // no need to hook change handler here as it's the same bindable as above + + timeSignature.Current = point.NewValue.TimeSignatureBindable; + timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -65,18 +70,19 @@ namespace osu.Game.Screens.Edit.Timing { if (!isNew) return; - if (double.TryParse(Current.Value, out double doubleVal)) + try { - try - { + if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0) beatLengthBindable.Value = beatLengthToBpm(doubleVal); - } - catch - { - // will restore the previous text value on failure. - beatLengthBindable.TriggerChange(); - } } + catch + { + // TriggerChange below will restore the previous text value on failure. + } + + // This is run regardless of parsing success as the parsed number may not actually trigger a change + // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state. + beatLengthBindable.TriggerChange(); }; beatLengthBindable.BindValueChanged(val => @@ -103,19 +109,26 @@ namespace osu.Game.Screens.Edit.Timing private const double sane_maximum = 240; private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; - private readonly BindableDouble bpmBindable = new BindableDouble(); + + private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH) + { + MinValue = sane_minimum, + MaxValue = sane_maximum, + }; public BPMSlider() { beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); - bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); + bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); - base.Bindable = bpmBindable; + base.Current = bpmBindable; + + TransferValueOnCommit = true; } - public override Bindable Bindable + public override Bindable Current { - get => base.Bindable; + get => base.Current; set { // incoming will be beat length, not bpm diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index fcb9aacd76..8368047d5a 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -42,8 +42,11 @@ namespace osu.Game.Screens.Menu ValidForResume = false; } + [Resolved] + private IAPIProvider api { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours, IAPIProvider api) + private void load(OsuColour colours) { InternalChildren = new Drawable[] { @@ -104,7 +107,9 @@ namespace osu.Game.Screens.Menu iconColour = colours.Yellow; - currentUser.BindTo(api.LocalUser); + // manually transfer the user once, but only do the final bind in LoadComplete to avoid thread woes (API scheduler could run while this screen is still loading). + // the manual transfer is here to ensure all text content is loaded ahead of time as this is very early in the game load process and we want to avoid stutters. + currentUser.Value = api.LocalUser.Value; currentUser.BindValueChanged(e => { supportFlow.Children.ForEach(d => d.FadeOut().Expire()); @@ -141,6 +146,8 @@ namespace osu.Game.Screens.Menu base.LoadComplete(); if (nextScreen != null) LoadComponentAsync(nextScreen); + + currentUser.BindTo(api.LocalUser); } public override void OnEntering(IScreen last) diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs index 0015feb26a..f07bd8c3b2 100644 --- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Multi.Components if (item?.Beatmap != null) { drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index b007e0349d..bda00b65b5 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Multi private void refresh() { - difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) }; + difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) }; beatmapText.Clear(); beatmapText.AddLink(Item.Beatmap.ToString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString()); diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 4f2cf5005c..eeea6777c6 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Timing; +using osu.Framework.Utils; namespace osu.Game.Screens.Play { @@ -14,12 +17,19 @@ namespace osu.Game.Screens.Play /// , as this should only be done once to ensure accuracy. /// /// - public class GameplayClock : IFrameBasedClock + public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler { private readonly IFrameBasedClock underlyingClock; public readonly BindableBool IsPaused = new BindableBool(); + /// + /// All adjustments applied to this clock which don't come from gameplay or mods. + /// + public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + + private readonly Bindable samplePlaybackDisabled = new Bindable(); + public GameplayClock(IFrameBasedClock underlyingClock) { this.underlyingClock = underlyingClock; @@ -29,16 +39,40 @@ namespace osu.Game.Screens.Play public double Rate => underlyingClock.Rate; + /// + /// The rate of gameplay when playback is at 100%. + /// This excludes any seeking / user adjustments. + /// + public double TrueGameplayRate + { + get + { + double baseRate = Rate; + + foreach (var adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment.Value, 0)) + return 0; + + baseRate /= adjustment.Value; + } + + return baseRate; + } + } + public bool IsRunning => underlyingClock.IsRunning; /// - /// Whether an ongoing seek operation is active. + /// Whether nested samples supporting the interface should be paused. /// - public virtual bool IsSeeking => false; + protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value; public void ProcessFrame() { - // we do not want to process the underlying clock. + // intentionally not updating the underlying clock (handled externally). + + samplePlaybackDisabled.Value = ShouldDisableSamplePlayback; } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; @@ -48,5 +82,7 @@ namespace osu.Game.Screens.Play public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; public IClock Source => underlyingClock; + + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 7a9cb3dddd..9f8e55f577 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -50,8 +51,11 @@ namespace osu.Game.Screens.Play /// /// The final clock which is exposed to underlying components. /// - [Cached] - public readonly GameplayClock GameplayClock; + public GameplayClock GameplayClock => localGameplayClock; + + [Cached(typeof(GameplayClock))] + [Cached(typeof(ISamplePlaybackDisabler))] + private readonly LocalGameplayClock localGameplayClock; private Bindable userAudioOffset; @@ -79,7 +83,7 @@ namespace osu.Game.Screens.Play userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); // the clock to be exposed via DI to children. - GameplayClock = new GameplayClock(userOffsetClock); + localGameplayClock = new LocalGameplayClock(userOffsetClock); GameplayClock.IsPaused.BindTo(IsPaused); } @@ -200,7 +204,9 @@ namespace osu.Game.Screens.Play protected override void Update() { if (!IsPaused.Value) + { userOffsetClock.ProcessFrame(); + } base.Update(); } @@ -215,6 +221,9 @@ namespace osu.Game.Screens.Play track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); + speedAdjustmentsApplied = true; } @@ -231,9 +240,24 @@ namespace osu.Game.Screens.Play track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); + speedAdjustmentsApplied = false; } + private class LocalGameplayClock : GameplayClock + { + public readonly List> MutableNonGameplayAdjustments = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public LocalGameplayClock(FramedOffsetClock underlyingClock) + : base(underlyingClock) + { + } + } + private class HardwareCorrectionOffsetClock : FramedOffsetClock { // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs index 7736541c92..fc4a1a5d83 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs @@ -13,7 +13,6 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { @@ -106,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD public void Flash(JudgementResult result) { - if (result.Type == HitResult.Miss) + if (!result.IsHit) return; Scheduler.AddOnce(flash); diff --git a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs new file mode 100644 index 0000000000..6b37021fe6 --- /dev/null +++ b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play +{ + /// + /// Allows a component to disable sample playback dynamically as required. + /// Handled by . + /// + public interface ISamplePlaybackDisabler + { + /// + /// Whether sample playback should be disabled (or paused for looping samples). + /// + IBindable SamplePlaybackDisabled { get; } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8e2ed583f2..175722c44e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -191,9 +191,25 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(gameplayBeatmap); - addUnderlayComponents(GameplayClockContainer); - addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); - addOverlayComponents(GameplayClockContainer, Beatmap.Value); + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); + + // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation + // full access to all skin sources. + var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); + + // load the skinning hierarchy first. + // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. + GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider)); + + rulesetSkinProvider.AddRange(new[] + { + // underlay and gameplay should have access the to skinning sources. + createUnderlayComponents(), + createGameplayComponents(Beatmap.Value, playableBeatmap) + }); + + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. + GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -238,45 +254,31 @@ namespace osu.Game.Screens.Play breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } - private void addUnderlayComponents(Container target) + private Drawable createUnderlayComponents() => + DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; + + private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay) { - target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); - } - - private void addGameplayComponents(Container target, WorkingBeatmap working, IBeatmap playableBeatmap) - { - var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin); - - // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation - // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); - - // load the skinning hierarchy first. - // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. - target.Add(new ScalingContainer(ScalingMode.Gameplay) - .WithChild(beatmapSkinProvider - .WithChild(target = rulesetSkinProvider))); - - target.AddRange(new Drawable[] + Children = new Drawable[] { - DrawableRuleset, + DrawableRuleset.With(r => + r.FrameStableComponents.Children = new Drawable[] + { + ScoreProcessor, + HealthProcessor, + breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Breaks = working.Beatmap.Breaks + } + }), new ComboEffects(ScoreProcessor) - }); + } + }; - DrawableRuleset.FrameStableComponents.AddRange(new Drawable[] - { - ScoreProcessor, - HealthProcessor, - breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Breaks = working.Beatmap.Breaks - } - }); - } - - private void addOverlayComponents(Container target, WorkingBeatmap working) + private Drawable createOverlayComponents(WorkingBeatmap working) => new Container { - target.AddRange(new[] + RelativeSizeAxes = Axes.Both, + Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) @@ -342,8 +344,8 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - }); - } + } + }; private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 24ddc277cd..16e29ac3c8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -51,14 +51,14 @@ namespace osu.Game.Screens.Play.PlayerSettings } }, }, - rateSlider = new PlayerSliderBar { Bindable = UserPlaybackRate } + rateSlider = new PlayerSliderBar { Current = UserPlaybackRate } }; } protected override void LoadComplete() { base.LoadComplete(); - rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); + rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index e06cf5c6d5..8f29fe7893 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -50,8 +50,8 @@ namespace osu.Game.Screens.Play.PlayerSettings [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); - blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); + dimSliderBar.Current = config.GetBindable(OsuSetting.DimLevel); + blurSliderBar.Current = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 0b85eeafa8..95ece1a9fb 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Users; @@ -116,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Contracted AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), - ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString())) + ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount)) }, new FillFlowContainer { @@ -198,6 +199,9 @@ namespace osu.Game.Screens.Ranking.Contracted }; } + private Drawable createStatistic(HitResult result, int count, int? maxCount) + => createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}"); + private Drawable createStatistic(string key, string value) => new Container { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 88c61ce267..1d093c2aba 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -65,8 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded }; var bottomStatistics = new List(); - foreach (var stat in score.SortedStatistics) - bottomStatistics.Add(new HitResultStatistic(stat.Key, stat.Value)); + + foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay()) + bottomStatistics.Add(new HitResultStatistic(key, value, maxCount)); statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(bottomStatistics); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 368075b13b..e070f76289 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class CounterStatistic : StatisticDisplay { private readonly int count; + private readonly int? maxCount; protected RollingCounter Counter { get; private set; } @@ -24,10 +26,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics /// /// The name of the statistic. /// The value to display. - public CounterStatistic(string header, int count) + /// The maximum value of . Not displayed if null. + public CounterStatistic(string header, int count, int? maxCount = null) : base(header) { this.count = count; + this.maxCount = maxCount; } public override void Appear() @@ -36,7 +40,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Counter.Current.Value = count; } - protected override Drawable CreateContent() => Counter = new StatisticCounter(); + protected override Drawable CreateContent() + { + var container = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Child = Counter = new Counter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + }; + + if (maxCount != null) + { + container.Add(new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Torus.With(size: 12, fixedWidth: true), + Spacing = new Vector2(-2, 0), + Text = $"/{maxCount}" + }); + } + + return container; + } private class StatisticCounter : RollingCounter { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs index faa4a6a96c..a86033713f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -12,8 +12,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly HitResult result; - public HitResultStatistic(HitResult result, int count) - : base(result.GetDescription(), count) + public HitResultStatistic(HitResult result, int count, int? maxCount = null) + : base(result.GetDescription(), count, maxCount) { this.result = result; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c95cf1066e..c48cd238c0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -273,10 +273,10 @@ namespace osu.Game.Screens.Ranking detachedPanelContainer.Add(expandedPanel); // Move into its original location in the local container first, then to the final location. - var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos); - expandedPanel.MoveTo(origLocation) + var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X; + expandedPanel.MoveToX(origLocation) .Then() - .MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint); + .MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); // Hide contracted panels. foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 10bd99c8ce..0d7d339df0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -99,6 +99,8 @@ namespace osu.Game.Screens.Ranking { var panel = new ScorePanel(score) { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, PostExpandAction = () => PostExpandAction?.Invoke() }.With(p => { diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 45fdc3ff33..aa2a83774e 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss).ToList(); + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c2ace6a04e..f1ae1f9d73 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -75,7 +75,23 @@ namespace osu.Game.Screens.Ranking.Statistics return; if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) - content.Add(new MessagePlaceholder("Score has no statistics :(")); + { + content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }); + } else { spinner.Show(); @@ -91,7 +107,10 @@ namespace osu.Game.Screens.Ranking.Statistics { var rows = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), Alpha = 0 diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index 18a2238784..055db143d1 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Statistics public UnstableRate(IEnumerable hitEvents) : base("Unstable Rate") { - var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss) + var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()) .Select(ev => ev.TimeOffset).ToArray(); Value = 10 * standardDeviation(timeOffsets); } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 5caf07b554..e38913b13a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -336,7 +336,7 @@ namespace osu.Game.Skinning case HitResult.Meh: return this.GetAnimation("hit50", true, false); - case HitResult.Good: + case HitResult.Ok: return this.GetAnimation("hit100", true, false); case HitResult.Great: diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 1d5412d93f..828804b9cb 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -18,7 +18,7 @@ namespace osu.Game.Skinning ComboPrefix, ComboOverlap, AnimationFramerate, - LayeredHitSounds, + LayeredHitSounds } } } diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs new file mode 100644 index 0000000000..d340f67575 --- /dev/null +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Audio; +using osu.Game.Screens.Play; + +namespace osu.Game.Skinning +{ + public class PausableSkinnableSound : SkinnableSound + { + protected bool RequestedPlaying { get; private set; } + + public PausableSkinnableSound(ISampleInfo hitSamples) + : base(hitSamples) + { + } + + public PausableSkinnableSound(IEnumerable hitSamples) + : base(hitSamples) + { + } + + private readonly IBindable samplePlaybackDisabled = new Bindable(); + + [BackgroundDependencyLoader(true)] + private void load(ISamplePlaybackDisabler samplePlaybackDisabler) + { + // if in a gameplay context, pause sample playback when gameplay is paused. + if (samplePlaybackDisabler != null) + { + samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + samplePlaybackDisabled.BindValueChanged(disabled => + { + if (!RequestedPlaying) return; + + // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). + if (!Looping) return; + + if (disabled.NewValue) + base.Stop(); + else + { + // schedule so we don't start playing a sample which is no longer alive. + Schedule(() => + { + if (RequestedPlaying) + base.Play(); + }); + } + }); + } + } + + public override void Play() + { + RequestedPlaying = true; + + if (samplePlaybackDisabled.Value) + return; + + base.Play(); + } + + public override void Stop() + { + RequestedPlaying = false; + base.Stop(); + } + } +} diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ee4b7bc8e7..7af400e807 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -33,7 +33,7 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; - public override string[] HandledExtensions => new[] { ".osk" }; + public override IEnumerable HandledExtensions => new[] { ".osk" }; protected override string[] HashableFileTypes => new[] { ".ini" }; diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index ba14049b41..f6e91811dd 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -11,7 +11,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; -using osu.Game.Screens.Play; namespace osu.Game.Skinning { @@ -22,8 +21,6 @@ namespace osu.Game.Skinning [Resolved] private ISampleStore samples { get; set; } - private bool requestedPlaying; - public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -37,7 +34,7 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - private readonly AudioContainer samplesContainer; + protected readonly AudioContainer SamplesContainer; public SkinnableSound(ISampleInfo hitSamples) : this(new[] { hitSamples }) @@ -47,28 +44,7 @@ namespace osu.Game.Skinning public SkinnableSound(IEnumerable hitSamples) { this.hitSamples = hitSamples.ToArray(); - InternalChild = samplesContainer = new AudioContainer(); - } - - private Bindable gameplayClockPaused; - - [BackgroundDependencyLoader(true)] - private void load(GameplayClock gameplayClock) - { - // if in a gameplay context, pause sample playback when gameplay is paused. - gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy(); - gameplayClockPaused?.BindValueChanged(paused => - { - if (requestedPlaying) - { - if (paused.NewValue) - stop(); - // it's not easy to know if a sample has finished playing (to end). - // to keep things simple only resume playing looping samples. - else if (Looping) - play(); - } - }); + InternalChild = SamplesContainer = new AudioContainer(); } private bool looping; @@ -82,34 +58,22 @@ namespace osu.Game.Skinning looping = value; - samplesContainer.ForEach(c => c.Looping = looping); + SamplesContainer.ForEach(c => c.Looping = looping); } } - public void Play() + public virtual void Play() { - requestedPlaying = true; - play(); - } - - private void play() - { - samplesContainer.ForEach(c => + SamplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); }); } - public void Stop() + public virtual void Stop() { - requestedPlaying = false; - stop(); - } - - private void stop() - { - samplesContainer.ForEach(c => c.Stop()); + SamplesContainer.ForEach(c => c.Stop()); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) @@ -138,33 +102,33 @@ namespace osu.Game.Skinning return ch; }).Where(c => c != null); - samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); + SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); // Start playback internally for the new samples if the previous ones were playing beforehand. if (wasPlaying) - play(); + Play(); } #region Re-expose AudioContainer - public BindableNumber Volume => samplesContainer.Volume; + public BindableNumber Volume => SamplesContainer.Volume; - public BindableNumber Balance => samplesContainer.Balance; + public BindableNumber Balance => SamplesContainer.Balance; - public BindableNumber Frequency => samplesContainer.Frequency; + public BindableNumber Frequency => SamplesContainer.Frequency; - public BindableNumber Tempo => samplesContainer.Tempo; + public BindableNumber Tempo => SamplesContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => samplesContainer.AddAdjustment(type, adjustBindable); + => SamplesContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => samplesContainer.RemoveAdjustment(type, adjustBindable); + => SamplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) - => samplesContainer.RemoveAllAdjustments(type); + => SamplesContainer.RemoveAllAdjustments(type); - public bool IsPlaying => samplesContainer.Any(s => s.Playing); + public bool IsPlaying => SamplesContainer.Any(s => s.Playing); #endregion } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 119c48836b..08811b9b8c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -4,15 +4,13 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSample : Component + public class DrawableStoryboardSample : PausableSkinnableSound { /// /// The amount of time allowable beyond the start time of the sample, for the sample to start. @@ -21,38 +19,37 @@ namespace osu.Game.Storyboards.Drawables private readonly StoryboardSampleInfo sampleInfo; - protected SampleChannel Channel { get; private set; } - public override bool RemoveWhenNotAlive => false; public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) + : base(sampleInfo) { this.sampleInfo = sampleInfo; LifetimeStart = sampleInfo.StartTime; } - [BackgroundDependencyLoader] - private void load(IBindable beatmap, IBindable> mods) - { - Channel = beatmap.Value.Skin.GetSample(sampleInfo); - if (Channel == null) - return; + [Resolved] + private IBindable> mods { get; set; } - Channel.Volume.Value = sampleInfo.Volume / 100.0; + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); foreach (var mod in mods.Value.OfType()) - mod.ApplyToSample(Channel); + { + foreach (var sample in SamplesContainer) + mod.ApplyToSample(sample); + } } protected override void Update() { base.Update(); - // TODO: this logic will need to be consolidated with other game samples like hit sounds. if (Time.Current < sampleInfo.StartTime) { // We've rewound before the start time of the sample - Channel?.Stop(); + Stop(); // In the case that the user fast-forwards to a point far beyond the start time of the sample, // we want to be able to fall into the if-conditional below (therefore we must not have a life time end) @@ -63,8 +60,8 @@ namespace osu.Game.Storyboards.Drawables { // We've passed the start time of the sample. We only play the sample if we're within an allowable range // from the sample's start, to reduce layering if we've been fast-forwarded far into the future - if (Time.Current - sampleInfo.StartTime < allowable_late_start) - Channel?.Play(); + if (!RequestedPlaying && Time.Current - sampleInfo.StartTime < allowable_late_start) + Play(); // In the case that the user rewinds to a point far behind the start time of the sample, // we want to be able to fall into the if-conditional above (therefore we must not have a life time start) @@ -72,13 +69,5 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = sampleInfo.StartTime; } } - - protected override void Dispose(bool isDisposing) - { - Channel?.Stop(); - Channel = null; - - base.Dispose(isDisposing); - } } } diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 31cced6ce4..9090a12d3f 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -35,8 +35,16 @@ namespace osu.Game.Tests Statistics[HitResult.Miss] = 1; Statistics[HitResult.Meh] = 50; - Statistics[HitResult.Good] = 100; + Statistics[HitResult.Ok] = 100; + Statistics[HitResult.Good] = 200; Statistics[HitResult.Great] = 300; + Statistics[HitResult.Perfect] = 320; + Statistics[HitResult.SmallTickHit] = 50; + Statistics[HitResult.SmallTickMiss] = 25; + Statistics[HitResult.LargeTickHit] = 100; + Statistics[HitResult.LargeTickMiss] = 50; + Statistics[HitResult.SmallBonus] = 10; + Statistics[HitResult.SmallBonus] = 50; Position = 1; } diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index f0ec638fc9..693c9cb792 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -20,6 +20,8 @@ namespace osu.Game.Tests.Visual protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); protected new readonly EditorClock Clock; + protected virtual bool ScrollUsingMouseWheel => true; + protected EditorClockTestScene() { Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false }; @@ -57,6 +59,9 @@ namespace osu.Game.Tests.Visual protected override bool OnScroll(ScrollEvent e) { + if (!ScrollUsingMouseWheel) + return false; + if (e.ScrollDelta.Y > 0) Clock.SeekBackward(true); else 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.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index ebb9995c66..48c6722bd9 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -79,6 +79,11 @@ namespace osu.Game.Updater bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage")); break; + case RuntimeInfo.Platform.iOS: + // iOS releases are available via testflight. this link seems to work well enough for now. + // see https://stackoverflow.com/a/32960501 + return "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; + case RuntimeInfo.Platform.Android: // on our testing device this causes the download to magically disappear. //bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk")); diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 9c95eff739..cc4fca9b94 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Users private void load() { Background.Width = 0.5f; + Background.Origin = Anchor.CentreRight; + Background.Anchor = Anchor.CentreRight; Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f)); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 71826e161c..fa2135580d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 90aa903318..20a51e5feb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,11 +80,11 @@ - + - + diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 3a16f81530..5125ad81e0 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,5 +11,7 @@ namespace osu.iOS public class OsuGameIOS : OsuGame { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); + + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); } } 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