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
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
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